Files
accounted/lib/company/first-year-defaults.ts
T
b436d47b64 feat(mcp): onboarding flow v2: migration-first SIE import in chat, one-confirm momentum, extended first-year heuristic (#1949)
Three changes from the first real E2E run (Arcim, 2026-08-26):

1. gnubok_sie_preflight: read-only scan of a SIE file shared in chat
   BEFORE anything is staged: parse, validate (per-verifikat balance, IB,
   closed-year P&L residual), CP1252-mojibake tripwire, duplicate
   file/period check, org-number match against the company (the
   wrong-company import is the worst silent failure this flow can have),
   and suggested account mappings shaped for direct passthrough to
   gnubok_import_sie. Both tools now also accept file_content_base64,
   decoded with the same encoding detection as the HTTP upload route so
   CP437 exports keep their åäö.

2. Onboarding skill v2: opens with TWO questions (orgnr + 'vilket system
   hade du innan?'), imports history before the bank (PSD2 rarely reaches
   far enough back), and a momentum rule: the create preview is the ONLY
   stop; connect tools are called without asking, and categorization
   starts as soon as the bank is active. connect_bank instructions now
   describe the account-selection dialog that actually gates the first
   sync, and the bank history cap.

3. deriveFirstYearDefaults: no closed fiscal period in the registry now
   extends the first-year window from 12 to 18 months (BFL 3 kap 3 §):
   a 13-month-old company with no annual report is still in its first,
   extended räkenskapsår (the Arcim case the 12-month rule missed).
   Applied in the web journey and the lookup tool.

tools/list ceiling 61.5K to 62K, documented in the bench.

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-26 14:16:40 +02:00

58 lines
2.4 KiB
TypeScript

/**
* First-fiscal-year defaults derived from TIC lookup data.
*
* Extracted from WelcomeOnboarding so both the wizard and the journey
* onboarding can share them (dev_docs/onboarding_migration_plan.md, PR A).
*/
/**
* Parse TIC v2's `startMonthDay` ("MM-DD": e.g. "07-01") into a month
* number 1-12. Returns null on missing / malformed input so the caller
* can fall through to the manual picker default.
*/
export function parseStartMonthDay(value: string | null | undefined): number | null {
if (!value) return null
const match = /^(\d{1,2})-\d{1,2}$/.exec(value)
if (!match) return null
const month = Number(match[1])
if (!Number.isInteger(month) || month < 1 || month > 12) return null
return month
}
/**
* Derive the first-year defaults from TIC's `registrationDate`.
* A company is treated as "first year" when registered less than 12 months
* ago, or less than 18 months ago when the registry shows NO closed fiscal
* period (`noClosedPeriod`): a company that has never filed an annual
* report is still in its first räkenskapsår, and BFL 3 kap 3 § allows that
* first year to run up to 18 months (no minimum). The 12-month floor alone
* missed exactly the extended-first-year companies the signal exists for
* (Arcim, registered 13 months before onboarding, first year to 31 Dec).
* Returns both the toggle state and a seeded `first_year_start` (always the
* 1st of the registration month, the format the date inputs expect).
*
* `now` exists for tests; production callers omit it.
*/
export function deriveFirstYearDefaults(
registrationDate: number | null | undefined,
now: number = Date.now(),
opts?: { noClosedPeriod?: boolean },
): {
isFirstFiscalYear: boolean
firstYearStart: string | undefined
} {
if (!registrationDate || !Number.isFinite(registrationDate)) {
return { isFirstFiscalYear: false, firstYearStart: undefined }
}
const regDate = new Date(registrationDate)
if (Number.isNaN(regDate.getTime())) {
return { isFirstFiscalYear: false, firstYearStart: undefined }
}
const windowMonths = opts?.noClosedPeriod ? 18 : 12
const monthsAgo = (now - regDate.getTime()) / (1000 * 60 * 60 * 24 * 30.44)
if (monthsAgo >= windowMonths) return { isFirstFiscalYear: false, firstYearStart: undefined }
const year = regDate.getUTCFullYear()
const month = String(regDate.getUTCMonth() + 1).padStart(2, '0')
return { isFirstFiscalYear: true, firstYearStart: `${year}-${month}-01` }
}