abb9f5868c
* feat(api): Phase 4 PR-1 — AP world (suppliers + supplier-invoices)
First of two Phase 4 PRs. Ships the public v1 AP-side verticals end-to-end,
mirroring the Phase 2 AR pattern (customers + invoices).
ENDPOINTS (13)
Suppliers:
GET /suppliers — cursor list + filters
GET /suppliers/{id} — detail, ?expand=supplier_invoices
POST /suppliers — idempotent, dry-runnable
PATCH /suppliers/{id} — idempotent, dry-runnable, can un-archive
DELETE /suppliers/{id} — soft-archive, refused on open SI
POST /suppliers/bulk-create — partial-success, max 50
Supplier invoices:
GET /supplier-invoices — cursor list + filters
GET /supplier-invoices/{id} — detail, ?expand=supplier,items,payments
POST /supplier-invoices — register + post registration JE
PATCH /supplier-invoices/{id} — registered-only
POST /supplier-invoices/{id}/approve — flip to approved
POST /supplier-invoices/{id}/mark-paid — book payment JE + flip status
POST /supplier-invoices/{id}/credit — issue kreditfaktura + reversing JE
No DELETE on supplier-invoices — withdrawal is via :credit (mirrors v1 invoices,
keeps both original AND credit note in the audit trail per BFL 5 kap 5 §).
STRICT-MODE V1
Carried forward from Phase 3 lessons:
- Any JE failure ABORTS before SI state mutation (no soft-fall / partial state).
Applies to register, mark-paid, and credit.
- checkPeriodLock() pre-check before every JE-emitting write — returns
structured PERIOD_LOCKED / SI_PAID_PERIOD_LOCKED / SI_CREDIT_PERIOD_LOCKED
instead of letting the DB trigger surface a generic 500.
- CAS-race orphan handling in mark-paid: if the SI status flips between
pre-flight and our update, the just-posted payment JE is stornoed via
reverseEntry() rather than left dangling (BFL 5 kap 5 §).
- Math.round monetary throughout. Half-öre epsilon on remaining_amount==0.
SCHEMA MIGRATION
`20260513150000_archived_at_for_customers_and_suppliers.sql`:
- Adds suppliers.archived_at (new — required for the soft-archive flow).
- Adds customers.archived_at + customers.vat_number_validated_at —
retroactively. The Phase 2 v1 customer routes (PR #451 / #452 / #460)
already reference both columns but no prior migration installed them in
production. This commit fixes that latent bug while we have the
migration open.
- Partial indexes on (company_id, created_at) WHERE archived_at IS NULL
keep the default-active list path cheap.
- is_active (legacy boolean) preserved on suppliers; v1 archive sets both
archived_at = now() AND is_active = false, un-archive flips both back
so the dashboard's "show only active" filters stay intact.
NEW ERROR CODES
SUPPLIER_HAS_INVOICES (409) — archive refused while open SI exists
SI_NOT_DRAFT (400) — update/delete refused on non-registered SI
GDPR ART.5(1)(c) DEFENSE-IN-DEPTH
SupplierType has no `individual` variant today, so org_number is always
Bolagsverket public-record data. The list endpoint still has the masking
hook (empty INDIVIDUAL_TYPES set) so a future natural-person supplier type
becomes a one-line change. Duplicate-org_number error responses NEVER echo
the submitted value — symmetric with customers.
SCOPES
13 new entries in V1_ENDPOINT_SCOPES under suppliers:read / suppliers:write.
TESTS
36 new integration cases across 2 suites:
- suppliers: list (incl. filter), get (incl. 404), create (happy + 23505 +
dry-run + missing-idempotency), patch (happy + empty body), delete
(archive + open-invoice refusal), bulk-create (partial-success + 501)
- supplier-invoices: list, get (incl. 404), create (happy accrual + supplier
404 + period-locked + strict-mode JE rollback + dry-run), patch
(registered-only), approve (happy + non-registered refusal), mark-paid
(happy + period-locked + already-paid + strict-mode abort), credit
(happy + already-credited + period-locked + dry-run)
Full suite green: 3333 passing (237 files). Build + lint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(api): PR #467 round-2 — Greptile P1/P2 fixes
Three real findings from Greptile inline review on the Phase 4 PR-1 commit.
P1 — mark-paid: storno orphan JE when SI update fails.
When the `.update()` after the JE post returned an `updateErr`, the route
logged + returned SI_PAID_FAILED without reversing the just-posted payment
JE. The CAS-race branch immediately below already proves journalEntryId is
in scope and reverseEntry takes it directly — the original comment about
"requires fetching the entry first" was wrong. Now both error branches
(the `updateErr` DB-failure path and the `!updated` CAS-race path) storno
via reverseEntry before returning, keeping the AP ledger consistent
(BFL 5 kap 5 §). Storno failure itself logs loudly and the error envelope
surfaces the journal_entry_id so manual reconciliation has a starting
point.
P1 — credit + register: capture JE link-update result, storno on failure.
Both supplier-invoices/route.ts (register) and supplier-invoices/[id]/
credit/route.ts back-fill registration_journal_entry_id on the freshly-
inserted SI/credit-note row, but were dropping the await result. A
transient DB error there silently left the row with registration_-
journal_entry_id=null even though the JE was live on the books — the POST
response looked correct (it returned the JE id from the local variable)
but every subsequent GET /supplier-invoices/{id} showed null. Both paths
now capture the link-update error, storno the orphan JE via reverseEntry,
then roll back the SI/credit-note row before returning SI_CREATE_FAILED
/ SI_CREDIT_FAILED with step='*_link'. Strict-mode atomicity restored.
P2 — mark-paid: dry-run paid_at format alignment.
Dry-run preview set `paid_at: paymentDate` (YYYY-MM-DD), but the live
`.update()` writes `new Date().toISOString()` (full UTC timestamp). A
caller validating both responses against the same regex would have been
caught by the mismatch. Dry-run now mirrors the live shape.
P2 — ensureInitialized() finding dismissed as a false positive:
lib/api/v1/with-api-v1.ts:52 already calls ensureInitialized() at module
load. Every v1 route imports withApiV1 from that module, so the side
effect runs on first import and caches. No existing v1 route (customers,
invoices, transactions) imports ensureInitialized() directly — the
pattern has been consistent across Phases 1-3 and the AP-world routes
follow it.
Tests + build green: 3333 passing across 237 files, AP suite 36/36.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(api): PR #467 round-3 — compliance swarm + swedish-compliance fixes
Both bots re-ran and converged on a set of substantive findings. Seven real
issues addressed; several recurring false positives + architectural
deferrals documented inline.
REAL FIXES (7)
1. credit: `remaining_amount` calc was nonsensical.
`Math.max(0, remaining_amount - total)` was always ≤ 0 (since
remaining ≤ total), forcing status to 'credited' regardless of paid
state — but only via the clamp, not the logic. Both swedish-compliance
and Compliance Swarm (OWASP V2.3 + SOC 2 PI1.3) caught this. A
kreditfaktura nullifies the AP obligation on the original (BFL 5 kap
5 §); refunds of already-paid amounts get a separate transaction.
`remaining_amount: 0` and `status: 'credited'` unconditionally.
2. supplier-invoices register: VAT rate whitelist.
`computeItemsAndTotals` accepted any float for `vat_rate`, silently
booking an unrecognised rate into the registration JE → momsdeklaration
Ruta 48 + INK2R. Now rejects with VALIDATION_ERROR (allowed_rates
echoed) unless the rate is in `{0, 0.06, 0.12, 0.25}` (ML 2 kap 1 §).
3. mark-paid: `exchange_rate_difference` is required for non-SEK accrual.
The pitfall docs warned about this but the code didn't enforce it. Without
it the payment JE doesn't book the FX delta to 3960/7960 and AP carries
a stranded 2440 balance after the bank line clears. Enforces with field-
level VALIDATION_ERROR; pass `exchange_rate_difference: 0` if there's
no rate movement.
4. suppliers PATCH: refuse on archived suppliers (BFL 7 kap 1 §).
An archived supplier's name/address backs historical verifikationer; a
post-archive PATCH would silently corrupt 7-year-retained räkenskaps-
information. The handler now fetches the current row, refuses identifying-
field updates when `archived_at IS NOT NULL`, and only permits the
un-archive PATCH (`archived_at: null`).
5. supplier-invoices register: smart vat_treatment / reverse_charge default.
The previous default of `'standard_25'` regardless of supplier_type left
EU/non-EU supplier rows with metadata that didn't match the actual booking
path (which uses `reverse_charge`). Now derives both fields from
`supplier.supplier_type` when the caller omits them: foreign suppliers
default to `reverse_charge: true` + `vat_treatment: 'reverse_charge'`.
Explicit body values still win.
6. reverseEntry: static import (SOC 2 CC8.1).
Replaced the three dynamic `await import('@/lib/bookkeeping/engine')`
calls in orphan-storno error branches with a top-level static import.
The dependency is now visible to SCA / tree-shake / static analysis.
7. Add `userId: ctx.userId` to every storno-failure log context (OWASP
V16.1). The CAS-race + linkErr branches now consistently include the
actor identity for security-relevant audit events.
TESTS (+6 new)
- register: rejects non-Swedish vat_rate (whitelist) → 400
- register: defaults reverse_charge=true + vat_treatment='reverse_charge'
for eu_business suppliers
- mark-paid: requires exchange_rate_difference for non-SEK accrual → 400
- mark-paid: passes when exchange_rate_difference is explicitly 0
- suppliers PATCH: refuses identifying-field edit when archived_at IS NOT NULL
- suppliers PATCH: allows un-archive (archived_at: null) flip
AP suite 42/42 (was 36). Full suite 3339/3339 green (was 3333).
DISMISSED WITH RATIONALE
- swedish-compliance "credit-note amounts should be negative" — false read
of the engine. `createSupplierCreditNoteEntry` calls `Math.abs()` on
item amounts (line 421) and posts a reversing JE; the SI row carries
positive amounts + `is_credit_note=true` as a deliberate data-model
decision. Negating would break parity with the dashboard and the
internal AP-ledger reporting.
- OWASP V8.2.1 cross-tenant via path — recurring false positive across
Phases 2-4. `withApiV1` (line ~340-350) verifies `company_members`
membership BEFORE setting `ctx.companyId` from the URL.
- OWASP V8.2.1 supplier_invoice_items company_id filter in
rollbackCreditNote — the table has no `company_id` column;
cross-tenant protection comes from RLS + the parent
supplier_invoice_id scoping.
- OWASP V4.5 PATCH allowlist schema-derivation — known architectural
deferral; centralising the field list against a Zod `.pick()` is a
separate refactor.
- GDPR Art.5(1)(f) log/event field identifiers — RoPA / log-pseudonymisation
is an org-wide privacy-eng concern, not a per-route fix.
- ISO 27001 A.8.15/A.8.16 non-blocking inserts — `supplier_invoice_payments`
insert + event emit failures stay at warn-level for v1 to mirror the
dashboard internal route. Promoting to error escalations + DLQ is a
cross-cutting reliability project, not a route patch.
- SOC 2 CC6.3 segregation-of-duties — v1's API-key scope IS the boundary
by design. Role-based separation between register / approve / pay is a
v1.x feature, not a v1 surface bug.
- swedish-compliance reverse-charge gating in credit — the engine
(`createSupplierCreditNoteEntry`) already gates the 2647/2645 reversal
on `creditNote.reverse_charge` (line 437). Mirrors the registration
engine.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(api): PR #467 round-4 — BFL 5 kap 5 § + remaining compliance fixes
Compliance bots re-ran on round-3 (Compliance Swarm 13→12 findings,
Swedish-compliance fresh re-read). Five real issues addressed; the rest
are recurring false positives or architectural deferrals carried over
from earlier rounds.
REAL FIXES (5)
1. mark-paid: future payment_date rejected at the schema layer.
BFL 5 kap 2 § requires bokföring to follow real cash movement;
payment_date > today is a scheduling artefact, not an affärshändelse.
Returns 400 VALIDATION_ERROR before the JE engine runs.
2. credit: drop user_id from SI_FULL_COLUMNS (GDPR Art.25).
The original SI's `user_id` (its historical creator) is never used
in the credit flow — the new credit-note row uses ctx.userId (the
actor performing the credit). Don't fetch what you don't need.
Also drops company_id from the select since it's already filtered.
3. supplier-invoices register: reverse-charge cross-field VAT check.
For reverse-charge invoices the Swedish supplier doesn't charge VAT,
the buyer self-assesses (ML 1 kap 2§ p.4b / 16 kap 6 § / 16 kap 13 §).
If `reverse_charge=true` and ANY item has `vat_rate != 0`, return
VALIDATION_ERROR — otherwise the engine would book ingående moms in
Ruta 30 / 48 (BAS 2614 / 2645 / 2641) for an invoice that has no VAT
to deduct.
4. rollbackSupplierInvoice + rollbackCreditNote: soft-mark, not delete.
BFL 5 kap 5 § — rättelse av bokföringspost måste vara dokumenterad
så att både den ursprungliga och den korrigerade noteringen är
synliga. Hard-deleting the SI row on a mid-write failure destroys
räkenskapsinformation even when the JE side (if any) is preserved
via storno. Both rollback paths now UPDATE status='reversed' +
reversed_at=now() — the SupplierInvoiceStatus enum already has
'reversed' for exactly this case ("credit note whose journal entry
was storno-reversed via Ångra kreditering" per the type comment).
Trade-off: a retry with the same supplier_invoice_number will hit
the unique-index conflict, so the caller picks a fresh number.
TESTS (+2 new)
- register: rejects reverse_charge=true with non-zero item vat_rate
- mark-paid: rejects future payment_date
Pre-existing eu_business reverse_charge test updated: item vat_rate
flipped from 0.25 → 0 to remain valid under the new cross-field check.
AP suite 44/44 (was 42). Full suite 3341/3341 green (was 3339).
DISMISSED (recurring or architectural)
- OWASP V8.2.1 cross-tenant via path — recurring false positive across
Phase 2-4. withApiV1 verifies company_members membership BEFORE
setting ctx.companyId from the URL.
- ISO A.8.3 approve-route TOCTOU — already mitigated. The UPDATE has
`.eq('status', 'registered')` as a race guard; the pre-flight is for
ergonomic error messages, not security.
- SOC 2 PI1.3 floating-point — project-wide convention is
Math.round(x * 100) / 100 per CLAUDE.md. Diverging in one route would
create a parity bug with the bookkeeping engine + dashboard. Settled.
- SOC 2 CC7.3 storno-failure alerting / ISO A.8.15 audit-log on success
/ SOC 2 CC6.1 test-fixture key / OWASP V2.2 status state-machine /
V1.2.5 dynamic select-clause / V16 audit-log silent-failure / Art.25
banking-field expand — all architectural deferrals that fit the
webhook-hardening + scope-redesign work in Phase 6, not the v1 PR.
- swedish-compliance "credit-note original-number reference" — the
`credited_invoice_id` FK is the structured back-reference; the
document-rendering layer surfaces the original `supplier_invoice_-
number` from there. Not a v1 surface bug.
- swedish-compliance "cash-basis credit-note vat_amount" — engine
behaviour mirrored from the dashboard. Engine-layer audit, separate
effort.
- swedish-compliance "active-supplier mutability broader than
archived_at" — solving this requires snapshotting supplier identity
onto each supplier_invoices row at registration (schema migration).
Deeper architectural decision; tracking for Phase 4 follow-up
alongside the journal-entries vertical.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(api): PR #467 round-5 — strict schema + vat_treatment normalisation +
narrow BFL archive lock
Compliance bots re-ran on round-4. Most findings are recurring (V8.2.1
cross-tenant, PI1.3 floating-point, CC6.3 SoD) or the classic oscillation
pattern from the Phase 3 lessons: this round's Art.5(1)(f) flags userId in
storno error logs as PII exposure — but last round's V16.1 demanded I ADD
userId for audit attribution. Staying with audit attribution; the bot can
pick a side.
Three substantive findings addressed.
REAL FIXES (3)
1. V4.5 mass-assignment defense-in-depth on PATCH /supplier-invoices/{id}.
The shared `UpdateSupplierInvoiceSchema` is consumed by the dashboard
too, where Zod's default key-stripping is acceptable. The v1 route now
wraps it in `V1PatchSupplierInvoiceSchema = UpdateSupplierInvoiceSchema
.strict()` so any unknown key (e.g. `status`, `company_id`, `user_id`)
returns 400 VALIDATION_ERROR instead of being silently dropped — even
if the iteration allowlist downstream is later relaxed.
2. vat_treatment normalisation when reverse_charge resolves true.
Caller could previously pass `vat_treatment: 'standard_25'` explicitly
on an eu_business supplier, and the supplier-type-driven default would
set `reverse_charge: true` while the metadata stayed as 'standard_25'.
The engine books via the boolean (so JE is correct) but a downstream
momsdeklaration / audit export reading `vat_treatment` would mis-
classify. Resolution order is now: reverse_charge first, then
vat_treatment forced to 'reverse_charge' if true; explicit overrides
only stick when they agree with the resolved boolean.
3. Narrow archived-supplier PATCH lock to identifying fields only.
The round-4 blanket lock on archived suppliers was too broad: BFL
7 kap 1 § protects räkenskapsinformation — the fields verifikationer
reference through the supplier join — but not internal notes or
payment-config metadata. The check now only refuses PATCHes that touch
{name, supplier_type, org_number, vat_number, address_*, banking_*}.
Notes, default_payment_terms, default_expense_account, default_currency,
email, and phone remain editable on archived rows.
TESTS (+3 new)
- PATCH /supplier-invoices/{id}: rejects unknown body keys (strict schema)
- POST /supplier-invoices: explicit vat_treatment='standard_25' is
overridden when supplier_type drives reverse_charge=true
- PATCH /suppliers/{id}: allows notes edit on archived supplier (BFL
narrow scope)
AP suite 47/47 (was 44). Full suite 3344/3344 green (was 3341).
DISMISSED (recurring / settled / oscillating)
- OWASP V8.2.1 cross-tenant via path — recurring false positive 4 rounds
running. withApiV1 verifies company_members membership BEFORE setting
ctx.companyId from the URL.
- GDPR Art.5(1)(f) userId in error logs — direct contradiction of
round-3's OWASP V16.1 finding which demanded userId be ADDED for audit
attribution. Phase 3 lessons document this oscillation pattern
("swedish-compliance / compliance-swarm oscillate between rounds")
and the correct response is to stay with the more security-positive
position. Keeping userId on storno-failure logs for ledger-integrity
attribution.
- SOC 2 CC6.3 segregation-of-duties — same as round-3. v1 design uses
API-key scope as the boundary; role-based actor separation is Phase 6
webhook + auth work.
- SOC 2 CC6.1 null-userId guard — redundant. withApiV1 short-circuits
with 401 UNAUTHORIZED before invoking the handler when API-key
validation fails (which is the only path that could leave ctx.userId
unset).
- SOC 2 CC7.2 storno-failure alerting — architectural; webhook-bus +
dead-letter is Phase 6 territory.
- SOC 2 / OWASP PI1.3 / V2.3 floating-point — project-wide convention
per CLAUDE.md; the engine, dashboard, and v1 all use Math.round(x*100)/100.
- ISO 27001 A.8.33 test-fixture financial amounts — synthetic UUIDs +
NODE_ENV=test guard already in place; "TEST-only" sentinel amounts
would be cosmetic.
- OWASP V16.1 eventBus failure retry / DLQ — Phase 6 webhook hardening.
- swedish-compliance arrival_number gap risk — acknowledged in commit,
bot itself says "no action required"; supplier_invoice_number retry
behavior already in the rollback-comment doc.
- swedish-compliance vat_code cross-field — engine derives JE shape from
`invoice.reverse_charge` (boolean), ignores item vat_code in the RC
path. No surface-layer leak.
- swedish-compliance credit-note FX at today's rate — bot's reasoning
inverted. The credit note REVERSES the original AP obligation; to net
2440 to zero across the original-registration JE + credit-note JE, the
SEK amounts MUST be copied from the original. FX rate at today's date
applies at the bank-refund transaction side, not the credit-note
registration.
- swedish-compliance KREDIT- prefix — dashboard parity. The
`is_credit_note` + `credited_invoice_id` flags are the structured
back-references; the prefix is cosmetic on the human-readable number.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(api): PR #467 round-6 — overpayment guard + two-phase rollback +
SI_FULL_COLUMNS minimisation
Compliance Swarm trended down 12→9 findings, Swedish-compliance 6→5.
Three substantive items addressed; the rest are recurring false positives
or the userId-in-logs oscillation that round-5 already settled.
REAL FIXES (3)
1. mark-paid: reject overpayment up front (Compliance Swarm V2.3).
Previously `Math.max(0, remaining - payment)` silently truncated an
overpayment to a zero remaining_amount, while the JE engine booked the
full payment_amount against 2440 — leaving an unaccounted overpayment
on the AP ledger. Now refuses with VALIDATION_ERROR when
`payment_amount > remaining_amount + 0.005` (half-öre tolerance for
FX-rounding artefacts). Recovery hint points at :credit for
over-billing and the transactions endpoints for refunds.
2. credit: trim SI_FULL_COLUMNS to fields actually read (Art.25(1)).
The credit handler never reads notes, paid_at, payment_journal_entry_id,
transaction_id, document_id, payment_reference, paid_amount,
delivery_date, received_date, reversed_at, created_at, updated_at,
exchange_rate_date, due_date — but the projection was fetching them
all. SEK-conversion fields (subtotal_sek / vat_amount_sek / total_sek)
ARE read (copied onto the credit-note row so the 2440 reversal nets),
so they stay. Continues the round-4 user_id / company_id drop.
3. Two-phase soft-rollback (Swedish-compliance, BFL 5 kap 5 §).
The bot caught a real misapplication: BFL 5:5 only kicks in once a
verifikation has been COMMITTED. Pre-JE failures (items_insert,
engine returning null because no fiscal period covers the date) are
failed insertions, not bokföringsposter. Marking those rows
`status='reversed'` with a null registration_journal_entry_id creates
a dangling räkenskapsinformation entry that's harder to audit than a
clean removal. Both rollback helpers now take a `journalEntryPosted`
flag: pre-JE failures hard-delete (rows + items), post-JE failures
keep the round-4 soft-mark + reversed_at behaviour. Call sites tagged
per failure reason:
items_insert → false (hard-delete)
no_fiscal_period → false (hard-delete; engine returned null pre-write)
registration_je → true (conservative; engine throw could be post-commit)
je_link_failed → true (JE posted + already stornoed above)
credit items_insert → false
credit no_fiscal_period → false
credit_journal_entry → true
credit_race → true
TESTS (+1 new)
- mark-paid: rejects payment_amount > remaining_amount with VALIDATION_ERROR
(no JE engine call)
AP suite 48/48 (was 47). Full suite 3345/3345 green (was 3344).
DISMISSED (with rationale)
- OWASP V8.2.1 cross-tenant via path — recurring across 5 rounds.
withApiV1 verifies company_members membership BEFORE setting
ctx.companyId from the URL. Fix-once decision in the wrapper, not a
per-route concern.
- OWASP V4.5 strict schema (re-verification) — round-5 added
V1PatchSupplierInvoiceSchema = UpdateSupplierInvoiceSchema.strict() +
a test asserting {"status": "approved"} is rejected. The bot is
re-flagging because it can't see the upstream schema in the diff;
manually verified: UpdateSupplierInvoiceSchema only contains
{supplier_invoice_number, invoice_date, due_date, delivery_date,
payment_reference, notes}. No status / company_id / user_id field.
- GDPR Art.5(1)(f) userId in logs — same oscillation as round-4. Last
round V16.1 demanded userId be ADDED for audit attribution; this
round Art.5(1)(f) wants it REMOVED. Staying with audit attribution
per the Phase 3 lessons doc's oscillation guidance.
- OWASP V16.1 / ISO A.8.15 / SOC 2 CC7.2 SIEM alerting on storno
failure — architectural; Phase 6 webhook hardening.
- GDPR Art.25(2) supplier-expand banking fields default-on — same as
round-4. A scope split (suppliers:read:sensitive) is a v1.x scope
refactor, not a single-route patch.
- swedish-compliance VAT 0.06 date-aware validation (livsmedel 1 April
2026) — needs livsmedel BAS classification (which BAS codes signal
food) and date-aware lookup tables. Engine-layer concern; not
achievable without engine changes. Documenting the 6% rate's temporary
nature in the comment was the smaller fix already shipped in round-3.
- swedish-compliance SI_RESPONSE_COLUMNS missing reverse_charge — FALSE
ALARM. `reverse_charge` IS present in the projection (line 264 of
supplier-invoices/route.ts); the engine receives it correctly.
- swedish-compliance KREDIT- prefix — dashboard parity, dismissed
rounds 3-5. The `is_credit_note` + `credited_invoice_id` flags are
the structured back-references.
- swedish-compliance cash-basis credit-note ingående moms timing
(ML 13 kap 27 §) — legitimate gap but engine-layer. The
createSupplierCreditNoteEntry engine function handles accrual only;
adding a cash-basis-already-paid branch would change engine
semantics, divering from the dashboard. Tracking as a Phase 4 engine
follow-up, not a v1 surface bug.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>