feat(vat): show already-booked banner when opening momsdeklaration (#1703)

The settlement check only ran on step 3, so Granska recalculated boxes with no signal that a vat_settlement (or momsomforing) already existed. Load the proposal with the report and reuse that detection for a top banner plus the stepper.

Signed-off-by: Daniel Stenborg <daniel@stenborg.se>
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Daniel Stenborg
2026-08-19 20:08:31 +02:00
committed by GitHub
co-authored by Cursor
parent eb0df1722b
commit 06e554f3cc
5 changed files with 297 additions and 68 deletions
+51 -1
View File
@@ -1,5 +1,12 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { buildVatSettlementProposal } from '../vat-settlement'
import {
buildVatSettlementProposal,
findDraftVatSettlement,
findPostedVatSettlement,
vatDeadlineTaxPeriod,
vatSettlementBookingStatus,
type VatSettlementExistingEntry,
} from '../vat-settlement'
// ============================================================
// Mock: fetchVatAccountTotals now goes through the
@@ -304,3 +311,46 @@ describe('buildVatSettlementProposal', () => {
).rejects.toThrow('existing vat_settlement lookup failed: boom')
})
})
describe('vat settlement UI gate helpers', () => {
const posted: VatSettlementExistingEntry = {
id: 'je-posted',
status: 'posted',
entry_date: '2026-06-30',
source_type: 'vat_settlement',
voucher_series: 'A',
voucher_number: 83,
}
const draft: VatSettlementExistingEntry = {
id: 'je-draft',
status: 'draft',
entry_date: '2026-06-30',
source_type: 'vat_settlement',
voucher_series: 'A',
voucher_number: 84,
}
it('picks the posted settlement over a draft', () => {
expect(findPostedVatSettlement([draft, posted])).toEqual(posted)
expect(findDraftVatSettlement([draft, posted])).toBeUndefined()
expect(vatSettlementBookingStatus([draft, posted])).toBe('booked')
})
it('surfaces a draft only when nothing is posted', () => {
expect(findPostedVatSettlement([draft])).toBeUndefined()
expect(findDraftVatSettlement([draft])).toEqual(draft)
expect(vatSettlementBookingStatus([draft])).toBe('draft')
})
it('is none when the period has no settlement', () => {
expect(findPostedVatSettlement([])).toBeUndefined()
expect(vatSettlementBookingStatus([])).toBe('none')
expect(vatSettlementBookingStatus(undefined)).toBe('none')
})
it('formats the calendar tax_period for monthly and quarterly VAT', () => {
expect(vatDeadlineTaxPeriod('monthly', 2026, 6)).toBe('2026-06')
expect(vatDeadlineTaxPeriod('quarterly', 2026, 2)).toBe('2026-Q2')
expect(vatDeadlineTaxPeriod('yearly', 2026, 1)).toBeNull()
})
})
+42
View File
@@ -88,6 +88,48 @@ export interface VatSettlementProposal {
existing_entries: VatSettlementExistingEntry[]
}
/**
* Posted settlement that gates re-booking. Same rule as VatBookingCard:
* tagged `vat_settlement` and shape-detected momsomföring both land in
* `existing_entries`; the first posted row is the one the UI links to.
*/
export function findPostedVatSettlement(
entries: VatSettlementExistingEntry[] | undefined,
): VatSettlementExistingEntry | undefined {
return entries?.find((e) => e.status === 'posted')
}
/** Draft settlement, ignored when a posted one already exists. */
export function findDraftVatSettlement(
entries: VatSettlementExistingEntry[] | undefined,
): VatSettlementExistingEntry | undefined {
if (findPostedVatSettlement(entries)) return undefined
return entries?.find((e) => e.status === 'draft')
}
export function vatSettlementBookingStatus(
entries: VatSettlementExistingEntry[] | undefined,
): 'booked' | 'draft' | 'none' {
if (findPostedVatSettlement(entries)) return 'booked'
if (findDraftVatSettlement(entries)) return 'draft'
return 'none'
}
/**
* `deadlines.tax_period` key for a VAT picker. Yearly (helårsmoms) is omitted:
* that row uses a fiscal-year label that needs company settings, and guessing
* calendar `YYYY` would mis-label broken räkenskapsår.
*/
export function vatDeadlineTaxPeriod(
periodType: VatPeriodType,
year: number,
period: number,
): string | null {
if (periodType === 'monthly') return `${year}-${String(period).padStart(2, '0')}`
if (periodType === 'quarterly') return `${year}-Q${period}`
return null
}
/**
* Build the settlement verifikat proposal for a VAT period. Reads the same
* aggregated ledger totals as the momsrapport (fetchVatAccountTotals), so the