Expand payment test coverage: lock contention, nonce-direct, provisional rows, GDPR scrub, validators
Close the coverage-gap round: terminal CreateCheckout-failure marks the provisional row failed, GetCheckoutStatus reference_id mismatch 400, deadline wire shape, concurrent loyalty redemption 409, delete_guest_user + stale-guest saved-card scrubbing, ValidateAmount and isTokenLike direct units, bounded try-lock timeout, buildSplitRecords tip-overflow, and concurrent same-key dedup for gift card / booking / tip / checkout.
This commit is contained in:
@@ -2023,6 +2023,52 @@ func TestBuildSplitRecords_PaymentLessThanDepositMax_NoSplit(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildSplitRecords_OverflowBeyondTotal_BecomesTip(t *testing.T) {
|
||||
// £60 on a £50 future booking: deposit caps at £25 (50%), balance covers
|
||||
// the remaining £25 owed, and the £10 overflow beyond the booking total
|
||||
// becomes a tip record.
|
||||
record := makeTestRecord("b5", "full", 60)
|
||||
info := &BookingPaymentInfo{
|
||||
StartTime: clock.Now().Add(48 * time.Hour),
|
||||
TotalAmount: 50,
|
||||
TotalPaid: 0,
|
||||
}
|
||||
records := buildSplitRecords(record, "full", info, 60)
|
||||
|
||||
if len(records) != 3 {
|
||||
t.Fatalf("expected 3 records (deposit + balance + tip), got %d", len(records))
|
||||
}
|
||||
if records[0].PaymentType != "deposit" {
|
||||
t.Errorf("expected first record 'deposit', got %q", records[0].PaymentType)
|
||||
}
|
||||
if records[0].Amount != 25 {
|
||||
t.Errorf("expected deposit amount 25, got %.2f", records[0].Amount)
|
||||
}
|
||||
if records[1].PaymentType != "balance" {
|
||||
t.Errorf("expected second record 'balance', got %q", records[1].PaymentType)
|
||||
}
|
||||
if records[1].Amount != 25 {
|
||||
t.Errorf("expected balance amount 25, got %.2f", records[1].Amount)
|
||||
}
|
||||
if records[2].PaymentType != "tip" {
|
||||
t.Errorf("expected third record 'tip', got %q", records[2].PaymentType)
|
||||
}
|
||||
if records[2].Amount != 10 {
|
||||
t.Errorf("expected tip amount 10 (overflow beyond the booking total), got %.2f", records[2].Amount)
|
||||
}
|
||||
// The tip is a split: zero fees and a derived idempotency key.
|
||||
if records[2].Fees != 0 {
|
||||
t.Errorf("expected tip fees=0, got %.2f", records[2].Fees)
|
||||
}
|
||||
if *records[2].IdempotencyKey != *records[0].IdempotencyKey+"-split-3" {
|
||||
t.Errorf("expected tip key derived from primary, got %q", *records[2].IdempotencyKey)
|
||||
}
|
||||
// The splits share the Square payment id of the primary record.
|
||||
if *records[2].SquarePaymentID != *records[0].SquarePaymentID {
|
||||
t.Error("tip split must share square_payment_id with the primary record")
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Handler-level atomicity — verify the full handler succeeds with split.
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -3405,7 +3451,7 @@ func TestSaveCardForUser_Success(t *testing.T) {
|
||||
t.Fatalf("failed to create user: %v", err)
|
||||
}
|
||||
svc := NewPaymentService()
|
||||
cardID, err := svc.SaveCardForUser(ctx, userID, "cfa_test_success", "VISA", "4242", 12, 2030, "fp_success")
|
||||
cardID, err := svc.SaveCardForUser(ctx, userID, "cus_test_success", "cfa_test_success", "VISA", "4242", 12, 2030, "fp_success")
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
@@ -3418,7 +3464,7 @@ func TestSaveCardForUser_InvalidUserID(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
_ = tx
|
||||
svc := NewPaymentService()
|
||||
_, err := svc.SaveCardForUser(ctx, "000000000001", "cfa_test", "VISA", "4242", 12, 2030, "fp_test")
|
||||
_, err := svc.SaveCardForUser(ctx, "000000000001", "cus_test_invalid", "cfa_test", "VISA", "4242", 12, 2030, "fp_test")
|
||||
if err == nil {
|
||||
t.Error("expected error for non-existent user ID (FK violation)")
|
||||
}
|
||||
@@ -3584,8 +3630,6 @@ func TestTerminalPayment_ValidatePaymentTypeFails(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
func TestSweepStalePendingPayments_MarksOldFailed(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
@@ -3728,7 +3772,6 @@ func TestSavedCardPayment_SweptFailed_Rejected(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// TestSweepStalePendingPayments_CoversTillSales verifies the R3 fix: the sweep
|
||||
// also marks stale pending till_sales rows (card payments) as failed, so a
|
||||
// lost-response till sale can't stay pending past Square's key retention.
|
||||
@@ -3782,3 +3825,41 @@ func TestSweepStalePendingPayments_CoversTillSales(t *testing.T) {
|
||||
t.Errorf("expected fresh pending till sale to stay pending, got %q", freshStatus)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildSplitRecords_TipOverflow_SeparateTipRecord(t *testing.T) {
|
||||
// A £60 payment on a £50 future booking with nothing paid yet:
|
||||
// deposit = min(60, 25) = £25, balance = min(35, 25) = £25,
|
||||
// tip = 60 - 25 - 25 = £10 — the overflow becomes a separate tip record.
|
||||
record := makeTestRecord("b-overflow", "full", 60)
|
||||
info := &BookingPaymentInfo{
|
||||
StartTime: clock.Now().Add(48 * time.Hour),
|
||||
TotalAmount: 50,
|
||||
TotalPaid: 0,
|
||||
}
|
||||
records := buildSplitRecords(record, "full", info, 60)
|
||||
|
||||
if len(records) != 3 {
|
||||
t.Fatalf("expected 3 records (deposit + balance + tip), got %d", len(records))
|
||||
}
|
||||
if records[0].PaymentType != "deposit" || records[0].Amount != 25 {
|
||||
t.Errorf("expected deposit 25, got %q %.2f", records[0].PaymentType, records[0].Amount)
|
||||
}
|
||||
if records[1].PaymentType != "balance" || records[1].Amount != 25 {
|
||||
t.Errorf("expected balance 25, got %q %.2f", records[1].PaymentType, records[1].Amount)
|
||||
}
|
||||
if records[2].PaymentType != "tip" || records[2].Amount != 10 {
|
||||
t.Errorf("expected tip record 10, got %q %.2f", records[2].PaymentType, records[2].Amount)
|
||||
}
|
||||
// Tip is an overflow split — zero fees, derived idempotency key -split-2.
|
||||
if records[2].Fees != 0 {
|
||||
t.Errorf("expected tip record fees=0, got %.2f", records[2].Fees)
|
||||
}
|
||||
wantKey := *record.IdempotencyKey + "-split-3"
|
||||
if *records[2].IdempotencyKey != wantKey {
|
||||
t.Errorf("expected tip key %q, got %q", wantKey, *records[2].IdempotencyKey)
|
||||
}
|
||||
// All three share the same SquarePaymentID (one charge, three ledger rows).
|
||||
if *records[2].SquarePaymentID != *record.SquarePaymentID {
|
||||
t.Error("tip split must share square_payment_id")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user