Files
accounted/extensions/general/mcp-server/skills/onboarding.ts
T
1976129478 feat(import): let the Fortnox import fetch older fiscal years: the three-year limit becomes a default selection (#2280)
* 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>
2026-09-04 17:14:49 +02:00

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',
}