Files
accounted/tests/pg/get_unlinked_1930_lines.pg.test.ts
T
Jakob Wennberg ec27228a8e style: remove em/en dashes repo-wide, add CLAUDE.md rule against them (#890)
Em dashes (—) and en dashes (–) had spread across comments, docs, tests,
and a few UI strings, reading as AI-generated boilerplate rather than
house style. Replaced each with punctuation matching its context: colon
for explanatory clauses, comma for asides, plain hyphen for numeric/legal
ranges (e.g. "21-23§"), "to"/"till" for date ranges, parentheses for
paired-dash asides. messages/en.json and messages/sv.json were fixed by
hand together to keep sv/en in sync.

Left untouched where the dash is the functional subject rather than
decorative punctuation: date-range-parser.ts's separator regex,
charset-repair.ts's CP1252 byte-mapping table (and its test), the SIE
encoding mojibake docs, generic-csv.ts's minus-sign normalizer, the
agent system-prompt files that already instruct against em dashes, and
a golden iXBRL test fixture compared byte-for-byte.

Also fixes two bugs surfaced along the way: an off-by-one in
ApiKeysPanel's scope-label split (a leftover from an earlier partial
pass), and a charset-repair test that had lost the literal en-dash it
exists to verify.

Regenerated the agent atom seed migration (skills:generate) since 27
SKILL.md files changed. Added a CLAUDE.md rule against em/en dashes,
with an explicit carve-out for the functional-dash cases above.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 15:58:06 +02:00

210 lines
7.8 KiB
TypeScript

