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:
Mattsson
2026-07-21 23:00:15 +02:00
committed by GitHub
parent 702512437a
commit e11f70b347
467 changed files with 17569 additions and 2221 deletions
+2
View File
@@ -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,
+537
View File
@@ -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)
})
})
+72
View File
@@ -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',
},
])
})
})
+47
View File
@@ -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',
])
})
})
+111
View File
@@ -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)
})
})