-- A7 / cash unification: the bank register (accounting.transactions, transfers,
-- deposits) is the real cash ledger but never posted to the GL, so the balance
-- sheet ignored transfers and deposited funds stayed stuck in Undeposited.
--
-- Fix: post the register to the GL and RETIRE the synthesized cash legs so cash
-- is sourced once, from the register:
--   * customer receipt (transactions.customer_id) -> Dr bank / Cr A/R
--   * vendor payment   (transactions.vendor_id)   -> Dr A/P  / Cr bank
--   * categorized      (transactions.coa_account_id) -> Dr/Cr bank + COA
--   * transfer (transfer_id) -> Dr destination bank / Cr source bank (one entry)
--   * deposit  (deposit_id)  -> Dr bank / Cr Undeposited Funds (one entry)
--   * payments_received (undeposited, owner-ledger) -> Dr Undeposited / Cr A/R
-- Invoices still post Dr A/R / Cr income; bills still post Dr expense / Cr A/P;
-- their cash settlement now comes from the register (acmacc_invpay / acmacc_billpay
-- removed). Convention: transactions use bank-statement signs (credit = money IN).
-- Scoped to gl_managed companies; keyed by external_source/external_id (idempotent).

-- Invoice: billing only (settlement now comes from register receipts).
create or replace function accounting.post_invoice_gl(_invoice_id uuid) returns void
language plpgsql security definer set search_path to 'public','accounting' as $$
declare i accounting.invoices%rowtype; _ar uuid; _inc uuid; _je uuid;
begin
  select * into i from accounting.invoices where id=_invoice_id;
  if not found then return; end if;
  perform accounting._gl_clear(i.company_id, 'acmacc_inv', i.id::text);
  perform accounting._gl_clear(i.company_id, 'acmacc_invpay', i.id::text); -- retire old settlement leg
  if not accounting.gl_managed(i.company_id) then return; end if;
  if coalesce(i.total,0) = 0 or i.status = 'void' then return; end if;
  _ar := accounting.coa_ar(i.company_id);
  _inc := accounting.coa_income_for(i.company_id, coalesce(nullif(i.notes,''), i.number));
  insert into accounting.journal_entries (company_id, date, description, reference, external_source, external_id)
  values (i.company_id, i.issue_date, coalesce(nullif(i.notes,''), 'Invoice ' || i.number), i.number, 'acmacc_inv', i.id::text)
  returning id into _je;
  insert into accounting.journal_entry_lines (journal_entry_id, account_id, debit, credit, description) values
    (_je, _ar,  i.total, 0, 'Invoice ' || i.number),
    (_je, _inc, 0, i.total, 'Invoice ' || i.number);
end$$;

-- Bill: expense / A/P only (payment now comes from register).
create or replace function accounting.post_bill_gl(_bill_id uuid) returns void
language plpgsql security definer set search_path to 'public','accounting' as $$
declare b accounting.bills%rowtype; _ap uuid; _defexp uuid; _je uuid; _debits numeric := 0; it record;
begin
  select * into b from accounting.bills where id=_bill_id;
  if not found then return; end if;
  perform accounting._gl_clear(b.company_id, 'acmacc_bill', b.id::text);
  perform accounting._gl_clear(b.company_id, 'acmacc_billpay', b.id::text); -- retire old payment leg
  if not accounting.gl_managed(b.company_id) then return; end if;
  if coalesce(b.total,0) = 0 or b.status = 'void' then return; end if;
  _ap := accounting.coa_ap(b.company_id);
  _defexp := accounting.coa_default_expense(b.company_id);
  insert into accounting.journal_entries (company_id, date, description, reference, external_source, external_id)
  values (b.company_id, b.issue_date, coalesce(nullif(b.notes,''), 'Bill ' || b.number), b.number, 'acmacc_bill', b.id::text)
  returning id into _je;
  for it in select coalesce(account_id, _defexp) as acct, amount from accounting.bill_items where bill_id=b.id and amount <> 0 loop
    insert into accounting.journal_entry_lines (journal_entry_id, account_id, debit, credit, description)
    values (_je, it.acct, it.amount, 0, 'Bill ' || b.number);
    _debits := _debits + it.amount;
  end loop;
  if _debits < b.total then
    insert into accounting.journal_entry_lines (journal_entry_id, account_id, debit, credit, description)
    values (_je, _defexp, b.total - _debits, 0, 'Bill ' || b.number);
  end if;
  insert into accounting.journal_entry_lines (journal_entry_id, account_id, debit, credit, description)
  values (_je, _ap, 0, b.total, 'Bill ' || b.number);
