feat(bridge): promote topup deposit audit row to Completed on crypto receive#457
Merged
Merged
Conversation
…receive The ERPNext Bridge Transfer Request audit trail left topup deposit rows stuck at "Fiat Received" forever: the IBEX crypto receive wrote a separate ibex:<txHash> Settled row, and nothing ever correlated the two. Operators could not tell a healthy completed topup from one stuck mid-conversion. Join the two sides on the IBEX destination tx hash, handling both webhook orderings: - Crypto receive after deposit: the crypto-receive handler now promotes the matching deposit row (ibex_tx_hash = txHash, excluding ibex:% rows) to Completed, stamping the credited account_id/wallet_id on it. Best-effort: a promotion failure alerts but never fails the webhook. - Deposit after crypto receive: writeBridgeDepositRequest checks for an existing ibex:<txHash> settle row when the event carries receipt.destination_tx_hash, and writes Completed directly. Guard rails in upsertBridgeTransferRequest: - Topup row statuses are now monotonic (Pending < Fiat Received < Settled < Completed < Failed) so Bridge webhook retries of earlier deposit events can no longer downgrade a promoted row. Cashout rows keep last-write-wins semantics. - source_systems_seen is merged as a set union on update instead of overwritten, so promoted rows keep recording both source systems. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXnQYUbKQe5wUCWEvdAau7
…n tests Review follow-ups on the topup promotion flow: - The crypto-first ordering now stamps account_id/wallet_id on the deposit row too: the settle-row lookup returns the full doc instead of a boolean (hasBridgeTransferRequest -> findBridgeTransferRequest), and the deposit writer lifts the attribution from it. Both orderings now produce the same enriched row. - The lookup checks the settle row is actually at Settled instead of treating bare existence as settled. - New tests: the raced-duplicate upsert path (POST 409 -> re-find -> PUT) preserves a promoted status; attribution asymmetry; not-Settled settle row ignored; missing existing status; source_systems_seen whitespace and dedupe normalization; already_completed promotion result on the crypto-receive route. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXnQYUbKQe5wUCWEvdAau7
A hash-less deposit write (or Bridge retry) must omit account_id/wallet_id from the upsert payload entirely — an explicit null would wipe the attribution a prior promotion stamped on the row, and the status guard does not cover those fields. Assert both the writer input and the JSON-serialized toErpnext() output, which is what axios actually sends. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXnQYUbKQe5wUCWEvdAau7
bobodread876
approved these changes
Jul 19, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Topup rows in the ERPNext Bridge Transfer Request audit trail were stuck at Fiat Received forever. The Bridge deposit webhook only ever wrote
Fiat Received, the IBEX crypto receive wrote a separateibex:<txHash>row atSettled, and nothing correlated the two —Completedwas only reachable on the Cashout path. An operator couldn't tell a healthy finished topup from one stuck mid-conversion without manually hunting for the siblingibex:row.Design
Join the two sides of a topup on the IBEX destination tx hash, covering both webhook orderings:
1. Crypto receive arrives after the deposit webhooks (common case)
After writing the
ibex:<txHash>settle row, the crypto-receive handler callspromoteBridgeDepositForCryptoReceive, which finds the deposit-side row (ibex_tx_hash = txHash,transaction_type = Topup,request_id not like "ibex:%") and promotes it to Completed, stamping the creditedaccount_id/wallet_idand appendingibex_crypto_receivetosource_systems_seen. Best-effort: a promotion failure logs + alerts (erpnext_promote_failed) but never fails the webhook — funds are already credited and the settle row exists.not_foundis normal when Bridge'spayment_processedevent (which carriesreceipt.destination_tx_hash) hasn't stamped the deposit row yet; the reverse path below converges it.2. Crypto receive arrives before the deposit's
payment_processedwebhookwriteBridgeDepositRequestnow checks for an existingibex:<txHash>settle row whenever the event carriesreceipt.destination_tx_hash, and writes the row as Completed directly. A failed check degrades toFiat Received(never blocks the audit write).Guard rails in
upsertBridgeTransferRequest(load-bearing for the above):Pending < Fiat Received < Settled < Completed < Failed): Bridge webhook retries of earlier deposit events can no longer stomp a promoted row back toFiat Received. Cashout rows keep their existing last-write-wins semantics.source_systems_seenmerges as a set union on update instead of overwriting, so a promoted row keeps recording bothbridge_depositandibex_crypto_receive.No ERPNext-side changes needed: the doctype's status options and the transfer-requests admin page already handle
Completedfor topups.Known limitation
If the
payment_processeddeposit event and the crypto receive race within the same sub-second window, both matching checks can miss (deposit row not yet stamped with the hash when promotion looks;ibex:row not yet written when the deposit write checks). The row self-heals on any subsequent Bridge webhook retry. The read-modify-write on promotion has no compare-and-swap, same as the existing upsert.Tests
33 unit tests across the three touched specs (all passing), including:
not_found, idempotentalready_completed, write-failure error pathCompletedwhen settle row exists, no lookup without a destination hash, degrade-to-Fiat Receivedon lookup failuresource_systems_seenunionnot_foundand on promotion failure (with alert)yarn tsc-checkand eslint clean.🤖 Generated with Claude Code
https://claude.ai/code/session_01UXnQYUbKQe5wUCWEvdAau7