diff --git a/supabase/migrations/20260604120000_accounting_bill_paid_back_sync_on_insert.sql b/supabase/migrations/20260604120000_accounting_bill_paid_back_sync_on_insert.sql new file mode 100644 index 0000000..47da015 --- /dev/null +++ b/supabase/migrations/20260604120000_accounting_bill_paid_back_sync_on_insert.sql @@ -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;