Arkivplats and Koppla filer are enabled for integration 39254 in the Fortnox
Developer Portal, so FORTNOX_DOCUMENT_SCOPES_APPROVED goes true and the opt-in
underlag reconnect starts asking for archive and connectfile. The ordinary
connect is untouched, so no customer is put in front of an Arkivplats licence
to connect at all.
This is the half that actually delivers the files. #1761 only stopped the loop
that told people to grant a permission we never requested; until now no Fortnox
token in production has ever carried these scopes, and no voucher attachment has
ever imported (166 companies, 24 consents since the feature shipped).
Set the flag back to false if the portal ever loses the scopes: authorize then
rejects with invalid_scope before login, which now costs the underlag flow
rather than every Fortnox connection.
Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Fortnox document import needs the archive and connectfile scopes, which the
registered Fortnox app does not have. Since #1549 pulled them out of the connect
request (they broke every connect with invalid_scope before login), every
attachment call fails and the user was told "Koppla om Fortnox och godkann
behorigheterna", under a button that reruns an authorize URL still not asking
for those scopes. Klura AB followed that loop four times and bought the Fortnox
Arkiv module trying to satisfy it. Prod evidence: no Fortnox attachment has ever
imported, across 166 companies and 24 consents since the feature shipped, and no
live token carries the scopes.
The error and the scope list now derive from one flag,
FORTNOX_DOCUMENT_SCOPES_APPROVED. While it is false a permission failure maps to
a new PROVIDER_DOCUMENT_SCOPES_UNAVAILABLE, which says the permission is missing
on our side, that reconnecting will not help, and that the rest of the migration
came through; the card offers no button, because no user action can succeed.
The attachment scopes also become an opt-in consent rather than part of every
connect. Fortnox derives customer licence requirements from what an integration
requests, so asking everyone for Arkivplats would put a licence in front of
customers who never import a receipt; and keeping it off the default connect
caps the blast radius of a wrong portal registration at the underlag flow rather
than every Fortnox connection. buildFortnoxAuthUrl already took per-call scopes,
provider-client simply never passed any, so this threads documentScopes from
that one button through /connect into the authorize URL.
A document consent is always a superset of an ordinary one: the callback
overwrites the consent's tokens in place, so a narrower grant would revoke the
migration's own ledger access. Pinned by a test that holds either way the flag
is set, alongside one for the 400-with-behorighet answer that six companies hit
between 08-13 and 08-19 and saw only a generic retry for.
Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Batch from a real migration walkthrough (Fortnox -> Accounted, 2026-08-20):
- Årsredovisning: the "Låst version" select was empty with no explanation
because the only version was a draft and "Lås version för underskrift"
is disabled while the four Lagstadgade upplysningar checkboxes and the
content confirmation count as blockers. The select is now disabled with a
hint that names the blocker count and links to Fullständighetskontroll,
the four AR-NOTE-*-UNCONFIRMED issues carry remediation text, the lock
button explains why it is grey, and "Markera som signerad" says what it
still needs (locked version, bevisreferens, date).
- Moderföretagets org.nr accepts a foreign registration identifier
(CHE-123.456.789, HRB 12345, 923 609 016); personnummer shapes stay out.
- Fortnox underlag discovery: log status, body and Fortnox's message on
failure, show the message in the UI, treat a 400 with behörighet/scope
text as scopes-required, and fall back to an unfiltered
voucherfileconnections list when the financialyear filter answers 400.
- Kontomapping: the Momskod column had min-w only; table-fixed collapsed it
and its selects overflowed into Konfidens. Real w-72 now.
- SIE import warnings pluralise correctly for one skipped voucher; the
Verifikationsserie option says the source series is preserved.
Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
PR #1541 added archive and connectfile to the Fortnox DEFAULT_SCOPES for
the voucher attachment import, but the registered Fortnox app does not
have those scopes approved in the Fortnox Developer Portal. Fortnox
rejects the authorize request with invalid_scope before login, which
broke every Fortnox connect in production within minutes of the deploy
(verified in Vercel runtime logs).
Remove the two scopes from the connect request; the attachment import
logic from #1541 stays fully intact and already degrades gracefully:
a 403 becomes PROVIDER_DOCUMENT_SCOPES_REQUIRED with a reconnect
follow-up card. Re-add the scopes once the portal registration has them
approved.
Also add charset=utf-8 to the OAuth callback HTML responses: without it
browsers render the Swedish error text as Latin-1 mojibake.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(customers): carry contact person and invoice copy recipients through migration
Extends the arcim-migration entity mapper, Fortnox provider mapper, canonical
DTOs, customer APIs (web + v1) and invoice send flows so contact person and
customer-level invoice CC/BCC addresses survive provider migrations. NULL
means unconfigured and empty means an explicit clear, so re-syncs enrich
legacy gaps without resurrecting deliberately removed values. Fortnox fixed
assets are split into a dedicated follow-up issue.
Fixes#1345
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore(db): bump customer metadata migration past pack-slug version
Main already contains 20260803230000; keep new versions strictly newest so
Supabase branching applies them in order.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(customers): complete Customer type consumers and make enrichment payload resolvable
The preview-pdf mock customer and the makeCustomer fixture now carry the
three new metadata fields, fixing the type-check failure in Build (zero
extensions) and Vercel.
The enrichment update in the migration orchestrator now spells its payload
as an object literal typed CustomerMetadataEnrichment (absent keys drop at
serialization), so the phantom-column guard resolves the columns instead of
counting another unresolvable dynamic payload past its ceiling. The cc/bcc
guards also verify element types instead of casting.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Em dashes (—) and en dashes (–) had spread across comments, docs, tests,
and a few UI strings, reading as AI-generated boilerplate rather than
house style. Replaced each with punctuation matching its context: colon
for explanatory clauses, comma for asides, plain hyphen for numeric/legal
ranges (e.g. "21-23§"), "to"/"till" for date ranges, parentheses for
paired-dash asides. messages/en.json and messages/sv.json were fixed by
hand together to keep sv/en in sync.
Left untouched where the dash is the functional subject rather than
decorative punctuation: date-range-parser.ts's separator regex,
charset-repair.ts's CP1252 byte-mapping table (and its test), the SIE
encoding mojibake docs, generic-csv.ts's minus-sign normalizer, the
agent system-prompt files that already instruct against em dashes, and
a golden iXBRL test fixture compared byte-for-byte.
Also fixes two bugs surfaced along the way: an off-by-one in
ApiKeysPanel's scope-label split (a leftover from an earlier partial
pass), and a charset-repair test that had lost the literal en-dash it
exists to verify.
Regenerated the agent atom seed migration (skills:generate) since 27
SKILL.md files changed. Added a CLAUDE.md rule against em/en dashes,
with an explicit carve-out for the functional-dash cases above.
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* 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>