feat(mcp): accept article_id on gnubok_create_invoice lines (#1638)

* feat(mcp): accept article_id on gnubok_create_invoice lines

Invoice lines staged via MCP can now reference a catalog article
(artikelregister). Staging prefills description, unit, unit_price,
vat_rate and revenue account from the article with explicit-wins
semantics, mirroring the web line picker. Unknown, foreign-company and
deactivated articles are refused at staging, as is a price prefill from
an article priced in another currency. The approval executor gains a
company-scope gate for staged article_id values: the FK on
invoice_items.article_id proves existence, not tenancy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(mcp): adopt article VAT rate only from the customer's default set

Web line picker parity (InvoiceEditor's applyArticle guard): an article's
stored vat_rate is its domestic rate. The prefill previously staged it
unconditionally, and the staging/commit gates check the wider permitted
set (which includes 25/12/6 for taxed-where-performed supplies), so
{article_id, quantity} to a validated EU business staged 25% Swedish VAT
onto a reverse-charge invoice. The customer is now fetched before the
prefill and the article rate is adopted only when it is in the
customer's default rate set; foreign-business lines fall back to the 0%
reverse-charge/export default unless the agent sets vat_rate explicitly.
Prefill logic extracted into resolveInvoiceLineFromArticle (CodeRabbit).

Found by the skeptic review pass (two independent refutations) and the
Swedish accounting compliance bot, all converging on the same defect.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(invoices): name the article VAT adoption set in vat-rules, not the MCP server

The vat-rate-gate-parity guard pins that no invoice write path mentions
getAvailableVatRates: gating on the picker default is the bug it exists
to prevent. The article-rate adoption in gnubok_create_invoice needs the
default set for a different purpose (prefill, not gating), so the
semantics move into lib/invoices/vat-rules.ts as
getArticleVatRateAdoptionSet(), with tests pinning that adoption is
empty for single-rate foreign customers and always a subset of the
permitted set. server.ts keeps gating on getPermittedVatRates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mattsson
2026-08-17 11:42:51 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent f8db38f989
commit caa0c3b41d
9 changed files with 591 additions and 40 deletions
+1
View File
@@ -1019,6 +1019,7 @@ One line per decision: `[YYYY-MM-DD] <decision>: <why>`. Appended by agents and
[2026-08-15] Confirmed intentional (Swedish-review note): with override=true and an unresolvable filename, the attach endpoint links a document to any same-company, same-declared-year, posted verifikat, migrated or not. This mirrors /api/documents/[id]/link, which imposes no filename check at all, so it introduces no new capability class; tenant, year and period-lock enforcement always apply.
[2026-08-15] BankID tabs bind to a random non-secret `flowId` signed into the shared flow cookie and sent as a request header after start or explicit resume: mode pinning alone cannot distinguish two same-mode tabs, so an older tab could otherwise silently follow, cancel, or complete a newer person's identification after `/start` replaced the origin-wide cookie. This supersedes the 2026-08-15 decision that deliberately skipped mode matching on active polls.
[2026-08-15] Did not apply BankID migration `20260815120000` to Supabase staging during PR #1625 follow-through: read-only reconciliation found 14 staging-only and 99 branch-only migration versions, so applying on top of that divergent ledger would violate the no-orphan rule. Production is reconciled with zero remote-only versions and exactly this PR migration local-only; hosted pg-real validates the migration until staging is reconciled.
[2026-08-17] MCP article refs on create_invoice only, not update_invoice: the update tool's full-replace item semantics need their own design pass; also refuse cross-currency price prefill instead of converting, the agent must pick the currency explicitly.
[2026-08-17] Session replay masking inverted to deny-by-default (founder-directed after user pushback on session recording): ALL input values are masked (maskAllInputs with no maskInputFn, so rrweb masks wholesale; placeholders are attributes and stay visible) and ALL text is masked unless it sits under data-ph-unmask chrome or a th (dry-table column headers are raw th per page, so the mask function treats th as chrome rather than tagging hundreds of sites). Chrome tags live on the shared primitives (PageHeader, Label, Button except role=combobox triggers which render selected values, TabsTrigger, Badge, Card/Dialog/Sheet titles and descriptions, tooltips, help popovers, empty states, settings labels); tagged chrome is still pattern-scrubbed for amounts and identity numbers, and data-ph-mask beats data-ph-unmask so call sites that interpolate user data into chrome stay masked. Toasts (title AND description) deliberately NOT tagged: they interpolate user data at too many call sites to audit, and an audit found live leaks (deadline titles, bank account names) in titles alone. Confirm-dialog wrappers (ConfirmDialog, ConfirmationDialog, DestructiveConfirmDialog) force data-ph-mask on their titles/descriptions centrally: convention 10 makes confirm copy describe the object being acted on, so it is user data by design; that one change closed 14+ audited leak sites. A very-thorough audit of user data flowing into unmasked primitives ran in the same change and every found site got a call-site data-ph-mask. Failure mode for untagged new UI is over-masking, never leakage. Supersedes the 2026-08-06 pattern-based default; privacy policy and RoPA updated in the same change.
[2026-08-17] Betalfil missing-bankgiro UX: advisory warning in PaymentFilePanel (download stays enabled, route stays the authority) + click-to-prefill from tic_snapshot instead of auto-seeding company_settings.bankgiro: sender payment data must be user-confirmed, and the snapshot is unvalidated registry JSON.
[2026-08-16] /transactions FyPicker double-fetch fixed by gating the initial fetch on FyPicker's existing onReady (fires after its restore onChange) instead of the analysis doc's literal "read the persisted period synchronously in initial state": localStorage only holds the period ID, not the FiscalPeriod bounds, so a synchronous read would suppress FyPicker's restore (value !== null) and leave the fetch permanently unscoped while the chip claimed a year. Same outcome (one scoped fetch per mount, background refetch on period change) without a stale-bounds cache or new FyPicker API.