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:
2026-08-22 00:34:50 +01:00
parent 1d9c87d6d6
commit 1429eddd34
43 changed files with 2211 additions and 1275 deletions
+20 -20
View File
@@ -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)