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:
@@ -53,16 +53,31 @@ func ValidateRefundReason(reason string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateCardInfo checks that exactly one of cardID or newCardToken is provided,
|
||||
// non-nil, and non-empty.
|
||||
func ValidateCardInfo(cardID, newCardToken *string) error {
|
||||
hasCardID := cardID != nil && *cardID != ""
|
||||
// ValidateCardInfo checks the card source shape of a payment request. Three
|
||||
// shapes are legal:
|
||||
//
|
||||
// - saved-card only: a non-empty saved-card reference (card_id or
|
||||
// saved_card_id — they are the same user_saved_cards.id, so callers pass
|
||||
// the effective reference) with no new_card_token — a plain saved-card
|
||||
// (ccof:) charge.
|
||||
// - new-card only: a non-empty new_card_token with NO saved-card reference —
|
||||
// a new-card (cnon:) one-off charge.
|
||||
// - saved-card reference + new_card_token together: the SCA tokenize-result
|
||||
// wire contract — the token (card.tokenize(verificationDetails, cardId)
|
||||
// result) is the one-time charge SOURCE and the saved-card row supplies the
|
||||
// Square customer. resolveChargeSource implements exactly this coexistence
|
||||
// (see charge_helpers.go), so the validation must not reject it.
|
||||
//
|
||||
// Both absent is invalid ("either a card reference or new_card_token is
|
||||
// required"). A bare new_card_token remains valid (the new-card path), and a
|
||||
// card reference with no token remains valid (the legacy saved-card path);
|
||||
// only the coexistence that used to be rejected — card ref + token — is now
|
||||
// legal, resolved as the SCA tokenize-result source.
|
||||
func ValidateCardInfo(cardID, savedCardID, newCardToken *string) error {
|
||||
hasCardRef := (cardID != nil && *cardID != "") || (savedCardID != nil && *savedCardID != "")
|
||||
hasToken := newCardToken != nil && *newCardToken != ""
|
||||
if hasCardID && hasToken {
|
||||
return errors.New("provide either card_id or new_card_token, not both")
|
||||
}
|
||||
if !hasCardID && !hasToken {
|
||||
return errors.New("either card_id or new_card_token is required")
|
||||
if !hasCardRef && !hasToken {
|
||||
return errors.New("either a saved card reference (card_id/saved_card_id) or new_card_token is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user