Files
Crussell/backend/handlers/payments/money_edge_test.go
T
popertots 1429eddd34 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
2026-08-22 00:34:50 +01:00

83 lines
3.7 KiB
Go

//go:build test && dev
package payments
import (
"math"
"testing"
"github.com/stretchr/testify/require"
)
// TestPenceLess_RoundingBoundary pins the pence rounding that decides whether a
// gift-card clawback was PARTIAL: penceLess compares pound-float balances by
// rounding to integer pence, so sub-penny residue around roundingEpsilon
// (0.004) must not flip the comparison. Amounts at or below the epsilon (0.4
// pence) round to zero pence; 0.5+ pence rounds up.
func TestPenceLess_RoundingBoundary(t *testing.T) {
t.Parallel()
tests := []struct {
name string
a, b float64
less bool
}{
{"both at the epsilon round to 0 pence — not less", 0.004, 0.004, false},
{"0.004 vs 0.0041 both round to 0 pence", 0.004, 0.0041, false},
{"0.0039 vs 0.004 both round to 0 pence", 0.0039, 0.004, false},
{"0.0041 vs 0.0039 both round to 0 pence", 0.0041, 0.0039, false},
{"0.004 (0 pence) is less than 0.005 (1 pence)", 0.004, 0.005, true},
{"0.005 (1 pence) is NOT less than 0.004 (0 pence)", 0.005, 0.004, false},
{"0.004 is less than 0.01 (1 pence)", 0.004, 0.01, true},
{"equal amounts are never less", 12.34, 12.34, false},
{"£12.34 is less than £12.35", 12.34, 12.35, true},
{"£12.35 is NOT less than £12.34", 12.35, 12.34, false},
{"a full pound difference", 0.0, 1.0, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.less, penceLess(tt.a, tt.b), "penceLess(%v, %v)", tt.a, tt.b)
})
}
}
// TestPenceLess_FloatToPenceRoundingEdges pins the float64→pence rounding the
// comparison is built on: Go's math.Round rounds half away from zero, so
// exactly 0.5 pence rounds UP (0.005 → 1 pence) while anything below rounds
// down. The clawback's partial-detection must agree with this everywhere a
// pound-denominated balance is compared.
func TestPenceLess_FloatToPenceRoundingEdges(t *testing.T) {
t.Parallel()
// Exactly 0.5 pence rounds up: 0.005 → 1p, so 0.005 < 0.006 is false
// (both round to 1) and 0.004999999 < 0.005000001 is true (0p vs 1p).
require.Equal(t, int64(1), int64(math.Round(0.005*100)), "0.005 pounds must round to 1 pence")
require.Equal(t, int64(0), int64(math.Round(0.004999999*100)), "0.004999999 pounds must round to 0 pence")
require.False(t, penceLess(0.005, 0.006), "0.005 and 0.006 both round to 1 pence")
require.True(t, penceLess(0.004999999, 0.005000001), "0p vs 1p — the partial boundary")
// A whole-pence gap always compares by rounded pence, never raw floats.
require.False(t, penceLess(1.009, 1.01), "1.009 rounds to 1.01 → equal pence")
require.False(t, penceLess(1.009, 1.011), "1.009 and 1.011 both round to 1.01 → equal pence")
require.True(t, penceLess(1.009, 1.021), "1.009 rounds to 1.01, 1.021 rounds to 1.02 → less")
}
// TestRoundingEpsilon_Value pins the "effectively zero" threshold shared by the
// split builders and the cash/gift-card terminal branches. It must stay 0.004
// (0.4 pence): sub-penny float residue from dividing pence by 100 must round to
// zero pence, so a phantom payment row is never created, while a real 0.5-penny
// value still rounds to 1 pence.
func TestRoundingEpsilon_Value(t *testing.T) {
t.Parallel()
require.Equal(t, 0.004, roundingEpsilon, "roundingEpsilon must be 0.004 (0.4 pence)")
// The epsilon itself rounds to zero pence; the first rounding-up value is
// 0.005. Any epsilon above 0.004 would silently drop legitimate half-pence
// amounts; anything below would over-report residue.
require.Equal(t, int64(0), int64(math.Round(roundingEpsilon*100)))
require.Equal(t, int64(1), int64(math.Round(0.005*100)))
require.Equal(t, int64(0), int64(math.Round(0.0039*100)))
require.Equal(t, int64(0), int64(math.Round(0.0041*100)))
}