/**
* pg-real test for get_unlinked_gl_lines.
*
* Verifies the RPC excludes opening_balance vouchers from the unmatched-1930
* set, while preserving the existing behavior for posted bank-import vouchers,
* date-range filtering, and company scoping. The RPC was renamed from
* get_unlinked_1930_lines in 20260519120000_get_unlinked_gl_lines.sql; the
* default p_account_number of '1930' keeps these assertions valid.
*/
import { describe, it, expect } from 'vitest'
import { randomUUID } from 'node:crypto'
import { getPool } from './setup'
import { insertAuthUser, insertCompany, insertFiscalPeriod } from './fixtures'
async function insertPostedJournalEntry(params: {
userId: string
companyId: string
fiscalPeriodId: string
entryDate: string
sourceType: 'opening_balance' | 'manual' | 'bank_transaction' | 'import' | 'storno' | 'correction'
voucherNumber: number
amount?: number
}): Promise<string> {
const id = randomUUID()
const amount = params.amount ?? 1000
// Insert as posted directly. This bypasses commit_journal_entry's voucher
// sequencing; that's fine for testing the read-side RPC, which only cares
// about (account_number, status, source_type, date_range, link presence).
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, $7, $8, 'posted')`,
[
id,
params.userId,
params.companyId,
params.fiscalPeriodId,
params.voucherNumber,
params.entryDate,
`Test ${params.sourceType}`,
params.sourceType,
],
)
// Balanced pair on 1930 + 2091 (balanserad vinst/förlust, the realistic
// carried-forward counterpart for an IB on a bank account; harmless for the
// other source_types where the test only cares about the 1930 side).
await getPool().query(
`INSERT INTO public.journal_entry_lines
(journal_entry_id, account_number, debit_amount, credit_amount)
VALUES ($1, '1930', $2, 0),
($1, '2091', 0, $2)`,
[id, amount],
)
return id
}
describe('get_unlinked_gl_lines RPC: opening_balance exclusion', () => {
it('excludes opening_balance vouchers from the unmatched-1930 set', async () => {
const userId = await insertAuthUser()
const companyId = await insertCompany({ createdBy: userId })
const fiscalPeriodId = await insertFiscalPeriod({
userId,
companyId,
periodStart: '2026-01-01',
periodEnd: '2026-12-31',
})
// Three vouchers on 1930, all posted, none linked to a transaction:
// IB voucher (source_type='opening_balance') : should be EXCLUDED
// Bank import voucher : should be RETURNED
// Manual voucher : should be RETURNED
await insertPostedJournalEntry({
userId, companyId, fiscalPeriodId,
entryDate: '2026-01-01',
sourceType: 'opening_balance',
voucherNumber: 1,
amount: 50000,
})
const bankEntryId = await insertPostedJournalEntry({
userId, companyId, fiscalPeriodId,
entryDate: '2026-03-15',
sourceType: 'bank_transaction',
voucherNumber: 2,
amount: 1500,
})
const manualEntryId = await insertPostedJournalEntry({
userId, companyId, fiscalPeriodId,
entryDate: '2026-04-20',
sourceType: 'manual',
voucherNumber: 3,
amount: 200,
})
const { rows } = await getPool().query(
`SELECT journal_entry_id, source_type FROM public.get_unlinked_gl_lines($1)`,
[companyId],
)
const returnedIds = new Set(rows.map((r) => r.journal_entry_id))
expect(returnedIds.has(bankEntryId)).toBe(true)
expect(returnedIds.has(manualEntryId)).toBe(true)
// IB voucher should NOT be returned regardless of company/date scope.
expect(rows.find((r) => r.source_type === 'opening_balance')).toBeUndefined()
})
it('excludes storno and correction vouchers from the unmatched-1930 set', async () => {
const userId = await insertAuthUser()
const companyId = await insertCompany({ createdBy: userId })
const fiscalPeriodId = await insertFiscalPeriod({
userId,
companyId,
periodStart: '2026-01-01',
periodEnd: '2026-12-31',
})
// A storno and a correction voucher on 1930 (the products of the correctEntry
// flow), plus a normal bank voucher. Stornos/corrections are book-only
// reversals with no bank-feed counterpart: they must be EXCLUDED so a
// reconciled period doesn't show them as omatchade verifikationer.
await insertPostedJournalEntry({
userId, companyId, fiscalPeriodId,
entryDate: '2026-05-02', sourceType: 'storno', voucherNumber: 20, amount: 25000,
})
await insertPostedJournalEntry({
userId, companyId, fiscalPeriodId,
entryDate: '2026-05-02', sourceType: 'correction', voucherNumber: 21, amount: 25000,
})
const bankEntryId = await insertPostedJournalEntry({
userId, companyId, fiscalPeriodId,
entryDate: '2026-05-03', sourceType: 'bank_transaction', voucherNumber: 22, amount: 1500,
})
const { rows } = await getPool().query(
`SELECT journal_entry_id, source_type FROM public.get_unlinked_gl_lines($1)`,
[companyId],
)
const returnedIds = new Set(rows.map((r) => r.journal_entry_id))
expect(returnedIds.has(bankEntryId)).toBe(true)
expect(rows.find((r) => r.source_type === 'storno')).toBeUndefined()
expect(rows.find((r) => r.source_type === 'correction')).toBeUndefined()
})
it('still applies date_from / date_to filtering', async () => {
const userId = await insertAuthUser()
const companyId = await insertCompany({ createdBy: userId })
const fiscalPeriodId = await insertFiscalPeriod({
userId,
companyId,
periodStart: '2026-01-01',
periodEnd: '2026-12-31',
})
await insertPostedJournalEntry({
userId, companyId, fiscalPeriodId,
entryDate: '2026-02-01',
sourceType: 'bank_transaction',
voucherNumber: 10,
})
await insertPostedJournalEntry({
userId, companyId, fiscalPeriodId,
entryDate: '2026-08-01',
sourceType: 'bank_transaction',
voucherNumber: 11,
})
// Window covers only the second voucher. Use named notation so we don't have
// to repeat the '1930' default just to reach the date params.
const { rows } = await getPool().query(
`SELECT entry_date FROM public.get_unlinked_gl_lines(
p_company_id => $1, p_date_from => $2, p_date_to => $3
) ORDER BY entry_date`,
[companyId, '2026-07-01', '2026-12-31'],
)
expect(rows).toHaveLength(1)
expect(rows[0].entry_date.toISOString().slice(0, 10)).toBe('2026-08-01')
})
it('scopes to the requested company only', async () => {
const userA = await insertAuthUser()
const userB = await insertAuthUser()
const companyA = await insertCompany({ createdBy: userA, name: 'A' })
const companyB = await insertCompany({ createdBy: userB, name: 'B' })
const fpA = await insertFiscalPeriod({ userId: userA, companyId: companyA })
const fpB = await insertFiscalPeriod({ userId: userB, companyId: companyB })
await insertPostedJournalEntry({
userId: userA, companyId: companyA, fiscalPeriodId: fpA,
entryDate: '2026-03-01', sourceType: 'bank_transaction', voucherNumber: 1,
})
await insertPostedJournalEntry({
userId: userB, companyId: companyB, fiscalPeriodId: fpB,
entryDate: '2026-03-01', sourceType: 'bank_transaction', voucherNumber: 1,
})
const { rows: rowsA } = await getPool().query(
`SELECT 1 FROM public.get_unlinked_gl_lines($1)`,
[companyA],
)
const { rows: rowsB } = await getPool().query(
`SELECT 1 FROM public.get_unlinked_gl_lines($1)`,
[companyB],
)
expect(rowsA).toHaveLength(1)
expect(rowsB).toHaveLength(1)
})
})