fix(skatteverket): request the AGI kvittens scope and stop misdiagnosing the 403 (#1596)

* fix(skatteverket): request the AGI kvittens scope, not just the inlamning one

AGI is backed by two SKV APIs and each needs its own scope, but DEFAULT_SCOPES
only carried `agd`. That covers arbetsgivardeklaration/inlamning (POST underlag,
kontrollresultat, spara, skapaGranskningsunderlag), so a filing submits and
signs perfectly. The kvittens read lives on hanteraredovisningsperiod, which
requires `agdredovisningperiod`, so the flow died on its very last step with
403 {"error": "The required scopes are not authorized"}.

Confirmed against production: the same APIGW client id and secret reach the
bearer check on both APIs (401 "Token has been revoked" from each with a bogus
token), proving both subscriptions exist and the gateway contract was never the
problem, and SKV's application registration lists `agd`, `agdredovisningperiod`
and `momsdeklaration` as three separate scopes.

The body is identical to the APIGW subscription gap of #973, which is why
api-client's classification cannot distinguish the two from the string alone;
that split still needs the gateway-side check, so it is left as is.

Note the spelling `agdredovisningperiod`: no genitive s, exactly as SKV
registers it. Pinned with a scope-set regression test, since this is the third
time a scope has gone missing (#431 removed `ska` the same way) and the damage
is always invisible until a real filing fails.

The AGIPanel missing-scope banner now checks both scopes. It only looked for
`agd`, so the token shape that actually hurts, one that gets all the way
through signing before failing, produced no warning at all.

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

* fix(skatteverket): stop blaming the APIGW subscription for a missing token scope

`{"error": "The required scopes are not authorized"}` has two causes and the
gateway never says which: our APIGW client has no subscription for the API
(#973), or the token lacks the scope that API requires. The message named only
the first, so a real production filing spent its debugging time in
Utvecklarportalen while the actual fix was a scope the code never requested.

The message now names both knobs, and says WHICH service refused. That last
part is what was really missing: the sibling inlamning API kept working
throughout, so "Skatteverket denied the call" pointed at nothing. Ruling the
two apart still needs the out-of-band test (call the API with a deliberately
invalid bearer and the same Client_Id: a subscription gap fails at the gateway
with this body, a scope gap reaches the bearer check and answers 401), which is
now documented at the predicate instead of living in someone's memory.

Classification is deliberately unchanged. ACCESS_DENIED stays out of
RECONSENT_ERROR_CODES: the scope case does need a reconnect, but only after the
scope exists, so an automatic reconsent would rebuild the self-perpetuating
banner of #1155.

This reverses one specific decision from #1250, which removed the
SKATTEVERKET_SYSTEM_SCOPES mention on the reasoning that the gateway rather
than the scope list had refused. That reasoning assumed the body could tell
them apart. It cannot. The test asserting the omission is inverted, with the
reason recorded.

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

* fix(salary): make the AGI kvittens-scope notice dismissible

The two AGI scopes get different treatments in AGIPanel: a token
without agd keeps the hard reconnect nudge, but a token missing only
agdredovisningperiod gets its own softer, dismissible notice. Every
existing hosted token lacks the new scope, and until Skatteverket's
application registration carries it a reconnect mints the same grant
again (SKV silently drops unregistered scope names), so a hard
reconnect demand would be the #1010 un-clearable banner, with each
futile attempt costing a BankID round-trip. Dismissal persists per
granted scope string: a futile reconnect stays quiet, a widened grant
re-evaluates from scratch.

Also asserts the shared prenumeration/scope message and the refused
API path on the 401 contract test, matching the 403 test (review nit).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Emil <emilmattsson14@gmail.com>

* fix(salary): point the kvittens-scope notice at Skatteverket's own e-service

The compliance review flagged that dismissing the notice could leave a
filer without a software-side path to the receipt. Retrieval never
depended on the notice (the kvittens cron retries server-side and the
period's status row stays unconfirmed until a kvittens lands), but the
copy now also names the fallback that always works: verifying the
receipt in Skatteverket's Arbetsgivardeklaration e-service.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Emil <emilmattsson14@gmail.com>

* fix(salary): use the ladder radius on the kvittens notice

The radius ladder guard landed on main after this notice was written;
rounded-md is dead vocabulary, bordered boxes are rounded-lg.

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

* fix(salary): scope the kvittens-notice dismissal to the employer

The localStorage key carried only the granted scope string, so on a
shared browser one company's dismissal hid the notice for every other
company with an identical grant. Skatteverket tokens are per company,
so the key now includes arbetsgivare alongside the scope string; the
same-grant reconnect behavior per company is unchanged.

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

---------

Signed-off-by: Emil <emilmattsson14@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Emil <emilmattsson14@gmail.com>
This commit is contained in:
bjornbergenheim
2026-08-14 12:01:02 +02:00
committed by GitHub
co-authored by Claude Fable 5 Emil
parent 315b94019b
commit 5984652e47
8 changed files with 278 additions and 62 deletions
+3
View File
@@ -946,8 +946,11 @@ One line per decision: `[YYYY-MM-DD] <decision>: <why>`. Appended by agents and
[2026-08-13] Kontantmetoden year-end VAT supersedes the 2026-08-06 VAT-reporting premise: BAS 2618/2628/2638 and 2648 feed the final declaration, reverse-charge purchases include both VAT sides and their basis, and only the mechanical day-one reversal is excluded from later VAT periods. Skatteverket requires unpaid invoice VAT in the final period and warns against reporting it twice after year end.
[2026-08-13] Per-account VAT treatment is class-aware and explicit values override the static BAS mapping; SIE #SRU and #KTYP never supply it because they encode tax-return fields and account class, not momsdeklaration treatment.
[2026-08-13] Per-account VAT treatment is class-aware; explicit values extend custom accounts while canonical accounts keep their static BAS momsdeklaration mapping. SIE #SRU and #KTYP never supply it because they encode tax-return fields and account class, not momsdeklaration treatment. VMB carries no default account rate because its VAT base is the margin, not gross sales.
[2026-08-13] AGI needs two SKV scopes, not one: added `agdredovisningperiod` (hanteraredovisningsperiod: kvittenser/las) alongside `agd` (inlamning) in the per-flow DEFAULT_SCOPES, after a real prod filing signed fine and then 403'd on "Hamta kvittens". Pinned with a scope-set regression test since this is the third scope-cleanup casualty; the AGIPanel missing-scope banner now checks both, because a token with `agd` alone fails only at the last step.
[2026-08-13] Reversed the #1250 rule that the APIGW "required scopes are not authorized" body must name only the subscription: production proved the same body also fires when the TOKEN lacks the scope (AGI kvittens needed agdredovisningperiod), so naming one cause and hiding the other presents a coin flip as a diagnosis. The message now names both knobs plus the refused API path, and system mode points at SKATTEVERKET_SYSTEM_SCOPES again. Classification is unchanged (ACCESS_DENIED, still not a reconsent code): a reconnect only helps once the scope exists, so it can never be automatic.
[2026-08-13] Issue #1408 separates evidence classification from lock and VAT overlays, and treats import and correction sources as false positives for live-template provenance: 1,316 of 1,361 production signature matches came from those sources, so a broad account signature must not become a correction queue.
[2026-08-13] Issue #1408 does not route locked candidates through the current correctEntry service or an unlock: the service creates its storno in the original period, so a compliant locked-period correction needs a separately tested open-period storno and replacement path that preserves the original and full rättelse trail.
[2026-08-13] The AGIPanel kvittens-scope notice is dismissible (persisted per granted scope string), unlike the hard agd banner: all existing hosted tokens lack agdredovisningperiod, and until Skatteverket's application registration carries that scope a reconnect mints the same grant again (SKV silently drops unregistered scope names), so a hard reconnect demand would be the #1010 un-clearable banner, each futile attempt costing a BankID round-trip via disconnect. Keying dismissal by the exact grant keeps a futile reconnect quiet while a widened grant re-evaluates from scratch.
[2026-08-13] Web picker no longer advertises HEIC/HEIF (accept lists in InvoiceInboxWorkspace): supersedes the 2026-08-01 HEIC decision (runtime sharp transcode + hint). The transcode+hint stays as fallback for drag-drop/API/channel HEIC, but without heic in accept iOS transcodes photo-library picks to JPEG, which makes AI extraction work and shrinks 413 risk; magic-byte validation now covers real HEIC/HEIF via ftyp brands instead of a blanket exemption.
[2026-08-13] Skipped the planned lib/browser/downscale-image.ts module: lib/documents/shrink-image.ts + upload-size.ts already shipped the client-side downscale wired into the inbox uploadFile (hosted 4 MB ceiling, 2400px long edge, honest refusal message); a second overlapping compressor with different thresholds would create contradictory policies.
[2026-08-13] WhatsApp M1 greeting: media from unlinked senders bypasses the 1-hour throttle behind a 10-minute burst window (3/day cap kept): a photo is a receipt someone expects to be handled, and pure hour-long silence taught senders their receipts were being filed; one burst still earns exactly one reply. In-channel silence after stopp stays absolute (only the literal start is read): stopp is an opt-out and any other reply would violate it, so the escape hatch is app-side instead (POST /link/unmute + panel button), where the user is already authenticated and asking.