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
@@ -259,6 +259,47 @@ describe('gnubok_agi_status: run-scoped filing state', () => {
|
||||
expect(result.local_state).toMatchObject({ status: 'signed', salaryRunId: 'sr-original' })
|
||||
})
|
||||
|
||||
it('cache deleted by the kvittens cron: the receipt is served from agi_declarations (#1597)', async () => {
|
||||
const { supabase, enqueue } = createQueuedMockSupabase()
|
||||
enqueueStatusReads(enqueue, {
|
||||
id: 'sr-original',
|
||||
period_year: 2026,
|
||||
period_month: 6,
|
||||
agi_generated_at: '2026-06-28T08:00:00Z',
|
||||
agi_submitted_at: '2026-07-01T09:00:00Z',
|
||||
}, null)
|
||||
enqueue({
|
||||
data: {
|
||||
salary_run_id: 'sr-original',
|
||||
status: 'submitted',
|
||||
kvittensnummer: 'KV-ORIG-1',
|
||||
submitted_at: '2026-07-01T09:00:00Z',
|
||||
response_data: {
|
||||
signeradAv: '191212121212',
|
||||
signeradTid: '2026-07-01T09:00:00Z',
|
||||
submittedAtEstimated: false,
|
||||
},
|
||||
},
|
||||
}) // agi_declarations
|
||||
|
||||
const result = (await agiStatus.execute(
|
||||
{ salary_run_id: 'sr-original' }, 'company-1', 'user-1', supabase as never, { type: 'api_key' },
|
||||
)) as {
|
||||
filing_state: string
|
||||
kvittensnummer: string | null
|
||||
local_state: Record<string, unknown> | null
|
||||
}
|
||||
|
||||
expect(result.filing_state).toBe('signed')
|
||||
expect(result.kvittensnummer).toBe('KV-ORIG-1')
|
||||
expect(result.local_state).toMatchObject({
|
||||
status: 'signed',
|
||||
signeradAv: '191212121212',
|
||||
submittedAtEstimated: false,
|
||||
source: 'declaration',
|
||||
})
|
||||
})
|
||||
|
||||
it('a run with neither XML nor record reports none', async () => {
|
||||
const { supabase, enqueue } = createQueuedMockSupabase()
|
||||
enqueueStatusReads(enqueue, {
|
||||
|
||||
@@ -216,6 +216,7 @@ import { extractInvoiceFields, ExtractionSchema as InvoiceExtractionSchema, Agen
|
||||
// the skatteverket extension via the registry (lib/pending-operations/commit.ts).
|
||||
import { skvRequest, SkatteverketAuthError } from '@/extensions/general/skatteverket/lib/api-client'
|
||||
import { agiGetKvittenser } from '@/extensions/general/skatteverket/lib/agi-client'
|
||||
import { readAgiSubmissionStatus } from '@/extensions/general/skatteverket/lib/agi-submission-status'
|
||||
import { buildMomsuppgift, resolveRedovisare } from '@/extensions/general/skatteverket/lib/declaration-prep'
|
||||
import { writeSkatteverketAudit } from '@/extensions/general/skatteverket/lib/audit'
|
||||
import { skvAuthCodeToStructured } from '@/extensions/general/skatteverket/lib/error-map'
|
||||
@@ -12011,7 +12012,9 @@ export const tools: McpTool[] = [
|
||||
if (!run) throw new Error('Salary run not found')
|
||||
const arbetsgivare = await resolveRedovisare(supabase, companyId)
|
||||
const period = formatRedovisningsperiod('monthly', run.period_year, run.period_month)
|
||||
// Local cached submission state (extension_data key agi_submission_${period}).
|
||||
// Local cached submission state (extension_data key agi_submission_${period}),
|
||||
// falling back to the receipt on agi_declarations once the kvittens
|
||||
// reconciliation has deleted that cache (same read as GET /agi/status).
|
||||
const { data: localRow } = await supabase
|
||||
.from('extension_data')
|
||||
.select('value')
|
||||
@@ -12019,10 +12022,9 @@ export const tools: McpTool[] = [
|
||||
.eq('extension_id', 'skatteverket')
|
||||
.eq('key', `agi_submission_${period}`)
|
||||
.maybeSingle()
|
||||
let periodRecord: AgiSubmissionState | null = null
|
||||
if (localRow?.value) {
|
||||
try { periodRecord = JSON.parse(localRow.value as string) as AgiSubmissionState } catch { periodRecord = null }
|
||||
}
|
||||
const periodRecord: AgiSubmissionState | null = await readAgiSubmissionStatus(
|
||||
supabase, companyId, period, localRow?.value ?? null,
|
||||
)
|
||||
// Run-scope the period-keyed record (lib/salary/agi-submission-state.ts,
|
||||
// same resolution AGIPanel and the run page use). salary_runs is unique
|
||||
// per period only for non-corrected runs (partial index, migration
|
||||
|
||||
Reference in New Issue
Block a user