fix: payments hardening — SCA wire contract (saved-card ref + tokenize-result), terminal/till token routing, tip-cap overflow carve, completion campaign atomicity, orphan B1-evidence gate, gift-card gates/locks, admin backstops
- ValidateCardInfo accepts saved-card ref + new_card_token coexistence (matches resolveChargeSource); new_card_token added to terminal/till request structs so SCA tokens are never dropped - maxOnlineTipPence (£250) enforced on the overflow-tip carve AND buildSplitRecords (both carve paths) — closes the £10k bypass - completion-path campaign increments made atomic reserve-first (conditional UPDATE ... RETURNING) + schema backstops (chk_times_redeemed, partial unique index on milestone redemptions) - webhook orphan detection gated on B1 evidence (b1_attempts / sweep-duplicate refund row) so a delayed legit completion is never marked failed - gift-card: per-user £500/day cap lock held across read-modify-write, expired-card top-up gate, NaN/Inf float bounds, refund_failed ack filter, on_the_house excluded from balance, postChargeRecheck notification - admin apply-redemption route + admin-or-owner, in-handler isAdminRequest on 4 gift-card handlers, tip lock key aligned - 2FA fallback machinery removed (insertTwoFAFallbackAudit/reissue/consent), dead fields stripped from charge structs - tests: prod-tag suite, mock SCA parity, tip-cap overflow, completion races, cards pagination, ValidateCardInfo tables
This commit is contained in:
@@ -826,16 +826,28 @@ func squarePaymentKnown(ctx context.Context, squarePaymentID string) (bool, erro
|
||||
// verbatim, preserving both). Only 'pending' rows WITHOUT a square_payment_id
|
||||
// are candidates — that is exactly the population the keyed stale-pending sweep
|
||||
// replays (sweep.go), so a match is the sweep-minted duplicate's origin.
|
||||
//
|
||||
// AGE GATE (webhook-vs-response race): the candidates are additionally limited
|
||||
// to rows old enough to have actually been replayed by the sweep
|
||||
// (created_at <= NOW() - payments.SweepKeyedReplayAge()). The sweep only
|
||||
// replays keyed rows past that age (sweep.go stalePendingKeyedAge), so a fresh
|
||||
// pending row can never be a sweep-minted duplicate's origin — it is a legit
|
||||
// charge whose completion webhook raced the handler's own square_payment_id
|
||||
// write (response loss), and marking it failed would kill the customer's real
|
||||
// payment. When the gate blocks a match the caller leaves the row pending
|
||||
// (critical-log), never fails it — the sweep will reconcile it later.
|
||||
func findPendingByOrphanKeys(ctx context.Context, payment squarePaymentPayload) (paymentID, bookingID string, found bool, err error) {
|
||||
replayEligibleSince := time.Now().Add(-payments.SweepKeyedReplayAge())
|
||||
if payment.IdempotencyKey != "" {
|
||||
var pid string
|
||||
var bid *string
|
||||
err := db.Conn.QueryRow(ctx, `
|
||||
SELECT id, booking_id FROM payments
|
||||
WHERE status = 'pending' AND idempotency_key = $1 AND square_payment_id IS NULL
|
||||
AND created_at <= $2
|
||||
ORDER BY created_at DESC, id DESC
|
||||
LIMIT 1
|
||||
`, payment.IdempotencyKey).Scan(&pid, &bid)
|
||||
`, payment.IdempotencyKey, replayEligibleSince).Scan(&pid, &bid)
|
||||
if err == nil {
|
||||
if bid != nil {
|
||||
bookingID = *bid
|
||||
@@ -859,9 +871,10 @@ func findPendingByOrphanKeys(ctx context.Context, payment squarePaymentPayload)
|
||||
WHERE status = 'pending' AND square_payment_id IS NULL
|
||||
AND ABS(amount - $2) < 0.005
|
||||
AND (booking_id = $1 OR gift_card_id = $1)
|
||||
AND created_at <= $3
|
||||
ORDER BY created_at DESC, id DESC
|
||||
LIMIT 1
|
||||
`, payment.ReferenceID, amount).Scan(&pid, &bid)
|
||||
`, payment.ReferenceID, amount, replayEligibleSince).Scan(&pid, &bid)
|
||||
if err == nil {
|
||||
if bid != nil {
|
||||
bookingID = *bid
|
||||
@@ -957,6 +970,30 @@ func detectOrphanedReplayCharge(ctx context.Context, payment squarePaymentPayloa
|
||||
log.Printf("[SQUARE-WEBHOOK] payment.updated: COMPLETED square payment %s matches no local row and no pending origin row by idempotency key/reference_id — acknowledging (not a sweep-minted duplicate)", payment.ID)
|
||||
return nil
|
||||
}
|
||||
// B1-EVIDENCE GATE: only treat the origin as a sweep-minted duplicate when
|
||||
// the sweep actually minted+attempted to refund it (b1_attempts > 0 or a
|
||||
// refunds row with the sweep-duplicate reason). Without that evidence a
|
||||
// COMPLETED payment is far more likely the ORIGINAL charge whose completion
|
||||
// webhook was delayed past the age gate — marking the origin failed would
|
||||
// kill a customer's real payment. Leave it pending: the keyed sweep rescues
|
||||
// the original 'completed' or triggers B1 if a duplicate was truly minted.
|
||||
var b1Evidence bool
|
||||
if err := db.Conn.QueryRow(ctx, `
|
||||
SELECT EXISTS(
|
||||
SELECT 1 FROM payments
|
||||
WHERE id = $1 AND b1_attempts > 0
|
||||
UNION ALL
|
||||
SELECT 1 FROM refunds
|
||||
WHERE payment_id = $1 AND reason = 'duplicate charge — sweep replay'
|
||||
)
|
||||
`, originID).Scan(&b1Evidence); err != nil {
|
||||
log.Printf("[SQUARE-WEBHOOK] Orphan-replay B1-evidence lookup failed for origin payment %s: %v", originID, err)
|
||||
return err
|
||||
}
|
||||
if !b1Evidence {
|
||||
log.Printf("[SQUARE-WEBHOOK] payment.updated: COMPLETED square payment %s matches pending origin %s by idempotency key but NO B1 evidence (b1_attempts=0, no sweep-duplicate refund) — leaving origin pending for the sweep to reconcile (delayed legit completion or the sweep has not replayed yet); not marking failed", payment.ID, originID)
|
||||
return nil
|
||||
}
|
||||
tag, err := db.Conn.Exec(ctx,
|
||||
`UPDATE payments SET status = 'failed', updated_at = NOW() WHERE id = $1 AND status = 'pending'`,
|
||||
originID)
|
||||
|
||||
@@ -77,7 +77,7 @@ func TestWebhook_AndSweep_DoNotDoubleComplete(t *testing.T) {
|
||||
event := SquareWebhookEvent{
|
||||
Type: "payment.updated",
|
||||
EventID: "evt_webhook_and_sweep_1",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{
|
||||
"type": "payment",
|
||||
"id": "` + squarePaymentID + `",
|
||||
|
||||
@@ -27,7 +27,7 @@ func TestHandleSquareWebhook_DispatchError_NoDedup_RetryReDispatches(t *testing.
|
||||
overflowEvent := SquareWebhookEvent{
|
||||
Type: "dispute.created",
|
||||
EventID: "evt_dispatch_err_1",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{
|
||||
"type": "dispute",
|
||||
"id": "dts_dispatch_err_1",
|
||||
@@ -55,7 +55,7 @@ func TestHandleSquareWebhook_DispatchError_NoDedup_RetryReDispatches(t *testing.
|
||||
retryEvent := SquareWebhookEvent{
|
||||
Type: "dispute.created",
|
||||
EventID: "evt_dispatch_err_1",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{
|
||||
"type": "dispute",
|
||||
"id": "dts_dispatch_err_1",
|
||||
|
||||
@@ -144,7 +144,7 @@ func TestWebhook_DisputeStateUpdated_Lost_EmptyPaymentID_FallsBackToDisputeRow(t
|
||||
event := SquareWebhookEvent{
|
||||
Type: "dispute.state.updated",
|
||||
EventID: "evt_dispute_fallback_1",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{
|
||||
"type": "dispute",
|
||||
"id": "dts_fallback_1",
|
||||
@@ -194,7 +194,7 @@ func TestWebhook_DisputeStateUpdated_EmptyPaymentID_NoDisputeRow_RaisesCritical(
|
||||
event := SquareWebhookEvent{
|
||||
Type: "dispute.state.updated",
|
||||
EventID: "evt_dispute_no_row_1",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{
|
||||
"type": "dispute",
|
||||
"id": "dts_no_dispute_row_1",
|
||||
@@ -310,7 +310,7 @@ func TestWebhook_DisputeEvidence_InformationalOnly(t *testing.T) {
|
||||
event := SquareWebhookEvent{
|
||||
Type: tc.eventType,
|
||||
EventID: tc.eventID,
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{
|
||||
"type": "dispute",
|
||||
"id": "` + tc.disputeID + `",
|
||||
@@ -348,7 +348,7 @@ func TestWebhook_DisputeEvidence_InformationalOnly(t *testing.T) {
|
||||
event := SquareWebhookEvent{
|
||||
Type: "dispute.evidence.created",
|
||||
EventID: "evt_evidence_log_1",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{
|
||||
"type": "dispute",
|
||||
"id": "dts_evidence_log_1",
|
||||
@@ -379,7 +379,7 @@ func TestWebhook_TerminalCheckout_InformationalOnly(t *testing.T) {
|
||||
event := SquareWebhookEvent{
|
||||
Type: tc.eventType,
|
||||
EventID: tc.eventID,
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{"type": "terminal.checkout", "id": "` + tc.checkoutID + `"}`),
|
||||
}
|
||||
w := deliverWebhook(t, event)
|
||||
@@ -407,7 +407,7 @@ func TestWebhook_TerminalCheckout_InformationalOnly(t *testing.T) {
|
||||
event := SquareWebhookEvent{
|
||||
Type: "terminal.checkout.updated",
|
||||
EventID: "evt_terminal_log_1",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{"type": "terminal.checkout", "id": "chk_round7_log_1"}`),
|
||||
}
|
||||
if w := deliverWebhook(t, event); w.Code != http.StatusOK {
|
||||
|
||||
@@ -10,12 +10,22 @@ import (
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"crussell/clock"
|
||||
"crussell/db"
|
||||
"crussell/testutils/fixtures"
|
||||
)
|
||||
|
||||
// nowInRFC3339 returns the current UTC instant (plus an offset) as an RFC3339
|
||||
// string. Webhook-event timestamps and gift-card/till-sale created_at values
|
||||
// must be clock-relative so sweep/expiry-window logic stays correct forever
|
||||
// instead of drifting against a hardcoded 2025 date.
|
||||
func nowInRFC3339(offset time.Duration) string {
|
||||
return clock.Now().Add(offset).UTC().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Helpers — DB-backed state assertions
|
||||
// =============================================================================
|
||||
@@ -157,7 +167,7 @@ func deliverPaymentUpdatedFailed(t *testing.T, squarePaymentID string) *httptest
|
||||
event := SquareWebhookEvent{
|
||||
Type: "payment.updated",
|
||||
EventID: "evt_" + squarePaymentID,
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{
|
||||
"type": "payment",
|
||||
"id": "` + squarePaymentID + `",
|
||||
@@ -202,7 +212,7 @@ func getGiftCardFunding(t *testing.T, id string) (totalFundsAdded, amountRemaini
|
||||
// does.
|
||||
func TestWebhook_PaymentUpdated_Failed_ClawsBackCreatedCard(t *testing.T) {
|
||||
const squarePaymentID = "sqp_clawback_create"
|
||||
const cardCreatedAt = "2025-01-01T00:00:00Z"
|
||||
cardCreatedAt := nowInRFC3339(0)
|
||||
saleID, giftCardID := createWebhookTestGiftCardAndSale(t, squarePaymentID, cardCreatedAt, cardCreatedAt, 40.00)
|
||||
|
||||
w := deliverPaymentUpdatedFailed(t, squarePaymentID)
|
||||
@@ -222,7 +232,8 @@ func TestWebhook_PaymentUpdated_Failed_ClawsBackCreatedCard(t *testing.T) {
|
||||
// of the card and the sale is marked failed.
|
||||
func TestWebhook_PaymentUpdated_Failed_ClawsBackTopup(t *testing.T) {
|
||||
const squarePaymentID = "sqp_clawback_topup"
|
||||
saleID, giftCardID := createWebhookTestGiftCardAndSale(t, squarePaymentID, "2025-01-01T00:00:00Z", "2025-01-02T00:00:00Z", 60.00)
|
||||
cardCreatedAt := nowInRFC3339(0)
|
||||
saleID, giftCardID := createWebhookTestGiftCardAndSale(t, squarePaymentID, cardCreatedAt, nowInRFC3339(24*time.Hour), 60.00)
|
||||
|
||||
w := deliverPaymentUpdatedFailed(t, squarePaymentID)
|
||||
if w.Code != http.StatusOK {
|
||||
@@ -243,7 +254,8 @@ func TestWebhook_PaymentUpdated_Failed_ClawsBackTopup(t *testing.T) {
|
||||
// untouched.
|
||||
func TestWebhook_PaymentUpdated_Failed_AlreadyResolved_Skipped(t *testing.T) {
|
||||
const squarePaymentID = "sqp_clawback_resolved"
|
||||
saleID, giftCardID := createWebhookTestGiftCardAndSale(t, squarePaymentID, "2025-01-01T00:00:00Z", "2025-01-01T00:00:00Z", 40.00)
|
||||
createdAt := nowInRFC3339(0)
|
||||
saleID, giftCardID := createWebhookTestGiftCardAndSale(t, squarePaymentID, createdAt, createdAt, 40.00)
|
||||
if _, err := db.Conn.Exec(context.Background(),
|
||||
"UPDATE till_sales SET status = 'completed', updated_at = NOW() WHERE id = $1", saleID); err != nil {
|
||||
t.Fatalf("failed to resolve till sale: %v", err)
|
||||
@@ -282,7 +294,7 @@ func TestWebhook_DisputeCreated_InsertsDisputeRow(t *testing.T) {
|
||||
event := SquareWebhookEvent{
|
||||
Type: "dispute.created",
|
||||
EventID: "evt_dispute_created_1",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{
|
||||
"type": "dispute",
|
||||
"id": "dts_dispute_created_1",
|
||||
@@ -347,7 +359,7 @@ func TestWebhook_DisputeCreated_NoLocalPayment_NoRow(t *testing.T) {
|
||||
event := SquareWebhookEvent{
|
||||
Type: "dispute.created",
|
||||
EventID: "evt_dispute_orphan_1",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{
|
||||
"type": "dispute",
|
||||
"id": "dts_orphan_1",
|
||||
@@ -407,7 +419,7 @@ func TestWebhook_DisputeCreated_Untracked_DistinctDisputes_DistinctNotifications
|
||||
event := SquareWebhookEvent{
|
||||
Type: "dispute.created",
|
||||
EventID: fmt.Sprintf("evt_untracked_distinct_%d", i),
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{
|
||||
"type": "dispute",
|
||||
"id": "` + d.disputeID + `",
|
||||
@@ -455,7 +467,7 @@ func TestWebhook_DisputeCreated_Untracked_SameDisputeRedelivered_SingleNotificat
|
||||
event := SquareWebhookEvent{
|
||||
Type: "dispute.created",
|
||||
EventID: eventID,
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{
|
||||
"type": "dispute",
|
||||
"id": "` + disputeID + `",
|
||||
@@ -494,7 +506,7 @@ func TestWebhook_DisputeCreated_LongReason_Truncated(t *testing.T) {
|
||||
event := SquareWebhookEvent{
|
||||
Type: "dispute.created",
|
||||
EventID: "evt_dispute_longreason_1",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{
|
||||
"type": "dispute",
|
||||
"id": "dts_longreason_1",
|
||||
@@ -544,7 +556,7 @@ func TestWebhook_DisputeCreated_Utf8Reason_StoredValid(t *testing.T) {
|
||||
event := SquareWebhookEvent{
|
||||
Type: "dispute.created",
|
||||
EventID: "evt_dispute_utf8_1",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{
|
||||
"type": "dispute",
|
||||
"id": "dts_utf8_1",
|
||||
@@ -594,7 +606,7 @@ func TestWebhook_DisputeStateUpdated_Lost_MarksPaymentFailed(t *testing.T) {
|
||||
event := SquareWebhookEvent{
|
||||
Type: "dispute.state.updated",
|
||||
EventID: "evt_dispute_lost_1",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{
|
||||
"type": "dispute",
|
||||
"id": "dts_lost_1",
|
||||
@@ -633,7 +645,7 @@ func TestWebhook_DisputeStateUpdated_Won_KeepsPaymentCompleted(t *testing.T) {
|
||||
event := SquareWebhookEvent{
|
||||
Type: "dispute.state.updated",
|
||||
EventID: "evt_dispute_won_1",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{
|
||||
"type": "dispute",
|
||||
"id": "dts_won_1",
|
||||
@@ -672,7 +684,7 @@ func TestWebhook_DisputeStateUpdated_Open_KeepsOpen(t *testing.T) {
|
||||
event := SquareWebhookEvent{
|
||||
Type: "dispute.state.updated",
|
||||
EventID: "evt_dispute_open_1",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{
|
||||
"type": "dispute",
|
||||
"id": "dts_open_1",
|
||||
@@ -709,7 +721,7 @@ func TestWebhook_PaymentUpdated_UpdatesPaymentStatus(t *testing.T) {
|
||||
event := SquareWebhookEvent{
|
||||
Type: "payment.updated",
|
||||
EventID: "evt_payment_updated_completed_1",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{
|
||||
"type": "payment",
|
||||
"id": "` + squarePaymentID + `",
|
||||
@@ -737,7 +749,7 @@ func TestWebhook_PaymentUpdated_FailedStatus(t *testing.T) {
|
||||
event := SquareWebhookEvent{
|
||||
Type: "payment.updated",
|
||||
EventID: "evt_payment_updated_failed_1",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{
|
||||
"type": "payment",
|
||||
"id": "` + squarePaymentID + `",
|
||||
@@ -765,7 +777,7 @@ func TestWebhook_PaymentUpdated_NonTerminal_LeavesPending(t *testing.T) {
|
||||
event := SquareWebhookEvent{
|
||||
Type: "payment.updated",
|
||||
EventID: "evt_payment_updated_approved_1",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{
|
||||
"type": "payment",
|
||||
"id": "` + squarePaymentID + `",
|
||||
@@ -798,7 +810,7 @@ func TestWebhook_PaymentUpdated_DoesNotRevertRefunded(t *testing.T) {
|
||||
event := SquareWebhookEvent{
|
||||
Type: "payment.updated",
|
||||
EventID: "evt_payment_updated_refunded_1",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{
|
||||
"type": "payment",
|
||||
"id": "` + squarePaymentID + `",
|
||||
@@ -833,7 +845,7 @@ func TestWebhook_PaymentUpdated_Completed_RescuesPendingTillSale(t *testing.T) {
|
||||
event := SquareWebhookEvent{
|
||||
Type: "payment.updated",
|
||||
EventID: "evt_payment_updated_till_rescue_1",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{
|
||||
"type": "payment",
|
||||
"id": "` + squarePaymentID + `",
|
||||
@@ -864,7 +876,7 @@ func TestWebhook_PaymentUpdated_IdempotentReplay(t *testing.T) {
|
||||
event := SquareWebhookEvent{
|
||||
Type: "payment.updated",
|
||||
EventID: "evt_payment_updated_idem_1",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{
|
||||
"type": "payment",
|
||||
"id": "` + squarePaymentID + `",
|
||||
@@ -905,9 +917,15 @@ func TestWebhook_PaymentUpdated_IdempotentReplay(t *testing.T) {
|
||||
func createWebhookTestPendingOrigin(t *testing.T, idempotencyKey string) string {
|
||||
t.Helper()
|
||||
var id string
|
||||
// The origin row is aged past the sweep's keyed-replay age
|
||||
// (payments.SweepKeyedReplayAge — 22h): the orphan detection only treats a
|
||||
// pending row as a sweep-minted duplicate's origin once the row is old
|
||||
// enough that the sweep could have replayed it (a fresh pending row is a
|
||||
// legit charge whose response was lost and must never be failed by a
|
||||
// webhook that raced it).
|
||||
err := db.Conn.QueryRow(context.Background(), `
|
||||
INSERT INTO payments (payment_type, payment_method, status, amount, idempotency_key, created_at, updated_at)
|
||||
VALUES ('full', 'online_square', 'pending', 10.00, $1, NOW(), NOW())
|
||||
VALUES ('full', 'online_square', 'pending', 10.00, $1, NOW() - INTERVAL '23 hours', NOW())
|
||||
RETURNING id
|
||||
`, idempotencyKey).Scan(&id)
|
||||
if err != nil {
|
||||
@@ -916,6 +934,18 @@ func createWebhookTestPendingOrigin(t *testing.T, idempotencyKey string) string
|
||||
return id
|
||||
}
|
||||
|
||||
// seedWebhookTestB1Evidence simulates the sweep having replayed the origin's
|
||||
// expired key, minted a duplicate charge, and attempted its B1 auto-refund
|
||||
// (b1_attempts > 0). The orphan-detection B1-evidence gate requires this
|
||||
// before marking the origin failed.
|
||||
func seedWebhookTestB1Evidence(t *testing.T, originID string) {
|
||||
t.Helper()
|
||||
if _, err := db.Conn.Exec(context.Background(),
|
||||
`UPDATE payments SET b1_attempts = 1 WHERE id = $1`, originID); err != nil {
|
||||
t.Fatalf("failed to seed b1_attempts on origin payment: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func countOrphanReplayNotifications(t *testing.T, originID string) int {
|
||||
t.Helper()
|
||||
var n int
|
||||
@@ -938,11 +968,14 @@ func TestWebhook_PaymentUpdated_OrphanedReplay_MarksOriginFailed(t *testing.T) {
|
||||
idemKey = "b1-orphan-key-001"
|
||||
)
|
||||
originID := createWebhookTestPendingOrigin(t, idemKey)
|
||||
// The sweep replayed the origin's expired key and attempted the B1
|
||||
// auto-refund — the evidence the orphan-detection gate requires.
|
||||
seedWebhookTestB1Evidence(t, originID)
|
||||
|
||||
event := SquareWebhookEvent{
|
||||
Type: "payment.updated",
|
||||
EventID: "evt_orphan_c2_1",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{
|
||||
"type": "payment",
|
||||
"id": "` + orphanSquareID + `",
|
||||
@@ -993,7 +1026,7 @@ func TestWebhook_PaymentUpdated_OrphanedReplay_NoOrigin_Noop(t *testing.T) {
|
||||
event := SquareWebhookEvent{
|
||||
Type: "payment.updated",
|
||||
EventID: "evt_orphan_noorigin_1",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{
|
||||
"type": "payment",
|
||||
"id": "` + orphanSquareID + `",
|
||||
@@ -1019,6 +1052,50 @@ func TestWebhook_PaymentUpdated_OrphanedReplay_NoOrigin_Noop(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestWebhook_PaymentUpdated_OrphanedReplay_NoB1Evidence_LeavesPending
|
||||
// verifies the B1-evidence gate: a COMPLETED payment.updated that matches a
|
||||
// pending origin row by idempotency key but with NO B1 evidence (the sweep
|
||||
// never replayed + auto-refunded) is treated as a delayed legit completion —
|
||||
// the origin is LEFT pending, never marked failed, and no orphan notification
|
||||
// is raised. The stale-pending sweep reconciles the row instead.
|
||||
func TestWebhook_PaymentUpdated_OrphanedReplay_NoB1Evidence_LeavesPending(t *testing.T) {
|
||||
const (
|
||||
orphanSquareID = "sqp_orphan_noevidence"
|
||||
idemKey = "b1-orphan-key-no-evidence"
|
||||
)
|
||||
originID := createWebhookTestPendingOrigin(t, idemKey)
|
||||
|
||||
event := SquareWebhookEvent{
|
||||
Type: "payment.updated",
|
||||
EventID: "evt_orphan_noevidence_1",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{
|
||||
"type": "payment",
|
||||
"id": "` + orphanSquareID + `",
|
||||
"object": {
|
||||
"payment": {
|
||||
"id": "` + orphanSquareID + `",
|
||||
"status": "COMPLETED",
|
||||
"idempotency_key": "` + idemKey + `",
|
||||
"amount_money": {"amount": 1000, "currency": "GBP"}
|
||||
}
|
||||
}
|
||||
}`),
|
||||
}
|
||||
w := deliverWebhook(t, event)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
// The origin must remain pending — the delayed legit completion must never
|
||||
// be failed without sweep B1 evidence.
|
||||
if got := getPaymentStatus(t, originID); got != "pending" {
|
||||
t.Errorf("expected origin pending payment to stay 'pending', got %q", got)
|
||||
}
|
||||
if n := countOrphanReplayNotifications(t, originID); n != 0 {
|
||||
t.Errorf("expected 0 orphan-replay notifications without B1 evidence, got %d", n)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWebhook_PaymentUpdated_OrphanedReplay_ReferenceFallback locks the
|
||||
// reference_id fallback of the origin lookup: a COMPLETED orphan event whose
|
||||
// payload carries no idempotency key still finds its pending origin row via the
|
||||
@@ -1029,18 +1106,22 @@ func TestWebhook_PaymentUpdated_OrphanedReplay_ReferenceFallback(t *testing.T) {
|
||||
refID = "b16bad0000aa"
|
||||
)
|
||||
var originID string
|
||||
// The origin row is aged past the sweep's keyed-replay age (22h) so the
|
||||
// orphan detection treats it as a sweep-replayable origin (a fresh pending
|
||||
// row is never failed by the orphan detection — see the age gate).
|
||||
if err := db.Conn.QueryRow(context.Background(), `
|
||||
INSERT INTO payments (payment_type, payment_method, status, amount, gift_card_id, created_at, updated_at)
|
||||
VALUES ('full', 'online_square', 'pending', 10.00, $1, NOW(), NOW())
|
||||
VALUES ('full', 'online_square', 'pending', 10.00, $1, NOW() - INTERVAL '23 hours', NOW())
|
||||
RETURNING id
|
||||
`, refID).Scan(&originID); err != nil {
|
||||
t.Fatalf("failed to create reference-origin payment: %v", err)
|
||||
}
|
||||
seedWebhookTestB1Evidence(t, originID)
|
||||
|
||||
event := SquareWebhookEvent{
|
||||
Type: "payment.updated",
|
||||
EventID: "evt_orphan_refc2_1",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{
|
||||
"type": "payment",
|
||||
"id": "` + orphanSquareID + `",
|
||||
@@ -1078,7 +1159,7 @@ func TestWebhook_PaymentUpdated_SettledRow_NoOrphanDetection(t *testing.T) {
|
||||
event := SquareWebhookEvent{
|
||||
Type: "payment.updated",
|
||||
EventID: "evt_settled_replay_1",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{
|
||||
"type": "payment",
|
||||
"id": "` + squarePaymentID + `",
|
||||
@@ -1119,7 +1200,7 @@ func TestWebhook_RefundUpdated_UpdatesRefundStatus(t *testing.T) {
|
||||
event := SquareWebhookEvent{
|
||||
Type: "refund.updated",
|
||||
EventID: "evt_refund_updated_completed_1",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{
|
||||
"type": "refund",
|
||||
"id": "` + squareRefundID + `",
|
||||
@@ -1152,7 +1233,7 @@ func TestWebhook_RefundUpdated_FailedStatus(t *testing.T) {
|
||||
event := SquareWebhookEvent{
|
||||
Type: "refund.updated",
|
||||
EventID: "evt_refund_updated_failed_1",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{
|
||||
"type": "refund",
|
||||
"id": "` + squareRefundID + `",
|
||||
@@ -1189,7 +1270,7 @@ func TestWebhook_RefundUpdated_RejectedStatus(t *testing.T) {
|
||||
event := SquareWebhookEvent{
|
||||
Type: "refund.updated",
|
||||
EventID: "evt_refund_updated_rejected_1",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{
|
||||
"type": "refund",
|
||||
"id": "` + squareRefundID + `",
|
||||
@@ -1229,7 +1310,7 @@ func TestWebhook_RefundUpdated_Failed_RaisesAdminNotification(t *testing.T) {
|
||||
event := SquareWebhookEvent{
|
||||
Type: "refund.updated",
|
||||
EventID: "evt_refund_updated_failed_notify_1",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{
|
||||
"type": "refund",
|
||||
"id": "` + squareRefundID + `",
|
||||
@@ -1275,7 +1356,7 @@ func TestWebhook_RefundUpdated_NonTerminal_LeavesPending(t *testing.T) {
|
||||
event := SquareWebhookEvent{
|
||||
Type: "refund.updated",
|
||||
EventID: "evt_refund_updated_pending_1",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{
|
||||
"type": "refund",
|
||||
"id": "` + squareRefundID + `",
|
||||
@@ -1316,7 +1397,7 @@ func TestWebhook_RefundUpdated_Approved_IsNonTerminal(t *testing.T) {
|
||||
approved := SquareWebhookEvent{
|
||||
Type: "refund.updated",
|
||||
EventID: "evt_refund_updated_approved_1",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{
|
||||
"type": "refund",
|
||||
"id": "` + squareRefundID + `",
|
||||
@@ -1342,7 +1423,7 @@ func TestWebhook_RefundUpdated_Approved_IsNonTerminal(t *testing.T) {
|
||||
failed := SquareWebhookEvent{
|
||||
Type: "refund.updated",
|
||||
EventID: "evt_refund_updated_approved_fail_1",
|
||||
CreatedAt: "2025-01-01T00:00:01Z",
|
||||
CreatedAt: nowInRFC3339(time.Second),
|
||||
Data: json.RawMessage(`{
|
||||
"type": "refund",
|
||||
"id": "` + squareRefundID + `",
|
||||
@@ -1387,7 +1468,7 @@ func TestWebhook_RefundUpdated_Completed_SweepDupResolvesParent(t *testing.T) {
|
||||
event := SquareWebhookEvent{
|
||||
Type: "refund.updated",
|
||||
EventID: "evt_refund_sweepdup_parent_1",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{
|
||||
"type": "refund",
|
||||
"id": "` + squareRefundID + `",
|
||||
@@ -1418,7 +1499,8 @@ func TestWebhook_RefundUpdated_Completed_SweepDupResolvesParent(t *testing.T) {
|
||||
// gift card and mark the sale failed — mirroring the sweep's re-poll resolution.
|
||||
func TestWebhook_RefundUpdated_Completed_SweepDupResolvesTillSale(t *testing.T) {
|
||||
const squareRefundID = "sqr_sweepdup_tillsale"
|
||||
saleID, giftCardID := createWebhookTestGiftCardAndSale(t, "sqp_sweepdup_tillsale_pay", "2025-01-01T00:00:00Z", "2025-01-01T00:00:00Z", 40.00)
|
||||
createdAt := nowInRFC3339(0)
|
||||
saleID, giftCardID := createWebhookTestGiftCardAndSale(t, "sqp_sweepdup_tillsale_pay", createdAt, createdAt, 40.00)
|
||||
|
||||
// A synthetic completed payments row anchors the refund (mirrors
|
||||
// recordSweepDuplicateRefundRow); the till_sale id lives in the reason.
|
||||
@@ -1439,7 +1521,7 @@ func TestWebhook_RefundUpdated_Completed_SweepDupResolvesTillSale(t *testing.T)
|
||||
event := SquareWebhookEvent{
|
||||
Type: "refund.updated",
|
||||
EventID: "evt_refund_sweepdup_tillsale_1",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{
|
||||
"type": "refund",
|
||||
"id": "` + squareRefundID + `",
|
||||
@@ -1482,7 +1564,7 @@ func TestWebhook_RefundUpdated_DoesNotDemoteCompleted(t *testing.T) {
|
||||
event := SquareWebhookEvent{
|
||||
Type: "refund.updated",
|
||||
EventID: "evt_refund_demote_1",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{
|
||||
"type": "refund",
|
||||
"id": "` + squareRefundID + `",
|
||||
@@ -1524,7 +1606,7 @@ func TestWebhook_EventTypeAliases_RouteToUpdatedHandlers(t *testing.T) {
|
||||
payEvent := SquareWebhookEvent{
|
||||
Type: "payment.created",
|
||||
EventID: "evt_alias_payment_created_1",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{
|
||||
"type": "payment",
|
||||
"id": "` + sqPayID + `",
|
||||
@@ -1554,7 +1636,7 @@ func TestWebhook_EventTypeAliases_RouteToUpdatedHandlers(t *testing.T) {
|
||||
refundEvent := SquareWebhookEvent{
|
||||
Type: "refund.created",
|
||||
EventID: "evt_alias_refund_created_1",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{
|
||||
"type": "refund",
|
||||
"id": "sqr_alias_refund",
|
||||
|
||||
@@ -211,7 +211,7 @@ func TestHandleSquareWebhook_PaymentUpdated(t *testing.T) {
|
||||
event := SquareWebhookEvent{
|
||||
Type: "payment.updated",
|
||||
EventID: "evt_payment_1",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{"id":"payment_1"}`),
|
||||
}
|
||||
body, _ := json.Marshal(event)
|
||||
@@ -229,7 +229,7 @@ func TestHandleSquareWebhook_RefundUpdated(t *testing.T) {
|
||||
event := SquareWebhookEvent{
|
||||
Type: "refund.updated",
|
||||
EventID: "evt_refund_1",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{"id":"refund_1"}`),
|
||||
}
|
||||
body, _ := json.Marshal(event)
|
||||
@@ -244,7 +244,7 @@ func TestHandleSquareWebhook_DisputeCreated(t *testing.T) {
|
||||
event := SquareWebhookEvent{
|
||||
Type: "dispute.created",
|
||||
EventID: "evt_dispute_1",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{"id":"dispute_1"}`),
|
||||
}
|
||||
body, _ := json.Marshal(event)
|
||||
@@ -267,7 +267,7 @@ func TestHandleSquareWebhook_UnknownEventType(t *testing.T) {
|
||||
event := SquareWebhookEvent{
|
||||
Type: "frobnicator.created",
|
||||
EventID: "evt_unknown_1",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{"id":"frob_1"}`),
|
||||
}
|
||||
body, _ := json.Marshal(event)
|
||||
@@ -305,7 +305,7 @@ func TestHandleSquareWebhook_KnownNonMoneyEvent_Acknowledged(t *testing.T) {
|
||||
event := SquareWebhookEvent{
|
||||
Type: "customer.created",
|
||||
EventID: "evt_nonmoney_1",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{"id":"cust_1"}`),
|
||||
}
|
||||
body, _ := json.Marshal(event)
|
||||
@@ -344,7 +344,7 @@ func TestHandleSquareWebhook_UnhandledMoneyEvent_NotAcknowledged(t *testing.T) {
|
||||
event := SquareWebhookEvent{
|
||||
Type: "payment.checkout_offer_created",
|
||||
EventID: "evt_money_unhandled_1",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{"id":"pco_1"}`),
|
||||
}
|
||||
body, _ := json.Marshal(event)
|
||||
@@ -427,7 +427,7 @@ func TestHandleSquareWebhook_ValidSignatureWithEnvKey(t *testing.T) {
|
||||
// returns 503 without a dedup row (Square retries), which is NOT this
|
||||
// test's intent. The unique event_id and square_payment_id avoid colliding
|
||||
// with the other tests' dedup rows and payment fixtures.
|
||||
body := []byte(`{"type":"payment.updated","event_id":"evt_envkey_1","created_at":"2025-01-01T00:00:00Z","data":{"object":{"payment":{"id":"sqp_env_key_1","status":"COMPLETED","amount_money":{"amount":5000,"currency":"GBP"},"updated_at":"2025-01-01T00:00:00Z"}}}}`)
|
||||
body := []byte(fmt.Sprintf(`{"type":"payment.updated","event_id":"evt_envkey_1","created_at":%q,"data":{"object":{"payment":{"id":"sqp_env_key_1","status":"COMPLETED","amount_money":{"amount":5000,"currency":"GBP"},"updated_at":%q}}}}`, nowInRFC3339(0), nowInRFC3339(0)))
|
||||
key := "env-signing-key"
|
||||
notificationURL := "http://localhost:8080/webhooks/square"
|
||||
|
||||
@@ -486,7 +486,7 @@ func TestHandleSquareWebhook_NoRawPayloadInLogs(t *testing.T) {
|
||||
event := SquareWebhookEvent{
|
||||
Type: "payment.updated",
|
||||
EventID: "evt_pii_1",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{"id":"payment_pii_1","buyer_email_address":"secret@example.com",
|
||||
"card_details":{"card":{"brand":"VISA","last_4":"1234","cardholder_name":"Jane Doe"}}}`),
|
||||
}
|
||||
@@ -525,7 +525,7 @@ func TestHandleSquareWebhook_DuplicateEventID(t *testing.T) {
|
||||
event := SquareWebhookEvent{
|
||||
Type: "payment.updated",
|
||||
EventID: "evt_http_dup_1",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{"id":"payment_dup_1"}`),
|
||||
}
|
||||
body, _ := json.Marshal(event)
|
||||
@@ -557,7 +557,7 @@ func TestHandleSquareWebhook_DistinctEventIDs(t *testing.T) {
|
||||
event := SquareWebhookEvent{
|
||||
Type: "payment.updated",
|
||||
EventID: id,
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{"id":"p"}`),
|
||||
}
|
||||
body, _ := json.Marshal(event)
|
||||
@@ -621,7 +621,7 @@ func TestHandleSquareWebhook_DedupDispatchOnce(t *testing.T) {
|
||||
event := SquareWebhookEvent{
|
||||
Type: "payment.updated",
|
||||
EventID: "evt_dispatch_once_1",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{"id":"payment_dispatch_once_1"}`),
|
||||
}
|
||||
body, _ := json.Marshal(event)
|
||||
@@ -663,7 +663,7 @@ func TestHandleSquareWebhook_DedupPersistsAcrossRestart(t *testing.T) {
|
||||
event := SquareWebhookEvent{
|
||||
Type: "payment.updated",
|
||||
EventID: "evt_restart_1",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{"id":"payment_restart_1"}`),
|
||||
}
|
||||
body, _ := json.Marshal(event)
|
||||
@@ -718,7 +718,7 @@ func TestHandleSquareWebhook_ConcurrentSameEvent_Serialized(t *testing.T) {
|
||||
event := SquareWebhookEvent{
|
||||
Type: "payment.updated",
|
||||
EventID: eventID,
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{
|
||||
"type": "payment",
|
||||
"id": "` + squarePaymentID + `",
|
||||
@@ -795,7 +795,7 @@ func TestHandleSquareWebhook_DedupCacheEviction_Redispatches(t *testing.T) {
|
||||
event := SquareWebhookEvent{
|
||||
Type: "payment.updated",
|
||||
EventID: "evt_dedup_eviction_target",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{
|
||||
"type": "payment",
|
||||
"id": "` + squarePaymentID + `",
|
||||
@@ -858,7 +858,7 @@ func TestHandleSquareWebhook_DedupInsertFails_FailsClosed(t *testing.T) {
|
||||
event := SquareWebhookEvent{
|
||||
Type: "payment.updated",
|
||||
EventID: "evt_db_down_1",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{"id":"payment_db_down_1"}`),
|
||||
}
|
||||
body, _ := json.Marshal(event)
|
||||
@@ -888,7 +888,7 @@ func TestHandleSquareWebhook_DedupNilConn_FailsClosed(t *testing.T) {
|
||||
event := SquareWebhookEvent{
|
||||
Type: "payment.updated",
|
||||
EventID: "evt_nil_conn_1",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{"id":"payment_nil_conn_1"}`),
|
||||
}
|
||||
body, _ := json.Marshal(event)
|
||||
@@ -920,7 +920,7 @@ func TestHandleSquareWebhook_EnvMismatch_Rejected(t *testing.T) {
|
||||
event := SquareWebhookEvent{
|
||||
Type: "payment.updated",
|
||||
EventID: "evt_env_mismatch_1",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{"id":"payment_env_mismatch_1"}`),
|
||||
}
|
||||
body, _ := json.Marshal(event)
|
||||
@@ -942,7 +942,7 @@ func TestHandleSquareWebhook_EnvMatch_Accepted(t *testing.T) {
|
||||
event := SquareWebhookEvent{
|
||||
Type: "payment.updated",
|
||||
EventID: "evt_env_match_1",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{"id":"payment_env_match_1"}`),
|
||||
}
|
||||
body, _ := json.Marshal(event)
|
||||
@@ -965,7 +965,7 @@ func TestHandleSquareWebhook_EnvHeaderAbsent_Allowed(t *testing.T) {
|
||||
event := SquareWebhookEvent{
|
||||
Type: "payment.updated",
|
||||
EventID: "evt_env_absent_1",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{"id":"payment_env_absent_1"}`),
|
||||
}
|
||||
body, _ := json.Marshal(event)
|
||||
@@ -986,7 +986,7 @@ func TestHandleSquareWebhook_EnvMismatch_DevNotEnforced(t *testing.T) {
|
||||
event := SquareWebhookEvent{
|
||||
Type: "payment.updated",
|
||||
EventID: "evt_env_dev_1",
|
||||
CreatedAt: "2025-01-01T00:00:00Z",
|
||||
CreatedAt: nowInRFC3339(0),
|
||||
Data: json.RawMessage(`{"id":"payment_env_dev_1"}`),
|
||||
}
|
||||
body, _ := json.Marshal(event)
|
||||
|
||||
Reference in New Issue
Block a user