Task 0242 investigation — invoice 1115903
Finding
The invoice was valid when entered. It was later corrupted by an edit to the petty-cash expense created during its credit-card payment workflow.
The source AP invoice had two details totaling 1,247.77. Credit-card payment
created a petty-cash deposit and generated CCI invoice. Each original petty-cash
item deliberately retained ap_invoice_detail_id pointing at the source AP
invoice detail so payment allocation could verify the parent invoice.
On August 12, the two expense lines were replaced by one combined line. The save
controller treated the absent old item IDs as deletions and called
PettyCashDepositItem#destroy_with_ap_invoice_detail. That method checked whether
the deposit's generated CCI invoice was paid, but then destroyed the detail from
self.ap_invoice_detail. In this data shape that detail belonged to the already
paid parent invoice, not the generated CCI invoice.
Result:
- both parent invoice details were destroyed;
- parent invoice amount callbacks reduced 1,247.77 to zero;
- the two old CCI details were not removed;
- a new combined CCI detail was added, doubling the CCI amount to 2,495.54; and
- the parent detail financial transactions were reversed by destroy callbacks.
The AP page then hit a separate type bug. ApInvoice#written_off_entries used
flatten!; for an invoice with no details that returns nil. The React Matches
component called reduce on the resulting null value and crashed. The null caused
the visible crash, while the earlier cross-invoice deletion caused the data loss.
Ownership failure
The core model assumption is false: a petty-cash item's
ap_invoice_detail_id is not always owned by petty_cash_deposit.ap_invoice.
During credit-card payment of an existing invoice, it can intentionally point to a
source parent detail. Update and destroy code must distinguish provenance from
ownership before mutating the referenced row.
Checking only whether the generated CCI invoice is paid also misses the actual
business lock. The incident deposit's generated invoice was unpaid, while the
source invoice linked through ap_invoices_petty_cash_deposits was paid by credit
card and had processed payments.
Repair decision
The repair SQL preserves the user's August 12 combined expense line and its finalized financial reconciliation. It restores the two original parent details, un-reverses their original financial transactions, removes the two obsolete CCI details that the edit should have replaced, reverses only those obsolete unreconciled CCI transactions, and corrects all three stored amounts to 1,247.77.
The script spans the financial and purchasing databases, so its two plain SQL
transactions must be run separately: financial first, purchasing second.
This artifact does not authorize production execution.
Code recommendation
Use both protections:
- Lock financial petty-cash edits/deletion after the source AP invoice has a processed credit-card allocation. The user must reverse that payment first.
- Independently reject any item update/delete whose referenced AP detail is not owned by the deposit's generated invoice.
Also normalize written_off_entries to an array in Ruby and defensively in React.
See the plan and
workflow diagram.
Scope check
A read-only query found other zero-detail invoices, including a small number marked as credit-card paid, but the evidence does not establish that they share this exact failure. Do not bulk-repair them from this incident script. Investigate each record separately or build a validated repair report before any broader backfill.