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>
This commit is contained in:
co-authored by
Jakob Wennberg
Claude Fable 5
parent
52bfd7a399
commit
b436d47b64
@@ -46,6 +46,18 @@ describe('deriveFirstYearDefaults', () => {
|
||||
expect(deriveFirstYearDefaults(registered, NOW).isFirstFiscalYear).toBe(false)
|
||||
})
|
||||
|
||||
it('extends the window to 18 months when the registry shows no closed period', () => {
|
||||
// Extended first räkenskapsår (BFL 3 kap 3 §): registered 13 months ago
|
||||
// with no annual report filed is still the first year.
|
||||
const registered = NOW - 13 * MONTH_MS
|
||||
expect(
|
||||
deriveFirstYearDefaults(registered, NOW, { noClosedPeriod: true }).isFirstFiscalYear
|
||||
).toBe(true)
|
||||
expect(
|
||||
deriveFirstYearDefaults(NOW - 19 * MONTH_MS, NOW, { noClosedPeriod: true }).isFirstFiscalYear
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('seeds first_year_start as the 1st of the UTC registration month', () => {
|
||||
const registered = new Date('2026-03-14T10:00:00Z').getTime()
|
||||
const result = deriveFirstYearDefaults(registered, NOW)
|
||||
|
||||
@@ -22,16 +22,21 @@ export function parseStartMonthDay(value: string | null | undefined): number | n
|
||||
/**
|
||||
* Derive the first-year defaults from TIC's `registrationDate`.
|
||||
* A company is treated as "first year" when registered less than 12 months
|
||||
* ago: comfortably inside BFL's 18-month cap on a first räkenskapsår, which
|
||||
* has no minimum length. Returns both the toggle state and a seeded
|
||||
* `first_year_start` (always the 1st of the registration month, the format
|
||||
* the date inputs expect).
|
||||
* 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
|
||||
@@ -43,8 +48,9 @@ export function deriveFirstYearDefaults(
|
||||
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 >= 12) return { isFirstFiscalYear: false, firstYearStart: undefined }
|
||||
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` }
|
||||
|
||||
Reference in New Issue
Block a user