---
name: gst_precision_project
description: "Multi-phase project reinstating Evolution's high-precision per-rate GST (more accurate than Xero); phase status + next steps"
metadata: 
  node_type: memory
  type: project
  originSessionId: 6b1efdbf-1da0-4547-a2b5-90d8dc872ba8
---

Reinstating Evolution's original high-precision GST: keep 4dp per line, round the header ONCE per tax rate so `Tax == 10% of subtotal` at every qty (no per-line "creep"). Explicitly NOT matching Xero (Xero has the same creep). Plan file: `/config/.claude/plans/happy-enchanting-volcano.md`. Sequence: quote → invoice → reconciliation/payment → bill, underpinned by a Phase 0 column migration.

**Locked decisions:** (1) Header tax = `Σ over rates of round(rate% × subtotal_of_that_rate, 2)` (group by taxId, join tax), matching the GL posting in commitInvoice. (2) 4dp per-line storage/display everywhere. (3) Reconciliation: within-band differences are authoritative (commit Evo as-is); rebuild only on STRUCTURAL mismatch. (4) Payment residual: auto-close within band, **netted against AR control (acct 11), NO separate rounding journal**, logged. (5) Auto-close band = `max(0.01, 0.01 × active-line-count)`.

**Status (as of 2026-06-14):**
- **Phase 1 (quote): DONE + user-verified.** `quotedit.js` + `quotedit.inc` header tax per-rate. Fence plugin needed no change.
- **Phase 2 (invoice): DONE + user-verified.** `library/invoice.php` (addLines 4dp; saveInvoiceTotal/updateTotals per-rate; importQuote 4dp recompute), `invoiceaddsave.php` (line 4dp + per-rate header), `invoiceadd.js` (invoiceTotal per-rate in CENTS convention; fixed Total Inc; 4dp lines; normalized iTaxValue[]→itaxValue[] name), `invoiceadd.inc:969` (on-load $totals ttax → per-rate scalar subquery).
- **Phase 3 (reconciliation/payment): CODE DONE, php -l clean, NOT yet verified.** `library/accounting.php::ensureInvoiceCommitted` split into subtotalGap (structural, threshold max($1, 1c/line)) vs taxGap (rounding, accepted); rebuildInvoiceItemsFromXero now structural-only. Payment auto-close in `library/invoice.php::updatePayments` + Xero AR path (`library/xero.php` ~788 now calls updatePayments). MINOR ADJUSTMENTS stub in invoiceaddsave.php annotated. Tradeoff flagged: AR control carries ≤1c/line dust (no contra journal).
- **Phase 0 (migration): RUN (user confirmed 2026-06-14).** billitems/purchaseitems now decimal(15,4), so Phase 4's 4dp writes land at full precision. Schema dump under .claude/schema still STALE — regen. `migrations/taxamount_precision_decimal4.sql` widens line `taxAmount` decimal(15,2)→decimal(15,4) on quoteitems/invoiceitems/billitems/purchaseitems, then draft-only backfill: quoteitems per-UNIT `round(adjPrice*rate/100,4)` all active lines; invoiceitems per-LINE `round(adjPrice*qty*rate/100,4)` where `invoices.commited='0'` & line status=1; billitems per-LINE `round(itemcost*qty*rate/100,4)` where `bills.status='1'` (draft; commitBill sets status='2'; billitems has NO status col so gate on parent only); purchaseitems column-widen ONLY (no backfill, out of scope). Header cols stay decimal(15,2) (correct). Mirrors money_columns migration structure (SET SESSION relax + Option A SELECT-generator [ALTERs only] + Option B shell loop [whole file]). NOTE: the 2026-06-12 money_columns migration is what set these to decimal(15,2) (truncating per-line 4dp); schema dump under .claude/schema is STALE (shows pre-money-migration floats) — regen after running. SECURITY: user runs SQL via own DB tool, no mysql CLI here.
- **Phase 4 (bill/AP): CODE DONE (2026-06-14), php -l + node --check clean, NOT yet user-verified.** (1) `billaddsave.php:287` per-line taxAmount `round(...,3)→,4`. (2) `billadd.js::billTotal()` now buckets line ex-tax cents by `data-rate` and rounds header tax ONCE per rate (was per-line `Math.round` creep) — mirrors invoiceadd.js; no billMultiplier needed (JS writes no per-line tax field, only header display). (3) `billadd.inc` on-load: replaced per-line `$taxTotal += (itaxValue/100)*total` with `$taxableByRate[(string)$itaxValue] += total` then post-loop `$taxTotal += round((rate/100)*subtotal, 2)` per rate. (4) commitBill GL: **NO change needed** — web-root `accounting.php::commitBill` (live path, called from billaddsave.php:342) ALREADY groups `sum(total) by taxid × rate` (per-rate), identical to commitInvoice; `gledger.debit/credit` are `float(10,2)` so the posted tax rounds to 2dp on store. `library/accounting.php::commitBill` (806) is a byte-identical copy with NO caller — left as-is. (5) Xero AP: bills DO push as ACCPAY via the SAME shared putInvoice-style fn in `library/xero.php` (~440-540), EXCLUSIVE line amounts, sets only TaxType (never setTaxAmount), within-band stance already documented/active (xero.php:447-454) — no change needed. KNOWN latent (out of scope, present in BOTH commit fns): `$taxRate=round($val['value']/100,2)` truncates non-integer rates (e.g. 12.5%→0.13); harmless at AU 10%; fix both fns together if ever needed.

