0bd3c27fba
* fix(assistant): the salary fact follows the ledger, never the column default The in-app assistant told a payroll-running aktiebolag in every answer that it "betalar inte löner" (support case 2026-09-04). company_settings. pays_salaries is NOT NULL DEFAULT false and only the Skatt settings form writes it, so for every company that never opened that form the flag reads false whatever the ledger says, and lib/agent/ask/snapshot.ts asserted that default as a fact. - Trigger salary_runs_booked_marks_employer (20260904191000): a booked salary run sets pays_salaries = true and fills a never-attested employer_registered, at the one place every writer (dashboard, MCP, v1, seeders) passes through. Backfill for the 12 companies on prod already booking payroll with the flag at its default (8 of them also lacked the employer flag, and with it their AGI deadline reminders). An explicit employer_registered = false stays the user's answer. - The assistant snapshot applies the composer's employee-facts doctrine: positive evidence (active employees, the flag, an attested employer registration) yields the fact, only an attested negative yields the negative, the default yields nothing. It also names Inställningar > Skatt / > Bokföring so the model can point at the page. - The composer's KÄNDA FAKTA no longer prints "Betalar ut lön: nej" from the same default. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S62AGZwsMoBc8x8obBDVqE * fix(migration): NOT EXISTS instead of NOT IN for the reset-source exclusion A NULL source_company_id in the subquery would make NOT IN never true and silently skip the whole backfill. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S62AGZwsMoBc8x8obBDVqE --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
82 lines
3.6 KiB
TypeScript
82 lines
3.6 KiB
TypeScript
/**
|
|
* pg-real tests for 20260904191000_salary_run_booked_marks_employer.sql.
|
|
*
|
|
* Booking a salary run is evidence that the company pays salaries: the
|
|
* trigger flips company_settings.pays_salaries and fills a never-attested
|
|
* employer_registered, but never overrides an explicit employer answer.
|
|
*/
|
|
import { randomUUID } from 'node:crypto'
|
|
import { describe, expect, it } from 'vitest'
|
|
import { getPool } from '@/tests/pg/setup'
|
|
import { insertAuthUser, insertCompany } from '@/tests/pg/fixtures'
|
|
|
|
async function seed(settings: { paysSalaries?: boolean; employerRegistered?: boolean | null } = {}) {
|
|
const userId = await insertAuthUser()
|
|
const companyId = await insertCompany({ createdBy: userId })
|
|
await getPool().query(
|
|
`INSERT INTO public.company_settings (user_id, company_id, pays_salaries, employer_registered)
|
|
VALUES ($1, $2, $3, $4)`,
|
|
[userId, companyId, settings.paysSalaries ?? false, settings.employerRegistered ?? null],
|
|
)
|
|
return { userId, companyId }
|
|
}
|
|
|
|
async function insertRun(companyId: string, userId: string, status: string, month = 6): Promise<string> {
|
|
const runId = randomUUID()
|
|
await getPool().query(
|
|
`INSERT INTO public.salary_runs (id, company_id, user_id, period_year, period_month, payment_date, status)
|
|
VALUES ($1, $2, $3, 2026, $4, '2026-06-25', $5)`,
|
|
[runId, companyId, userId, month, status],
|
|
)
|
|
return runId
|
|
}
|
|
|
|
async function readFlags(companyId: string) {
|
|
const { rows } = await getPool().query<{ pays_salaries: boolean; employer_registered: boolean | null }>(
|
|
`SELECT pays_salaries, employer_registered FROM public.company_settings WHERE company_id = $1`,
|
|
[companyId],
|
|
)
|
|
return rows[0]!
|
|
}
|
|
|
|
describe('salary_runs_booked_marks_employer trigger', () => {
|
|
it('leaves the flags at their defaults while the run is a draft', async () => {
|
|
const { companyId, userId } = await seed()
|
|
await insertRun(companyId, userId, 'draft')
|
|
expect(await readFlags(companyId)).toEqual({ pays_salaries: false, employer_registered: null })
|
|
})
|
|
|
|
it('marks the company as paying salaries when a run is booked', async () => {
|
|
const { companyId, userId } = await seed()
|
|
const runId = await insertRun(companyId, userId, 'draft')
|
|
await getPool().query(`UPDATE public.salary_runs SET status = 'booked' WHERE id = $1`, [runId])
|
|
expect(await readFlags(companyId)).toEqual({ pays_salaries: true, employer_registered: true })
|
|
})
|
|
|
|
it('also fires for a run inserted directly as booked', async () => {
|
|
const { companyId, userId } = await seed()
|
|
await insertRun(companyId, userId, 'booked')
|
|
expect(await readFlags(companyId)).toEqual({ pays_salaries: true, employer_registered: true })
|
|
})
|
|
|
|
it('never overrides an explicitly attested employer_registered = false', async () => {
|
|
const { companyId, userId } = await seed({ employerRegistered: false })
|
|
await insertRun(companyId, userId, 'booked')
|
|
expect(await readFlags(companyId)).toEqual({ pays_salaries: true, employer_registered: false })
|
|
})
|
|
|
|
it('is a no-op for a company whose flags are already set', async () => {
|
|
const { companyId, userId } = await seed({ paysSalaries: true, employerRegistered: true })
|
|
const before = await getPool().query<{ updated_at: string }>(
|
|
`SELECT updated_at FROM public.company_settings WHERE company_id = $1`,
|
|
[companyId],
|
|
)
|
|
await insertRun(companyId, userId, 'booked')
|
|
const after = await getPool().query<{ updated_at: string }>(
|
|
`SELECT updated_at FROM public.company_settings WHERE company_id = $1`,
|
|
[companyId],
|
|
)
|
|
expect(after.rows[0]!.updated_at).toEqual(before.rows[0]!.updated_at)
|
|
})
|
|
})
|