Files
accounted/lib/bookkeeping/__tests__/voucher-series-defaults.pg.test.ts
T
bf7773d74b feat(settings): standard verifikationsserier for new companies, opt-in action for existing (#2358)
* feat(settings): standard verifikationsserier for new companies, opt-in action for existing (#2184)

A new company_settings row now defaults to the standard series set
(A manual/bank, B kundfakturor, C inbetalningar, D leverantörsfakturor,
E utbetalningar, H periodisering, I bokslut, K lön, L kontantfaktura,
M moms) instead of everything on A. The set lives once, as the
exhaustive STANDARD_VOUCHER_SERIES_MAP in the resolver; a pg-real test
holds the column default equal to it and to the source_type CHECK.

Existing rows are not remapped: the per-type settings form gets an
"Använd standarduppsättningen" action that fills the set for review
and save through the existing PUT, so the switch is a deliberate,
audited act rather than a mid-year numbering change nobody decided.
Payment rows bound to the other bokföringsmetod are dimmed, not hidden.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LvMaHcTnwAfxzgYD1fGYX1

* test(bookkeeping): fresh company_settings row asserts the standard series set, not all-A

voucher-series-defaults.pg.test.ts codified the pre-#2184 column default
(every source type on A). Migration 20260906210500 replaces that default
with the standard set, so the "freshly inserted row" case now asserts the
representative letters and full equality with STANDARD_VOUCHER_SERIES_MAP.
The explicit-override case keeps proving a company's own layout replaces
the default wholesale. No other pg or tool test asserted on the old map.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LvMaHcTnwAfxzgYD1fGYX1

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 21:18:22 +02:00

106 lines
4.1 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { getPool } from '@/tests/pg/setup'
import { insertAuthUser, insertCompany, insertCompanyMember } from '@/tests/pg/fixtures'
import { STANDARD_VOUCHER_SERIES_MAP } from '@/lib/bookkeeping/voucher-series-resolver'
describe('company_settings.default_voucher_series_per_source_type', () => {
it('column exists with the expected default JSONB shape', async () => {
const result = await getPool().query<{ column_default: string | null }>(
`SELECT column_default
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'company_settings'
AND column_name = 'default_voucher_series_per_source_type'`,
)
expect(result.rows).toHaveLength(1)
// The default is a JSONB literal cast: we don't pin the exact whitespace,
// just verify the migration installed a default that includes the expected
// source_type keys.
const defaultValue = result.rows[0]?.column_default ?? ''
expect(defaultValue).toMatch(/jsonb/)
expect(defaultValue).toMatch(/manual/)
expect(defaultValue).toMatch(/supplier_invoice_registered/)
expect(defaultValue).toMatch(/salary_payment/)
})
// Since migration 20260906210500 (#2184) a fresh row starts on the standard
// set, not on all-A; the exhaustive equality with the TS map and the
// "existing rows untouched" rule live in
// tests/pg/voucher-series-standard-default.pg.test.ts. This test keeps the
// representative letters readable at a glance.
it('a freshly inserted company_settings row gets the standard series set', async () => {
const userId = await insertAuthUser()
const companyId = await insertCompany({ createdBy: userId })
await insertCompanyMember({ companyId, userId, role: 'owner' })
// upsert in case a trigger has already created the row.
await getPool().query(
`INSERT INTO public.company_settings (user_id, company_id)
VALUES ($1, $2)
ON CONFLICT (company_id) DO NOTHING`,
[userId, companyId],
)
const { rows } = await getPool().query<{
default_voucher_series_per_source_type: Record<string, string>
}>(
`SELECT default_voucher_series_per_source_type
FROM public.company_settings WHERE company_id = $1`,
[companyId],
)
expect(rows).toHaveLength(1)
const map = rows[0]!.default_voucher_series_per_source_type
expect(map).toBeDefined()
// A stays the general series; the reskontra, lön and moms flows get their own.
expect(map.manual).toBe('A')
expect(map.bank_transaction).toBe('A')
expect(map.invoice_created).toBe('B')
expect(map.invoice_paid).toBe('C')
expect(map.supplier_invoice_registered).toBe('D')
expect(map.supplier_invoice_paid).toBe('E')
expect(map.salary_payment).toBe('K')
expect(map.vat_settlement).toBe('M')
expect(map).toEqual(STANDARD_VOUCHER_SERIES_MAP)
})
it('accepts user updates to per-source-type series mapping', async () => {
const userId = await insertAuthUser()
const companyId = await insertCompany({ createdBy: userId })
await insertCompanyMember({ companyId, userId, role: 'owner' })
await getPool().query(
`INSERT INTO public.company_settings (user_id, company_id)
VALUES ($1, $2)
ON CONFLICT (company_id) DO NOTHING`,
[userId, companyId],
)
// An explicit layout of the company's own (Björn Lundén style letters)
// replaces the default wholesale; nothing merges the standard set back in.
const updated = {
manual: 'A',
supplier_invoice_registered: 'L',
supplier_invoice_paid: 'U',
salary_payment: 'N',
}
await getPool().query(
`UPDATE public.company_settings
SET default_voucher_series_per_source_type = $1::jsonb
WHERE company_id = $2`,
[JSON.stringify(updated), companyId],
)
const { rows } = await getPool().query<{
default_voucher_series_per_source_type: Record<string, string>
}>(
`SELECT default_voucher_series_per_source_type
FROM public.company_settings WHERE company_id = $1`,
[companyId],
)
expect(rows[0]!.default_voucher_series_per_source_type).toEqual(updated)
})
})