fix(bookkeeping): classify template audit evidence (#1594)

This commit is contained in:
Mattsson
2026-08-13 18:58:09 +02:00
committed by GitHub
parent d02fd82191
commit e494662530
4 changed files with 489 additions and 98 deletions
+329 -22
View File
@@ -6,11 +6,12 @@
-- named accounts that never existed in BAS, so account-backfill could not seed
-- them and every booking through those templates failed. Nothing was posted.
--
-- This query is diagnostic, not a list of confirmed errors. There is no
-- provenance link from a posted entry back to the template that produced it.
-- Both signatures also have legitimate shapes: 5820 is correct for actual car
-- hire, and representation can lawfully carry 25% VAT when the supply itself is
-- subject to 25% and the invoice is correct. Review the underlag before acting.
-- The query separates template-remediation evidence from broad account
-- signatures. Imported entries and earlier corrections cannot have been
-- generated by either live template in this database. The remaining signatures
-- can still have legitimate shapes: 5820 is correct for actual car hire, and
-- representation can lawfully carry 25% VAT when the supply itself is subject
-- to 25% and the invoice is correct. Review the underlag before acting.
--
-- The query performs no writes, creates no objects, and returns one row per
-- posted journal entry and defect. Companion procedure:
@@ -36,9 +37,11 @@ candidate_entry_ids as (
where account_number in ('5820', '6072')
),
transaction_refs as (
candidate_transactions as (
select
t.journal_entry_id,
t.id as transaction_id,
t.document_id,
concat_ws(' ', nullif(t.merchant_name, ''), nullif(t.description, '')) as transaction_description
from public.transactions t
join candidate_entry_ids candidate on candidate.journal_entry_id = t.journal_entry_id
@@ -48,6 +51,8 @@ transaction_refs as (
select
tvl.journal_entry_id,
t.id as transaction_id,
t.document_id,
concat_ws(' ', nullif(t.merchant_name, ''), nullif(t.description, '')) as transaction_description
from public.transaction_voucher_links tvl
join candidate_entry_ids candidate on candidate.journal_entry_id = tvl.journal_entry_id
@@ -61,7 +66,7 @@ transaction_context as (
distinct transaction_description,
' | ' order by transaction_description
) as transaction_description
from transaction_refs
from candidate_transactions
group by journal_entry_id
),
@@ -77,6 +82,7 @@ hotel_candidates as (
je.entry_date,
je.committed_at,
je.fiscal_period_id,
je.source_type,
cost.line_ids as cost_line_ids,
coalesce(vat.line_ids, '{}'::uuid[]) as vat_line_ids,
'5820'::text as observed_cost_account,
@@ -117,6 +123,7 @@ representation_candidates as (
je.entry_date,
je.committed_at,
je.fiscal_period_id,
je.source_type,
cost.line_ids as cost_line_ids,
vat.line_ids as vat_line_ids,
'6072'::text as observed_cost_account,
@@ -152,6 +159,7 @@ all_candidates as (
entry_date,
committed_at,
fiscal_period_id,
source_type,
cost_line_ids,
vat_line_ids,
observed_cost_account,
@@ -177,6 +185,7 @@ all_candidates as (
entry_date,
committed_at,
fiscal_period_id,
source_type,
cost_line_ids,
vat_line_ids,
observed_cost_account,
@@ -217,18 +226,310 @@ entry_line_snapshots as (
from all_candidates
) candidate on candidate.journal_entry_id = jel.journal_entry_id
group by jel.journal_entry_id
),
candidate_documents as (
-- Resolve every current underlag path that can legally support the entry:
-- direct entry or line links, transaction links, and supplier-invoice
-- references. Return document ids only, never storage paths or file names.
select distinct
c.journal_entry_id,
d.id as document_id,
d.extracted_data
from all_candidates c
join public.document_attachments d
on d.is_current_version = true
and (
d.journal_entry_id = c.journal_entry_id
or exists (
select 1
from public.journal_entry_lines jel
where jel.journal_entry_id = c.journal_entry_id
and jel.id = d.journal_entry_line_id
)
)
union
select distinct
tx.journal_entry_id,
d.id as document_id,
d.extracted_data
from candidate_transactions tx
join public.document_attachments d
on d.id = tx.document_id
and d.is_current_version = true
union
select distinct
c.journal_entry_id,
d.id as document_id,
d.extracted_data
from all_candidates c
join public.supplier_invoices si
on si.registration_journal_entry_id = c.journal_entry_id
or si.payment_journal_entry_id = c.journal_entry_id
or exists (
select 1
from public.supplier_invoice_payments sip
where sip.supplier_invoice_id = si.id
and sip.journal_entry_id = c.journal_entry_id
)
join public.document_attachments d
on d.id = si.document_id
and d.is_current_version = true
),
candidate_document_vat_rates as (
-- Extraction payloads are historical data and may have missing, malformed,
-- or differently scaled rate values. Accept only numeric values, normalizing
-- both 0.25 and 25 to percent before they reach the classifier.
select
cd.journal_entry_id,
cd.document_id,
case
when rate.raw_rate::numeric <= 1 then rate.raw_rate::numeric * 100
else rate.raw_rate::numeric
end as normalized_rate
from candidate_documents cd
cross join lateral (
select vat ->> 'rate' as raw_rate
from jsonb_array_elements(
case
when jsonb_typeof(cd.extracted_data -> 'vatBreakdown') = 'array'
then cd.extracted_data -> 'vatBreakdown'
else '[]'::jsonb
end
) vat
union all
select item ->> 'vatRate' as raw_rate
from jsonb_array_elements(
case
when jsonb_typeof(cd.extracted_data -> 'lineItems') = 'array'
then cd.extracted_data -> 'lineItems'
else '[]'::jsonb
end
) item
) rate
where rate.raw_rate ~ '^\s*[0-9]+(?:\.[0-9]+)?\s*$'
),
document_signals as (
select
cd.journal_entry_id,
array_agg(distinct cd.document_id order by cd.document_id) as document_ids,
count(distinct cd.document_id)::integer as document_count,
count(distinct cd.document_id) filter (
where cd.extracted_data is not null
)::integer as extracted_document_count,
max(
case
when cd.extracted_data ->> 'confidence'
~ '^\s*[0-9]+(?:\.[0-9]+)?\s*$'
then (cd.extracted_data ->> 'confidence')::numeric
end
) as max_extraction_confidence,
bool_or(rate.normalized_rate between 11.9 and 12.1)
as document_has_12pct_vat,
bool_or(rate.normalized_rate between 24.9 and 25.1)
as document_has_25pct_vat
from candidate_documents cd
left join candidate_document_vat_rates rate
on rate.journal_entry_id = cd.journal_entry_id
and rate.document_id = cd.document_id
group by cd.journal_entry_id
),
candidate_output as (
select
c.*,
co.name as company_name,
lines.original_lines,
jsonb_array_length(lines.original_lines) as line_count,
coalesce(doc.document_ids, '{}'::uuid[]) as document_ids,
coalesce(doc.document_count, 0) as document_count,
coalesce(doc.extracted_document_count, 0) as extracted_document_count,
doc.max_extraction_confidence,
coalesce(doc.document_has_12pct_vat, false) as document_has_12pct_vat,
coalesce(doc.document_has_25pct_vat, false) as document_has_25pct_vat,
case
when c.defect = 'representation_25pct_vat'
then 'participants_purpose_and_300_sek_vat_base_cap_required'
else 'not_applicable'
end as independent_representation_review,
c.defect = 'representation_25pct_vat'
and c.cost_debit_amount > 300
as observed_6072_cost_exceeds_300_sek,
reporting.vat_reporting_period,
coalesce(filing.deadline_statuses, '{}'::text[]) as vat_deadline_statuses,
case
when c.defect <> 'representation_25pct_vat' then 'not_applicable'
when reporting.vat_reporting_period is null
then 'unknown_reporting_period'
when coalesce(filing.has_confirmed_record, false)
then 'confirmed_in_app'
when coalesce(filing.has_submitted_record, false)
then 'submitted_in_app'
else 'not_proven_by_in_app_records'
end as vat_filing_status,
case
when fp.id is null then 'missing_fiscal_period'
when fp.is_closed then 'closed'
when fp.locked_at is not null then 'locked'
when cs.bookkeeping_locked_through is not null
and c.entry_date <= cs.bookkeeping_locked_through then 'behind_company_lock_date'
else 'open'
end as effective_lock_status
from all_candidates c
join public.companies co on co.id = c.company_id
join entry_line_snapshots lines on lines.journal_entry_id = c.journal_entry_id
left join document_signals doc on doc.journal_entry_id = c.journal_entry_id
left join public.company_settings cs on cs.company_id = c.company_id
left join public.fiscal_periods fp on fp.id = c.fiscal_period_id
left join lateral (
select case
when c.defect <> 'representation_25pct_vat' then null
when cs.moms_period = 'monthly'
then to_char(c.entry_date, 'YYYY-MM')
when cs.moms_period = 'quarterly'
then concat(
extract(year from c.entry_date)::integer,
'-Q',
ceil(extract(month from c.entry_date) / 3.0)::integer
)
when cs.moms_period = 'yearly' and fp.period_end is not null
then case
when extract(month from fp.period_end) = 12
then extract(year from fp.period_end)::integer::text
else concat(
extract(year from fp.period_end)::integer - 1,
'/',
extract(year from fp.period_end)::integer
)
end
else null
end as vat_reporting_period
) reporting on true
left join lateral (
select
array_agg(distinct d.status order by d.status) as deadline_statuses,
bool_or(d.status = 'submitted') as has_submitted_record,
bool_or(d.status = 'confirmed') as has_confirmed_record
from public.deadlines d
where c.defect = 'representation_25pct_vat'
and d.company_id = c.company_id
and d.tax_deadline_type = case cs.moms_period
when 'monthly' then 'moms_monthly'
when 'quarterly' then 'moms_quarterly'
when 'yearly' then 'moms_yearly'
end
and d.tax_period = reporting.vat_reporting_period
) filing on true
),
classified as (
select
candidate.*,
case
-- Imports and earlier corrections were not generated by either live
-- template in this database. They can have independent accounting
-- issues, but are false positives for this template-remediation audit.
when candidate.source_type in ('import', 'correction')
then 'false_positive'
when candidate.defect = 'representation_25pct_vat'
and candidate.review_priority = 'high_vat_is_25pct_of_6072_cost'
and candidate.document_has_25pct_vat
and not candidate.document_has_12pct_vat
then 'false_positive'
when candidate.defect = 'representation_25pct_vat'
and candidate.review_priority = 'high_vat_is_25pct_of_6072_cost'
then 'insufficient_evidence'
when candidate.defect = 'representation_25pct_vat'
and candidate.observed_vat_rate between 0.119 and 0.121
then 'false_positive'
when candidate.defect = 'representation_25pct_vat'
and candidate.document_has_12pct_vat
and not candidate.document_has_25pct_vat
then 'false_positive'
when candidate.defect = 'representation_25pct_vat'
then 'insufficient_evidence'
-- The defective hotel template deterministically emitted three lines:
-- 5820 cost, 2641 at 12 percent, and one settlement credit. The hotel
-- counterparty signal supplies the final discriminator from car hire.
when candidate.defect = 'travel_hotel_5820'
and candidate.line_count = 3
and candidate.observed_vat_rate between 0.119 and 0.121
and candidate.review_priority = 'high_hotel_counterparty_on_car_hire_account'
then 'confirmed_correction'
when candidate.defect = 'travel_hotel_5820'
and candidate.line_count = 3
and candidate.observed_vat_rate between 0.119 and 0.121
then 'insufficient_evidence'
else 'false_positive'
end as evidence_classification,
case
when candidate.source_type in ('import', 'correction')
then 'entry source cannot be either live template'
when candidate.defect = 'representation_25pct_vat'
and candidate.review_priority = 'high_vat_is_25pct_of_6072_cost'
and candidate.document_has_25pct_vat
and not candidate.document_has_12pct_vat
then 'extracted underlag confirms a 25 percent supply'
when candidate.defect = 'representation_25pct_vat'
and candidate.review_priority = 'high_vat_is_25pct_of_6072_cost'
then 'exact defective signature but no decisive underlag or provenance'
when candidate.defect = 'representation_25pct_vat'
and candidate.observed_vat_rate between 0.119 and 0.121
then 'voucher has the intended 12 percent aggregate VAT signature'
when candidate.defect = 'representation_25pct_vat'
and candidate.document_has_12pct_vat
and not candidate.document_has_25pct_vat
then 'extracted underlag confirms a 12 percent supply'
when candidate.defect = 'representation_25pct_vat'
then 'mixed representation voucher needs decisive underlag or provenance'
when candidate.defect = 'travel_hotel_5820'
and candidate.line_count = 3
and candidate.observed_vat_rate between 0.119 and 0.121
and candidate.review_priority = 'high_hotel_counterparty_on_car_hire_account'
then 'exact defective hotel shape plus hotel counterparty'
when candidate.defect = 'travel_hotel_5820'
and candidate.line_count = 3
and candidate.observed_vat_rate between 0.119 and 0.121
then 'exact defective hotel shape without hotel evidence'
else 'voucher does not have the defective hotel template shape'
end as classification_reason
from candidate_output candidate
)
select
c.defect,
c.evidence_classification,
c.classification_reason,
case
when c.evidence_classification = 'confirmed_correction'
then 'explicit_write_approval_required_after_underlag_review'
when c.evidence_classification = 'insufficient_evidence'
then 'underlag_required_before_approval_request'
else 'no_template_remediation'
end as approval_status,
case
when c.defect = 'representation_25pct_vat'
and c.evidence_classification <> 'false_positive'
then 'potential_if_confirmed'
else 'none_from_this_defect'
end as vat_return_impact,
c.company_id,
co.name as company_name,
c.company_name,
c.journal_entry_id,
c.voucher_series,
c.voucher_number,
c.entry_date,
c.committed_at,
c.fiscal_period_id,
c.source_type,
c.cost_line_ids,
c.vat_line_ids,
c.observed_cost_account,
@@ -241,21 +542,27 @@ select
c.expected_vat_rate,
c.transaction_description,
c.review_priority,
lines.original_lines,
case
when fp.id is null then 'missing_fiscal_period'
when fp.is_closed then 'closed'
when fp.locked_at is not null then 'locked'
when cs.bookkeeping_locked_through is not null
and c.entry_date <= cs.bookkeeping_locked_through then 'behind_company_lock_date'
else 'open'
end as effective_lock_status
from all_candidates c
join public.companies co on co.id = c.company_id
join entry_line_snapshots lines on lines.journal_entry_id = c.journal_entry_id
left join public.company_settings cs on cs.company_id = c.company_id
left join public.fiscal_periods fp on fp.id = c.fiscal_period_id
c.line_count,
c.document_ids,
c.document_count,
c.extracted_document_count,
c.max_extraction_confidence,
c.document_has_12pct_vat,
c.document_has_25pct_vat,
c.independent_representation_review,
c.observed_6072_cost_exceeds_300_sek,
c.vat_reporting_period,
c.vat_deadline_statuses,
c.vat_filing_status,
c.original_lines,
c.effective_lock_status
from classified c
order by
case c.evidence_classification
when 'confirmed_correction' then 0
when 'insufficient_evidence' then 1
else 2
end,
c.defect,
case
when c.review_priority like 'high%' then 0