Files
accounted/lib/salary/__tests__/ef-no-owner-employee.pg.test.ts
T
Jakob WennbergandClaude Sonnet 5 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

119 lines
4.7 KiB
TypeScript

import { randomUUID } from 'crypto'
import { describe, expect, it } from 'vitest'
import { insertAuthUser, insertCompany } from '@/tests/pg/fixtures'
import { getPool } from '@/tests/pg/setup'
/**
* Locks in the enforce_ef_no_owner_employee trigger (migration
* 20260628120000): an enskild firma may employ staff, but never put its owner
* or board on payroll (owner compensation is egna uttag, not lön). Ordinary
* employees stay allowed for every entity type. See #782.
*
* Inserts go through getPool() (superuser, bypasses RLS): this exercises the
* trigger, not tenant isolation.
*/
async function insertEmployee(params: {
userId: string
companyId: string
employmentType: string
}): Promise<string> {
const id = randomUUID()
await getPool().query(
`INSERT INTO public.employees
(id, user_id, company_id, first_name, last_name, personnummer,
personnummer_last4, employment_start, employment_type, monthly_salary,
tax_table_number)
VALUES ($1, $2, $3, 'Test', 'Person', '199001011234', '1234',
'2026-01-01', $4, 30000, 32)`,
[id, params.userId, params.companyId, params.employmentType],
)
return id
}
describe('enforce_ef_no_owner_employee.pg: owner/board payroll blocked for EF', () => {
it('rejects company_owner for an enskild firma', async () => {
const userId = await insertAuthUser()
const companyId = await insertCompany({ createdBy: userId, entityType: 'enskild_firma' })
await expect(
insertEmployee({ userId, companyId, employmentType: 'company_owner' }),
).rejects.toThrow(/kan inte ha sin ägare/i)
})
it('rejects board_member for an enskild firma', async () => {
const userId = await insertAuthUser()
const companyId = await insertCompany({ createdBy: userId, entityType: 'enskild_firma' })
await expect(
insertEmployee({ userId, companyId, employmentType: 'board_member' }),
).rejects.toThrow(/kan inte ha sin ägare/i)
})
it('allows ordinary employees for an enskild firma', async () => {
const userId = await insertAuthUser()
const companyId = await insertCompany({ createdBy: userId, entityType: 'enskild_firma' })
await expect(
insertEmployee({ userId, companyId, employmentType: 'employee' }),
).resolves.not.toThrow()
})
it('allows company_owner for an aktiebolag', async () => {
const userId = await insertAuthUser()
const companyId = await insertCompany({ createdBy: userId, entityType: 'aktiebolag' })
await expect(
insertEmployee({ userId, companyId, employmentType: 'company_owner' }),
).resolves.not.toThrow()
})
it('uses company_settings.entity_type over companies (settings is read-primary)', async () => {
const userId = await insertAuthUser()
// companies says aktiebolag, but the user re-classified to EF in settings.
const companyId = await insertCompany({ createdBy: userId, entityType: 'aktiebolag' })
await getPool().query(
`INSERT INTO public.company_settings (user_id, company_id, entity_type)
VALUES ($1, $2, 'enskild_firma')`,
[userId, companyId],
)
await expect(
insertEmployee({ userId, companyId, employmentType: 'company_owner' }),
).rejects.toThrow(/kan inte ha sin ägare/i)
})
})
describe('enforce_ef_no_owner_employee.pg: UPDATE semantics', () => {
it('rejects changing an EF employee to company_owner', async () => {
const userId = await insertAuthUser()
const companyId = await insertCompany({ createdBy: userId, entityType: 'enskild_firma' })
const empId = await insertEmployee({ userId, companyId, employmentType: 'employee' })
await expect(
getPool().query(
`UPDATE public.employees SET employment_type = 'company_owner' WHERE id = $1`,
[empId],
),
).rejects.toThrow(/kan inte ha sin ägare/i)
})
it('allows unrelated edits to an EF employee (trigger fires only on employment_type)', async () => {
const userId = await insertAuthUser()
const companyId = await insertCompany({ createdBy: userId, entityType: 'enskild_firma' })
const empId = await insertEmployee({ userId, companyId, employmentType: 'employee' })
await expect(
getPool().query(
`UPDATE public.employees SET monthly_salary = 42000 WHERE id = $1`,
[empId],
),
).resolves.not.toThrow()
})
it('allows changing an aktiebolag employee to company_owner', async () => {
const userId = await insertAuthUser()
const companyId = await insertCompany({ createdBy: userId, entityType: 'aktiebolag' })
const empId = await insertEmployee({ userId, companyId, employmentType: 'employee' })
await expect(
getPool().query(
`UPDATE public.employees SET employment_type = 'company_owner' WHERE id = $1`,
[empId],
),
).resolves.not.toThrow()
})
})