Skip to content

Flag over-billed bills on PO return + standing COGS on cancel (audit-C4, audit-H8)#194

Merged
OneTwo3D merged 2 commits into
developmentfrom
feature/c4h8-purchasing-reversal-alerts
Jun 13, 2026
Merged

Flag over-billed bills on PO return + standing COGS on cancel (audit-C4, audit-H8)#194
OneTwo3D merged 2 commits into
developmentfrom
feature/c4h8-purchasing-reversal-alerts

Conversation

@OneTwo3D

Copy link
Copy Markdown
Owner

Wave 1, item 4 of the workflow-audit remediation (epic onetwo3d-ims-r3xh). Phase-1 alert-only coverage for two purchasing-reversal gaps. Neither posts an accounting correction yet — phase 2 (credit memo / COGS journal) is tracked separately.

audit-C4 — PO returns don't flag the over-billed bill

returnPurchaseOrder reverses stock + FIFO cost layers but never touches supplier invoices recorded against the PO. Returning billed goods left the AP liability at the full amount silently.

  • New pure helper computePurchaseOrderOverBilling() sums billed qty/value per PO line (across all bills) and compares against the quantity now kept (received − returned).
  • On return, when a billed line is over-billed, emits a return_overbilled_bill WARNING activity log with machine-readable metadata (per-line over-billed qty/value + bill references).
  • PO detail page shows an amber alert naming the bills and the over-billed amount, telling the operator to raise a supplier credit.

audit-H8 — cancellation leaves consumed-unit COGS standing

reversePurchaseOrderCostLayersForCancellation only reverses remainingQty; units already sold/used keep COGS sourced from the cancelled receipt — P&L silently carries it.

  • New summarizeConsumedCostLayers() (pure) + readPurchaseOrderConsumedCostForCancellation() compute consumed = receivedQty − remainingQty and its value.
  • The cancellation service reads this before the reversal runs, so the about-to-be-zeroed remaining qty isn't miscounted as consumed (covered by a test that asserts read-before-reversal ordering).
  • When consumed qty is non-zero: emits a cancelled_consumed_cogs_standing WARNING, returns consumedCost on the result, and the client shows an amber post-cancel alert. Cancellation stays fully blocked once an invoice exists (unchanged).

Tests

  • purchasing-reversal-alerts.test.ts — 6 cases (over-billing across multiple bills, no-over-billing, unbilled PO, freight lines with no poLineId).
  • po-cancellation.test.tssummarizeConsumedCostLayers totals + the service WARNING/consumedCost/read-ordering path; existing idempotency test updated for the new dep + result shape.
  • 86/86 purchasing suite, type-check + lint clean.

Closes onetwo3d-ims-pbu4, onetwo3d-ims-j5ic.

…on cancel (closes audit-C4, audit-H8)

Phase 1 (alert-only) for two purchasing-reversal gaps the workflow audit found:

audit-C4 — PO returns vs. supplier bills:
returnPurchaseOrder reverses stock and FIFO cost layers but never touches the
supplier invoices already recorded against the PO. Returning billed goods left
the AP liability at the full amount with no prompt. New pure helper
computePurchaseOrderOverBilling() sums billed qty/value per PO line and compares
against the quantity kept (received − returned). When a billed line is now
over-billed, the return emits a return_overbilled_bill WARNING (machine-readable
metadata: per-line over-billed qty/value + bill references) and the PO detail
page shows an amber alert telling the operator to raise a supplier credit.

audit-H8 — cancellation leaves consumed-unit COGS standing:
reversePurchaseOrderCostLayersForCancellation only reverses remainingQty; units
already sold/used keep COGS sourced from the cancelled receipt. New
summarizeConsumedCostLayers() / readPurchaseOrderConsumedCostForCancellation()
compute consumed = receivedQty − remainingQty and its value. The cancellation
service reads this BEFORE the reversal runs (so the about-to-be-zeroed remaining
qty is not miscounted as consumed), emits a cancelled_consumed_cogs_standing
WARNING when non-zero, returns it on the result, and the client shows an amber
post-cancel alert. Cancellation stays fully blocked once an invoice exists.

Neither posts an accounting correction yet — phase 2 (credit memo / COGS
journal) is tracked separately. Pure functions are DI-seam testable.

Tests: purchasing-reversal-alerts (6) + po-cancellation consumed-cost & WARNING
paths; 86/86 purchasing suite, type-check + lint clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

…l review)

Codex adversarial pass on PR #194:

HIGH — C4 over-billing was read AFTER the return tx committed, so a
concurrent second return could inflate the figure. Moved the read inside
the transaction (qtyReturned now reflects exactly this return) and return
the summary from db.$transaction; logging stays outside, isolated.

