Files
bank/hooks/webhook_order_test.go
zeekayandHanzo Dev 3898a52c20 a webhook arriving out of order is acknowledged, not retried forever
Webhooks arrive out of order. A transaction that has reached a terminal
state has nowhere left to go, so a late delivery reporting an earlier step
names a status it cannot take — the record hook refuses that save, which
is right, and the three CurrencyCloud and IFX handlers turned the refusal
into a 500. A provider reads that as "try again", so one late delivery
became a retry every few minutes for as long as the transaction existed.

They acknowledge it now, the way the payments callback beside them already
did for a transaction it does not recognise. Nothing moves; the reason
goes to the log, where an operator can see how often a rail is delivering
late.

A repeated delivery of a status a transaction already holds is one fact
twice rather than a move, and stays allowed — providers redeliver.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-27 12:55:13 -07:00

76 lines
2.3 KiB
Go

package hooks
import (
"testing"
"github.com/hanzoai/base/core"
"github.com/luxfi/bank/collections"
)
// Webhooks arrive out of order. A transaction that has reached a terminal
// state has nowhere left to go, so a late delivery reporting an earlier step is
// a status it cannot take — and that is not something the sender can fix.
// Refusing the save answers 500, which a provider reads as "try again", so one
// late delivery becomes a retry every few minutes for as long as the
// transaction exists.
func TestAStatusATransactionCannotTakeIsNotAnError(t *testing.T) {
app := limitApp(t)
acct := account(t, app)
col, err := app.FindCollectionByNameOrId(collections.TransactionCollectionName)
if err != nil {
t.Fatal(err)
}
r := core.NewRecord(col)
r.Set("account", acct)
r.Set("type", "payment")
r.Set("direction", "debit")
r.Set("amount", 1000)
r.Set("currency", "USD")
r.Set("status", "completed")
if err := app.Save(r); err != nil {
t.Fatalf("save: %v", err)
}
for _, to := range []string{"processing", "pending", "failed", "cancelled"} {
if movesTo(r, to) {
t.Errorf("a completed transaction reports it can move to %q", to)
}
}
// The same status twice is one fact delivered twice, not a move — a
// provider redelivering is ordinary and must not be treated as a conflict.
if !movesTo(r, "completed") {
t.Error("a repeated delivery of the status a transaction already holds reads as a move it cannot make")
}
}
// And the moves a transaction can make are still moves.
func TestTheMovesATransactionCanMakeAreAllowed(t *testing.T) {
app := limitApp(t)
acct := account(t, app)
col, err := app.FindCollectionByNameOrId(collections.TransactionCollectionName)
if err != nil {
t.Fatal(err)
}
for from, tos := range map[string][]string{
"pending": {"processing", "completed", "failed", "cancelled"},
"processing": {"completed", "failed", "cancelled"},
} {
r := core.NewRecord(col)
r.Set("account", acct)
r.Set("type", "payment")
r.Set("direction", "debit")
r.Set("amount", 1000)
r.Set("currency", "USD")
r.Set("status", from)
if err := app.Save(r); err != nil {
t.Fatalf("save %s: %v", from, err)
}
for _, to := range tos {
if !movesTo(r, to) {
t.Errorf("%s -> %s is a move a payment makes and it was refused", from, to)
}
}
}
}