v1.6.105 — partial-paid return debt-deduction + stale-productId repair
A focused patch on top of v1.6.104. The marquee item is a customer-reported bug — returns against partially-paid invoices were paying out cash but never reducing the customer’s debt, leaving the books short by the refunded amount on every such return. v96 also repairs a class of stale productId values left in invoices.items and return_items by an earlier sync-pull regression, and snapshot restore now replays gap migrations so backfills shipped after a snapshot was taken still apply when that snapshot is imported.
Returns — partial-paid invoice debt-deduction
A customer-reported bug. Returning items on a partially-paid invoice paid out cash but never reduced the customer’s debt or invoice.amount_owed. Root cause: GET /api/customers/{id} returned the raw row without the computed balance / creditBalance fields, so the Returns wizard’s Deduct from debt button stayed permanently hidden — leaving cashiers only the cash-refund and store-credit paths, both of which leak money on returns against unpaid invoices.
Three fixes ship together:
- fix
GET /api/customers/{id}now hydratesbalance,creditBalance, andtotalPaid. Same shape as the list endpoint already returns. The Returns wizard sees the computed values, surfaces the Deduct from debt option, and defaults to it when the customer carries debt. - fix Returns reject cash refunds greater than
invoice.amount_paidwith a structured 422. A return can never pay back more cash than the customer paid in. The rest goes to debt deduction or store credit. The handler now enforces this on the server so the bug can’t recur from a stale frontend. - fix Returns wizard defaults
refundTypetodebt_deductionwhen the customer has debt and surfaces an allocation hint so the cashier sees how the refund will split between cash, debt, and credit before they confirm. Pairs with the server-side cap above. - feature
GET /api/returns/audit-overpaymentsdiagnostic endpoint returns historical refunds that exceededamount_paid, for operator triage on tenants that already shipped overpaid refunds before this fix.
See The 5-step return wizard and Process refunds with credit.
Returns wizard — string-price crash
fix StepSelectItems no longer crashes with s.price.toFixed is not a function on invoices whose items[].price is a string. Postgres-via-pgx surfaces numeric JSON values as strings on cloud tenants, so any cloud invoice with a string-typed price tripped this on render. item.price is now wrapped in Number() at the call site.
Sibling .toFixed sites in the other step files have the same shape and may still be vulnerable on edge-case invoice JSON shapes; sweeping them is scoped to a follow-up.
Stale productId repair (migration v96)
perf Migration v96 repairs stale productId values left behind by an early sync-pull bug. v20 only fixed inventory and sale-time inventory_transactions rows via a created_at heuristic; v96 catches everything v20 missed.
The migration walks invoices.items[].(productId, productName) per tenant, builds a stale_id → real_id map by resolving each name against the tenant’s products table (skipping ambiguous names where multiple products share a name), then applies the map to:
return_items.product_idinventory_transactions.product_id— return-to-stock ingress rows that v20’s timestamp heuristic missedinvoices.itemsJSON — rewritten in placecarts.itemsJSON — rewritten in place
The bug surfaced on the Al-Hayat snapshot: the Returns view rendered “Product #5” or, worse, leaked another tenant’s product name through the unfiltered cross-tenant LEFT JOIN (see below). After v96, return rows resolve to the right product on the right tenant.
Cross-tenant JOIN fix (returns)
fix returns.go LEFT JOINs to products / product_variants / serialized_items now scope on (id AND tenant_id). Pre-fix the joins matched on id alone, so a stale product_id on a return row resolved to whatever tenant happened to own that numeric id in Postgres — a silent cross-tenant name leak. The fix combined with v96’s productId repair closes the leak path entirely.
Snapshot restore — replay the gap
feature Snapshot restore now detects “gap” migrations and reports them on the result. Scenario this closes: a snapshot is taken at server version N, a backfill migration ships at version N+1 (e.g. v96), the snapshot is restored onto a server already at N+1. Pre-fix the destination’s app_migrations marked v96 applied so RunAppMigrations skipped it on the next startup; the freshly-imported tenant rows never went through the backfill and stayed broken.
Mechanics:
snapshot.Buildnow bundles the snapshot-timeapp_migrationsrows inenv.Tablesso the destination can see what versions the source had applied.ApplyRestorecomputesgap = destination.versions − snapshot.versionsafter the data import transaction commits and reports it onRestoreResult.PendingMigrationson every dbType.- On SQLite (single-tenant desktop)
ApplyRestoreauto-replays the gap viadb.ReplayMigrationsand lists the replayed versions onRestoreResult.ReplayedMigrations. - On Postgres (multi-tenant cloud) auto-replay is intentionally skipped — most backfills iterate every tenant on the server, which is too expensive on the interactive restore path. Operators trigger explicit replay separately when ready.
The replay leans on CLAUDE.md’s Backfill Rule: every backfill migration is required to be idempotent (NOT EXISTS / IF NOT EXISTS guards), so re-running them is safe by construction.
Web — UI hardening
- fix Inventory History — cost column gated behind
cost:view#9 v1.6.105 . Closes a data-leak path where users without cost permission could see per-row unit costs on the per-product audit timeline. Now the column simply doesn’t render for those roles. See Per-product inventory history. - fix Invoices — Total Revenue tile gated behind
analytics:view. Same shape: the KPI summary card on the Invoices list page no longer renders for users without analytics permission. See Invoice list & filters. - fix Dashboard revenue formula now matches the Invoices page exactly #9 v1.6.105 . Pre-fix the Dashboard summed gross invoice totals while Invoices subtracted returns and deferred revenue, so the two pages disagreed for any period that had refunds. They now use the same formula end-to-end.
- fix CreateInvoice — variant picker for variable products #5 v1.6.105 #6 v1.6.105 . Picking a variable product on the manual-invoice form now opens the variant picker so the cashier can specify which size / colour they’re billing. Pre-fix the form silently fell back to the parent product, which then failed to deduct stock from the right variant. See Create a manual invoice.
- fix CreateInvoice — backend errors surfaced inline + hardcoded validation messages localized. Per-field server validation errors now render next to the input that failed, and the previously-English-only client-side messages are pulled into the i18n catalogue (translates with the rest of the app).
- fix Settings —
FieldRowinputs no longer collapse on narrow windows #4 v1.6.105 . Pre-fix the input column shrunk to zero width below ~900px viewport, leaving a label-only row. The fix stacksFieldRowalways, which also resolved a follow-on regression where labels wrapped char-by-char on tablet widths. See General settings.
Database migrations
| # | What it does | Idempotent? |
|---|---|---|
v96 | Per-tenant stale productId repair across return_items, inventory_transactions, invoices.items, and carts.items | Yes |
Snapshot restore’s gap-migration replay is a runtime mechanism, not a numbered migration of its own.
Upgrading
The desktop app auto-updates on next launch. The cloud server picks up the new build on the next deploy. Migration v96 runs on first boot of the new binary; on a typical store it completes in seconds, on tenants with very large invoice histories it may take a minute or two.
There’s no manual step. After the upgrade, returns against partially-paid invoices will offer the Deduct from debt option by default; existing overpaid refunds (refunds that exceeded amount_paid) can be triaged via GET /api/returns/audit-overpayments.
Related
- The 5-step return wizard — the rebuilt refund flow, now with debt-deduction defaulting
- Process refunds with credit — the debt-deduction and store-credit paths
- Per-product inventory history — where the cost column gate landed
- Invoice list & filters — where the Total Revenue tile gate landed
- Create a manual invoice — where the variant picker fix landed
- General settings — where the FieldRow layout fix landed
- v1.6.100 — the previous wiki-documented release (FIFO foundations + return-wizard rebuild)
- Release notes index — full version list