Peppol sending has been live since #1780 behind a per-company access grant, but the MCP skills, the swedish-invoice-compliance atom, docs/PEPPOL_FOUNDATION.md and the v1 :send / :mark-sent descriptions still told agents it did not exist. Every text now says gated per company (requested under Installningar > Fakturering) and keeps the restrictions explicit: aktiebolag senders, standard invoices only, Swedish org-number buyers, no MCP or v1 Peppol send verb yet, :mark-sent as the recovery step when a network-accepted send fails issuance. The skills guard test pins the truthful claim across all surfaces. Includes the regenerated agent_atom_registry seeds and skills/accounted-api references. Refs #546
This commit is contained in:
@@ -1340,6 +1340,9 @@ One line per decision: `[YYYY-MM-DD] <decision>: <why>`. Appended by agents and
|
||||
[2026-08-28] /migrate SIE guard skips company-info-only runs (all entity flags false) and the wizard derives "SIE already imported" from the preview OR this session's successful /import-sie results: company info writes no accounts, balances or subledger rows, so the BFL rationale does not apply; and the one-shot preview went stale after phase 1 succeeded and phase 2 failed, falsely blocking an entities-only retry (#2000 review).
|
||||
[2026-08-28] get_vat_ruta_source_lines (the VAT ruta drill-down) now applies the same four exclusions as get_vat_declaration_totals (the filed figure): posted closing entries, source_type 'vat_settlement', the two kontantmetod year-end reversals, and settlement-SHAPED entries (a line on a ruta account plus a line on 2650/1650). It previously filtered on company, status and date only, so expanding a ruta listed verifikat that are not in the number it claims to explain, with no total on the panel to reveal the mismatch. Measured on prod 2026-08-28: 322 posted/reversed entries carrying 26xx lines across 214 companies sit in those excluded classes. A momsdeklaration is räkenskapsinformation (BFL 5 kap.) and this drill-down is what substantiates a filed figure, so the two must agree exactly. The exclusion CTEs are lifted VERBATIM from the figure rather than re-derived: any divergence reintroduces exactly this bug, and an identical copy is easy to diff when the figure changes. Settlement-shape is detected against journal_entry_lines directly instead of through the figure's vat_lines CTE, which is EQUIVALENT not a shortcut (p_ruta_accounts = VAT_ACCOUNTS and p_net_accounts = ['2650','1650'] are both strict subsets of the figure's p_accounts, so restricting to vat_lines first cannot change which entries match); that keeps p_accounts meaning "the accounts of the ruta being expanded" without a fourth account parameter. opening_balance entries are deliberately NOT excluded: the figure exempts them from `shaped`, which keeps their lines IN the totals, so dropping them here would break the equality in the other direction (pinned by its own test). VAT_ACCOUNTS is now exported from lib/reports/vat-declaration.ts so the route detects shape from the same list the figure uses; a second copy is what let the two disagree. DROP + CREATE OR REPLACE, not CREATE OR REPLACE alone: the signature gains p_ruta_accounts/p_net_accounts and adding parameters registers a second overload PostgREST cannot choose between (trap documented in 20260421140000); OR REPLACE on the new arity keeps the file re-runnable. Verified the new pg test actually catches the bug by reinstalling the old body and watching 3 of 4 tests fail with the real misreporting (2611: drill-down 250/240 vs figure 0/200), then restoring.
|
||||
[2026-08-28] Bankavstamning NULL-link fix scoped to transfer legs with contradicting sign (20260828220000): the naive rule (NULL counts only for the primary account) and the formula-only variant (drop far-leg-settled vouchers from unexplained) were both simulated against prod and rejected; the naive rule worsened 4 of 11 affected cards (worst -37 000 kr false alarm on single-leg vouchers with no user action available), the formula variant blew up healthy cards by up to 474 550 kr. The shipped three-condition rule changes 24 vouchers on 7 cards in 6 companies, all verified per-card.
|
||||
[2026-08-28] Skill, atom and API-doc texts must never claim a capability is absent when it is gated (#546): Peppol send has been live since 2026-08-21 behind a per-company access grant (app/api/invoices/[id]/peppol/send, scripts/peppol/access.ts), yet invoicing-rules, customer-onboarding, the swedish-invoice-compliance atom, docs/PEPPOL_FOUNDATION.md and the v1 :send/:mark-sent descriptions still told agents it did not exist, which sends B2G users to a competitor. Every touched text now says "gated per company, request under Inställningar > Fakturering", keeps the restrictions explicit (aktiebolag senders only, standard invoices only, no MCP or v1 send verb yet) so agents do not over-promise to enskild firma users, and keeps the external-provider + gnubok_mark_invoice_as_sent fallback for companies without access. The skills guard test was inverted from pinning "no Peppol send" to pinning the truthful claim, so a future regression in either direction fails CI.
|
||||
[2026-08-28] Generated agent_atom_registry seed renamed from the generator's max-plus-one version (20260828220001) to 20260829000100 (#546): the migration rule for this batch is a version newer than every existing file and dated after today's parallel worktrees, which also emit seeds with generator-picked 20260828 names; skills:check compares the content-hash manifest, not the filename, and the pg replay test globs the seed, so the rename is inert for both.
|
||||
[2026-08-29] PR #2021 review round (#546): the v1 :send/:mark-sent descriptions now say :mark-sent is not needed after a SUCCESSFUL dashboard Peppol send and IS the documented recovery when the network accepted the document but issuance failed (send route returns 201 with issuance.ok=false and the invoice still draft; the dashboard toast peppol_issue_failed_description points the user to Markera som skickad; ensureInvoiceNumber is idempotent so no second F-number is consumed). The unconditional "never call :mark-sent after a Peppol send" would have made an agent refuse the one action that completes the bookkeeping. The relayed restriction list in all three agent texts gained the exporter's other refusals (SEK, taxable 6/12/25 % VAT only, no ROT/RUT deductions, Er referens) and the send cap, because the ROT/RUT section sits directly above the Peppol section in invoicing-rules and a bygg company would otherwise be promised a send the exporter refuses. The atom edit was seeded as a SECOND generated migration (20260829010000, atom v8) next to the PR's 20260829000100 (v7) rather than editing that file in place: the generator is append-only by design, and the Supabase preview branch for the PR has already applied 20260829000100, so replacing or removing it would orphan the preview's migration history; both seeds are idempotent upserts with the version guard, so prod applying both is harmless. The guard test's "capability absent" patterns were re-anchored on the capability as subject (active, passive and adjective forms, each proven against the pre-#546 wording inline) so the true v1 sentence "a v1 or MCP Peppol send action is not yet available" stays legal and a passive rewording no longer evades the test.
|
||||
[2026-08-29] Bank 1:N (#1553, one bank row over several verifikat) is stored junction-only: transactions.journal_entry_id stays NULL and one transaction_voucher_links row per verifikat carries a signed role='bank_line' slice in the transaction's sign convention (the same shape bulk_book_transactions writes), never a pointer at "the first" voucher: the pointer column is a 1:1 fact, every reader that matters already asks isTransactionBooked / is_transaction_booked() / the GL RPCs' junction count, and a pointer at one of N would make koppla-bort, storno and the bridge all disagree about which voucher the row "belongs" to. The slices must sum to the row (VOUCHER_LINK_AMOUNT_TOLERANCE), each must have the sign of, and be no larger than, that voucher's net line on the account; the locked transactions UPDATE (.is journal_entry_id null, or .eq the stale pointer of a reversed entry per #988) runs BEFORE the one junction insert so a concurrent linker matches zero rows, and a failed insert rolls the lock back.
|
||||
[2026-08-29] Storno of one of a split's N vouchers releases the row WHOLE (engine reverseEntry): after the reversed voucher's junction rows go, a pointer-less row whose remaining anchors are all role='bank_line' but no longer sum to its amount has its surviving slices deleted and returns to Att bokföra (is_business/category/reconciliation_method null), so the surviving verifikat resurface as unmatched and the user re-splits against the corrected booking. The alternative (keep the partial anchor) would show the row as booked while the bridge moved by the reversed slice with nothing to act on. Rows whose remaining slices still cover the amount (bulk-book, one row per transaction) and rows with a non-bank_line anchor (a residual's 'other' row) are untouched, as before.
|
||||
[2026-08-29] The re-booking guards (manualLink, categorize-core, link-journal-entry) refuse on a transaction_voucher_links row of role 'bank_line' only (hasBankLineJunctionRow), not on any junction row, while the list/ignore readers (fetchJunctionLinkedTxIds, is_transaction_booked()) keep counting every role: a residual booking's 'other' row survives a storno of the main verifikat, which also nulls reconciliation_method so koppla-bort refuses the row; refusing to re-book it would strand it with no path out. linkTransactionToVouchers itself stays strict (any junction row blocks a split) because the UNIQUE (transaction_id, journal_entry_id) key would refuse re-anchoring that voucher anyway.
|
||||
@@ -1354,3 +1357,4 @@ One line per decision: `[YYYY-MM-DD] <decision>: <why>`. Appended by agents and
|
||||
[2026-08-29] get_vat_ruta_source_lines ACL restored in a NEW migration (20260829090500) rather than by editing 20260828172003: that file DROPped the 9-arg overload and CREATEd the 11-arg one without restating REVOKE/GRANT, and DROP FUNCTION discards the ACL, so the new signature silently fell back to EXECUTE for PUBLIC (anon included); the migration is already applied on prod, so a follow-up file is the only compliant path. Rule going forward: every DROP + CREATE of an RPC must restate its REVOKE ALL FROM PUBLIC, anon / GRANT EXECUTE TO authenticated, service_role, and tests/pg/vat-ruta-drilldown-reconcile.pg.test.ts now pins it with has_function_privilege (anon false, authenticated and service_role true, exactly one overload).
|
||||
[2026-08-29] PR #1756 replacement (rebind on PSD2 remap, amends the 2026-07-09 #916 entry): when upsertFromPsd2 resolves a duplicate row for the same connection+uid, the duplicate's MOVABLE transactions (unbooked, unmatched, not anchored via transaction_voucher_links or a payment row: the #1570 single-row move gate) are rebound onto the promoted row BEFORE the duplicate is resolved, so categorize/booking proposes the ledger the user just mapped instead of the overflow slot; a duplicate that still holds booked or anchored rows is demoted to manual as before and never deleted (their vouchers carry the old 19xx line, and the #1643 orphan guards handle the released twin). The contributor's unconditional rebind-all-then-delete was narrowed for that reason.
|
||||
[2026-08-29] Database errors now keep their SQLSTATE: new lib/errors/db-error.ts (dbError/errorCauseTag), applied at the 54 `throw new Error(\`Database error: ${err.message}\`)` sites in the MCP server AND, far more importantly, at lib/supabase/fetch-all.ts:74 where `throw new Error(error.message)` was the single highest-traffic strip point in the codebase (31 callers; every paginated read). isTransientFailure() checks the driver code FIRST and 57014 (statement timeout) is already in TRANSIENT_SQLSTATES, so discarding it turned a retryable timeout into UNKNOWN_ERROR ("Något gick fel. Försök igen."), which an agent cannot dispatch on. Traced end to end: gnubok_query_journal -> fetchEntryLines -> fetchAllRows (code stripped here) -> the tool's own sanitizeDbError, which ALREADY had a correct TRANSIENT_ERROR branch with a "retry or narrow with date_from/date_to" hint that could never fire because getStructuredError saw an anonymous Error. Measured on prod over 60 days with bot actors excluded: 1 024 real-agent failures, 645 UNKNOWN_ERROR across 60 actors and 57 companies; query_journal failed 164 times at p50 8 110 ms while every other failing tool sat at 1-315 ms; 82 retry streaks, 462 wasted repeat calls, 53.1% of error calls inside a streak. fetch-all passes context=null so the driver message stays VERBATIM (sanitizeDbError and other callers match on the existing text; this change adds the code, it does not reword). Attaching `code` is safe because extractCode() only accepts /^[A-Z_]+$/ and every SQLSTATE/PostgREST code contains digits, so it cannot hijack the application error registry (pinned by a test). dbError also never renders the literal "undefined": a driver-level failure with no message produced "Database error: undefined", the string that made these unsearchable. errorCauseTag() returns a PII-safe SQLSTATE for telemetry; the raw driver message can quote row values in a constraint violation and belongs in the server log, never in event_log. NOT ratcheted: check:types reports 538 vs baseline 539 because main fixed an unrelated error in own-account-detector.test.ts after the baseline was set; the gate only fails on an INCREASE, so the baseline is left alone rather than adding unrelated churn to this diff.
|
||||
[2026-08-30] PR #2021 round 2 (#546): the relayed Peppol buyer restriction now says the customer's org number must not be a personnummer (prepareParty('buyer') in lib/invoices/peppol-bis-billing.ts refuses it with BUYER_PARTICIPANT_IDENTIFIER_UNSUPPORTED, so an enskild firma CUSTOMER is refused, not only an enskild firma sender), Step 4 of the invoicing-rules workflow points at the Peppol section so a top-down reader never reaches the external-provider fallback first, the mark-sent recovery is scoped to the still-draft invoice in every text (INVOICE_MARK_SENT_REPAIR_REQUIRED leaves the invoice sent with the verifikat posted and a second mark-sent returns 409; the reviewer's proposed repair tool gnubok_link_invoice_to_voucher is the PAYMENT link and requires status sent/overdue/partially_paid, so no tool is named and the repair is left to support), and the verifikat parenthetical says "under faktureringsmetoden" (kontantmetod and defer_invoice_booking companies get none at issue). The guard test now also pins the two v1 route descriptions by reading the route source (apiskill:check only detects generated-vs-source drift, not a truth regression). The atom bump was seeded as a THIRD append-only migration (20260830101500, atom v9) rather than consolidating to one: the Supabase preview branch for the PR (xxnqggttsefleehmarjo) has applied both 20260829000100 and 20260829010000 per its schema_migrations, so deleting either would leave a remote with versions absent from the repo, the orphan class the migration rule forbids; all three seeds are idempotent upserts with the version guard, so prod applying them in sequence ends at v9. The generator's max-plus-one name (20260829010001) was renamed to 20260830101500 for the same reason as round 1 (newer than every file on origin/main and every sibling worktree; skills:check hashes content, the pg replay test globs the seed).
|
||||
|
||||
Reference in New Issue
Block a user