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:

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:

  1. Lock financial petty-cash edits/deletion after the source AP invoice has a processed credit-card allocation. The user must reverse that payment first.
  2. 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.