---
name: evolution_xero_ap_payment_sync
description: How Xero AP (bill) payment sync works in xero_payment_cron and its cross-file gotchas
metadata: 
  node_type: memory
  type: project
  originSessionId: b8569d3d-a9d0-4ac1-8644-c5027720d49b
---

`cron/xero_payment_cron.php` syncs Xero payments into Evo. To pull payments, it builds a `where` of `Invoice.InvoiceID=guid("...")` and calls `evoXero::getPayments($where)` — so a document's Xero GUID MUST be in `$where` or its payments never come back.

Gotchas when touching AP (bill) payment sync:
- **bills store the RAW GUID** in `bills.externalRef`; **invoices store a serialized array** (`serialize(["target"=>"xero","value"=>$guid])`) in `invoices.externalRef`. Set in `library/xero.php` putInvoice (~510 vs 512). Build the AP where-clause from the raw value, the AR one from `unserialize()[...]['value']`.
- `addPayment()` (library/xero.php) self-commits via `accounts::commitPayments()`, but **commitPayments()'s AP branch is DEAD** — it builds `$values` and never calls doJournal, never marks AP committed. The working AP GL poster is **`commitBillPayments()`** (posts the AP control account + bank). addPayment now calls it when `$vars['source']=="AP"`.
- **AP control account = `getPref("accPay")`, NOT a hardcoded id.** `commitBillPayments`/`uncommitBillPayments` previously hardcoded acct **`"43"`** for the AP downdate line, while the bill *commit* raises the liability against `getPref("accPay")` (`commitBill`, accounting.php ~1202). When accPay≠43 (and 43 isn't in the chart) the payment line posted to a non-existent account → AP left overstated AND the [[evolution_ledger_filter_left_join]] inner join dropped the row. Fixed to use `getPref("accPay")` in BOTH accounting.php files. Legacy "43" rows reverse to accPay on uncommit (small imbalance; surface via the GL filter and journal manually).
- `commitBillPayments()` historically blanket-marked every `committed='0'` payment committed — fixed to mark per-row by `source='AP'` only (else an unposted AR payment gets flagged committed without hitting the GL). Patched in BOTH `accounting.php` (web-root global) and `library/accounting.php` (class) per [[evolution_two_accounting_files]].

paymentDetail AP management (uncommit/commit/delete from `paymentDetail.inc` → `payment.php`):
- `payment.php` now branches on the payment's `source` (looked up by keyID). AR uses `uncommitPayments`/`commitPayments`/`invoice::delPayment`+`updatePayments` (all AR-only) and redirects to `invoiceadd`; AP uses the bill path and redirects to `billadd`. Before, all three actions were AR-hardcoded so AP did nothing to the GL/bill and dumped you on the wrong page.
- Added **`accounts::uncommitBillPayments($keyid)`** (class, `library/accounting.php`) — the AP twin of `uncommitPayments`. Posts mirrored reversing lines (debit accPay / credit bank, the swap of `commitBillPayments`) and flips `committed=0`. Nets cleanly because P&L/BAS reports SUM gledger regardless of `status`.
- `commitBillPayments($keyid=null)` (class) gained an optional keyID filter so the commit button scopes to one payment group; no-arg callers (billaddsave, xero) keep commit-all. payment.php uses the CLASS methods (`$acc->...`), NOT the web-root globals.
- AP delete recomputes `bills.balance`/`pstatus` with the same SQL billaddsave uses (`total+tax-retentionBal-sum(AP payments)`); redirect moved OUTSIDE the delete loop so multi-bill AP keys (one shared keyID) all delete — AR keys are single-row (each `addPayment` mints its own md5 key) so unchanged.

Related: [[gst_precision_project]], [[evolution_gl_pl_data_model]], [[evolution_float_money_columns]]
