8299ee9fb4
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>
199 lines
6.9 KiB
SQL
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;
|