Accounting: back-sync bill paid status to bill_approvals on INSERT

trg_acct_bill_paid_back was AFTER UPDATE only, so bills mirrored into
accounting already-paid never triggered the back-sync that flips
public.bills + bill_approvals to 'paid'. Fire the trigger on INSERT too
and reconcile existing already-paid mirrored bills. Also backfill
invoice-track approvals that uniquely match a bill (bill_id was null).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-04 18:17:39 -04:00
parent 6634907799
commit 84541a6813
@@ -0,0 +1,87 @@
-- Fix: bill_approvals not reflecting "paid" when an accounting.bills row is
-- created already-paid.
--
-- Background (see 20260601140000_accounting_sync_bills.sql):
-- accounting.sync_accounting_bill_paid() flips public.bills + public.bill_approvals
-- to 'paid' once the matching accounting bill is fully paid. It runs from the
-- trigger trg_acct_bill_paid_back, which was AFTER UPDATE only.
--
-- When a public bill is ALREADY paid at the moment it first mirrors into
-- accounting, the forward sync INSERTs the accounting row at status='paid'
-- with paid_amount set. With no subsequent UPDATE to paid_amount/total, the
-- UPDATE-only trigger never fired, so the bill's approvals were left stuck at
-- 'approved'/'pending'. This widens the back-sync to also run on INSERT.
-- ---------------------------------------------------------------------------
-- Back-sync trigger: now fires on INSERT as well as UPDATE.
-- ---------------------------------------------------------------------------
create or replace function accounting.tg_accounting_bill_paid_sync()
returns trigger
language plpgsql security definer set search_path to 'public','accounting'
as $$
begin
begin
if tg_op = 'INSERT' then
-- a bill mirrored in already-paid never produces an UPDATE; handle it here
perform accounting.sync_accounting_bill_paid(new.id);
-- on UPDATE only act when the paid position actually changed, to avoid loops
elsif old.paid_amount is distinct from new.paid_amount
or old.total is distinct from new.total then
perform accounting.sync_accounting_bill_paid(new.id);
end if;
exception when others then
raise warning 'accounting: accounting bill paid sync failed for %: %', new.id, sqlerrm;
end;
return new;
end;
$$;
drop trigger if exists trg_acct_bill_paid_back on accounting.bills;
create trigger trg_acct_bill_paid_back
after insert or update on accounting.bills
for each row execute function accounting.tg_accounting_bill_paid_sync();
-- ---------------------------------------------------------------------------
-- One-time reconciliation: back-sync any accounting bills that are already
-- fully paid but whose linked public bill / approvals were never updated
-- (the rows that fell into the INSERT gap above).
-- ---------------------------------------------------------------------------
do $$
declare _id uuid;
begin
for _id in
select id from accounting.bills
where external_source = 'acmacc_bill' and external_id is not null
and coalesce(paid_amount,0) >= coalesce(total,0) and coalesce(total,0) > 0
loop
perform accounting.sync_accounting_bill_paid(_id);
end loop;
end $$;
-- ---------------------------------------------------------------------------
-- Backfill orphaned, invoice-track approvals (bill_id IS NULL) that were
-- created via the invoice flow and never linked to a public.bills row.
-- Only link when exactly one matching bill exists for the same association,
-- invoice number and amount; adopt that bill's status when it is paid.
-- ---------------------------------------------------------------------------
update public.bill_approvals ba
set bill_id = m.bill_id,
status = case when m.bill_status = 'paid' then 'paid' else ba.status end,
updated_at = now()
from (
select i.id as invoice_id, b.id as bill_id, b.status as bill_status
from public.invoices i
join public.bills b
on b.association_id = i.association_id
and b.amount = i.amount
and b.invoice_number is not distinct from i.invoice_number
group by i.id, b.id, b.status
having (
select count(*) from public.bills b2
where b2.association_id = i.association_id
and b2.amount = i.amount
and b2.invoice_number is not distinct from i.invoice_number
) = 1
) m
where ba.bill_id is null
and ba.invoice_id = m.invoice_id;