fix(salary): show the AGI kvittensnummer from agi_declarations regardless of who fetched it (#1692)

* fix(salary): show the AGI kvittensnummer from agi_declarations regardless of who fetched it

When the kvittens cron (or the post-connect refresh) picks up a signed AGI it
deletes the period-scoped agi_submission_{period} cache on purpose, and the
salary run then rendered "Skickad till Skatteverket <date>" with no
kvittensnummer, signatory or signing time even though all three were stored
on agi_declarations. Since the cron runs every 15 minutes while the panel
polls only three times after the signing link is created, that was the
normal outcome for anyone who signs at an unhurried pace (#1597).

GET /agi/status now serves the receipt from agi_declarations
(kvittensnummer, response_data.signeradAv/signeradTid, submitted_at,
submittedAtEstimated) whenever the cache is absent; the cache still wins
when present because it is the only place the in-flight states live. The
declaration-sourced record deliberately carries no salaryRunId (the period
row is repointed at a correction run on regeneration), so ownership is
resolved from signeradTid/submittedAt against the run's agi_submitted_at
stamp and from updatedAt = submitted_at. AGIPanel labels the timestamp as
approximate when it is our reconciliation-time fallback rather than
Skatteverket's signeradTid. The MCP gnubok_agi_status tool uses the same
read.

Closes #1597

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

* ci: retry stalled Vercel preview build

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

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-08-18 17:32:23 +02:00
committed by GitHub
co-authored by Claude Fable 5 Jakob Wennberg
parent 7d56e0ec01
commit 83932f2e07
11 changed files with 571 additions and 25 deletions
+28 -10
View File
@@ -935,8 +935,17 @@ export function AGIPanel(props: AGIPanelProps) {
const forcedAdvanced = draftIsStale || underlagRejected
const advancedOpen = showAdvanced || forcedAdvanced
const signedAtRaw = runSubmission?.signeradTid ?? agiSubmittedAt ?? null
const signedAtRaw =
runSubmission?.signeradTid ?? runSubmission?.submittedAt ?? agiSubmittedAt ?? null
const signedAtText = signedAtRaw ? new Date(signedAtRaw).toLocaleString('sv-SE') : null
// A signed record without Skatteverket's signeradTid carries our
// reconciliation-time stamp instead (an upper bound on the signing moment,
// see agi-kvittens-reconcile.ts): say so rather than presenting it as the
// legal signing time. Without any record we cannot tell and print the run
// stamp as before.
const signedAtEstimated =
runSubmission?.status === 'signed' &&
(runSubmission.submittedAtEstimated === true || !runSubmission.signeradTid)
const chainStepState = (step: ChainStep): 'done' | 'running' | 'failed' | 'upcoming' => {
if (!chain) return 'upcoming'
@@ -979,10 +988,12 @@ export function AGIPanel(props: AGIPanelProps) {
<CardContent className="space-y-4">
{/* Filed: the terminal state deserves more than a gray status row.
Kvittensnummer + signature metadata come from the run-scoped
record; a run stamped only via agi_submitted_at (e.g. cron
reconciliation with an evicted cache, or an original whose cached
receipt a later correction has replaced) still gets the card, just
without a number: better than showing another declaration's. */}
record, which /agi/status serves from the in-flight cache or,
once the kvittens reconciliation has deleted that cache, from
agi_declarations (#1597). A run stamped only via agi_submitted_at
(an original whose receipt a later correction has replaced) still
gets the card, just without a number: better than showing another
declaration's. */}
{isSigned && (
<div className="rounded-lg border border-border bg-muted/30 p-4">
<div className="flex items-start gap-3">
@@ -1000,12 +1011,19 @@ export function AGIPanel(props: AGIPanelProps) {
<p className="text-sm text-muted-foreground">
{runSubmission?.signeradAv
? signedAtText
? t('success_card_signed_by_at', {
name: runSubmission.signeradAv,
date: signedAtText,
})
? t(
signedAtEstimated
? 'success_card_signed_by_at_estimated'
: 'success_card_signed_by_at',
{ name: runSubmission.signeradAv, date: signedAtText },
)
: t('success_card_signed_by', { name: runSubmission.signeradAv })
: t('success_card_signed_at', { date: signedAtText ?? '' })}
: t(
signedAtEstimated
? 'success_card_signed_at_estimated'
: 'success_card_signed_at',
{ date: signedAtText ?? '' },
)}
</p>
)}
</div>