end$$;

-- payments_received = cash received into Undeposited Funds (owner-ledger path).
create or replace function accounting.post_payment_gl(_payment_id uuid) returns void
language plpgsql security definer set search_path to 'public','accounting' as $$
declare p accounting.payments_received%rowtype; _ar uuid; _und uuid; _je uuid;
begin
  select * into p from accounting.payments_received where id=_payment_id;
  if not found then return; end if;
  perform accounting._gl_clear(p.company_id, 'acmacc_pay', p.id::text);
  if not accounting.gl_managed(p.company_id) then return; end if;
  if coalesce(p.amount,0) = 0 then return; end if;
  _und := accounting.coa_undeposited(p.company_id);
  _ar := accounting.coa_ar(p.company_id);
  insert into accounting.journal_entries (company_id, date, description, reference, external_source, external_id)
  values (p.company_id, p.payment_date, coalesce(nullif(p.memo,''), 'Payment received'), p.reference, 'acmacc_pay', p.id::text)
  returning id into _je;
  insert into accounting.journal_entry_lines (journal_entry_id, account_id, debit, credit, description) values
    (_je, _und, p.amount, 0, 'Payment received'),
    (_je, _ar,  0, p.amount, 'Payment received');
end$$;

-- A bank-register transaction (not a transfer/deposit). Bank-statement convention:
-- credit = money IN (Dr bank), debit = money OUT (Cr bank). Counter by linkage.
create or replace function accounting.post_transaction_gl(_txn_id uuid) returns void
language plpgsql security definer set search_path to 'public','accounting' as $$
declare t accounting.transactions%rowtype; _counter uuid; _je uuid; _amt numeric;
begin
  select * into t from accounting.transactions where id=_txn_id;
  if not found then return; end if;
  perform accounting._gl_clear(t.company_id, 'acmacc_txn', t.id::text);
  if not accounting.gl_managed(t.company_id) then return; end if;
  if t.transfer_id is not null or t.deposit_id is not null then return; end if; -- handled elsewhere
  if t.account_id is null then return; end if;
  _amt := coalesce(t.amount,0);
  if _amt = 0 then return; end if;
  _counter := case
    when t.customer_id is not null then accounting.coa_ar(t.company_id)
    when t.vendor_id is not null then accounting.coa_ap(t.company_id)
    when t.coa_account_id is not null then t.coa_account_id
    else null end;
  if _counter is null then return; end if; -- uncategorized (e.g. opening dup) — skip

  insert into accounting.journal_entries (company_id, date, description, reference, external_source, external_id)
  values (t.company_id, t.date, coalesce(nullif(t.description,''), 'Bank transaction'), t.reference, 'acmacc_txn', t.id::text)
  returning id into _je;
  if t.type = 'credit' then -- money in
    insert into accounting.journal_entry_lines (journal_entry_id, account_id, debit, credit, description) values
      (_je, t.account_id, _amt, 0, t.description),
      (_je, _counter,     0, _amt, t.description);
  else -- money out
    insert into accounting.journal_entry_lines (journal_entry_id, account_id, debit, credit, description) values
      (_je, _counter,     _amt, 0, t.description),
      (_je, t.account_id, 0, _amt, t.description);
  end if;
end$$;

-- Transfer: Dr destination bank / Cr source bank (one entry per transfer_id).
create or replace function accounting.post_transfer_gl(_transfer_id uuid) returns void
language plpgsql security definer set search_path to 'public','accounting' as $$
declare _company uuid; _dest uuid; _src uuid; _amt numeric; _je uuid;
begin
  select company_id, account_id, amount into _company, _dest, _amt
  from accounting.transactions where transfer_id=_transfer_id and type='credit' limit 1; -- money in = destination
  select account_id into _src
  from accounting.transactions where transfer_id=_transfer_id and type='debit' limit 1;  -- money out = source
  if _company is null then return; end if;
  perform accounting._gl_clear(_company, 'acmacc_xfer', _transfer_id::text);
  if not accounting.gl_managed(_company) then return; end if;
  if _dest is null or _src is null or coalesce(_amt,0)=0 then return; end if;
  insert into accounting.journal_entries (company_id, date, description, reference, external_source, external_id)
  values (_company, (select date from accounting.transactions where transfer_id=_transfer_id limit 1), 'Account transfer', null, 'acmacc_xfer', _transfer_id::text)
  returning id into _je;
  insert into accounting.journal_entry_lines (journal_entry_id, account_id, debit, credit, description) values
    (_je, _dest, _amt, 0, 'Transfer in'),
    (_je, _src,  0, _amt, 'Transfer out');
