fix(vat): make the ruta drill-down reconcile with the figure it explains (#2016)

* fix(vat): make the ruta drill-down reconcile with the figure it explains

get_vat_declaration_totals drops four classes of entry before summing: posted
closing entries, source_type 'vat_settlement', the two kontantmetod year-end
reversals, and anything shaped like a momsredovisning. The drill-down behind
each ruta filtered on company, status and date only.

So expanding a ruta listed verifikat that are not in the number it claims to
explain, and the panel shows no total that would reveal the mismatch. On
production, 322 posted/reversed entries carrying 26xx lines across 214
companies sit in those excluded classes.

A momsdeklaration is räkenskapsinformation under BFL 5 kap. and this
drill-down is what a consultant uses to substantiate a filed figure, so the
two have to agree exactly.

The exclusion CTEs are lifted verbatim from the figure rather than re-derived,
because any divergence reintroduces exactly this bug. The new pg test asserts
the equality for the whole account set at once, so editing one function and
not the other fails CI instead of silently misreporting.

opening_balance entries are deliberately kept: the figure exempts them from
its `shaped` set, which leaves their lines in the totals, so excluding them
here would break the equality in the other direction. That has its own test.

Verified the test catches the defect by reinstalling the old function body and
watching it fail with the real numbers (2611: drill-down 250/240 vs figure
0/200), then restoring.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(vat): update the existing drill-down pg test to the new signature

get_vat_ruta_source_lines gained p_ruta_accounts / p_net_accounts, and
production-error-regressions.pg.test.ts still called the old 9-argument form,
so pg-real failed with 42883 "function does not exist". I had grepped app/,
lib/ and extensions/ for callers and not tests/.

Neither fixture in that paging test is settlement-shaped, so paging behaviour
is unchanged; the equality itself is covered by the new reconcile test.

Also documents, in the tool-pg reset script, that its blanket grant to `anon`
(which PostgREST requires) makes that database invalid for the pg-real suite:
~29 of those files assert least privilege and fail there even on unmodified
main. That cost a confusing local run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-08-29 00:29:11 +02:00
committed by GitHub
co-authored by Claude Opus 5 Jakob Wennberg
parent 4e1eb3d662
commit 338ac4e913
7 changed files with 415 additions and 4 deletions
@@ -0,0 +1,155 @@
-- Make the VAT ruta drill-down return exactly the lines the filed figure sums.
--
-- `get_vat_declaration_totals` (the FIGURE, migration 20260813124510) drops
-- four classes of entry before summing: posted closing entries, source_type
-- 'vat_settlement', the two kontantmetod year-end reversals, and any entry
-- shaped like a momsredovisning (a line on a ruta account AND a line on
-- 2650/1650). `get_vat_ruta_source_lines` (the DRILL-DOWN, migration
-- 20260721103000) filtered on company, status and date only, so expanding a
-- ruta listed verifikat that are not in the number it claims to explain, with
-- no total on the panel to reveal the mismatch.
--
-- Measured on production 2026-08-28: 322 posted/reversed entries carrying 26xx
-- lines across 214 companies sit in those excluded classes. A momsdeklaration
-- is räkenskapsinformation (BFL 5 kap.), and this drill-down is what a
-- consultant uses to substantiate a filed figure, so the two must agree
-- exactly.
--
-- The exclusion CTEs below are lifted VERBATIM from get_vat_declaration_totals
-- rather than re-derived. That is deliberate: any divergence between the two
-- reintroduces exactly this bug, and a copy that reads identically is easy to
-- diff when the figure changes. tests/pg/vat-ruta-drilldown-reconcile.pg.test.ts
-- asserts the equality directly, so a future edit to one and not the other
-- fails CI rather than silently misreporting.
--
-- Settlement-shape is detected against journal_entry_lines directly instead of
-- through the figure's `vat_lines` CTE. That is equivalent, not a shortcut:
-- the figure passes p_ruta_accounts = VAT_ACCOUNTS and p_net_accounts =
-- ['2650','1650'], both strict subsets of its p_accounts, so restricting to
-- vat_lines first cannot change which entries match. It lets the drill-down
-- keep p_accounts meaning "the accounts of the ruta being expanded" without a
-- fourth account parameter.
--
-- opening_balance entries are deliberately NOT excluded. They are excluded
-- from the figure's `shaped` set, which keeps their lines IN the totals, so
-- dropping them here would break the equality in the other direction.
--
-- pg-test: tests/pg/vat-ruta-drilldown-reconcile.pg.test.ts
-- The signature gains p_ruta_accounts / p_net_accounts. DROP first: adding
-- parameters with CREATE OR REPLACE registers a second overload, and PostgREST
-- then cannot choose between them (documented in 20260421140000).
DROP FUNCTION IF EXISTS public.get_vat_ruta_source_lines(
uuid, date, date, text[], date, integer, uuid, uuid, integer
);
-- OR REPLACE on the NEW signature, so re-running this file is a no-op rather
-- than "function already exists with same argument types". The DROP above is
-- what removes the old arity; this cannot reintroduce an overload.
CREATE OR REPLACE FUNCTION public.get_vat_ruta_source_lines(
p_company_id uuid,
p_start date,
p_end date,
-- Accounts of the ruta being expanded: which lines to RETURN.
p_accounts text[],
-- Settlement-shape detectors, mirroring the figure's own parameters.
p_ruta_accounts text[],
p_net_accounts text[],
p_cursor_date date DEFAULT NULL,
p_cursor_voucher_number integer DEFAULT NULL,
p_cursor_entry_id uuid DEFAULT NULL,
p_cursor_line_id uuid DEFAULT NULL,
p_limit integer DEFAULT 501
)
RETURNS TABLE (
line_id uuid,
journal_entry_id uuid,
voucher_number integer,
voucher_series text,
entry_date date,
description text,
debit_amount numeric,
credit_amount numeric
)
LANGUAGE sql
STABLE
SECURITY INVOKER
SET search_path TO 'public'
AS $$
WITH closing_entries AS (
SELECT fp.closing_entry_id AS id
FROM public.fiscal_periods fp
WHERE fp.company_id = p_company_id
AND fp.closing_entry_id IS NOT NULL
),
scoped_entries AS (
SELECT e.id, e.status, e.entry_date, e.source_type, e.description,
e.voucher_series, e.voucher_number
FROM public.journal_entries e
WHERE e.company_id = p_company_id
AND e.status IN ('posted', 'reversed')
AND e.entry_date >= p_start
AND e.entry_date <= p_end
AND NOT (
e.status = 'posted'
AND EXISTS (SELECT 1 FROM closing_entries c WHERE c.id = e.id)
)
),
non_settlement_entries AS (
SELECT * FROM scoped_entries
WHERE source_type IS DISTINCT FROM 'vat_settlement'
AND NOT (
source_type = 'year_end'
AND description IN (
'Vändning kundfordringar bokslut (kontantmetoden)',
'Vändning leverantörsskulder bokslut (kontantmetoden)'
)
)
),
shaped AS (
SELECT e.id
FROM non_settlement_entries e
WHERE e.source_type IS DISTINCT FROM 'opening_balance'
AND EXISTS (
SELECT 1 FROM public.journal_entry_lines l
WHERE l.journal_entry_id = e.id
AND l.account_number = ANY (p_ruta_accounts)
)
AND EXISTS (
SELECT 1 FROM public.journal_entry_lines l
WHERE l.journal_entry_id = e.id
AND l.account_number = ANY (p_net_accounts)
)
)
SELECT
l.id AS line_id,
je.id AS journal_entry_id,
je.voucher_number,
COALESCE(je.voucher_series, 'A') AS voucher_series,
je.entry_date,
COALESCE(je.description, '') AS description,
l.debit_amount,
l.credit_amount
FROM non_settlement_entries je
JOIN public.journal_entry_lines l ON l.journal_entry_id = je.id
WHERE l.account_number = ANY (p_accounts)
AND NOT EXISTS (SELECT 1 FROM shaped s WHERE s.id = je.id)
AND (
p_cursor_date IS NULL
OR (
je.entry_date,
je.voucher_number,
je.id,
l.id
) > (
p_cursor_date,
p_cursor_voucher_number,
COALESCE(p_cursor_entry_id, 'ffffffff-ffff-ffff-ffff-ffffffffffff'::uuid),
COALESCE(p_cursor_line_id, 'ffffffff-ffff-ffff-ffff-ffffffffffff'::uuid)
)
)
ORDER BY je.entry_date, je.voucher_number, je.id, l.id
LIMIT LEAST(GREATEST(COALESCE(p_limit, 501), 1), 501);
$$;
NOTIFY pgrst, 'reload schema';