//go:build test package webhooks // Regression test for the at-least-once webhook delivery contract: a dispatch // error must NOT commit the dedup row, so Square's retry is not swallowed. import ( "encoding/json" "net/http" "testing" ) // TestHandleSquareWebhook_DispatchError_NoDedup_RetryReDispatches verifies the // handler's dispatch-first/commit-dedup-after order: when the dispatch handler // errors (here the disputes INSERT fails because the amount overflows // NUMERIC(10,2)), the handler returns 503 and records NO dedup row — so a // replayed delivery (Square's retry) is re-dispatched, not 200-skipped. Before // the at-least-once ordering this invariant silently dropped events on any // transient dispatch failure. func TestHandleSquareWebhook_DispatchError_NoDedup_RetryReDispatches(t *testing.T) { const squarePaymentID = "sqp_dispatch_err" _ = createWebhookTestPayment(t, squarePaymentID, "completed") // amount_money.amount = 9,999,999,999,999,999 pence → £99,999,999,999,999.99, // far beyond disputes.amount NUMERIC(10,2) — the INSERT fails. overflowEvent := SquareWebhookEvent{ Type: "dispute.created", EventID: "evt_dispatch_err_1", CreatedAt: nowInRFC3339(0), Data: json.RawMessage(`{ "type": "dispute", "id": "dts_dispatch_err_1", "object": { "dispute": { "id": "dts_dispatch_err_1", "state": "UNDER_REVIEW", "amount_money": {"amount": 9999999999999999, "currency": "GBP"}, "reason": "OVERFLOW", "disputed_payment": {"payment_id": "` + squarePaymentID + `"} } } }`), } w1 := deliverWebhook(t, overflowEvent) if w1.Code != http.StatusServiceUnavailable { t.Fatalf("expected 503 on dispatch error, got %d: %s", w1.Code, w1.Body.String()) } if n := countWebhookEvents(t, overflowEvent.EventID); n != 0 { t.Fatalf("expected NO dedup row after a dispatch error (Square must retry), got %d", n) } // Square retries with a well-formed payload under the SAME event_id. retryEvent := SquareWebhookEvent{ Type: "dispute.created", EventID: "evt_dispatch_err_1", CreatedAt: nowInRFC3339(0), Data: json.RawMessage(`{ "type": "dispute", "id": "dts_dispatch_err_1", "object": { "dispute": { "id": "dts_dispatch_err_1", "state": "UNDER_REVIEW", "amount_money": {"amount": 1234, "currency": "GBP"}, "reason": "NO_KNOWLEDGE", "disputed_payment": {"payment_id": "` + squarePaymentID + `"} } } }`), } w2 := deliverWebhook(t, retryEvent) if w2.Code != http.StatusOK { t.Fatalf("expected retry 200, got %d: %s", w2.Code, w2.Body.String()) } if got := getDisputeStatus(t, "dts_dispatch_err_1"); got != "open" { t.Errorf("expected retried dispute to be recorded with status 'open', got %q", got) } if n := countWebhookEvents(t, retryEvent.EventID); n != 1 { t.Errorf("expected exactly 1 dedup row after the successful retry, got %d", n) } }