**taxSell mapping (inv 59861): RESOLVED** — importQuote taxSell override now only sets `taxId` from `inventory.taxSell` when the quote line's taxId is NULL/0 (CASE in invoice.php:288); quote taxId authoritative otherwise. php -l clean, not yet user-verified end-to-end. See [[evolution_importquote_tax_drift]].

**Phase 3b (non-destructive reconciliation): CODE DONE, php -l/node --check clean, NOT yet user-verified.** `ensureInvoiceCommitted` structural path no longer uses the destructive `rebuildInvoiceItemsFromXero` (REMOVED) — it now calls new private `insertAlignmentAdjustment($id,$invoice,$guid,$evoSub,$evoTax,$xeroSub,$xeroTax,$dryRun)` which appends ONE `src='adjustment',srcid=0,itemid=0` line for the net delta (originals + srcid→jobInventory links preserved), sets header to Xero authoritative, `review_flag=2`+review_note, logs, then `commitInvoice`. Adjustment GL account = `getSetting('adjustmentAccount')` (if valid active) else `resolveDefaultIncomeAccount()` (bails to flagInvoiceForReview if none). review_flag encoding: 0=ok, 1=not-synced/uncommitted (flagInvoiceForReview), 2=synced+adjusted/committed — never co-exist. Surfaced: `migrations/review_flag_adjustment_comment.sql` (column COMMENT, no data change), `invoiceaddsave.php` SELECT adds `invoices.review_flag`, `invoicereg.js` Adjusted(bg-warning)/Review(bg-danger) badge after id cell, `invoiceadd.inc` warning/danger banner after pageheader, new report `reports/invoicesMisaligned.{inc,js}`+`invoicesMisalignedSave.php` (gate admin||mod_gl==2||mod_ar>=1||mod_rep>=1; branch+flag filters; adj delta via correlated sum over src='adjustment') registered in reportdb.inc under Debtors & Creditors. Full spec in plan file happy-enchanting-volcano.md Phase 3b.

**Phase 3c (adjustment LIFECYCLE rework + Xero gating): CODE DONE, php -l clean, NOT yet user-verified (2026-06-16).** Makes the adjustment self-healing regardless of committed state & cleared when the driving payment is deleted. In `library/accounting.php`: (1) `ensureInvoiceCommitted` DROPPED the `commited==1` early-return — now reconciles regardless of committed state (rationale: Evo commits+pushes to Xero BEFORE the Xero payment is taken & synced back, so committed invoices still legitimately diverge); MATCH+committed = no-op, MISMATCH reverses GL + recommits with adjustment line. (2) new private `reverseInvoiceGl($id)` — mirror-posts every `status=1` `sourcetype='Invoice'` gledger row via doJournal then flips originals status=0 (mirrors invoiceaddsave reversal). Safe: commitInvoice only moves stock under `dispatch_check` (never set in cron); reports SUM gledger by status not accounts.balance cache. (3) `insertAlignmentAdjustment` calls reverseInvoiceGl first when invoice already committed. (4) new public `clearInvoiceAdjustment($invoiceid)` — soft-deletes `src='adjustment'` lines, recomputes header via per-rate subquery, resets review_flag=0/note='', reverse+recommit GL if was committed, `invoice::updatePayments()`. Recommit uses `commitInvoice($id,true)` (skipXeroPush) → no sync loop. Wired into BOTH payment-delete paths: `payment.php` AR delete branch (after delPayment) + `library/xero.php` addPayment DELETED branch. **Xero-active gating:** new global `xeroActive()` in functions.php (next to getPref) — prefers `$_SESSION['xero_auth_version']` (web), falls back to `getPref('xero_auth_version')` (cron emulates session but never sets it; getPref reads tenant pref per $userdb), cached per userdb (array_key_exists so false caches too); active == `=="2"`. Gates `ensureInvoiceCommitted` + `clearInvoiceAdjustment` (return true early if !xeroActive). Did NOT gate commitInvoice (core, all customers) or evoXero methods (file is Xero-only). **Double-clear guard:** xero.php DELETED branch clears adjustment only when `$rows>0` (it actually deleted a payment row = Xero-side-first delete); Evo-side-first delete already cleared via payment.php + pushed delete to Xero, so the synced-back DELETED finds $rows==0 and skips — no redundant uncommit.

Per-rate calc is independent of the taxAmount column precision (derives from subtotal×rate), so Phases 1-3 work before Phase 0. Float >$8.4M issue is per-column-value, not per-sum — see [[evolution_float_money_columns]].
