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:
co-authored by
Claude Fable 5
Jakob Wennberg
parent
7d56e0ec01
commit
83932f2e07
@@ -254,6 +254,80 @@ describe('resolveRunAgiSubmission', () => {
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Once the kvittens reconciliation (cron / post-connect refresh) has promoted
|
||||
* the declaration it deletes the period cache, and GET /agi/status serves the
|
||||
* receipt from agi_declarations instead (#1597). That record carries no
|
||||
* salaryRunId (the period's single row is repointed at a correction run when
|
||||
* one regenerates its XML), so ownership rests on the timestamps below.
|
||||
*/
|
||||
describe('resolveRunAgiSubmission: receipt served from agi_declarations', () => {
|
||||
const filedRun: AgiFilingRun = {
|
||||
id: 'r1',
|
||||
agi_generated_at: '2026-07-13T09:00:00Z',
|
||||
agi_submitted_at: '2026-07-14T08:30:00+00:00',
|
||||
}
|
||||
|
||||
it('the run whose stamp matches signeradTid owns the receipt with its metadata', () => {
|
||||
const record: AgiSubmissionState = {
|
||||
status: 'signed',
|
||||
kvittensnummer: 'K1',
|
||||
signeradAv: '191212121212',
|
||||
signeradTid: '2026-07-14T08:30:00Z',
|
||||
submittedAt: '2026-07-14T08:30:00+00:00',
|
||||
submittedAtEstimated: false,
|
||||
updatedAt: '2026-07-14T08:30:00+00:00',
|
||||
source: 'declaration',
|
||||
}
|
||||
expect(resolveRunAgiSubmission(filedRun, record)).toBe(record)
|
||||
expect(resolveRunAgiKvittensnummer(filedRun, record)).toBe('K1')
|
||||
expect(deriveAgiFilingState(filedRun, record)).toBe('signed')
|
||||
})
|
||||
|
||||
it('without signeradTid the reconciliation-time stamp decides ownership', () => {
|
||||
// Skatteverket omitted signeradTid: the reconciler stamped both the
|
||||
// declaration and the run row with its own clock, and the record says so.
|
||||
const estimated: AgiSubmissionState = {
|
||||
status: 'signed',
|
||||
kvittensnummer: 'K1',
|
||||
signeradAv: '191212121212',
|
||||
submittedAt: '2026-07-14T08:30:00+00:00',
|
||||
submittedAtEstimated: true,
|
||||
updatedAt: '2026-07-14T08:30:00+00:00',
|
||||
source: 'declaration',
|
||||
}
|
||||
expect(resolveRunAgiSubmission(filedRun, estimated)).toBe(estimated)
|
||||
|
||||
// A stamp from another moment is another declaration's receipt.
|
||||
const otherRun: AgiFilingRun = { ...filedRun, agi_submitted_at: '2026-06-01T10:00:00Z' }
|
||||
expect(resolveRunAgiSubmission(otherRun, estimated)).toBeNull()
|
||||
expect(resolveRunAgiKvittensnummer(otherRun, estimated)).toBeNull()
|
||||
})
|
||||
|
||||
it('a correction run generated after the receipt does not inherit it', () => {
|
||||
// The declaration row now points at the correction run, which is exactly
|
||||
// why the record carries no salaryRunId: updatedAt (= submitted_at)
|
||||
// predates the correction's XML, so it belongs to the original.
|
||||
const record: AgiSubmissionState = {
|
||||
status: 'signed',
|
||||
kvittensnummer: 'K1',
|
||||
signeradTid: '2026-07-14T08:30:00Z',
|
||||
submittedAt: '2026-07-14T08:30:00+00:00',
|
||||
updatedAt: '2026-07-14T08:30:00+00:00',
|
||||
source: 'declaration',
|
||||
}
|
||||
const correction: AgiFilingRun = {
|
||||
id: 'r2',
|
||||
agi_generated_at: '2026-07-20T09:00:00Z',
|
||||
agi_submitted_at: null,
|
||||
}
|
||||
expect(resolveRunAgiSubmission(correction, record)).toBeNull()
|
||||
expect(deriveAgiFilingState(correction, record)).toBe('generated')
|
||||
// The original keeps its number.
|
||||
expect(resolveRunAgiKvittensnummer(filedRun, record)).toBe('K1')
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* The salary run page (`app/(dashboard)/salary/runs/[id]/page.tsx`) and the
|
||||
* AGI panel (`components/salary/AGIPanel.tsx`) read the same period-scoped
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
* Combines the run row's authoritative timestamps (agi_generated_at,
|
||||
* agi_submitted_at) with the Skatteverket extension's per-period submission
|
||||
* record (extension_data key `agi_submission_{period}`, surfaced via
|
||||
* GET /api/extensions/ext/skatteverket/agi/status).
|
||||
* GET /api/extensions/ext/skatteverket/agi/status; once the kvittens
|
||||
* reconciliation has deleted that cache the same route serves the receipt
|
||||
* recorded on `agi_declarations`, see `source`).
|
||||
*
|
||||
* The submission record is optional: self-hosted installs without the
|
||||
* Skatteverket extension, users without the capability, and periods that
|
||||
@@ -40,6 +42,25 @@ export interface AgiSubmissionState {
|
||||
kvittensnummer?: string
|
||||
signeradAv?: string
|
||||
signeradTid?: string
|
||||
/**
|
||||
* The moment recorded as the filing time: Skatteverket's signeradTid, or
|
||||
* the reconciliation time when Skatteverket omitted it. Written by the
|
||||
* extension's status read from `agi_declarations.submitted_at`, the same
|
||||
* value that stamps `salary_runs.agi_submitted_at`.
|
||||
*/
|
||||
submittedAt?: string
|
||||
/**
|
||||
* True when `submittedAt` is our reconciliation-time upper bound rather
|
||||
* than Skatteverket's signing moment (signeradTid absent), so the UI can
|
||||
* label it as an estimate instead of as the legal signing time.
|
||||
*/
|
||||
submittedAtEstimated?: boolean
|
||||
/**
|
||||
* Where the record was read from: the in-flight `agi_submission_{period}`
|
||||
* cache, or the receipt on `agi_declarations` once the kvittens
|
||||
* reconciliation has deleted that cache (#1597).
|
||||
*/
|
||||
source?: 'cache' | 'declaration'
|
||||
inlamningId?: number
|
||||
tillstand?: string
|
||||
meddelande?: string
|
||||
@@ -110,9 +131,11 @@ export function resolveRunAgiSubmission(
|
||||
// 2. This run already carries a kvittens stamp. Its own receipt is the one
|
||||
// whose signeradTid produced that stamp; a record signed at any other moment
|
||||
// belongs to another declaration for the same period. Skatteverket may omit
|
||||
// signeradTid, in which case there is nothing to contradict ownership.
|
||||
// signeradTid: the record then carries the reconciliation-time stamp as
|
||||
// `submittedAt` (the same value written to the run row), and a record with
|
||||
// neither has nothing to contradict ownership.
|
||||
if (submission.status === 'signed' && submittedAt !== null) {
|
||||
const signeradTid = epoch(submission.signeradTid)
|
||||
const signeradTid = epoch(submission.signeradTid ?? submission.submittedAt)
|
||||
return signeradTid === null || signeradTid === submittedAt ? submission : null
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user