end$$;

-- Deposit: move cash from Undeposited Funds into the bank (Dr bank / Cr Undeposited).
create or replace function accounting.post_deposit_gl(_deposit_id uuid) returns void
language plpgsql security definer set search_path to 'public','accounting' as $$
declare d accounting.deposits%rowtype; _und uuid; _je uuid;
begin
  select * into d from accounting.deposits where id=_deposit_id;
  if not found then return; end if;
  perform accounting._gl_clear(d.company_id, 'acmacc_dep', d.id::text);
  if not accounting.gl_managed(d.company_id) then return; end if;
  if coalesce(d.amount,0)=0 or d.bank_account_id is null then return; end if;
  _und := accounting.coa_undeposited(d.company_id);
  insert into accounting.journal_entries (company_id, date, description, reference, external_source, external_id)
  values (d.company_id, d.date, coalesce(nullif(d.memo,''), 'Deposit'), null, 'acmacc_dep', d.id::text)
  returning id into _je;
  insert into accounting.journal_entry_lines (journal_entry_id, account_id, debit, credit, description) values
    (_je, d.bank_account_id, d.amount, 0, 'Deposit'),
    (_je, _und,              0, d.amount, 'Deposit');
end$$;

-- Triggers
create or replace function accounting.tg_transaction_gl() returns trigger
language plpgsql security definer set search_path to 'public','accounting' as $$
declare r accounting.transactions%rowtype;
begin
  begin
    r := coalesce(new, old);
    if tg_op='DELETE' then
      if old.transfer_id is not null then perform accounting.post_transfer_gl(old.transfer_id);
      elsif old.deposit_id is not null then perform accounting.post_deposit_gl(old.deposit_id);
      else perform accounting._gl_clear(old.company_id,'acmacc_txn',old.id::text); end if;
      return old;
    end if;
    if r.transfer_id is not null then perform accounting.post_transfer_gl(r.transfer_id);
    elsif r.deposit_id is not null then perform accounting.post_deposit_gl(r.deposit_id);
    else perform accounting.post_transaction_gl(r.id); end if;
  exception when others then raise warning 'accounting: txn GL post failed: %', sqlerrm; end;
  return coalesce(new, old);
end$$;

create or replace function accounting.tg_deposit_gl() returns trigger
language plpgsql security definer set search_path to 'public','accounting' as $$
begin
  begin
    if tg_op='DELETE' then perform accounting._gl_clear(old.company_id,'acmacc_dep',old.id::text); return old; end if;
    perform accounting.post_deposit_gl(new.id);
  exception when others then raise warning 'accounting: deposit GL post failed: %', sqlerrm; end;
  return coalesce(new, old);
end$$;

drop trigger if exists trg_acct_txn_gl on accounting.transactions;
create trigger trg_acct_txn_gl after insert or update or delete on accounting.transactions
  for each row execute function accounting.tg_transaction_gl();

drop trigger if exists trg_acct_deposit_gl on accounting.deposits;
create trigger trg_acct_deposit_gl after insert or update or delete on accounting.deposits
  for each row execute function accounting.tg_deposit_gl();

-- Backfill managed companies
do $$
declare r record;
begin
  for r in select i.id from accounting.invoices i join accounting.companies c on c.id=i.company_id where accounting.gl_managed(c.id) loop
    perform accounting.post_invoice_gl(r.id);
  end loop;
  for r in select b.id from accounting.bills b join accounting.companies c on c.id=b.company_id where accounting.gl_managed(c.id) loop
    perform accounting.post_bill_gl(r.id);
  end loop;
  for r in select p.id from accounting.payments_received p join accounting.companies c on c.id=p.company_id where accounting.gl_managed(c.id) loop
    perform accounting.post_payment_gl(r.id);
  end loop;
  for r in select distinct t.transfer_id from accounting.transactions t join accounting.companies c on c.id=t.company_id
           where accounting.gl_managed(c.id) and t.transfer_id is not null loop
    perform accounting.post_transfer_gl(r.transfer_id);
  end loop;
  for r in select distinct t.deposit_id from accounting.transactions t join accounting.companies c on c.id=t.company_id
           where accounting.gl_managed(c.id) and t.deposit_id is not null loop
    perform accounting.post_deposit_gl(r.deposit_id);
  end loop;
  for r in select t.id from accounting.transactions t join accounting.companies c on c.id=t.company_id
           where accounting.gl_managed(c.id) and t.transfer_id is null and t.deposit_id is null loop
    perform accounting.post_transaction_gl(r.id);
  end loop;
end$$;