fix(bookkeeping): classify template audit evidence (#1594)
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user