HIGH — the consumed-COGS WARNING logActivity sat in the outer try with no
local guard; a log failure after a committed cancellation would surface as
{ success:false }. Wrapped it in its own try/catch.

MEDIUM — over-billing 'bills' listed EVERY invoice on the PO, not just the
ones that billed an over-billed line; the WARNING count overstated N. Now
tracks contributing invoiceIds and names only those.

MEDIUM — the PO-detail alert heading was backwards ('returned goods exceed
what was billed back'); corrected to 'supplier bill exceeds goods kept'.

MEDIUM — negative netReceived (corrupt qtyReturned > qtyReceived) would
inflate over-billing; clamp netReceived to >= 0.

MEDIUM — dropped the hardcoded £ in both WARNING descriptions (values are
base currency; label them so a non-GBP base isn't mislabelled).

LOW — clarified the UI bills line shows gross totals incl. tax; documented
why the consumed-cost reader keeps ORDER BY / omits FOR UPDATE; switched
the over-billing/consumed epsilons to lte(0) for Decimal-exact comparison.

Tests: +2 (contributing-bills filter, negative-clamp); 88/88 purchasing
suite, type-check + lint clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@OneTwo3D

Copy link
Copy Markdown
Owner Author

Codex adversarial review — findings + fixes (commit 5f4e3b7)

Codex was at its usage cap; the adversarial pass ran by hand with equal rigour. It found two HIGH issues plus several MEDIUM/LOW — all addressed.

HIGH — C4 over-billing read after commit (race). The over-billing figure was re-read from the DB after the return transaction closed, so a concurrent second return could increment qtyReturned again and inflate the logged figure. Fixed: the read now happens inside the transaction (so qtyReturned reflects exactly this return), and the summary is returned from db.$transaction; the WARNING is logged outside, isolated in a try/catch.

HIGH — consumed-COGS WARNING could fail a committed cancellation. The new cancelled_consumed_cogs_standing logActivity sat in the outer try with no local guard — a log failure after the PO was already cancelled/committed would propagate to the outer catch and return { success: false }, desyncing UI from DB. Fixed: wrapped in its own try/catch (matching the existing enqueueStockSync pattern).

MEDIUM — over-billing named every bill on the PO. bills and the WARNING's "across N bill(s)" counted all invoices, not just those that billed an over-billed line — sending finance to investigate correctly-billed invoices. Fixed: tracks contributing invoiceIds and names only those. New test names only the bills that billed an over-billed line.

MEDIUM — alert heading was backwards. "Returned goods exceed what was billed back" described the opposite scenario. Fixed to "Supplier bill exceeds goods kept after return."

MEDIUM — negative netReceived inflated over-billing. A corrupt qtyReturned > qtyReceived made netReceived negative, so billed − netReceived over-stated the figure. Fixed: netReceived clamps to >= 0. New test clamps a corrupt qtyReturned > qtyReceived.

MEDIUM — hardcoded £. Both WARNING descriptions hardcoded the GBP symbol though the values are base-currency. Fixed: dropped the symbol, labelled "(base currency)". (UI alerts already format with the real base-currency symbol via formatMoney.)

LOW — clarity/consistency. UI bills line now states the totals are gross (incl. tax); added a comment on why the consumed-cost reader keeps ORDER BY and omits FOR UPDATE (consumed portion is immutable, same tx, read before the reversal locks remaining layers); switched the over-billing/consumed thresholds to lte(0) for Decimal-exact comparison.

Verified-clean angles (no action needed)

  • Decimal serialization across RSC→client: both summaries are fully string-typed; no Decimal crosses the boundary.
  • Read-before-reversal (H8): consumed read and reversal are in the same tx; the read runs first and sees pre-reversal remainingQty. Test asserts the ordering.
  • Opening-stock / production layers: excluded via poLineId = ANY(...) — correct, they aren't PO-originated.
  • SQL column casing + ANY($)::text[]: match schema; same pattern as the existing reversal query.
  • Cancellation still fully blocked once an invoice exists (unchanged).

Validation

  • purchasing-reversal-alerts.test.ts 8 + po-cancellation.test.ts consumed-cost/WARNING/ordering paths; 88/88 purchasing suite.
  • type-check + lint clean.

Deferred to phase 2 (alert-only scope, accepted)

  • No credit-memo / COGS-correction journal is posted yet (tracked separately).
  • Over-billing WARNING re-fires on each partial return (no dedup); the alert persists on the page until the PO state changes, since the supplier credit is external to IMS. Documented in docs/workflows.md.

@OneTwo3D OneTwo3D merged commit a362e43 into development Jun 13, 2026
5 of 11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant