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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user