Problem
In processAutoExchange (pkg/exchange/auto.go), after the irreversible AWS Execute call succeeds, the completed-record save error is only logged, never surfaced:
// pkg/exchange/auto.go:441-447
if err := params.Store.SaveRIExchangeRecord(ctx, record); err != nil {
logging.Errorf("failed to save completed exchange record for %s: %v", rec.SourceRIID, err)
}
outcome.RecordID = record.ID
outcome.ExchangeID = exchangeID
return outcome
This is the same fail-open shape as COR-04 (PR #1229, just merged) but on a different code path:
- AWS exchange has been committed (money irreversibly spent)
- DB row is missing (status="completed" never persisted)
- Next iteration of the
for _, rec := range recs loop calls GetRIExchangeDailySpend, which sums status = 'completed' rows -- and now under-reports total spend by exactly this exchange's payment_due
- The under-reported
dailySpent allows the cap check newTotal.Cmp(dailyCap) > 0 to pass on the next iteration when it should have rejected, blowing past MaxPaymentDailyUSD by the missing amount
Likelihood is low (DB write failure on Postgres is rare on the same connection that just queried), but the impact is direct cap bypass on a money path. This is also the exact shape feedback_no_silent_fallbacks.md calls out.
Acceptance criteria
SaveRIExchangeRecord failure on the completed-record path must either:
- Block the loop from continuing (return an error from
RunAutoExchange, halting subsequent exchanges until the audit trail is reliable), OR
- Be reflected in the outcome and the next iteration's cap check (e.g., maintain an in-process tally that the cap check adds on top of
GetRIExchangeDailySpend), so an in-memory sum carries forward when the DB row is missing.
Mirror the existing dailySpent / payment-due failure-closed branches' shape: logging.Errorf + outcome.Error set + return. The caller (RunAutoExchange) should stop processing further recommendations once any audit save has failed during an auto run; otherwise an operator has no way to know the cap budget left.
Regression test: drive processAutoExchange with a mock store whose SaveRIExchangeRecord fails only for completed-status records, run two back-to-back recommendations whose sum exceeds MaxPaymentDailyUSD, and assert the second exchange is rejected (cap check blocks) even though the first row never made it to the DB.
References
Problem
In
processAutoExchange(pkg/exchange/auto.go), after the irreversible AWSExecutecall succeeds, the completed-record save error is only logged, never surfaced:This is the same fail-open shape as COR-04 (PR #1229, just merged) but on a different code path:
for _, rec := range recsloop callsGetRIExchangeDailySpend, which sumsstatus = 'completed'rows -- and now under-reports total spend by exactly this exchange'spayment_duedailySpentallows the cap checknewTotal.Cmp(dailyCap) > 0to pass on the next iteration when it should have rejected, blowing pastMaxPaymentDailyUSDby the missing amountLikelihood is low (DB write failure on Postgres is rare on the same connection that just queried), but the impact is direct cap bypass on a money path. This is also the exact shape
feedback_no_silent_fallbacks.mdcalls out.Acceptance criteria
SaveRIExchangeRecordfailure on the completed-record path must either:RunAutoExchange, halting subsequent exchanges until the audit trail is reliable), ORGetRIExchangeDailySpend), so an in-memory sum carries forward when the DB row is missing.Mirror the existing dailySpent / payment-due failure-closed branches' shape:
logging.Errorf+outcome.Errorset + return. The caller (RunAutoExchange) should stop processing further recommendations once any audit save has failed during an auto run; otherwise an operator has no way to know the cap budget left.Regression test: drive
processAutoExchangewith a mock store whoseSaveRIExchangeRecordfails only for completed-status records, run two back-to-back recommendations whose sum exceedsMaxPaymentDailyUSD, and assert the second exchange is rejected (cap check blocks) even though the first row never made it to the DB.References
PaymentDuein the same functioninternal/purchase/execution.go(different code path, same anti-pattern)feedback_no_silent_fallbacks.md(CUDly memory garden)