* fix: reconcile annual reports with final closing entries * test: cover annual report depreciation and VAT balances * Merge remote-tracking branch 'origin/main' into fix/usr-fdbck-ch * fix: show exact invoice delivery details * fix: use currency account in invoice emails * fix: address invoice delivery review feedback * fix: harden invoice delivery and payment accounts * test: assert RLS-denied zero-row updates * fix: close remaining invoice compliance gaps * fix: harden invoice archive authorization * fix: close invoice delivery review findings * fix: verify delivery finalization results * fix: cap combined invoice email recipients * fix: close final invoice compliance findings * fix: prevent stale payment account saves * test: prove invoice delivery isolation * fix: close invoice privacy review findings * test: normalize delivery retention dates
153 lines
5.4 KiB
TypeScript
153 lines
5.4 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest'
|
|
import type { CanonicalAnnualReport } from '../compliance-types'
|
|
import {
|
|
annualReportContentHash,
|
|
createAnnualReportVersion,
|
|
} from '../version-service'
|
|
import { mapTrialBalancesToK2 } from '../../ixbrl/k2-mapper'
|
|
import { buildBrRows, buildRrRows } from '../statement-rows'
|
|
|
|
function model(signedAt: string | null): CanonicalAnnualReport {
|
|
const full = [
|
|
{ account_number: '1930', account_name: 'Bank', closing_debit: 100, closing_credit: 0 },
|
|
{ account_number: '2081', account_name: 'Share capital', closing_debit: 0, closing_credit: 80 },
|
|
{ account_number: '2099', account_name: 'Current result', closing_debit: 0, closing_credit: 20 },
|
|
{ account_number: '3010', account_name: 'Revenue', closing_debit: 20, closing_credit: 20 },
|
|
]
|
|
const preClosing = [
|
|
{ account_number: '1930', account_name: 'Bank', closing_debit: 100, closing_credit: 0 },
|
|
{ account_number: '2081', account_name: 'Share capital', closing_debit: 0, closing_credit: 80 },
|
|
{ account_number: '3010', account_name: 'Revenue', closing_debit: 0, closing_credit: 20 },
|
|
]
|
|
const mapping = mapTrialBalancesToK2({ full, preClosing }, null)
|
|
const balanceRows = buildBrRows(mapping)
|
|
return {
|
|
schema_version: '1.0',
|
|
generated_at: '2026-07-21T10:00:00Z',
|
|
company_id: 'company-1',
|
|
fiscal_period_id: 'period-1',
|
|
entity_type: 'aktiebolag',
|
|
report: {
|
|
accounting_framework: 'k2',
|
|
signatures: [{ role: 'Styrelseledamot', name: 'Anna Andersson', signed_at: signedAt }],
|
|
resultatrakning: buildRrRows(mapping),
|
|
balansrakning: {
|
|
assets: balanceRows.assets,
|
|
equity_liabilities: balanceRows.equityLiabilities,
|
|
total_assets: 100,
|
|
total_equity_liabilities: 100,
|
|
},
|
|
forvaltningsberattelse: {
|
|
resultatdisposition_amounts: { current_year_result: 20 },
|
|
},
|
|
},
|
|
profile: { reporting_currency: 'SEK' },
|
|
disclosures: {},
|
|
eligibility: {
|
|
digital_filing_eligible: true,
|
|
digital_issues: [],
|
|
},
|
|
validation: { ok: true },
|
|
ixbrl: {
|
|
entryPointId: 'k2-ab-risbs-2024-09-12',
|
|
rr: mapping.rr,
|
|
br: mapping.br,
|
|
totals: mapping.totals,
|
|
underskrifter: {
|
|
dateringsdatum: signedAt,
|
|
signers: [
|
|
{
|
|
firstName: 'Anna',
|
|
lastName: 'Andersson',
|
|
role: 'Styrelseledamot',
|
|
signedDate: signedAt,
|
|
},
|
|
],
|
|
},
|
|
faststallelseintyg: {
|
|
signerFirstName: 'Anna',
|
|
signerLastName: 'Andersson',
|
|
signerRole: 'Styrelseledamot',
|
|
genereratDatum: '2026-07-21',
|
|
},
|
|
},
|
|
} as unknown as CanonicalAnnualReport
|
|
}
|
|
|
|
describe('annualReportContentHash', () => {
|
|
it('does not change when evidence dates are overlaid after locking', () => {
|
|
expect(annualReportContentHash(model(null))).toBe(
|
|
annualReportContentHash(model('2026-03-01T10:00:00Z')),
|
|
)
|
|
})
|
|
|
|
it('changes when signed content changes', () => {
|
|
const original = model(null)
|
|
const changed = model(null)
|
|
changed.report.signatures[0].name = 'Bertil Andersson'
|
|
expect(annualReportContentHash(changed)).not.toBe(annualReportContentHash(original))
|
|
})
|
|
|
|
it('locks the digital eligibility decision into the validation snapshot', async () => {
|
|
const rpc = vi.fn().mockResolvedValue({
|
|
data: [{ id: 'version-1', version_number: 1, status: 'draft' }],
|
|
error: null,
|
|
})
|
|
await createAnnualReportVersion({ rpc } as never, 'user-1', model(null), false)
|
|
expect(rpc).toHaveBeenCalledWith(
|
|
'create_annual_report_version',
|
|
expect.objectContaining({
|
|
p_validation_summary: expect.objectContaining({
|
|
digital_filing_eligible: true,
|
|
digital_issues: [],
|
|
profile: expect.objectContaining({ reporting_currency: 'SEK' }),
|
|
disclosures: {},
|
|
eligibility: expect.objectContaining({ digital_filing_eligible: true }),
|
|
}),
|
|
}),
|
|
)
|
|
})
|
|
|
|
it('refuses to snapshot inconsistent financial statements', async () => {
|
|
const rpc = vi.fn()
|
|
const inconsistent = model(null)
|
|
inconsistent.report.resultatrakning.find(
|
|
(row) => row.label === 'Årets resultat',
|
|
)!.current = 21
|
|
inconsistent.validation = { ...inconsistent.validation, ok: true, issues: [] }
|
|
|
|
await expect(
|
|
createAnnualReportVersion({ rpc } as never, 'user-1', inconsistent, false),
|
|
).rejects.toThrow('inconsistent financial statements')
|
|
expect(rpc).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('refuses a PDF and iXBRL line reclassification with unchanged totals', async () => {
|
|
const rpc = vi.fn()
|
|
const inconsistent = model(null)
|
|
inconsistent.ixbrl!.rr = {
|
|
...inconsistent.ixbrl!.rr,
|
|
Nettoomsattning: { current: 0, previous: null },
|
|
OvrigaRorelseintakter: { current: 20, previous: null },
|
|
}
|
|
|
|
await expect(
|
|
createAnnualReportVersion({ rpc } as never, 'user-1', inconsistent, false),
|
|
).rejects.toThrow('inconsistent financial statements')
|
|
expect(rpc).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('refuses a mutated visible balance-sheet result row', async () => {
|
|
const rpc = vi.fn()
|
|
const inconsistent = model(null)
|
|
inconsistent.report.balansrakning.equity_liabilities.find(
|
|
(row) => row.label === 'Årets resultat',
|
|
)!.current = 19
|
|
|
|
await expect(
|
|
createAnnualReportVersion({ rpc } as never, 'user-1', inconsistent, false),
|
|
).rejects.toThrow('inconsistent financial statements')
|
|
expect(rpc).not.toHaveBeenCalled()
|
|
})
|
|
})
|