Files
accounted/scripts/audit-settlement-account-mismatches.sql
T
Mattsson 8299ee9fb4 fix(bookkeeping): resolve settlement account in all categorization flows and ship mis-booking audit (#1383)
Completes the #985/#986/#987 caller sweep: categorize-core, v1
batch-categorize, pending-operation edits and the MCP categorize path now
resolve the settlement leg from the transaction's cash account instead of
inheriting a hardcoded or stale account. Extends the correct_entry preview
with currency, tax and dimension line metadata so staged corrections
preserve full line fidelity. Adds a read-only audit query and a runbook for
reviewing and correcting historical mis-bookings via staged storno with
explicit approval; no automated bulk mutation.

Fixes #1001

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 18:45:38 +02:00

199 lines
6.9 KiB
SQL

-- Read-only audit for journal entries whose settlement leg may disagree with
-- the cash account linked to the source bank transaction.
--
-- This query is deliberately diagnostic. A current cash_account_id does not
-- prove that the same link existed when the entry was posted. Every result
-- therefore requires review against the bank feed, the original journal entry,
-- and the transaction history before any correction is staged.
--
-- The query performs no writes, creates no temporary objects, and only returns
-- current posted entries. Reversed originals disappear once their transaction
-- is linked to the posted correction.
with transaction_context as (
select
t.company_id,
t.id as transaction_id,
t.date as transaction_date,
t.amount as transaction_amount,
coalesce(t.currency, 'SEK') as transaction_currency,
t.amount_sek,
t.exchange_rate,
t.cash_account_id,
t.import_source,
t.created_at as transaction_created_at,
t.updated_at as transaction_updated_at,
je.id as journal_entry_id,
je.entry_date,
je.committed_at,
je.voucher_series,
je.voucher_number,
je.fiscal_period_id,
ca.ledger_account as expected_settlement_account,
ca.created_at as cash_account_created_at,
fp.name as fiscal_period_name,
fp.is_closed as fiscal_period_closed,
fp.locked_at as fiscal_period_locked_at,
cs.bookkeeping_locked_through,
round(
abs(
case
when coalesce(t.currency, 'SEK') = 'SEK' then t.amount
when t.amount_sek is not null then t.amount_sek
when t.exchange_rate is not null then t.amount * t.exchange_rate
else null
end
),
2
) as settlement_amount_sek,
exists (
select 1
from public.invoice_payments ip
where ip.transaction_id = t.id
and ip.journal_entry_id = je.id
) as is_customer_invoice_payment,
exists (
select 1
from public.supplier_invoice_payments sip
where sip.transaction_id = t.id
and sip.journal_entry_id = je.id
) as is_supplier_invoice_payment
from public.transactions t
join public.journal_entries je
on je.id = t.journal_entry_id
and je.company_id = t.company_id
and je.status = 'posted'
join public.cash_accounts ca
on ca.id = t.cash_account_id
and ca.company_id = t.company_id
join public.fiscal_periods fp
on fp.id = je.fiscal_period_id
and fp.company_id = t.company_id
left join public.company_settings cs
on cs.company_id = t.company_id
), directional_exact_lines as (
select
tc.*,
jel.id as observed_line_id,
jel.account_number as observed_settlement_account,
jel.debit_amount as observed_debit_amount,
jel.credit_amount as observed_credit_amount,
count(*) over (partition by tc.transaction_id, tc.journal_entry_id) as exact_directional_line_count
from transaction_context tc
join public.journal_entry_lines jel
on jel.journal_entry_id = tc.journal_entry_id
and case
when tc.transaction_amount < 0 then
round(jel.credit_amount, 2) = tc.settlement_amount_sek
and round(jel.debit_amount, 2) = 0
else
round(jel.debit_amount, 2) = tc.settlement_amount_sek
and round(jel.credit_amount, 2) = 0
end
where tc.settlement_amount_sek is not null
), candidates as (
select dl.*
from directional_exact_lines dl
where dl.observed_settlement_account <> dl.expected_settlement_account
and not exists (
select 1
from public.journal_entry_lines expected
where expected.journal_entry_id = dl.journal_entry_id
and expected.account_number = dl.expected_settlement_account
and case
when dl.transaction_amount < 0 then
round(expected.credit_amount, 2) = dl.settlement_amount_sek
and round(expected.debit_amount, 2) = 0
else
round(expected.debit_amount, 2) = dl.settlement_amount_sek
and round(expected.credit_amount, 2) = 0
end
)
)
select
c.company_id,
c.transaction_id,
c.journal_entry_id,
c.observed_line_id,
concat(c.voucher_series, '-', c.voucher_number) as voucher,
c.transaction_date,
c.entry_date,
c.committed_at,
c.transaction_amount,
c.transaction_currency,
c.settlement_amount_sek,
c.expected_settlement_account,
c.observed_settlement_account,
c.observed_debit_amount,
c.observed_credit_amount,
c.exact_directional_line_count,
c.cash_account_id,
c.import_source,
c.transaction_created_at,
c.transaction_updated_at,
c.cash_account_created_at,
c.fiscal_period_id,
c.fiscal_period_name,
c.fiscal_period_closed,
c.fiscal_period_locked_at,
c.bookkeeping_locked_through,
case
when c.fiscal_period_closed then 'closed'
when c.fiscal_period_locked_at is not null then 'period_locked'
when c.bookkeeping_locked_through is not null
and c.entry_date <= c.bookkeeping_locked_through
then 'company_lock_date'
else 'open'
end as effective_lock_status,
(
select jsonb_agg(
jsonb_build_object(
'account_number', original.account_number,
'debit_amount', original.debit_amount,
'credit_amount', original.credit_amount,
'line_description', original.line_description,
'currency', original.currency,
'amount_in_currency', original.amount_in_currency,
'exchange_rate', original.exchange_rate,
'tax_code', original.tax_code,
'dimensions', original.dimensions,
'cost_center', original.cost_center,
'project', original.project
)
order by original.sort_order, original.id
)
from public.journal_entry_lines original
where original.journal_entry_id = c.journal_entry_id
) as original_lines,
case
when c.is_customer_invoice_payment then 'customer_invoice_payment'
when c.is_supplier_invoice_payment then 'supplier_invoice_payment'
else 'categorization_or_manual_link'
end as booking_flow,
case
when c.exact_directional_line_count <> 1 then 'manual_review_multiple_exact_lines'
when c.is_customer_invoice_payment or c.is_supplier_invoice_payment
then 'manual_review_payment_aware_correction_required'
when c.cash_account_created_at > c.committed_at then 'manual_review_cash_account_created_later'
when c.expected_settlement_account <> '1930'
and c.observed_settlement_account = '1930'
and abs(extract(epoch from (c.transaction_updated_at - c.committed_at))) <= 60
then 'high_review_priority_hardcoded_1930_signature'
else 'manual_review_link_history_required'
end as review_priority
from candidates c
order by
case
when c.expected_settlement_account <> '1930'
and c.observed_settlement_account = '1930'
and c.cash_account_created_at <= c.committed_at
and abs(extract(epoch from (c.transaction_updated_at - c.committed_at))) <= 60
then 1
when c.is_customer_invoice_payment or c.is_supplier_invoice_payment then 2
else 3
end,
c.company_id,
c.entry_date,
c.voucher_series,
c.voucher_number;