-- Accrual A/P guard: link a bank transaction to the bill it settles, and enforce -- that a bill-linked payment never carries an expense category. -- -- Under accrual, a vendor expense is recognized once — when the bill is entered -- (Dr Expense / Cr A/P). Paying the bill must only relieve the liability -- (Dr A/P / Cr Bank) via accounting.post_transaction_gl's vendor → A/P branch, -- which only runs when coa_account_id IS NULL. So any transaction that settles a -- bill must have coa_account_id NULL; otherwise it re-recognizes the expense. alter table accounting.transactions add column if not exists bill_id uuid references accounting.bills(id) on delete set null; create index if not exists idx_transactions_bill_id on accounting.transactions(bill_id) where bill_id is not null; -- Invariant: a transaction linked to a bill posts against Accounts Payable, never -- an expense account. This makes the accrual rule enforceable, not just convention. alter table accounting.transactions drop constraint if exists chk_bill_payment_no_coa; alter table accounting.transactions add constraint chk_bill_payment_no_coa check (bill_id is null or coa_account_id is null);