fix(assistant): the salary fact follows the ledger, never the column default (#2290)
* 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>
This commit is contained in:
co-authored by
Claude Fable 5.1
Jakob Wennberg
parent
e684606b23
commit
0bd3c27fba
@@ -0,0 +1,62 @@
|
||||
-- A booked salary run is evidence that the company pays salaries (support
|
||||
-- case 2026-09-04: the assistant told a payroll-running aktiebolag in every
|
||||
-- answer that it "betalar inte löner").
|
||||
--
|
||||
-- company_settings.pays_salaries is NOT NULL DEFAULT false and its only
|
||||
-- writer is the Skatt settings form, so for every company that never opened
|
||||
-- that form the flag reads false regardless of what the ledger says, while
|
||||
-- the app itself books the salary verifikat. The flag feeds the assistant's
|
||||
-- profile block, the composer, the MCP company resource, the Personal menu
|
||||
-- for enskild firma and (as the fallback for employer_registered) the AGI
|
||||
-- deadline reminders, so the stale default is visible in five places.
|
||||
--
|
||||
-- 20260717151000 already treated in-app payroll as employer evidence once,
|
||||
-- as a one-off backfill of employer_registered from salary_runs. This makes
|
||||
-- the same rule continuous and extends it to pays_salaries, at the one place
|
||||
-- every writer (dashboard, MCP executor, v1 API, seeders) passes through:
|
||||
-- the run flipping to 'booked'.
|
||||
--
|
||||
-- employer_registered is only filled when NULL (never attested): an explicit
|
||||
-- false is the user's own answer and stays theirs. SECURITY DEFINER because
|
||||
-- company_settings_update requires company admin, while any member with
|
||||
-- write access can book payroll; the function touches only the booking
|
||||
-- company's own row, and Postgres refuses to call a trigger function
|
||||
-- directly, so it is not reachable through the API.
|
||||
-- pg-test: covered-by tests/pg/salary-run-booked-marks-employer.pg.test.ts
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.mark_company_pays_salaries_on_booked_run()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
BEGIN
|
||||
UPDATE public.company_settings
|
||||
SET pays_salaries = true,
|
||||
employer_registered = COALESCE(employer_registered, true)
|
||||
WHERE company_id = NEW.company_id
|
||||
AND (pays_salaries = false OR employer_registered IS NULL);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DROP TRIGGER IF EXISTS salary_runs_booked_marks_employer ON public.salary_runs;
|
||||
CREATE TRIGGER salary_runs_booked_marks_employer
|
||||
AFTER INSERT OR UPDATE OF status ON public.salary_runs
|
||||
FOR EACH ROW
|
||||
WHEN (NEW.status = 'booked')
|
||||
EXECUTE FUNCTION public.mark_company_pays_salaries_on_booked_run();
|
||||
|
||||
-- Backfill: companies that already booked payroll in the app. Archived
|
||||
-- migration-reset sources are skipped: block_migration_reset_source_mutation()
|
||||
-- makes their rows immutable and would abort the migration.
|
||||
UPDATE public.company_settings cs
|
||||
SET pays_salaries = true,
|
||||
employer_registered = COALESCE(cs.employer_registered, true)
|
||||
FROM (SELECT DISTINCT company_id FROM public.salary_runs WHERE status = 'booked') sr
|
||||
WHERE sr.company_id = cs.company_id
|
||||
AND (cs.pays_salaries = false OR cs.employer_registered IS NULL)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM public.company_migration_resets r
|
||||
WHERE r.source_company_id = cs.company_id
|
||||
);
|
||||
Reference in New Issue
Block a user