diff --git a/extensions/general/mcp-server/__tests__/sie-drop-widget.test.ts b/extensions/general/mcp-server/__tests__/sie-drop-widget.test.ts index a91fcbbf..30c1fd42 100644 --- a/extensions/general/mcp-server/__tests__/sie-drop-widget.test.ts +++ b/extensions/general/mcp-server/__tests__/sie-drop-widget.test.ts @@ -18,6 +18,15 @@ describe('SIE drop widget', () => { expect(widget?.html).toContain("addEventListener('drop'") }) + it('carries the approval in-card: two-click BFL confirm on the staged import', () => { + const html = widget!.html + expect(html).toContain("callTool('gnubok_approve_pending_operation'") + // High-risk approve arms confirmed=true only on the second, deliberate + // click: the human acknowledgment, never a default. + expect(html).toContain('args.confirmed = true') + expect(html).toContain('BFL 5 kap 5') + }) + it('passes exact bytes through tools/call with a sha256, never retyped or fetched', () => { const html = widget!.html expect(html).toContain("callTool('gnubok_sie_preflight'") diff --git a/extensions/general/mcp-server/skills/onboarding.ts b/extensions/general/mcp-server/skills/onboarding.ts index 085ee6c9..36a6ce08 100644 --- a/extensions/general/mcp-server/skills/onboarding.ts +++ b/extensions/general/mcp-server/skills/onboarding.ts @@ -23,9 +23,13 @@ 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.** 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. +**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 @@ -128,14 +132,15 @@ reaches far enough back anyway. \`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. Summarize the preflight in a few lines: source system, fiscal years, - verifikat count, balance status, org-number match, the one warning that - matters. On the user's go-ahead: \`gnubok_import_sie\` with the same - source (\`upload_id\` or content) and the preflight's \`mappings\`. It - stages for approval; after commit verify with - \`gnubok_get_trial_balance\`, and explain any skipped voucher numbers - with \`gnubok_explain_voucher_gap\` (BFNAR 2013:2; an unexplained gap - blocks year-end). +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 @@ -157,6 +162,28 @@ turn; on claude.ai/Desktop both render connect cards with buttons. 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 4b: after the import: efterkontroll (this is where trust is won) + +Run a short audit pass as soon as history + bank are in, and fix findings +through the normal staged flow, a few lines per finding: + +- \`gnubok_get_trial_balance\`: does the book balance and match the SIE? +- Skattekonto vs 1630: if Skatteverket is connected, reconcile the + skattekonto events against the ledger. Common finds: paid payroll taxes + still standing as liabilities on 2710/2731, the 1630 account missing + entirely, unbooked ränta/avgifter (kostnadsränta 8423, skattefri + intäktsränta 8314, ej avdragsgill förseningsavgift 6992: never the + ordinary cost accounts, or the year-end tax computation goes wrong). +- 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. +- Voucher gaps: explain each with \`gnubok_explain_voucher_gap\`. + +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 diff --git a/extensions/general/mcp-server/widgets/sie-drop.ts b/extensions/general/mcp-server/widgets/sie-drop.ts index 2811f3ef..8702657c 100644 --- a/extensions/general/mcp-server/widgets/sie-drop.ts +++ b/extensions/general/mcp-server/widgets/sie-drop.ts @@ -246,7 +246,16 @@ export const SIE_DROP_HTML = ` else hide('actions'); } + // After staging, the same button becomes the approval: first click arms + // the BFL acknowledgment, second click commits (confirmed=true for the + // high-risk import). The human click IS the acknowledgment, exactly like + // the pending-operations widget. Keeping approval in the card closes the + // loop that previously stranded the staged import until the user prodded + // the agent ("kolla igen", E2E #10). + let stagedOp = null; // { id, risk, armed } + el('import').addEventListener('click', function() { + if (stagedOp) { approveStaged(); return; } if (!fileState || !fileState.preflight) return; el('import').disabled = true; el('import').textContent = 'Importerar…'; @@ -260,10 +269,17 @@ export const SIE_DROP_HTML = ` import_transactions: true }).then(function(res) { const sc = parseResult(res); - el('import').textContent = 'Import förberedd'; - note('Importen är förberedd och väntar på godkännande: säg till i chatten eller godkänn i Accounted, så bokförs verifikationerna.'); + if (sc && sc.operation_id) { + stagedOp = { id: sc.operation_id, risk: sc.risk_level || 'high', armed: false }; + el('import').disabled = false; + el('import').textContent = 'Godkänn bokföringen'; + note('Importen är förberedd. Godkänn här i kortet så bokförs verifikationerna, eller säg till i chatten.'); + } else { + el('import').textContent = 'Import förberedd'; + note('Importen är förberedd och väntar på godkännande i chatten eller i Accounted.'); + } sendNotification('ui/updateContext', { - content: 'SIE-importen av ' + fileState.name + ' är stagead' + (sc && sc.operation_id ? ' (operation ' + sc.operation_id + ')' : '') + ' och väntar på godkännande.' + content: 'SIE-importen av ' + fileState.name + ' är stagead' + (sc && sc.operation_id ? ' (operation ' + sc.operation_id + ')' : '') + ' och väntar på godkännande i importkortet.' }); }).catch(function(err) { el('import').disabled = false; @@ -271,6 +287,31 @@ export const SIE_DROP_HTML = ` note('Import misslyckades: ' + (err && err.message ? err.message : 'okänt fel')); }); }); + + function approveStaged() { + if (stagedOp.risk === 'high' && !stagedOp.armed) { + stagedOp.armed = true; + el('import').textContent = 'Bekräfta: oåterkalleligt (BFL 5 kap 5 §)'; + note('Bokförda verifikat kan inte raderas, endast rättas med storno. Klicka igen för att bokföra.'); + return; + } + el('import').disabled = true; + el('import').textContent = 'Bokför…'; + const args = { operation_id: stagedOp.id }; + if (stagedOp.risk === 'high') args.confirmed = true; + callTool('gnubok_approve_pending_operation', args).then(function() { + el('import').textContent = 'Bokfört'; + note('Importen är godkänd och bokförd. Fortsätt i chatten: verifiera med råbalansen och förklara eventuella verifikatluckor.'); + sendNotification('ui/updateContext', { + content: 'SIE-importen är GODKÄND och bokförd via importkortet (operation ' + stagedOp.id + '). Nästa steg: gnubok_get_trial_balance för verifiering och gnubok_explain_voucher_gap för eventuella luckor.' + }); + }).catch(function(err) { + el('import').disabled = false; + stagedOp.armed = false; + el('import').textContent = 'Godkänn bokföringen'; + note('Godkännandet misslyckades: ' + (err && err.message ? err.message : 'okänt fel')); + }); + } })();