For engineering leaders
What unreversed transfers say about your codebase
Your refund function sets both flags correctly. Code review approved it. Tests cover it. And money still leaves weekly — because the function everyone reviews is not where refunds actually happen anymore. Admin tools, support macros, retry queues and cron jobs accumulated secondary paths that never received primary-path scrutiny. Beyond the inventory problem lies a concurrency lesson your team will otherwise learn expensively: webhook events race, payloads lie, and locks are load-bearing.
The primary-function fallacy
Inventory every path that can issue a refund: the API route everyone knows, plus the internal admin endpoint, the support-tool action, the bulk-import script, the scheduled job forgiving edge cases, and whichever dashboard a CS lead bookmarked. Each is a place reverse_transfer defaults silently false. Grep is your friend; assumptions are not.
# Find every refund creation point
rg -n "refunds\.create" --type ts app lib scripts
# Then check each call site for both flagsConcurrency lessons worth stealing
charge.refunded and transfer.reversed arrive within seconds in either order, unordered. Naive handlers read pre-reversal state and raise phantom shortfalls. Production-grade handling needs two mechanisms together: a short delay before processing refund events (letting in-flight reversals settle) and a distributed lock serialising per charge. Either alone leaves holes; together they make findings trustworthy.
Payload staleness is architectural, not incidental
Webhook payloads snapshot at queue time, not processing time. Reconciling from event.data.object bakes stale state into decisions during exactly the retries and backoffs where correctness matters. Payload routes; live state decides. One extra API call removes an entire false-positive class — cheap insurance most codebases skip.
Tests that would have caught it
Happy-path tests pass while flags default false — the failure mode is silence. Assert false positives as hard as positives: a detector firing on correctly-reconciled charges must fail CI. Our own self-test corpus runs 38 scenarios including engineered negatives for precisely this reason; steal the philosophy even if you build nothing else.
Build vs adopt, priced honestly
The detection math is a weekend; the surrounding system — queues, locks, idempotency, backfill pagination, alerting, multi-tenant isolation, ongoing schema-drift maintenance — is a product. Adopting buys the whole stack read-only today; building makes sense when bespoke ledger integration demands it. Decide with the full ledger visible, not just the fun part.
Common questions
Our tests pass — aren’t we fine?
They test the happy path, which is precisely where the bug isn’t. Secondary paths and concurrency windows escape test suites structurally, not accidentally.
How long would building this in-house take?
Detection logic: days. The reliable operational wrapper — queues, locks, retries, backfill, alerting, audit trail — is where quarters go. Price maintenance, not construction.