* fix(import): say which fiscal years the Fortnox connection fetches, list the ones left out Root cause: the guided provider migration fetches SIE only for fiscal years that start within a rolling three-calendar-year window (getAllowedFiscalYears in extensions/general/arcim-migration/lib/sie-fetcher.ts: current year and the two before it). A first, broken year 2022/2023 starts in 2022 and falls outside the window in 2026, and the wizard said nothing: not before the import, not after. Users concluded the books were complete, or that they had done something wrong (issue #2211, second report via support 2026-08-27). Fix: - The fetcher already lists every fiscal year at the source before applying the window, so the left-out years are derived from that same list at no extra provider call: `omittedYears` (years starting before the window, oldest first, with the provider's own from/to dates so a broken year is named as "2022-09-01 till 2023-12-31"). Fortnox and Briox year refs now carry those bounds; WINT's listYears reports the unfiltered year list. - GET /preview returns `fiscalYearWindow` and `omittedYears`; GET /sie-data returns `omittedYears` next to `failedYears`. - Wizard, preview step (before the import runs): one muted sentence that the direct connection fetches the three latest fiscal years (years starting in {fromYear} or later); when years are left out, they are named with a link to the SIE import (one SIE file per year under Import, oldest first). - Wizard, result step: a "Räkenskapsår som inte följde med" section naming the omitted years with the same SIE pointer, shown when SIE data was imported in the run. - MCP: the connect_migration tool description, its instructions and the onboarding skill claimed the wizard "fetches every fiscal year"; they now say three latest, older years via SIE. - Strings in both messages/sv.json and messages/en.json. Out of scope: fetching more years through the connection (#2238). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015qgLgdt4mLmha1ZLFMwq1u * feat(import): make the Fortnox import's three-year limit a default selection, not a cap Root cause, from first principles: the guided provider migration fetched SIE only for fiscal years starting within a rolling three-calendar-year window (getAllowedFiscalYears in extensions/general/arcim-migration/lib/sie-fetcher.ts, introduced in #718 with no stated reason). The window was a silent cap: the wizard never said it existed, and a first broken year 2022/2023 simply never arrived (#2211). The user's actual problem is that the year is missing from the books, so explaining the cap (the previous commit) treats the symptom. What the window gated, by evidence: only the SIE fetch. Documents already list every Fortnox financial year and match against the vouchers that exist locally (import-documents.ts), invoices, customers, suppliers and assets are not year-gated, and the SIE import itself is one request per year (hosted function limit 300 s, import_sie_journal_entries statement_timeout 290 s), so its cost is linear in wall time and bounded per year regardless of how many years are imported. The only place the number of years multiplies inside one invocation is /preview and /sie-data: one SIE export per year (Fortnox client: 15 s per-call timeout, 3 attempts, backoff up to 30 s, 4 req/s) fetched and parsed inside a single 300 s function, and /sie-data returns every raw file in one response. The repo holds no measurement of Fortnox's per-year SIE export latency, and the maintainer's memory is that a full history can take unreasonably long, so a fixed lift to every year cannot be shown safe for a long history. Fix: the window becomes the DEFAULT selection, and the user chooses. - sie-fetcher: fetchProviderSieFiles takes `years` (explicit start years); without it the default window applies. The result carries `sourceYears` (every year at the source, oldest first, with the provider's own bounds and an inDefaultSelection flag) and `omittedYears` (source years outside the selection). Both derived from the year list already fetched: no extra provider call. Fortnox and Briox year refs carry their bounds; WINT's listYears reports the unfiltered list and its voucher chain follows the selection. - GET /preview returns `sourceYears`; GET /sie-data honours `?years=` (validated, deduplicated, oldest first; 400 VALIDATION_ERROR when malformed) and returns `omittedYears`. PROVIDER_SIE_NO_YEARS names the selection. - Wizard, preview step: a "Räkenskapsår att hämta" picker with one checkbox row per source year, the three latest ticked by default, older years marked "tar längre tid"; Fortsätt is disabled with an attn line until at least one year is ticked. The selection is sent to /sie-data, so each extra year is the user's own wait, and it fails loudly there, before any ledger write, if it is too much. - Wizard, result step: the per-year lines already report exactly what was imported; a "Räkenskapsår som inte hämtades" section names the source years outside the selection, with the re-run path (documents come along) and the SIE path. - MCP connect_migration description, instructions and the onboarding skill say "three latest by default, older years selectable" instead of "every fiscal year". - Strings in both messages/sv.json and messages/en.json. Closes #2238 as well: the wish to fetch more years is the same control. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015qgLgdt4mLmha1ZLFMwq1u * fix(import): bound the fiscal-year selection per import run Superagent P2 on #2280: the `years` selection was unbounded and every selected year is one provider export fetched and parsed inside the single 300 s /sie-data invocation, so nothing bounded the work before provider calls. The bound: MAX_SELECTED_FISCAL_YEARS = 6, exported from sie-fetcher.ts with the derivation. One export call is 15 s per attempt (Fortnox client FETCH_TIMEOUT_MS), 3 attempts with 1 s and 2 s backoff (retry defaults), so a year that times out on every attempt costs 48 s; six such years are 288 s, leaving 12 s of the 300 s hosted function for the year listing, parsing and the response; seven would be 336 s. Enforced server-side: - /sie-data refuses a selection of more than the cap with 400 VALIDATION_ERROR naming the cap, before the consent is resolved, so an oversized request does no provider work. - fetchProviderSieFiles throws FiscalYearSelectionError for a selected year the source does not have, right after the year listing and before any export; /sie-data maps it to 400 VALIDATION_ERROR naming the year. - /preview returns maxSelectedYears so the picker enforces the same number without a client-side copy: Fortsätt is disabled and an attn line says how many can be fetched at once and that older years go in a second run (sv + en). Tests: cap accepted at 6 and refused at 7 with no provider call, unknown year refused (route and fetcher), the cap's arithmetic, maxSelectedYears on /preview. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015qgLgdt4mLmha1ZLFMwq1u --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
258 lines
14 KiB
TypeScript
258 lines
14 KiB
TypeScript
import type { Skill } from './types'
|
|
|
|
const body = `# Onboarding: set up a company in Accounted from the conversation
|
|
|
|
From "my company is not in Accounted yet" to a working ledger without the
|
|
user opening the web app first. The only browser steps are the ones that
|
|
legally need a human with BankID: connecting (creating the account),
|
|
approving the bank consent, and authorising Skatteverket. Everything else
|
|
happens here.
|
|
|
|
**Momentum rule: one confirmation, then keep going.** The only stop in this
|
|
flow is the create-company preview ("stämmer detta? ja"). Never end a turn
|
|
with "säg till när du vill fortsätta": after the confirm, call the connect
|
|
tools immediately; when the user reports a connection done, verify it and
|
|
continue straight into import or categorization. Staged writes still go
|
|
through their normal approval, but that approval IS the conversation, not an
|
|
extra pause around it.
|
|
|
|
**Brevity rule: this is onboarding, not a course.** Keep every reply under
|
|
~8 short lines. At most ONE warning per step, one line, and only when it
|
|
changes what the user should do right now. No legal essays, no deadline
|
|
tables, no cross-checks the user did not ask about (bolagsstämma dates,
|
|
EU-moms edge cases, K-regelverk). The user can always ask for depth; the
|
|
flow must never make them scroll past it.
|
|
|
|
**Memory first, memory back.** Before asking the opening questions, check
|
|
what you already know about the user (memory, earlier conversation):
|
|
orgnr, company name, bank, previous system. Ask only for what is
|
|
genuinely missing. AFTER the company is created, save the durable facts
|
|
to memory (orgnr, company name, bank, fiscal year, moms period, previous
|
|
system, that Accounted is set up): the user's next conversation should
|
|
need zero of these questions.
|
|
|
|
## When to use
|
|
|
|
- "Sätt upp bokföring för mitt AB / min enskilda firma"
|
|
- "Jag har precis startat bolag, hur kommer jag igång?"
|
|
- "Lägg till ett nytt bolag" (an existing user adding a second company)
|
|
- A byrå/consultant onboarding a new client company (pass \`team_id\`)
|
|
|
|
**Already set up? Deliver value immediately instead.** When
|
|
\`gnubok_get_agent_briefing\` shows a company that already exists with
|
|
data and connections (the user onboarded via the web app), do NOT walk
|
|
the setup steps: go straight to the Step 4b reconciliation pass and the
|
|
Att göra-list, and open with the findings: a numbered list with amounts
|
|
in your FIRST reply. That first impression is the product.
|
|
|
|
## Step 0: connect
|
|
|
|
If this session is not connected yet, the first company-scoped call (for
|
|
example \`gnubok_create_company\`) returns an authentication challenge that the
|
|
client shows as a Connect prompt. Tell the user: "Klicka på Connect; har du
|
|
inget konto skapar du det där (BankID eller e-post), det tar en minut." The
|
|
call is retried automatically once connected. Do not send the user to the web
|
|
app to sign up first.
|
|
|
|
## Step 1: THREE opening questions, then look up
|
|
|
|
Open with exactly three questions, together:
|
|
|
|
1. **Organisationsnummer?** (10 digits; an enskild firma's org number is the
|
|
owner's personnummer, fine to use here)
|
|
2. **Har du bokfört i ett annat system tidigare?** (Fortnox, Visma, Bokio,
|
|
Björn Lundén, Briox, Wint, annat system, eller helt nytt bolag)
|
|
3. **Vilken bank har företaget?** (so the bank connect link later opens that
|
|
bank's consent directly instead of a picker)
|
|
|
|
Never re-ask a question the user already answered in this conversation
|
|
(the opening round included): reuse the answer.
|
|
|
|
Then call \`gnubok_lookup_company\` with the org number. The registry answers
|
|
most of the form; present the facts as a SHORT summary to confirm ("Jag
|
|
hittade Example AB, Storgatan 1 i Stockholm, godkänd för F-skatt och
|
|
momsregistrerad. Stämmer det?") and ask ONLY what \`still_to_ask\` lists.
|
|
Never re-ask what the registry answered.
|
|
|
|
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** 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
|
|
one. No closed period in the registry = FIRST räkenskapsår: suggest a
|
|
start at the registration date. The end differs by form: an enskild
|
|
firma MUST end 31 December; an AB may pick any end within 18 months of
|
|
the start (BFL 3 kap 3 §), with 31 December as the common default, so
|
|
present the AB's choice rather than assuming it.
|
|
|
|
\`not_found\`/\`unavailable\`: fall back to asking the \`still_to_ask\` list and
|
|
continue. Only \`aktiebolag\` and \`enskild_firma\` are supported today.
|
|
|
|
## Step 2: preview, ONE confirm, create, keep moving
|
|
|
|
Call \`gnubok_create_company\` WITHOUT \`confirm\`. Read the preview back in
|
|
plain Swedish (form, orgnr, fiscal period dates, VAT + period, method).
|
|
After the user's "ja": call it again with \`confirm: true\`, and in the SAME
|
|
turn continue with step 3 or 4. Creation sets up chart, settings, first
|
|
fiscal period, tax deadlines, and starts the 30-day trial; the connection
|
|
uses the new company automatically.
|
|
|
|
## Step 3 (existing bookkeeping): import it FIRST
|
|
|
|
When the user had a previous system, history comes before the bank: it is
|
|
the fastest path to a ledger that shows real value, and bank history rarely
|
|
reaches far enough back anyway.
|
|
|
|
1. Branch on WHICH system they name:
|
|
- **Fortnox / Björn Lundén / Briox / Wint** (API-connected systems):
|
|
offer TWO paths and recommend by need. The FULL migration: call
|
|
\`gnubok_connect_migration\` with the provider; it renders a connect
|
|
card (same feel as bank/Skatteverket) whose button opens the wizard
|
|
that logs into the old system and fetches the three latest fiscal
|
|
years by default (older years can be ticked in the wizard; they take
|
|
longer) PLUS invoices, customers, suppliers and documents: recommend
|
|
it when they have open fakturor or want underlag along. The QUICK path is a SIE
|
|
export dropped here (Fortnox: Register → Exportera → SIE 4): ledger
|
|
only, fastest. Either way the result lands in the same books.
|
|
- **Visma eEkonomi / Bokio**: no API export exists; ask for the SIE
|
|
file (Visma: Bokföring → Export SIE; Bokio: Inställningar →
|
|
Exportera data → SIE) and use the drop card FIRST. Then
|
|
\`gnubok_connect_migration\` with the provider renders the card that
|
|
complements with invoices and customers.
|
|
- **Annat/okänt system**: every Swedish system exports SIE4
|
|
(.se/.sie); ask them to export it and drop it here.
|
|
2. As soon as SIE import is the next step, call
|
|
\`gnubok_create_sie_upload\`. On claude.ai/Desktop it renders a
|
|
DRAG-AND-DROP card: the user drops the file on it and the card itself
|
|
runs the preflight and stages the import with exact bytes; you only
|
|
narrate the verdict and handle the approval. Without the card: PUT the
|
|
raw bytes to \`upload_url\` (from your code sandbox), compute sha256,
|
|
and call \`gnubok_sie_preflight\` with \`upload_id\` + \`sha256\` +
|
|
\`filename\`; smaller files may go inline as \`file_content_base64\` +
|
|
\`sha256\`. NEVER reproduce a large file token by token: unhashed
|
|
oversized inline content is refused because a mid-verifikat truncation
|
|
imports silently incomplete bookkeeping.
|
|
3. THE CARD ACTS WITHOUT YOU SEEING IT: it stages the import and can
|
|
approve it too. When the user writes after a card was shown, your
|
|
FIRST call is \`gnubok_list_pending_operations\`: an empty ledger does
|
|
NOT mean the file never arrived; the import may be staged (approve it
|
|
on the user's word) or already booked. Without the card: summarize the
|
|
preflight in a few lines and stage \`gnubok_import_sie\` with the
|
|
preflight's \`mappings\`. After commit: \`gnubok_get_trial_balance\`
|
|
and \`gnubok_explain_voucher_gap\` for any skipped numbers (BFNAR
|
|
2013:2; unexplained gaps block year-end).
|
|
4. Multiple fiscal years = multiple files: import oldest first so IB/UB
|
|
chains. The web wizard at \`/import?mode=sie\` is the fallback when no
|
|
upload path works; Fortnox users can also run the full API migration
|
|
(invoices, customers, documents) at
|
|
\`/import?mode=migration&provider=fortnox\`.
|
|
|
|
## Step 4: connect bank and Skatteverket (together, no pause)
|
|
|
|
Call \`gnubok_connect_bank\` (pass \`bank\` from step 1 so the link opens that
|
|
bank's consent directly) AND \`gnubok_connect_skatteverket\` in the same
|
|
turn; on claude.ai/Desktop both render connect cards with buttons. When a
|
|
card rendered, do NOT paste the URL as text too: the card button IS the
|
|
link, and duplicate raw URLs read as clutter.
|
|
|
|
- Bank: BankID + PSD2 consent, then an **account selection dialog** in the
|
|
browser: transactions start syncing when the user saves it. Banks cap
|
|
PSD2 history (often ~90 days); older history is the SIE import's job.
|
|
- Skatteverket: BankID as firmatecknare; enables skattekonto sync and
|
|
moms/AGI filing. Optional, never block on it.
|
|
|
|
When the user says they are done (or comes back), re-call
|
|
\`gnubok_connect_bank\` to verify \`connected\`, then go DIRECTLY to step 5.
|
|
|
|
## Step 3b: DIRECTLY after the import: verify and PREPARE (before the bank)
|
|
|
|
The moment the import commits, run a fast pass so the connections land in
|
|
a book that is READY for them: the user should feel value before the bank
|
|
even connects.
|
|
|
|
- \`gnubok_get_trial_balance\`: balances, and matches the SIE's UB.
|
|
- Sanity-read the content: no income accounts? liabilities that look
|
|
already-paid? Say so in one line each; do not fix yet.
|
|
- PREPARE the chart for what the connections will bring: if the import
|
|
lacks 1630 (skattekonto), 8423 (kostnadsränta skattekonto), 8314
|
|
(skattefri intäktsränta) or 6992 (ej avdragsgilla avgifter), create
|
|
them NOW so tax payments and fees book correctly from the first sync.
|
|
- Voucher gaps: explain each with \`gnubok_explain_voucher_gap\`.
|
|
|
|
## Step 4b: after the connections: reconcile (this is where trust is won)
|
|
|
|
Once bank + Skatteverket deliver data, run the reconciliation pass and
|
|
fix findings through the normal staged flow, a few lines per finding:
|
|
|
|
- Bank rows covered by the SIE period: MATCH them against existing
|
|
verifikat with \`gnubok_reconcile_match\` (per account; \`dry_run\`
|
|
first): never categorize them again, that double-books salaries and
|
|
everything else. Only rows after the SIE's last date get booked fresh.
|
|
- Skattekonto vs 1630: reconcile the events against the ledger. Common
|
|
finds: paid payroll taxes still standing on 2710/2731, ränta/avgifter
|
|
unbooked (8423/8314/6992: never ordinary cost accounts, or the
|
|
year-end tax computation goes wrong). End state must match
|
|
Skatteverket's saldo to the öre.
|
|
- Auto-created bank accounts (1930/1931/1935) named after the company:
|
|
suggest proper names.
|
|
- Underlag coverage: verifikat over ~5 000 kr without documents (BFL 5
|
|
kap 6 §): list them, offer the receipt-matcher flow.
|
|
|
|
Present findings as a short numbered list with amounts, fix in priority
|
|
order on the user's go-ahead, and re-verify the reconciled balances match
|
|
external truth (skattekonto saldo, bank balance) to the krona.
|
|
|
|
## Step 5: first bookkeeping, immediately
|
|
|
|
Call \`gnubok_list_uncategorized_transactions\` as soon as the bank is
|
|
connected: do not ask whether to proceed. Walk the categorize flow
|
|
(\`gnubok_suggest_categories\`, \`gnubok_categorize_transaction\`, approval).
|
|
If nothing has synced yet, say so and check again on the user's next
|
|
message instead of making them ask.
|
|
|
|
## Tools
|
|
|
|
- \`gnubok_lookup_company\`: registry facts + prefill from the orgnr; call first
|
|
- \`gnubok_create_company\`: preview (no confirm) then create (confirm=true)
|
|
- \`gnubok_create_sie_upload\`: byte-exact upload URL for the SIE file
|
|
- \`gnubok_sie_preflight\`: scan a shared SIE file, nothing written
|
|
- \`gnubok_import_sie\`: staged import; use the preflight's mappings
|
|
- \`gnubok_connect_bank\` / \`gnubok_connect_skatteverket\`: status + connect links
|
|
- \`gnubok_connect_migration\`: connect card into the previous-system wizard
|
|
- \`gnubok_list_companies\`, \`gnubok_get_agent_briefing\`: state checks
|
|
- \`gnubok_list_uncategorized_transactions\`: the first real bookkeeping step
|
|
|
|
## Pitfalls
|
|
|
|
- A VAT-registered company without a moms period is refused on purpose; do
|
|
not work around it by claiming the company is not VAT-registered.
|
|
- Do not create a company twice on a retry: check \`gnubok_list_companies\`
|
|
if a create call was interrupted.
|
|
- A preflight org-number mismatch means the file is another company's
|
|
bookkeeping: stop and confirm, never import across companies.
|
|
- Bookkeeping duty starts when the company exists in Accounted with a fiscal
|
|
period. Never create a company "to try things out" for a real
|
|
organisation; use the sandbox in the web app for demos.
|
|
`
|
|
|
|
export const onboardingSkill: Skill = {
|
|
slug: 'onboarding',
|
|
name: 'Onboarding: New Company Setup',
|
|
summary:
|
|
'Set up a company in chat: orgnr + previous system first, prefill via gnubok_lookup_company, one confirm to create, SIE history via preflight + staged import, then bank/Skatteverket cards.',
|
|
tags: ['onboarding', 'setup', 'company', 'bank', 'skatteverket', 'sie', 'migration', 'agent-first'],
|
|
body,
|
|
tier: 'workflow',
|
|
}
|