feat(onboarding): minimal input: orgnr + moms period is the whole ask (#1952)

* feat(onboarding): minimal input: orgnr + moms period is the whole ask

Two fixes from the third E2E attempt (2026-08-26):

1. accounting_method is now optional in CompanySetupSchema and defaults by
   form in planCompanySetup: aktiebolag = accrual (the norm), enskild
   firma = cash (the common small-EF choice; legal under 3 MSEK, BFL 4
   kap 4 paragraf). The plan flags the default (resolved.accountingMethodDefaulted)
   and gnubok_create_company's preview carries accounting_method_defaulted
   so the readback names it and the user overrides in the same 'ja'.
   Never silent: the preview is the checkpoint. Applies to the MCP tool
   and POST /api/v1/companies (additive; response shows the resolved
   value). The lookup tool's still_to_ask no longer lists it.

2. The agent refused a real orgnr because the user said 'nytt bolag' and
   the registry showed an established company ('Stopp. Numret matchar
   inte ett nytt bolag'): lookup instructions now state that an
   established company with F-skatt/VAT is the NORMAL case (new = new to
   Accounted) and the orgnr is never second-guessed for looking
   established.

Skill + plugin (v1.2.1) updated; API skill regenerated; DECISIONS.md entry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(onboarding): surface the kontantmetod 3-MSEK condition on the defaulted cash method

Compliance-review finding on #1952: the EF cash default carries a legal
eligibility condition (turnover normally under 3 MSEK, BFL 4 kap 4 §)
that a client not reading the onboarding skill would never see. The
create preview now carries accounting_method_note with the condition
whenever cash was defaulted, and the v1 pitfall states it for API
integrators. The registry cannot verify turnover, so the confirm-time
human check is the gate; the default itself stays (a brand-new EF has
zero turnover by definition).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-08-26 15:15:09 +02:00
committed by GitHub
co-authored by Claude Fable 5 Jakob Wennberg
parent 4ac7b45c8a
commit 4af7469523
11 changed files with 109 additions and 17 deletions
@@ -55,6 +55,25 @@ describe('gnubok_create_company', () => {
expect(tool.annotations.destructiveHint).toBe(false)
})
it('defaults an omitted accounting_method by form and flags it in the preview', async () => {
const supabase = supabaseWithTeam(TEAM_ID)
const { accounting_method: _dropped, ...withoutMethod } = setup
const result = (await tool.execute(withoutMethod, '', 'user-1', supabase as never)) as Record<string, unknown>
expect(result.created).toBe(false)
const preview = result.preview as Record<string, unknown>
expect(preview.accounting_method).toBe('accrual')
expect(preview.accounting_method_defaulted).toBe(true)
})
it('does not flag an explicitly chosen accounting_method as defaulted', async () => {
const supabase = supabaseWithTeam(TEAM_ID)
const result = (await tool.execute(setup, '', 'user-1', supabase as never)) as Record<string, unknown>
const preview = result.preview as Record<string, unknown>
expect(preview.accounting_method).toBe('accrual')
expect('accounting_method_defaulted' in preview).toBe(false)
})
it('previews without creating when confirm is not true', async () => {
const supabase = supabaseWithTeam(TEAM_ID)
const result = (await tool.execute(setup, '', 'user-1', supabase as never)) as Record<string, unknown>
@@ -83,7 +83,9 @@ describe('gnubok_lookup_company', () => {
const ask = result.still_to_ask as string[]
expect(ask.some((q) => q.startsWith('moms_period ('))).toBe(true)
expect(ask.some((q) => q.startsWith('accounting_method'))).toBe(true)
// accounting_method defaults by form in create_company (flagged in its
// preview) and is deliberately not a question here.
expect(ask.some((q) => q.startsWith('accounting_method'))).toBe(false)
// Registry facts are confirmed, never re-asked.
expect(ask.some((q) => q.startsWith('entity_type'))).toBe(false)
expect(ask.some((q) => q.startsWith('vat_registered'))).toBe(false)
+20 -7
View File
@@ -3126,7 +3126,9 @@ export const tools: McpTool[] = [
if (!vatIsFact) stillToAsk.push('vat_registered (the registry shows no VAT registration; confirm with the user)')
if (vatIsFact) stillToAsk.push('moms_period (monthly, quarterly or yearly; never guess)')
else stillToAsk.push('moms_period IF vat_registered turns out true')
stillToAsk.push('accounting_method (accrual = faktureringsmetoden, cash = kontantmetoden; never guess)')
// accounting_method is deliberately NOT in this list: gnubok_create_company
// defaults it by form (AB accrual, EF cash) and flags the default in the
// preview, where the user confirms or overrides it in the same "ja".
const startMonth = parseStartMonthDay(lookup.fiscalYear?.startMonthDay)
// No closed fiscal period in the registry = no annual report filed yet
@@ -3182,7 +3184,7 @@ export const tools: McpTool[] = [
still_to_ask: stillToAsk,
warnings,
instructions:
'Present the company facts as a short summary for the user to CONFIRM (name, address, F-skatt, VAT status; do not re-ask them). Then ask ONLY the still_to_ask questions, merge the answers into suggested_create_company_input, and call gnubok_create_company (preview first, read it back, then confirm=true).',
'Present the company facts as a short summary for the user to CONFIRM (name, address, F-skatt, VAT status; do not re-ask them). An established company with F-skatt/VAT is the NORMAL case even when the user says "nytt bolag" (new = new to Accounted): never refuse or second-guess the orgnr because the company looks established. Ask ONLY the still_to_ask questions, merge answers into suggested_create_company_input, and call gnubok_create_company (preview, read back incl. the defaulted accounting method, then confirm=true).',
}
},
},
@@ -3191,7 +3193,7 @@ export const tools: McpTool[] = [
name: 'gnubok_create_company',
title: 'Create Company',
description:
'Create a NEW company for the connected user, set up for bookkeeping (chart, settings, first fiscal period, tax deadlines; 30-day trial). Call gnubok_lookup_company FIRST to prefill facts from the orgnr. Preview (no confirm), read it back, then confirm=true. Skill: onboarding.',
'Create a NEW company, set up for bookkeeping (chart, settings, first fiscal period, tax deadlines; 30-day trial). Ask ONLY orgnr + moms period: gnubok_lookup_company prefills the rest, accounting_method defaults by form. Preview (no confirm), read back, then confirm=true.',
inputSchema: {
type: 'object',
additionalProperties: false,
@@ -3201,7 +3203,7 @@ export const tools: McpTool[] = [
org_number: { type: 'string', description: '10 digits; required when VAT-registered' },
vat_registered: { type: 'boolean' },
moms_period: { type: 'string', enum: ['monthly', 'quarterly', 'yearly'], description: 'Required when vat_registered' },
accounting_method: { type: 'string', enum: ['accrual', 'cash'] },
accounting_method: { type: 'string', enum: ['accrual', 'cash'], description: 'Omit to default by form: aktiebolag accrual, enskild firma cash; the preview flags the default' },
f_skatt: { type: 'boolean' },
fiscal_year_start_month: { type: 'integer', minimum: 1, maximum: 12 },
first_fiscal_year: {
@@ -3217,7 +3219,7 @@ export const tools: McpTool[] = [
team_id: { type: 'string', format: 'uuid' },
confirm: { type: 'boolean', description: 'true creates; omitted = preview' },
},
required: ['name', 'entity_type', 'vat_registered', 'accounting_method', 'f_skatt'],
required: ['name', 'entity_type', 'vat_registered', 'f_skatt'],
},
outputSchema: {
type: 'object',
@@ -3258,7 +3260,18 @@ export const tools: McpTool[] = [
vat_registered: parsed.data.vat_registered,
vat_number: (plan.input.settings.vat_number as string | null) ?? null,
moms_period: parsed.data.vat_registered ? parsed.data.moms_period ?? null : null,
accounting_method: parsed.data.accounting_method,
accounting_method: plan.resolved.accountingMethod,
// Present, never silent: the readback must name the defaulted method
// so the user can override it before confirm. The cash default also
// carries its eligibility condition (BFL 4 kap 4 §): the registry
// cannot verify turnover, so the human must.
...(plan.resolved.accountingMethodDefaulted ? { accounting_method_defaulted: true } : {}),
...(plan.resolved.accountingMethodDefaulted && plan.resolved.accountingMethod === 'cash'
? {
accounting_method_note:
'Kontantmetoden förutsätter en omsättning som normalt understiger 3 MSEK (BFL 4 kap 4 §); annars gäller faktureringsmetoden. Bekräfta detta med användaren.',
}
: {}),
f_skatt: parsed.data.f_skatt,
fiscal_period: plan.fiscalPeriod,
team_id: teamId,
@@ -3270,7 +3283,7 @@ export const tools: McpTool[] = [
requires_confirmation: true,
preview,
message:
'Nothing was created. Read the preview back to the user (especially the fiscal period dates and the VAT setup), then call gnubok_create_company again with the same arguments and confirm=true.',
'Nothing was created. Read the preview back to the user (especially the fiscal period dates, the VAT setup, and the accounting method when accounting_method_defaulted is true: name the default and let them override), then call gnubok_create_company again with the same arguments and confirm=true.',
}
}
@@ -54,9 +54,14 @@ Rules baked into that split (same as the web onboarding):
- **F-skatt** from the registry is a fact, both true and false.
- **VAT** is a fact ONLY when positively registered. "No VAT registration
found" is a question, never an assumption (ML 17 kap 24 §).
- **Moms period** and **accounting method** are ALWAYS the user's answer.
Rules of thumb if unsure: under 1 MSEK turnover may report VAT yearly,
under 40 MSEK quarterly, above monthly; cash method only under 3 MSEK.
- **Moms period** is ALWAYS the user's answer when VAT-registered. Rules of
thumb if unsure: under 1 MSEK turnover may report VAT yearly, under 40
MSEK quarterly, above monthly.
- **Accounting method is NOT a question**: it defaults by form (AB =
faktureringsmetoden, enskild firma = kontantmetoden) and the preview
flags the default. Name it in the readback ("faktureringsmetoden,
standard för AB, säg till om du vill ha kontantmetoden") so the user can
override in the same "ja". Cash requires turnover under 3 MSEK.
- **Enskild firma name**: verksamhetsnamnet is freely choosable; suggest the
registered name but let the user pick. An AB's registered name is a fact.
- **Fiscal year**: registry data becomes a confirm question, never an open