Bug/customer invoice bug (#628)

* fix(supplier-invoices): self-assess reverse-charge VAT + link payments to vouchers

Reverse-charge supplier invoices now carry a per-item reverse_charge_rate (0.06/0.12/0.25). Under omvänd skattskyldighet the supplier charges 0% VAT, so the line vat_rate stays 0 and the buyer self-assesses fiktiv moms at the statutory rate. Centralizes rate resolution (resolveReverseChargeRate) and the ruta 20-24 basis-account guard (isReverseChargeBasisAccount) in vat-entries so the booking engine and review-dialog preview can no longer drift.

Adds the link_supplier_invoice_voucher pending operation: mark a leverantorsfaktura paid by linking an existing posted verifikat that debits 2440, with no new journal entry. Exposes find-candidates/link MCP tools and the bulk-reconcile helper, scoped under suppliers:read/write.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(vat): report yearly VAT over the rakenskapsar, not the calendar year

Annual VAT (helarsmoms) is filed per beskattningsar/rakenskapsar (SFL 26 kap), which can be extended or shortened up to 18 months. The previous Jan-Dec calendar span silently dropped part of an extended first year. calculateVatDeclaration now accepts a fiscalPeriodId and resolves the period's actual bounds for yearly; monthly/quarterly stay calendar. The reports UI passes the selected fiscal period, defaults the periodicity from the company's moms_period setting, and carries the period into the ruta drill-down. full-archive export threads the period id through too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(migration): resolve supplier invoice status from payment amounts

The provider's lifecycle status and its payment status are computed independently upstream and can contradict each other (e.g. a Fortnox invoice marked booked but fully paid). Both the arcim entity-mapper and the Fortnox mapper now let payment state win: fully paid -> paid, partial -> partially_paid, otherwise the mapped lifecycle status, with credit notes forced terminal. Balance is compared numerically (never strict === 0) so float drift or a residual ore resolves cleanly, and an absent Balance is treated as unpaid.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(enable-banking): only ingest booked transactions to stop re-import drift

Pending entries are skipped during sync: a pending row is unstable across syncs (a later 'synka nu' returns it still pending or finally booked, often with a different effective date). Because both the dedup external_id and the content-dedup key are date-derived, that drift minted a new id and re-imported a transaction that already existed - observed in production as the same amount+description landing twice with different dates. Gating the import set on a stable booking_date removes the drift at the source and leaves booked rows' ids byte-identical.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(gitignore): ignore local SIE test fixtures

tests/fixtures/sie/ may contain real or scrubbed company data and must never be committed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(invoice): handle errors during registration journal entry creation and ensure invoice rollback
feat(tests): add test for reverse charge rate handling on supplier invoice line items
feat(fortnox): ensure paid status reflects zero balance for fully paid invoices
chore(migrations): add reverse_charge_rate to supplier_invoice_items and backfill link_supplier_invoice_voucher

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mattsson
2026-06-02 13:25:48 +02:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 358c25094d
commit f6ee0c2a82
43 changed files with 2226 additions and 182 deletions
@@ -0,0 +1,61 @@
-- Backfill `link_supplier_invoice_voucher` into the
-- pending_operations.operation_type CHECK constraint.
--
-- The supplier-side mirror of `link_invoice_voucher` (added in
-- 20260528120001). The new MCP tool gnubok_link_supplier_invoice_to_voucher
-- stages a `link_supplier_invoice_voucher` pending operation, which is then
-- committed by commitLinkSupplierInvoiceVoucher via the
-- link_supplier_invoice_to_voucher RPC (20260529130000 / 20260529140000).
-- The op also has a risk-tier entry ('medium', reversible — no journal entry
-- is created or modified).
--
-- Without this migration any INSERT staged by the new tool would be rejected
-- with a constraint violation, silently blocking the supplier_invoice_payments
-- audit-trail row required by BFL 5 kap 6–7§ (every affärshändelse must have a
-- verifikation with a logged payment match).
ALTER TABLE public.pending_operations
DROP CONSTRAINT IF EXISTS pending_operations_operation_type_check;
ALTER TABLE public.pending_operations
ADD CONSTRAINT pending_operations_operation_type_check
CHECK (operation_type IN (
'categorize_transaction',
'create_customer',
'create_invoice',
'mark_invoice_paid',
'send_invoice',
'mark_invoice_sent',
'match_transaction_invoice',
'close_period',
'lock_period',
'unlock_period',
'set_opening_balances',
'run_year_end',
'run_currency_revaluation',
'import_sie',
'explain_voucher_gap',
'uncategorize_transaction',
'approve_supplier_invoice',
'credit_supplier_invoice',
'credit_invoice',
'convert_invoice',
'create_transaction',
'attach_document_to_transaction',
'create_voucher',
'correct_entry',
'reverse_entry',
'create_supplier',
'create_supplier_invoice_from_inbox',
'post_annual_depreciation',
'link_invoice_voucher',
'undo_sie_import',
'match_batch_allocate',
'bulk_book_transactions',
'create_salary_run',
'generate_agi',
'link_transaction_journal_entry',
'link_supplier_invoice_voucher' -- supplier-side mirror of link_invoice_voucher
));
NOTIFY pgrst, 'reload schema';
@@ -0,0 +1,26 @@
-- Add reverse_charge_rate to supplier_invoice_items.
--
-- For omvänd skattskyldighet (reverse charge, ML 16 kap) the EU/non-EU or
-- domestic-RC supplier charges no VAT, so the line's own vat_rate is 0 — the
-- v1 supplier-invoice API even mandates vat_rate=0 on every RC line. The buyer
-- must nonetheless self-assess BOTH output and input VAT at the Swedish
-- statutory rate that would apply to the service domestically: 25% under
-- huvudregeln for EU services (ML 6 kap 34 §), or 12%/6% for reduced-rated
-- services. That self-assessed rate is conceptually distinct from "what the
-- supplier charged" (0%), so it gets its own column instead of overloading
-- vat_rate — which previously caused the engine to skip the fiktiv-moms lines
-- (2614/2624/2634 + 2645/2647) and basbeloppsrader (44xx/45xx) entirely,
-- understating momsdeklaration ruta 20-24 / 30-32 / 48.
--
-- NULL = not a reverse-charge line (the booking engine then falls back to a
-- positive vat_rate if present, else the 25% huvudregel default).
-- 0.06 / 0.12 / 0.25 = explicit self-assessed rate (set by the UI picker).
ALTER TABLE supplier_invoice_items
ADD COLUMN IF NOT EXISTS reverse_charge_rate numeric
CHECK (reverse_charge_rate IS NULL OR reverse_charge_rate IN (0.06, 0.12, 0.25));
COMMENT ON COLUMN supplier_invoice_items.reverse_charge_rate IS
'Self-assessed VAT rate for omvänd skattskyldighet (decimal 0.06/0.12/0.25). NULL for non-RC lines. The line vat_rate stays 0 (supplier charges no VAT); this rate drives fiktiv moms (2614/2624/2634 + 2645/2647) and basbelopp (44xx/45xx) booking — see lib/bookkeeping/supplier-invoice-entries.ts.';
NOTIFY pgrst, 'reload schema';