feat(mcp): approve the SIE import in the drop card + post-import efterkontroll in the skill (#1963)
E2E #10 (the first fully successful drop-card run) left the staged import stranded until the user prodded the agent, and the agent then improvised an excellent post-import audit (skattekonto reconciliation, missing 1630, over-stated payroll liabilities). Codify both: 1. The drop card now carries the approval: after staging, the button becomes 'Godkänn bokföringen' with a two-click BFL confirmation (confirmed=true armed on the deliberate second click, exactly the pending-operations widget pattern), then 'Bokfört' + a ui/updateContext pointing the agent at trial balance and voucher-gap follow-ups. 2. Skill: when the user writes after a card was shown, the FIRST call is list_pending_operations (an empty ledger does not mean the file never arrived); new Step 4b 'efterkontroll' codifies the audit pass (trial balance vs SIE, skattekonto vs 1630 with 8423/8314/6992 for ränta/avgifter, auto-created bank account names, underlag coverage, voucher gaps); memory-first extended with memory-back (save orgnr, bank, fiscal year, moms period after creation so the next conversation needs zero questions). 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
84e6b1eb17
commit
53c4d0d194
@@ -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'")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -246,7 +246,16 @@ export const SIE_DROP_HTML = `<!DOCTYPE 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 = `<!DOCTYPE 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 = `<!DOCTYPE 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'));
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user