-- Sync the main-app owner ledger (public.owner_ledger_entries) into the -- Accounting platform per association: -- * a CHARGE (debit > 0) -> accounting.invoices (the customer's AR ledger) -- * a PAYMENT (credit > 0) -> accounting.payments_received, deposited=false -- (Undeposited Funds) so the funds flow into the "Make Deposit" page. -- The accounting customer's balance is recomputed authoritatively from the -- source ledger. Scoped to associations that have an accounting.companies row. -- Keyed by external_source='acmacc_ledger', external_id=owner_ledger_entries.id. -- payments_received needs external linkage columns (invoices/bills/vendors already have them) alter table accounting.payments_received add column if not exists external_source text; alter table accounting.payments_received add column if not exists external_id text; create unique index if not exists ux_payments_received_ext on accounting.payments_received (company_id, external_source, external_id) where external_id is not null; -- --------------------------------------------------------------------------- -- Recompute a synced customer's balance from the source ledger and FIFO-apply -- ledger payments across that customer's ledger invoices (so paid_amount/status -- look right). Only touches acmacc_ledger rows — manual invoices/payments are -- left alone. -- --------------------------------------------------------------------------- create or replace function accounting.recompute_customer_from_ledger( _company_id uuid, _customer_id uuid, _unit_id uuid, _association_id uuid ) returns void language plpgsql security definer set search_path to 'public','accounting' as $$ declare _bal numeric; _pool numeric; _alloc numeric; inv record; begin if _customer_id is null or _unit_id is null then return; end if; -- Authoritative AR balance = sum(debit - credit) of the unit's live ledger. select coalesce(sum(debit - credit), 0) into _bal from public.owner_ledger_entries where unit_id = _unit_id and association_id = _association_id and coalesce(is_archived, false) = false; update accounting.customers set balance = _bal where id = _customer_id; -- FIFO-allocate ledger payments across ledger invoices. select coalesce(sum(amount), 0) into _pool from accounting.payments_received where customer_id = _customer_id and external_source = 'acmacc_ledger'; for inv in select id, total from accounting.invoices where customer_id = _customer_id and external_source = 'acmacc_ledger' order by issue_date asc, created_at asc loop _alloc := greatest(0, least(_pool, inv.total)); update accounting.invoices set paid_amount = _alloc, status = case when _alloc >= inv.total and inv.total > 0 then 'paid'::accounting.invoice_status else 'sent'::accounting.invoice_status end, updated_at = now() where id = inv.id; _pool := _pool - _alloc; end loop; end; $$; -- --------------------------------------------------------------------------- -- Sync one ledger entry into accounting (idempotent). -- --------------------------------------------------------------------------- create or replace function accounting.sync_owner_ledger_entry(_entry_id uuid) returns void language plpgsql security definer set search_path to 'public','accounting' as $$ declare e public.owner_ledger_entries%rowtype; _company_id uuid; _customer_id uuid; begin select * into e from public.owner_ledger_entries where id = _entry_id; if not found then return; end if; select id into _company_id from accounting.companies where association_id = e.association_id; if _company_id is null then return; end if; select id into _customer_id from accounting.customers where company_id = _company_id and external_source = 'acmacc_unit' and external_id = e.unit_id::text; if _customer_id is null then return; end if; -- unit not synced; nothing to attach to if coalesce(e.is_archived, false) then delete from accounting.invoices where company_id=_company_id and external_source='acmacc_ledger' and external_id=e.id::text; delete from accounting.payments_received where company_id=_company_id and external_source='acmacc_ledger' and external_id=e.id::text; elsif coalesce(e.debit, 0) > 0 then -- charge -> invoice (drop any stale payment row for this entry) delete from accounting.payments_received where company_id=_company_id and external_source='acmacc_ledger' and external_id=e.id::text; insert into accounting.invoices (company_id, customer_id, number, issue_date, due_date, status, subtotal, tax, total, notes, external_source, external_id) values (_company_id, _customer_id, 'AR-' || to_char(e.date, 'YYYYMMDD') || '-' || left(replace(e.id::text, '-', ''), 6), e.date, e.date, 'sent'::accounting.invoice_status, e.debit, 0, e.debit, coalesce(nullif(e.description, ''), initcap(e.transaction_type)), 'acmacc_ledger', e.id::text) on conflict (company_id, external_source, external_id) where external_id is not null do update set customer_id=excluded.customer_id, issue_date=excluded.issue_date, due_date=excluded.due_date, subtotal=excluded.subtotal, total=excluded.total, notes=excluded.notes, number=excluded.number, updated_at=now(); elsif coalesce(e.credit, 0) > 0 then -- payment -> payments_received (Undeposited Funds); never reset deposited/deposit_id delete from accounting.invoices where company_id=_company_id and external_source='acmacc_ledger' and external_id=e.id::text; insert into accounting.payments_received (company_id, customer_id, payment_date, amount, method, reference, memo, deposited, external_source, external_id) values (_company_id, _customer_id, e.date, e.credit, coalesce(nullif(e.transaction_type, ''), 'payment'), e.reference_id, e.description, false, 'acmacc_ledger', e.id::text) on conflict (company_id, external_source, external_id) where external_id is not null do update set customer_id=excluded.customer_id, payment_date=excluded.payment_date, amount=excluded.amount, method=excluded.method, reference=excluded.reference, memo=excluded.memo, updated_at=now(); else delete from accounting.invoices where company_id=_company_id and external_source='acmacc_ledger' and external_id=e.id::text; delete from accounting.payments_received where company_id=_company_id and external_source='acmacc_ledger' and external_id=e.id::text; end if; perform accounting.recompute_customer_from_ledger(_company_id, _customer_id, e.unit_id, e.association_id); end; $$; -- Remove synced rows for a deleted/moved ledger entry, then recompute. create or replace function accounting.unsync_owner_ledger_entry( _entry_id uuid, _association_id uuid, _unit_id uuid ) returns void language plpgsql security definer set search_path to 'public','accounting' as $$ declare _company_id uuid; _customer_id uuid; begin select id into _company_id from accounting.companies where association_id=_association_id; if _company_id is null then return; end if; delete from accounting.invoices where company_id=_company_id and external_source='acmacc_ledger' and external_id=_entry_id::text; delete from accounting.payments_received where company_id=_company_id and external_source='acmacc_ledger' and external_id=_entry_id::text; select id into _customer_id from accounting.customers where company_id=_company_id and external_source='acmacc_unit' and external_id=_unit_id::text; perform accounting.recompute_customer_from_ledger(_company_id, _customer_id, _unit_id, _association_id); end; $$; -- Trigger glue (error-swallowed so a sync hiccup never blocks a ledger write). create or replace function accounting.tg_owner_ledger_sync() returns trigger language plpgsql security definer set search_path to 'public','accounting' as $$ begin begin if tg_op = 'DELETE' then perform accounting.unsync_owner_ledger_entry(old.id, old.association_id, old.unit_id); return old; end if; if tg_op = 'UPDATE' and (old.unit_id is distinct from new.unit_id or old.association_id is distinct from new.association_id) then perform accounting.unsync_owner_ledger_entry(old.id, old.association_id, old.unit_id); end if; perform accounting.sync_owner_ledger_entry(new.id); return new; exception when others then raise warning 'accounting: owner_ledger sync failed for %: %', coalesce(new.id, old.id), sqlerrm; return coalesce(new, old); end; end; $$; drop trigger if exists trg_acct_sync_owner_ledger on public.owner_ledger_entries; create trigger trg_acct_sync_owner_ledger after insert or update or delete on public.owner_ledger_entries for each row execute function accounting.tg_owner_ledger_sync();