Bug/gh issues fiz (#1103)
* refactor: optimize page loading and data fetching * fix: resolve recurring production runtime errors * feat: add MCP company and customer updates * fix: handle year-end tax adjustments * feat: harden annual report compliance * fix: expand invoice logo and font support * fix: sanitize API route error responses * fix: sanitize user-facing error messages * feat: persist onboarding and tax assessment notices * fix: reduce cloud backup audit churn * feat: refine invoice editor layout * fix: show saved tax adjustments in INK2 * fix: complete annual report API mappings * docs: record operational safeguards and decisions * fix: harden annual report review findings * fix: adjust column span for description based on VAT registration * New css class name
This commit is contained in:
@@ -595,6 +595,8 @@ export function makeCompanySettings(
|
||||
invoice_primary_color: '#1a1a1a',
|
||||
invoice_accent_color: '#666666',
|
||||
invoice_font_family: 'Helvetica',
|
||||
invoice_custom_font_path: null,
|
||||
invoice_custom_font_name: null,
|
||||
invoice_header_text: null,
|
||||
invoice_footer_text: null,
|
||||
invoice_email_texts: null,
|
||||
|
||||
@@ -0,0 +1,537 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { PoolClient } from 'pg'
|
||||
import { getClient, getPool, withUserContext } from './setup'
|
||||
import { insertAuthUser, seedCompany } from './fixtures'
|
||||
|
||||
async function withCommittedRoleContext<T>(
|
||||
userId: string,
|
||||
role: 'authenticated' | 'service_role',
|
||||
fn: (client: PoolClient) => Promise<T>,
|
||||
): Promise<T> {
|
||||
const client = await getClient()
|
||||
try {
|
||||
await client.query('BEGIN')
|
||||
await client.query(`SELECT set_config('request.jwt.claims', $1, true)`, [
|
||||
JSON.stringify({ sub: userId, role }),
|
||||
])
|
||||
await client.query(`SELECT set_config('request.jwt.claim.sub', $1, true)`, [userId])
|
||||
await client.query(`SELECT set_config('request.jwt.claim.role', $1, true)`, [role])
|
||||
await client.query(`SET LOCAL ROLE ${role}`)
|
||||
const result = await fn(client)
|
||||
await client.query('COMMIT')
|
||||
return result
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK').catch(() => {})
|
||||
throw error
|
||||
} finally {
|
||||
client.release()
|
||||
}
|
||||
}
|
||||
|
||||
function reportData(fiscalPeriodId: string) {
|
||||
return {
|
||||
accounting_framework: 'k2',
|
||||
company: { name: 'Test AB', org_number: '556012-5790' },
|
||||
fiscal_period: {
|
||||
id: fiscalPeriodId,
|
||||
period_start: '2025-01-01',
|
||||
period_end: '2025-12-31',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async function createVersion(params: {
|
||||
companyId: string
|
||||
userId: string
|
||||
fiscalPeriodId: string
|
||||
hash?: string
|
||||
status?: 'draft' | 'ready_for_signature'
|
||||
}) {
|
||||
let validationSummary: Record<string, unknown> = {
|
||||
stage: 'draft',
|
||||
ok: true,
|
||||
error_count: 0,
|
||||
warning_count: 0,
|
||||
issues: [],
|
||||
}
|
||||
if (params.status === 'ready_for_signature') {
|
||||
const profile = await getPool().query<{ signer_roster_confirmed_at: string }>(
|
||||
`INSERT INTO public.annual_report_profiles
|
||||
(company_id, fiscal_period_id, user_id, is_in_liquidation, signer_roster_confirmed_at)
|
||||
VALUES ($1, $2, $3, false, now())
|
||||
ON CONFLICT (company_id, fiscal_period_id) DO UPDATE
|
||||
SET signer_roster_confirmed_at = now()
|
||||
RETURNING signer_roster_confirmed_at::text`,
|
||||
[params.companyId, params.fiscalPeriodId, params.userId],
|
||||
)
|
||||
validationSummary = {
|
||||
stage: 'signing',
|
||||
ok: true,
|
||||
error_count: 0,
|
||||
warning_count: 0,
|
||||
issues: [],
|
||||
digital_filing_eligible: true,
|
||||
digital_issues: [],
|
||||
profile: {
|
||||
company_id: params.companyId,
|
||||
fiscal_period_id: params.fiscalPeriodId,
|
||||
signer_roster_confirmed_at: profile.rows[0].signer_roster_confirmed_at,
|
||||
},
|
||||
disclosures: {},
|
||||
eligibility: {},
|
||||
}
|
||||
}
|
||||
const rpcName =
|
||||
params.status === 'ready_for_signature'
|
||||
? 'create_annual_report_version_with_signatures'
|
||||
: 'create_annual_report_version'
|
||||
return withCommittedRoleContext(
|
||||
params.userId,
|
||||
params.status === 'ready_for_signature' ? 'service_role' : 'authenticated',
|
||||
(client) =>
|
||||
client.query<{
|
||||
id: string
|
||||
version_number: number
|
||||
status: string
|
||||
}>(
|
||||
`SELECT id, version_number, status
|
||||
FROM public.${rpcName}(
|
||||
$1, $2, '1.0', 'k2', $3, $4::jsonb, $5::jsonb, $6, '2024-09-12',
|
||||
'k2-ab-risbs-2024-09-12', $7::jsonb, $8
|
||||
)`,
|
||||
[
|
||||
params.companyId,
|
||||
params.fiscalPeriodId,
|
||||
params.status ?? 'draft',
|
||||
JSON.stringify(reportData(params.fiscalPeriodId)),
|
||||
JSON.stringify({ entryPointId: 'k2-ab-risbs-2024-09-12' }),
|
||||
params.hash ?? 'a'.repeat(64),
|
||||
JSON.stringify(validationSummary),
|
||||
params.userId,
|
||||
],
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
describe('annual report profile and version enforcement', () => {
|
||||
it('isolates profiles by membership and rejects cross-company period links', async () => {
|
||||
const owner = await seedCompany()
|
||||
const strangerId = await insertAuthUser()
|
||||
const viewerId = await insertAuthUser()
|
||||
await getPool().query(
|
||||
`INSERT INTO public.company_members (company_id, user_id, role)
|
||||
VALUES ($1, $2, 'viewer')`,
|
||||
[owner.companyId, viewerId],
|
||||
)
|
||||
const profileId = randomUUID()
|
||||
await getPool().query(
|
||||
`INSERT INTO public.annual_report_profiles
|
||||
(id, company_id, fiscal_period_id, user_id, is_public_limited_company)
|
||||
VALUES ($1, $2, $3, $4, false)`,
|
||||
[profileId, owner.companyId, owner.fiscalPeriodId, owner.userId],
|
||||
)
|
||||
|
||||
const ownerView = await withUserContext(owner.userId, (client) =>
|
||||
client.query('SELECT id FROM public.annual_report_profiles WHERE id = $1', [profileId]),
|
||||
)
|
||||
expect(ownerView.rows).toHaveLength(1)
|
||||
const strangerView = await withUserContext(strangerId, (client) =>
|
||||
client.query('SELECT id FROM public.annual_report_profiles WHERE id = $1', [profileId]),
|
||||
)
|
||||
expect(strangerView.rows).toHaveLength(0)
|
||||
const viewerView = await withUserContext(viewerId, (client) =>
|
||||
client.query('SELECT id FROM public.annual_report_profiles WHERE id = $1', [profileId]),
|
||||
)
|
||||
expect(viewerView.rows).toHaveLength(1)
|
||||
await expect(
|
||||
withUserContext(viewerId, (client) =>
|
||||
client.query(
|
||||
`UPDATE public.annual_report_profiles
|
||||
SET is_public_limited_company = true
|
||||
WHERE id = $1`,
|
||||
[profileId],
|
||||
),
|
||||
),
|
||||
).resolves.toMatchObject({ rowCount: 0 })
|
||||
|
||||
const second = await seedCompany()
|
||||
await expect(
|
||||
getPool().query(
|
||||
`INSERT INTO public.annual_report_profiles
|
||||
(company_id, fiscal_period_id, user_id)
|
||||
VALUES ($1, $2, $3)`,
|
||||
[owner.companyId, second.fiscalPeriodId, owner.userId],
|
||||
),
|
||||
).rejects.toThrow(/does not belong to annual report company/i)
|
||||
})
|
||||
|
||||
it('allocates sequential versions, deduplicates content, and finalizes a draft atomically', async () => {
|
||||
const owner = await seedCompany()
|
||||
const first = await createVersion(owner)
|
||||
expect(first.rows[0]).toMatchObject({ version_number: 1, status: 'draft' })
|
||||
|
||||
const signatureId = randomUUID()
|
||||
await getPool().query(
|
||||
`INSERT INTO public.arsredovisning_signature_requests
|
||||
(id, user_id, company_id, fiscal_period_id, role, signer_name)
|
||||
VALUES ($1, $2, $3, $4, 'Styrelseledamot', 'Anna Andersson')`,
|
||||
[signatureId, owner.userId, owner.companyId, owner.fiscalPeriodId],
|
||||
)
|
||||
|
||||
const finalized = await createVersion({ ...owner, status: 'ready_for_signature' })
|
||||
expect(finalized.rows[0]).toMatchObject({
|
||||
id: first.rows[0].id,
|
||||
version_number: 1,
|
||||
status: 'ready_for_signature',
|
||||
})
|
||||
const boundSignature = await getPool().query(
|
||||
`SELECT annual_report_version_id
|
||||
FROM public.arsredovisning_signature_requests
|
||||
WHERE id = $1`,
|
||||
[signatureId],
|
||||
)
|
||||
expect(boundSignature.rows[0].annual_report_version_id).toBe(first.rows[0].id)
|
||||
|
||||
const second = await createVersion({ ...owner, hash: 'b'.repeat(64) })
|
||||
expect(second.rows[0]).toMatchObject({ version_number: 2, status: 'draft' })
|
||||
})
|
||||
|
||||
it('does not finalize a version without a signer roster', async () => {
|
||||
const owner = await seedCompany()
|
||||
await expect(
|
||||
createVersion({ ...owner, status: 'ready_for_signature' }),
|
||||
).rejects.toThrow(/requires at least one signer slot/i)
|
||||
})
|
||||
|
||||
it('keeps finalization behind the trusted service boundary and rejects forged validation', async () => {
|
||||
const owner = await seedCompany()
|
||||
await expect(
|
||||
withUserContext(owner.userId, (client) =>
|
||||
client.query(
|
||||
`SELECT * FROM public.create_annual_report_version_with_signatures(
|
||||
$1, $2, '1.0', 'k2', 'ready_for_signature', $3::jsonb, $4::jsonb,
|
||||
$5, '2024-09-12', 'k2-ab-risbs-2024-09-12', $6::jsonb, $7
|
||||
)`,
|
||||
[
|
||||
owner.companyId,
|
||||
owner.fiscalPeriodId,
|
||||
JSON.stringify(reportData(owner.fiscalPeriodId)),
|
||||
JSON.stringify({ entryPointId: 'k2-ab-risbs-2024-09-12' }),
|
||||
'd'.repeat(64),
|
||||
JSON.stringify({ ok: true, issues: [] }),
|
||||
owner.userId,
|
||||
],
|
||||
),
|
||||
),
|
||||
).rejects.toThrow(/permission denied|trusted application service/i)
|
||||
|
||||
await expect(
|
||||
withCommittedRoleContext(owner.userId, 'service_role', (client) =>
|
||||
client.query(
|
||||
`SELECT * FROM public.create_annual_report_version_with_signatures(
|
||||
$1, $2, '1.0', 'k2', 'ready_for_signature', $3::jsonb, $4::jsonb,
|
||||
$5, '2024-09-12', 'k2-ab-risbs-2024-09-12', $6::jsonb, $7
|
||||
)`,
|
||||
[
|
||||
owner.companyId,
|
||||
owner.fiscalPeriodId,
|
||||
JSON.stringify(reportData(owner.fiscalPeriodId)),
|
||||
JSON.stringify({ entryPointId: 'k2-ab-risbs-2024-09-12' }),
|
||||
'e'.repeat(64),
|
||||
JSON.stringify({ ok: true, issues: [] }),
|
||||
owner.userId,
|
||||
],
|
||||
),
|
||||
),
|
||||
).rejects.toThrow(/complete server validation snapshot/i)
|
||||
})
|
||||
|
||||
it('keeps signature evidence transitions behind the trusted service boundary', async () => {
|
||||
const owner = await seedCompany()
|
||||
const signatureId = randomUUID()
|
||||
|
||||
const directInsert = await withUserContext(owner.userId, (client) =>
|
||||
client.query(
|
||||
`INSERT INTO public.arsredovisning_signature_requests
|
||||
(id, user_id, company_id, fiscal_period_id, role, signer_name)
|
||||
VALUES ($1, $2, $3, $4, 'Styrelseledamot', 'Anna Andersson')
|
||||
RETURNING id`,
|
||||
[signatureId, owner.userId, owner.companyId, owner.fiscalPeriodId],
|
||||
),
|
||||
)
|
||||
expect(directInsert.rowCount).toBe(1)
|
||||
await getPool().query(
|
||||
`INSERT INTO public.arsredovisning_signature_requests
|
||||
(id, user_id, company_id, fiscal_period_id, role, signer_name)
|
||||
VALUES ($1, $2, $3, $4, 'Styrelseledamot', 'Anna Andersson')`,
|
||||
[signatureId, owner.userId, owner.companyId, owner.fiscalPeriodId],
|
||||
)
|
||||
const version = await createVersion({ ...owner, status: 'ready_for_signature' })
|
||||
|
||||
const directUpdate = await withUserContext(owner.userId, (client) =>
|
||||
client.query(
|
||||
`UPDATE public.arsredovisning_signature_requests
|
||||
SET status = 'signed', signed_at = now(),
|
||||
signing_method = 'paper_original', evidence_reference = 'archive:A-1',
|
||||
evidence_recorded_by = $2, evidence_recorded_at = now()
|
||||
WHERE id = $1
|
||||
RETURNING id`,
|
||||
[signatureId, owner.userId],
|
||||
),
|
||||
)
|
||||
expect(directUpdate.rowCount).toBe(0)
|
||||
|
||||
const directDelete = await withUserContext(owner.userId, (client) =>
|
||||
client.query(
|
||||
`DELETE FROM public.arsredovisning_signature_requests
|
||||
WHERE id = $1
|
||||
RETURNING id`,
|
||||
[signatureId],
|
||||
),
|
||||
)
|
||||
expect(directDelete.rowCount).toBe(0)
|
||||
|
||||
await expect(
|
||||
withUserContext(owner.userId, (client) =>
|
||||
client.query(
|
||||
`INSERT INTO public.arsredovisning_signature_requests
|
||||
(user_id, company_id, fiscal_period_id, role, signer_name, status,
|
||||
annual_report_version_id, signed_at, signing_method, evidence_reference,
|
||||
evidence_recorded_by, evidence_recorded_at)
|
||||
VALUES ($1, $2, $3, 'VD', 'Erik Eriksson', 'signed', $4, now(),
|
||||
'paper_original', 'archive:A-2', $1, now())`,
|
||||
[owner.userId, owner.companyId, owner.fiscalPeriodId, version.rows[0].id],
|
||||
),
|
||||
),
|
||||
).rejects.toThrow(/row-level security policy/i)
|
||||
})
|
||||
|
||||
it('invalidates representative confirmation when the draft signer roster changes', async () => {
|
||||
const owner = await seedCompany()
|
||||
await getPool().query(
|
||||
`INSERT INTO public.annual_report_profiles
|
||||
(company_id, fiscal_period_id, user_id, signer_roster_confirmed_at)
|
||||
VALUES ($1, $2, $3, now())`,
|
||||
[owner.companyId, owner.fiscalPeriodId, owner.userId],
|
||||
)
|
||||
await getPool().query(
|
||||
`INSERT INTO public.arsredovisning_signature_requests
|
||||
(user_id, company_id, fiscal_period_id, role, signer_name)
|
||||
VALUES ($1, $2, $3, 'Styrelseledamot', 'Anna Andersson')`,
|
||||
[owner.userId, owner.companyId, owner.fiscalPeriodId],
|
||||
)
|
||||
const profile = await getPool().query<{ signer_roster_confirmed_at: string | null }>(
|
||||
`SELECT signer_roster_confirmed_at::text
|
||||
FROM public.annual_report_profiles
|
||||
WHERE company_id = $1 AND fiscal_period_id = $2`,
|
||||
[owner.companyId, owner.fiscalPeriodId],
|
||||
)
|
||||
expect(profile.rows[0].signer_roster_confirmed_at).toBeNull()
|
||||
})
|
||||
|
||||
it('does not allow callers to bypass signer preparation with the draft RPC', async () => {
|
||||
const owner = await seedCompany()
|
||||
await expect(
|
||||
withUserContext(owner.userId, (client) =>
|
||||
client.query(
|
||||
`SELECT * FROM public.create_annual_report_version(
|
||||
$1, $2, '1.0', 'k2', 'ready_for_signature', $3::jsonb, $4::jsonb,
|
||||
$5, '2024-09-12', 'k2-ab-risbs-2024-09-12', $6::jsonb, $7
|
||||
)`,
|
||||
[
|
||||
owner.companyId,
|
||||
owner.fiscalPeriodId,
|
||||
JSON.stringify(reportData(owner.fiscalPeriodId)),
|
||||
JSON.stringify({ entryPointId: 'k2-ab-risbs-2024-09-12' }),
|
||||
'c'.repeat(64),
|
||||
JSON.stringify({ ok: true, issues: [] }),
|
||||
owner.userId,
|
||||
],
|
||||
),
|
||||
),
|
||||
).rejects.toThrow(/only permits draft status/i)
|
||||
})
|
||||
|
||||
it('keeps version content immutable and blocks member deletion', async () => {
|
||||
const owner = await seedCompany()
|
||||
const version = await createVersion(owner)
|
||||
|
||||
await expect(
|
||||
withUserContext(owner.userId, (client) =>
|
||||
client.query(
|
||||
`UPDATE public.annual_report_versions
|
||||
SET report_data = '{"changed":true}'::jsonb
|
||||
WHERE id = $1`,
|
||||
[version.rows[0].id],
|
||||
),
|
||||
),
|
||||
).rejects.toThrow(/content is immutable/i)
|
||||
|
||||
const deleted = await withUserContext(owner.userId, (client) =>
|
||||
client.query('DELETE FROM public.annual_report_versions WHERE id = $1 RETURNING id', [
|
||||
version.rows[0].id,
|
||||
]),
|
||||
)
|
||||
expect(deleted.rowCount).toBe(0)
|
||||
await expect(
|
||||
getPool().query('DELETE FROM public.annual_report_versions WHERE id = $1', [
|
||||
version.rows[0].id,
|
||||
]),
|
||||
).rejects.toThrow(/retained as immutable accounting information/i)
|
||||
await expect(
|
||||
getPool().query('DELETE FROM public.fiscal_periods WHERE id = $1', [
|
||||
owner.fiscalPeriodId,
|
||||
]),
|
||||
).rejects.toThrow()
|
||||
})
|
||||
|
||||
it('requires version-bound signature evidence and freezes signed rows', async () => {
|
||||
const owner = await seedCompany()
|
||||
const signatureId = randomUUID()
|
||||
await getPool().query(
|
||||
`INSERT INTO public.arsredovisning_signature_requests
|
||||
(id, user_id, company_id, fiscal_period_id, role, signer_name)
|
||||
VALUES ($1, $2, $3, $4, 'Styrelseledamot', 'Anna Andersson')`,
|
||||
[signatureId, owner.userId, owner.companyId, owner.fiscalPeriodId],
|
||||
)
|
||||
const version = await createVersion({ ...owner, status: 'ready_for_signature' })
|
||||
|
||||
await expect(
|
||||
getPool().query(
|
||||
`UPDATE public.annual_report_versions
|
||||
SET status = 'signed'
|
||||
WHERE id = $1`,
|
||||
[version.rows[0].id],
|
||||
),
|
||||
).rejects.toThrow(/before every locked signer has signed/i)
|
||||
|
||||
await expect(
|
||||
getPool().query(
|
||||
`UPDATE public.arsredovisning_signature_requests
|
||||
SET status = 'signed', signed_at = now(), annual_report_version_id = $2
|
||||
WHERE id = $1`,
|
||||
[signatureId, version.rows[0].id],
|
||||
),
|
||||
).rejects.toThrow(/signature_evidence_consistency|check constraint/i)
|
||||
|
||||
await expect(
|
||||
getPool().query(
|
||||
`UPDATE public.arsredovisning_signature_requests
|
||||
SET status = 'signed', signed_at = now() - interval '1 day',
|
||||
signing_method = 'paper_original', evidence_reference = 'archive:A-1',
|
||||
evidence_recorded_by = $2, evidence_recorded_at = now()
|
||||
WHERE id = $1`,
|
||||
[signatureId, owner.userId],
|
||||
),
|
||||
).rejects.toThrow(/signature date must be between version finalization and today/i)
|
||||
|
||||
await expect(
|
||||
getPool().query(
|
||||
`UPDATE public.arsredovisning_signature_requests
|
||||
SET status = 'signed', signed_at = now() + interval '1 day',
|
||||
signing_method = 'paper_original', evidence_reference = 'archive:A-1',
|
||||
evidence_recorded_by = $2, evidence_recorded_at = now()
|
||||
WHERE id = $1`,
|
||||
[signatureId, owner.userId],
|
||||
),
|
||||
).rejects.toThrow(/signature date must be between version finalization and today/i)
|
||||
|
||||
await getPool().query(
|
||||
`UPDATE public.arsredovisning_signature_requests
|
||||
SET status = 'signed', signed_at = now(), annual_report_version_id = $2,
|
||||
signing_method = 'paper_original', evidence_reference = 'archive:A-1',
|
||||
evidence_recorded_by = $3, evidence_recorded_at = now()
|
||||
WHERE id = $1`,
|
||||
[signatureId, version.rows[0].id, owner.userId],
|
||||
)
|
||||
const signedVersion = await getPool().query(
|
||||
'SELECT status FROM public.annual_report_versions WHERE id = $1',
|
||||
[version.rows[0].id],
|
||||
)
|
||||
expect(signedVersion.rows[0].status).toBe('signed')
|
||||
await expect(
|
||||
getPool().query(
|
||||
`UPDATE public.arsredovisning_signature_requests
|
||||
SET evidence_reference = 'changed' WHERE id = $1`,
|
||||
[signatureId],
|
||||
),
|
||||
).rejects.toThrow(/cannot modify a signed signature request/i)
|
||||
})
|
||||
|
||||
it('rejects signature evidence after the linked version stops accepting signatures', async () => {
|
||||
const owner = await seedCompany()
|
||||
const signatureId = randomUUID()
|
||||
await getPool().query(
|
||||
`INSERT INTO public.arsredovisning_signature_requests
|
||||
(id, user_id, company_id, fiscal_period_id, role, signer_name)
|
||||
VALUES ($1, $2, $3, $4, 'Styrelseledamot', 'Anna Andersson')`,
|
||||
[signatureId, owner.userId, owner.companyId, owner.fiscalPeriodId],
|
||||
)
|
||||
const version = await createVersion({ ...owner, status: 'ready_for_signature' })
|
||||
await getPool().query(
|
||||
`UPDATE public.annual_report_versions
|
||||
SET status = 'superseded'
|
||||
WHERE id = $1`,
|
||||
[version.rows[0].id],
|
||||
)
|
||||
|
||||
await expect(
|
||||
getPool().query(
|
||||
`UPDATE public.arsredovisning_signature_requests
|
||||
SET status = 'signed', signed_at = now(),
|
||||
signing_method = 'paper_original', evidence_reference = 'archive:A-1',
|
||||
evidence_recorded_by = $2, evidence_recorded_at = now()
|
||||
WHERE id = $1`,
|
||||
[signatureId, owner.userId],
|
||||
),
|
||||
).rejects.toThrow(/annual report version is not ready for signature evidence/i)
|
||||
})
|
||||
|
||||
it('supersedes an older signed version when a corrected version is locked', async () => {
|
||||
const owner = await seedCompany()
|
||||
const signatureId = randomUUID()
|
||||
await getPool().query(
|
||||
`INSERT INTO public.arsredovisning_signature_requests
|
||||
(id, user_id, company_id, fiscal_period_id, role, signer_name)
|
||||
VALUES ($1, $2, $3, $4, 'Styrelseledamot', 'Anna Andersson')`,
|
||||
[signatureId, owner.userId, owner.companyId, owner.fiscalPeriodId],
|
||||
)
|
||||
const first = await createVersion({ ...owner, status: 'ready_for_signature' })
|
||||
await getPool().query(
|
||||
`UPDATE public.arsredovisning_signature_requests
|
||||
SET status = 'signed', signed_at = now(),
|
||||
signing_method = 'paper_original', evidence_reference = 'archive:A-1',
|
||||
evidence_recorded_by = $2, evidence_recorded_at = now()
|
||||
WHERE id = $1`,
|
||||
[signatureId, owner.userId],
|
||||
)
|
||||
|
||||
const correction = await createVersion({
|
||||
...owner,
|
||||
hash: 'b'.repeat(64),
|
||||
status: 'ready_for_signature',
|
||||
})
|
||||
expect(correction.rows[0]).toMatchObject({ version_number: 2, status: 'ready_for_signature' })
|
||||
|
||||
const previous = await getPool().query(
|
||||
'SELECT status FROM public.annual_report_versions WHERE id = $1',
|
||||
[first.rows[0].id],
|
||||
)
|
||||
expect(previous.rows[0].status).toBe('superseded')
|
||||
})
|
||||
|
||||
it('rejects validation rows linked to another company version', async () => {
|
||||
const first = await seedCompany()
|
||||
const second = await seedCompany()
|
||||
const version = await createVersion(first)
|
||||
await expect(
|
||||
getPool().query(
|
||||
`INSERT INTO public.annual_report_validation_runs
|
||||
(company_id, fiscal_period_id, version_id, user_id, validation_layer, status)
|
||||
VALUES ($1, $2, $3, $4, 'local', 'passed')`,
|
||||
[second.companyId, second.fiscalPeriodId, version.rows[0].id, second.userId],
|
||||
),
|
||||
).rejects.toThrow(/belongs to another company or period/i)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,72 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { insertAuthUser, insertCompany } from './fixtures'
|
||||
import { getPool } from './setup'
|
||||
|
||||
describe('API-key audit history', () => {
|
||||
it('skips request telemetry while retaining security and configuration changes', async () => {
|
||||
const userId = await insertAuthUser()
|
||||
const companyId = await insertCompany({ createdBy: userId })
|
||||
const apiKeyId = randomUUID()
|
||||
const keyHash = randomUUID().replaceAll('-', '')
|
||||
|
||||
await getPool().query(
|
||||
`INSERT INTO public.api_keys
|
||||
(id, user_id, company_id, key_hash, key_prefix, name, scopes)
|
||||
VALUES ($1, $2, $3, $4, 'gnubok_sk_test', 'Audit test key', $5)`,
|
||||
[apiKeyId, userId, companyId, keyHash, ['reports:read']],
|
||||
)
|
||||
|
||||
await getPool().query(
|
||||
`SELECT * FROM public.validate_and_increment_api_key($1)`,
|
||||
[keyHash],
|
||||
)
|
||||
await getPool().query(
|
||||
`SELECT * FROM public.validate_and_increment_api_key($1)`,
|
||||
[keyHash],
|
||||
)
|
||||
|
||||
const afterTelemetry = await getPool().query<{ action: string }>(
|
||||
`SELECT action
|
||||
FROM public.audit_log
|
||||
WHERE table_name = 'api_keys'
|
||||
AND record_id = $1
|
||||
ORDER BY created_at, id`,
|
||||
[apiKeyId],
|
||||
)
|
||||
expect(afterTelemetry.rows.map((row) => row.action)).toEqual(['INSERT'])
|
||||
|
||||
await getPool().query(
|
||||
`UPDATE public.api_keys
|
||||
SET name = 'Renamed audit test key',
|
||||
scopes = ARRAY['reports:read', 'customers:read']::text[]
|
||||
WHERE id = $1`,
|
||||
[apiKeyId],
|
||||
)
|
||||
|
||||
const afterConfigurationChange = await getPool().query<{
|
||||
action: string
|
||||
old_name: string | null
|
||||
new_name: string | null
|
||||
}>(
|
||||
`SELECT
|
||||
action,
|
||||
old_state ->> 'name' AS old_name,
|
||||
new_state ->> 'name' AS new_name
|
||||
FROM public.audit_log
|
||||
WHERE table_name = 'api_keys'
|
||||
AND record_id = $1
|
||||
ORDER BY created_at, id`,
|
||||
[apiKeyId],
|
||||
)
|
||||
|
||||
expect(afterConfigurationChange.rows).toEqual([
|
||||
{ action: 'INSERT', old_name: null, new_name: 'Audit test key' },
|
||||
{
|
||||
action: 'UPDATE',
|
||||
old_name: 'Audit test key',
|
||||
new_name: 'Renamed audit test key',
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,47 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { getPool } from './setup'
|
||||
import { insertAuthUser, insertCompany, insertFiscalPeriod } from './fixtures'
|
||||
|
||||
describe('BFL retention expiry', () => {
|
||||
it('retains a fiscal year through the end of the seventh following calendar year', async () => {
|
||||
const userId = await insertAuthUser()
|
||||
const companyId = await insertCompany({ createdBy: userId })
|
||||
const fiscalPeriodId = await insertFiscalPeriod({
|
||||
userId,
|
||||
companyId,
|
||||
periodStart: '2025-07-01',
|
||||
periodEnd: '2026-06-30',
|
||||
name: '2025/2026',
|
||||
})
|
||||
|
||||
const result = await getPool().query<{ retention_expires_at: string }>(
|
||||
`SELECT retention_expires_at::text
|
||||
FROM public.fiscal_periods
|
||||
WHERE id = $1`,
|
||||
[fiscalPeriodId],
|
||||
)
|
||||
|
||||
expect(result.rows[0].retention_expires_at).toBe('2034-01-01')
|
||||
})
|
||||
|
||||
it('recalculates the first allowed deletion date when an open period end changes', async () => {
|
||||
const userId = await insertAuthUser()
|
||||
const companyId = await insertCompany({ createdBy: userId })
|
||||
const fiscalPeriodId = await insertFiscalPeriod({ userId, companyId })
|
||||
|
||||
await getPool().query(
|
||||
`UPDATE public.fiscal_periods
|
||||
SET period_end = '2027-03-31'
|
||||
WHERE id = $1`,
|
||||
[fiscalPeriodId],
|
||||
)
|
||||
const result = await getPool().query<{ retention_expires_at: string }>(
|
||||
`SELECT retention_expires_at::text
|
||||
FROM public.fiscal_periods
|
||||
WHERE id = $1`,
|
||||
[fiscalPeriodId],
|
||||
)
|
||||
|
||||
expect(result.rows[0].retention_expires_at).toBe('2035-01-01')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,174 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { getPool, withUserContext } from './setup'
|
||||
import {
|
||||
insertAuthUser,
|
||||
insertCompany,
|
||||
insertCompanyMember,
|
||||
insertFiscalPeriod,
|
||||
seedCompany,
|
||||
} from './fixtures'
|
||||
|
||||
async function setActiveCompany(userId: string, companyId: string): Promise<void> {
|
||||
await getPool().query(
|
||||
`INSERT INTO public.user_preferences (user_id, active_company_id)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (user_id) DO UPDATE SET active_company_id = EXCLUDED.active_company_id`,
|
||||
[userId, companyId],
|
||||
)
|
||||
}
|
||||
|
||||
async function insertAdjustment(params: {
|
||||
companyId: string
|
||||
userId: string
|
||||
fiscalPeriodId: string
|
||||
}): Promise<string> {
|
||||
const id = randomUUID()
|
||||
await getPool().query(
|
||||
`INSERT INTO public.fiscal_period_tax_adjustments
|
||||
(id, company_id, user_id, fiscal_period_id, adjustment_type, source,
|
||||
source_key, description, account_number, amount, included)
|
||||
VALUES ($1, $2, $3, $4, 'non_deductible_expense', 'detected',
|
||||
'account:6992', 'Ej avdragsgill kostnad', '6992', 2994, true)`,
|
||||
[id, params.companyId, params.userId, params.fiscalPeriodId],
|
||||
)
|
||||
return id
|
||||
}
|
||||
|
||||
describe('fiscal_period_tax_adjustments constraints and RLS', () => {
|
||||
it('uses the standard UUID default and locks the fiscal period in the guard', async () => {
|
||||
const result = await getPool().query<{ default_expression: string; function_definition: string }>(
|
||||
`SELECT
|
||||
pg_get_expr(d.adbin, d.adrelid) AS default_expression,
|
||||
pg_get_functiondef('public.guard_fiscal_period_tax_adjustment()'::regprocedure)
|
||||
AS function_definition
|
||||
FROM pg_attrdef d
|
||||
JOIN pg_attribute a
|
||||
ON a.attrelid = d.adrelid AND a.attnum = d.adnum
|
||||
WHERE d.adrelid = 'public.fiscal_period_tax_adjustments'::regclass
|
||||
AND a.attname = 'id'`,
|
||||
)
|
||||
|
||||
expect(result.rows[0]?.default_expression).toContain('uuid_generate_v4')
|
||||
expect(result.rows[0]?.function_definition).toMatch(/FOR UPDATE/i)
|
||||
})
|
||||
|
||||
it('isolates adjustments by company membership', async () => {
|
||||
const owner = await seedCompany()
|
||||
const adjustmentId = await insertAdjustment(owner)
|
||||
const strangerId = await insertAuthUser()
|
||||
|
||||
const ownerView = await withUserContext(owner.userId, (client) =>
|
||||
client.query(
|
||||
'SELECT id FROM public.fiscal_period_tax_adjustments WHERE id = $1',
|
||||
[adjustmentId],
|
||||
),
|
||||
)
|
||||
expect(ownerView.rows).toHaveLength(1)
|
||||
|
||||
const strangerView = await withUserContext(strangerId, (client) =>
|
||||
client.query(
|
||||
'SELECT id FROM public.fiscal_period_tax_adjustments WHERE id = $1',
|
||||
[adjustmentId],
|
||||
),
|
||||
)
|
||||
expect(strangerView.rows).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('rejects a company that does not own the fiscal period', async () => {
|
||||
const first = await seedCompany()
|
||||
const second = await seedCompany()
|
||||
|
||||
await expect(
|
||||
insertAdjustment({
|
||||
companyId: first.companyId,
|
||||
userId: first.userId,
|
||||
fiscalPeriodId: second.fiscalPeriodId,
|
||||
}),
|
||||
).rejects.toThrow(/does not match fiscal period company/i)
|
||||
})
|
||||
|
||||
it('allows an owner to insert an adjustment for the active company', async () => {
|
||||
const owner = await seedCompany()
|
||||
await setActiveCompany(owner.userId, owner.companyId)
|
||||
|
||||
await withUserContext(owner.userId, async (client) => {
|
||||
const result = await client.query(
|
||||
`INSERT INTO public.fiscal_period_tax_adjustments
|
||||
(company_id, user_id, fiscal_period_id, adjustment_type, source,
|
||||
source_key, description, account_number, amount, included)
|
||||
VALUES ($1, $2, $3, 'non_deductible_expense', 'detected',
|
||||
'account:6992', 'Ej avdragsgill kostnad', '6992', 2994, true)
|
||||
RETURNING id`,
|
||||
[owner.companyId, owner.userId, owner.fiscalPeriodId],
|
||||
)
|
||||
expect(result.rows).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
it('blocks a viewer from inserting an adjustment directly under RLS', async () => {
|
||||
const owner = await seedCompany()
|
||||
const viewerId = await insertAuthUser()
|
||||
await insertCompanyMember({ companyId: owner.companyId, userId: viewerId, role: 'viewer' })
|
||||
await setActiveCompany(viewerId, owner.companyId)
|
||||
|
||||
await withUserContext(viewerId, async (client) => {
|
||||
await expect(
|
||||
client.query(
|
||||
`INSERT INTO public.fiscal_period_tax_adjustments
|
||||
(company_id, user_id, fiscal_period_id, adjustment_type, source,
|
||||
source_key, description, account_number, amount, included)
|
||||
VALUES ($1, $2, $3, 'non_deductible_expense', 'detected',
|
||||
'account:6992', 'Ej avdragsgill kostnad', '6992', 2994, true)`,
|
||||
[owner.companyId, viewerId, owner.fiscalPeriodId],
|
||||
),
|
||||
).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
it('blocks writes to a member company that is not the active company', async () => {
|
||||
const owner = await seedCompany()
|
||||
const otherCompanyId = await insertCompany({ createdBy: owner.userId })
|
||||
await insertCompanyMember({ companyId: otherCompanyId, userId: owner.userId, role: 'owner' })
|
||||
const otherPeriodId = await insertFiscalPeriod({
|
||||
userId: owner.userId,
|
||||
companyId: otherCompanyId,
|
||||
})
|
||||
await setActiveCompany(owner.userId, owner.companyId)
|
||||
|
||||
await withUserContext(owner.userId, async (client) => {
|
||||
await expect(
|
||||
client.query(
|
||||
`INSERT INTO public.fiscal_period_tax_adjustments
|
||||
(company_id, user_id, fiscal_period_id, adjustment_type, source,
|
||||
source_key, description, account_number, amount, included)
|
||||
VALUES ($1, $2, $3, 'non_deductible_expense', 'detected',
|
||||
'account:6992', 'Ej avdragsgill kostnad', '6992', 2994, true)`,
|
||||
[otherCompanyId, owner.userId, otherPeriodId],
|
||||
),
|
||||
).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
it('blocks adjustment changes after the fiscal period is locked', async () => {
|
||||
const owner = await seedCompany()
|
||||
const adjustmentId = await insertAdjustment(owner)
|
||||
await getPool().query(
|
||||
'UPDATE public.fiscal_periods SET locked_at = now() WHERE id = $1',
|
||||
[owner.fiscalPeriodId],
|
||||
)
|
||||
|
||||
await expect(
|
||||
getPool().query(
|
||||
'UPDATE public.fiscal_period_tax_adjustments SET amount = 3000 WHERE id = $1',
|
||||
[adjustmentId],
|
||||
),
|
||||
).rejects.toThrow(/locked for tax adjustments/i)
|
||||
await expect(
|
||||
getPool().query(
|
||||
'DELETE FROM public.fiscal_period_tax_adjustments WHERE id = $1',
|
||||
[adjustmentId],
|
||||
),
|
||||
).rejects.toThrow(/locked for tax adjustments/i)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,165 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { getPool } from './setup'
|
||||
import { insertAuthUser, insertCompany, insertFiscalPeriod } from './fixtures'
|
||||
|
||||
async function insertEntry(params: {
|
||||
userId: string
|
||||
companyId: string
|
||||
fiscalPeriodId: string
|
||||
voucherNumber: number
|
||||
entryDate: string
|
||||
lines: Array<{ account: string; debit: number; credit: number }>
|
||||
}): Promise<string> {
|
||||
const entryId = randomUUID()
|
||||
await getPool().query(
|
||||
`INSERT INTO public.journal_entries
|
||||
(id, user_id, company_id, fiscal_period_id, voucher_number, voucher_series,
|
||||
entry_date, description, source_type, status)
|
||||
VALUES ($1, $2, $3, $4, $5, 'A', $6, 'Production regression test', 'manual', 'posted')`,
|
||||
[
|
||||
entryId,
|
||||
params.userId,
|
||||
params.companyId,
|
||||
params.fiscalPeriodId,
|
||||
params.voucherNumber,
|
||||
params.entryDate,
|
||||
],
|
||||
)
|
||||
|
||||
for (const line of params.lines) {
|
||||
await getPool().query(
|
||||
`INSERT INTO public.journal_entry_lines
|
||||
(journal_entry_id, account_number, debit_amount, credit_amount)
|
||||
VALUES ($1, $2, $3, $4)`,
|
||||
[entryId, line.account, line.debit, line.credit],
|
||||
)
|
||||
}
|
||||
return entryId
|
||||
}
|
||||
|
||||
async function seedCompany() {
|
||||
const userId = await insertAuthUser()
|
||||
const companyId = await insertCompany({ createdBy: userId })
|
||||
const fiscalPeriodId = await insertFiscalPeriod({
|
||||
userId,
|
||||
companyId,
|
||||
periodStart: '2026-01-01',
|
||||
periodEnd: '2026-12-31',
|
||||
})
|
||||
return { userId, companyId, fiscalPeriodId }
|
||||
}
|
||||
|
||||
describe('production error regressions', () => {
|
||||
it('registers PendingOperationApproved in the processing history catalog', async () => {
|
||||
const { rows } = await getPool().query(
|
||||
`SELECT event_type
|
||||
FROM public.processing_event_types
|
||||
WHERE event_type = 'PendingOperationApproved'`,
|
||||
)
|
||||
|
||||
expect(rows).toEqual([{ event_type: 'PendingOperationApproved' }])
|
||||
})
|
||||
|
||||
it('aggregates period activity and excludes a specified opening entry', async () => {
|
||||
const ctx = await seedCompany()
|
||||
const openingId = await insertEntry({
|
||||
...ctx,
|
||||
voucherNumber: 1,
|
||||
entryDate: '2026-01-01',
|
||||
lines: [
|
||||
{ account: '1930', debit: 1_000, credit: 0 },
|
||||
{ account: '2010', debit: 0, credit: 1_000 },
|
||||
],
|
||||
})
|
||||
await insertEntry({
|
||||
...ctx,
|
||||
voucherNumber: 2,
|
||||
entryDate: '2026-03-15',
|
||||
lines: [
|
||||
{ account: '1930', debit: 250, credit: 0 },
|
||||
{ account: '3001', debit: 0, credit: 250 },
|
||||
],
|
||||
})
|
||||
|
||||
const { rows } = await getPool().query(
|
||||
`SELECT account_number, debit::text, credit::text
|
||||
FROM public.get_account_period_activity($1, $2, $3, $4, $5)`,
|
||||
[ctx.companyId, '2026-01-01', '2026-12-31', ['1930', '3001'], openingId],
|
||||
)
|
||||
|
||||
expect(rows).toEqual([
|
||||
{ account_number: '1930', debit: '250', credit: '0' },
|
||||
{ account_number: '3001', debit: '0', credit: '250' },
|
||||
])
|
||||
})
|
||||
|
||||
it('pages VAT source lines with a stable entry and line cursor', async () => {
|
||||
const ctx = await seedCompany()
|
||||
await insertEntry({
|
||||
...ctx,
|
||||
voucherNumber: 1,
|
||||
entryDate: '2026-03-01',
|
||||
lines: [
|
||||
{ account: '1930', debit: 125, credit: 0 },
|
||||
{ account: '2611', debit: 0, credit: 25 },
|
||||
{ account: '3001', debit: 0, credit: 100 },
|
||||
],
|
||||
})
|
||||
await insertEntry({
|
||||
...ctx,
|
||||
voucherNumber: 2,
|
||||
entryDate: '2026-03-02',
|
||||
lines: [
|
||||
{ account: '1930', debit: 250, credit: 0 },
|
||||
{ account: '2611', debit: 0, credit: 50 },
|
||||
{ account: '3001', debit: 0, credit: 200 },
|
||||
],
|
||||
})
|
||||
|
||||
const first = await getPool().query(
|
||||
`SELECT * FROM public.get_vat_ruta_source_lines(
|
||||
$1, $2, $3, $4, NULL, NULL, NULL, NULL, 1
|
||||
)`,
|
||||
[ctx.companyId, '2026-03-01', '2026-03-31', ['2611']],
|
||||
)
|
||||
expect(first.rows).toHaveLength(1)
|
||||
expect(first.rows[0].voucher_number).toBe(1)
|
||||
|
||||
const second = await getPool().query(
|
||||
`SELECT * FROM public.get_vat_ruta_source_lines(
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, 1
|
||||
)`,
|
||||
[
|
||||
ctx.companyId,
|
||||
'2026-03-01',
|
||||
'2026-03-31',
|
||||
['2611'],
|
||||
first.rows[0].entry_date,
|
||||
first.rows[0].voucher_number,
|
||||
first.rows[0].journal_entry_id,
|
||||
first.rows[0].line_id,
|
||||
],
|
||||
)
|
||||
expect(second.rows).toHaveLength(1)
|
||||
expect(second.rows[0].voucher_number).toBe(2)
|
||||
})
|
||||
|
||||
it('installs the covering indexes used by the timeout fixes', async () => {
|
||||
const { rows } = await getPool().query(
|
||||
`SELECT indexname
|
||||
FROM pg_indexes
|
||||
WHERE schemaname = 'public'
|
||||
AND indexname IN (
|
||||
'idx_audit_log_company_created_id',
|
||||
'idx_journal_entries_company_posted_date_id'
|
||||
)
|
||||
ORDER BY indexname`,
|
||||
)
|
||||
|
||||
expect(rows.map((row) => row.indexname)).toEqual([
|
||||
'idx_audit_log_company_created_id',
|
||||
'idx_journal_entries_company_posted_date_id',
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,111 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { getPool, withUserContext } from './setup'
|
||||
import { insertAuthUser, seedCompany } from './fixtures'
|
||||
|
||||
async function insertNotice(params: {
|
||||
companyId: string
|
||||
userId: string
|
||||
fiscalPeriodId: string
|
||||
decisionDate?: string
|
||||
paymentDueDate?: string
|
||||
}): Promise<string> {
|
||||
const id = randomUUID()
|
||||
await getPool().query(
|
||||
`INSERT INTO public.tax_assessment_notices
|
||||
(id, company_id, user_id, fiscal_period_id, decision_type, decision_date, payment_due_date)
|
||||
VALUES ($1, $2, $3, $4, 'final', $5, $6)`,
|
||||
[
|
||||
id,
|
||||
params.companyId,
|
||||
params.userId,
|
||||
params.fiscalPeriodId,
|
||||
params.decisionDate ?? '2026-07-01',
|
||||
params.paymentDueDate ?? '2026-10-12',
|
||||
],
|
||||
)
|
||||
return id
|
||||
}
|
||||
|
||||
describe('tax_assessment_notices constraints and RLS', () => {
|
||||
it('isolates notices by company membership', async () => {
|
||||
const owner = await seedCompany()
|
||||
const noticeId = await insertNotice(owner)
|
||||
const strangerId = await insertAuthUser()
|
||||
|
||||
const ownerView = await withUserContext(owner.userId, (client) =>
|
||||
client.query<{ id: string }>(
|
||||
`SELECT id FROM public.tax_assessment_notices WHERE id = $1`,
|
||||
[noticeId],
|
||||
),
|
||||
)
|
||||
expect(ownerView.rows).toHaveLength(1)
|
||||
|
||||
const strangerView = await withUserContext(strangerId, (client) =>
|
||||
client.query<{ id: string }>(
|
||||
`SELECT id FROM public.tax_assessment_notices WHERE id = $1`,
|
||||
[noticeId],
|
||||
),
|
||||
)
|
||||
expect(strangerView.rows).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('rejects inserts into another company', async () => {
|
||||
const owner = await seedCompany()
|
||||
const strangerId = await insertAuthUser()
|
||||
|
||||
await expect(withUserContext(strangerId, (client) =>
|
||||
client.query(
|
||||
`INSERT INTO public.tax_assessment_notices
|
||||
(company_id, user_id, fiscal_period_id, decision_type, decision_date, payment_due_date)
|
||||
VALUES ($1, $2, $3, 'final', '2026-07-01', '2026-10-12')`,
|
||||
[owner.companyId, strangerId, owner.fiscalPeriodId],
|
||||
),
|
||||
)).rejects.toThrow(/row-level security|policy/i)
|
||||
})
|
||||
|
||||
it('requires the exact due date to be on or after the decision date', async () => {
|
||||
const owner = await seedCompany()
|
||||
await expect(insertNotice({
|
||||
...owner,
|
||||
decisionDate: '2026-07-01',
|
||||
paymentDueDate: '2026-06-30',
|
||||
})).rejects.toThrow(/due_after_decision|check constraint/i)
|
||||
})
|
||||
|
||||
it('prevents deleting a notice referenced by a generated deadline', async () => {
|
||||
const owner = await seedCompany()
|
||||
const noticeId = await insertNotice(owner)
|
||||
await getPool().query(
|
||||
`INSERT INTO public.deadlines
|
||||
(user_id, company_id, title, due_date, deadline_type, source,
|
||||
tax_deadline_type, tax_period, tax_assessment_notice_id)
|
||||
VALUES ($1, $2, 'Kvarskatt', '2026-10-12', 'tax', 'system',
|
||||
'kvarskatt', $3, $4)`,
|
||||
[owner.userId, owner.companyId, `notice:${noticeId}`, noticeId],
|
||||
)
|
||||
|
||||
await expect(
|
||||
getPool().query(`DELETE FROM public.tax_assessment_notices WHERE id = $1`, [noticeId]),
|
||||
).rejects.toThrow(/foreign key|violates/i)
|
||||
})
|
||||
})
|
||||
|
||||
describe('company_settings initial setup state', () => {
|
||||
it('accepts supported paths and rejects unknown values', async () => {
|
||||
const owner = await seedCompany()
|
||||
await getPool().query(
|
||||
`INSERT INTO public.company_settings
|
||||
(user_id, company_id, onboarding_complete, initial_setup_path)
|
||||
VALUES ($1, $2, true, 'migration')`,
|
||||
[owner.userId, owner.companyId],
|
||||
)
|
||||
|
||||
await expect(
|
||||
getPool().query(
|
||||
`UPDATE public.company_settings SET initial_setup_path = 'unknown' WHERE company_id = $1`,
|
||||
[owner.companyId],
|
||||
),
|
||||
).rejects.toThrow(/initial_setup_path_check|check constraint/i)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user