- 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
83 lines
3.7 KiB
Go
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)))
|
|
}
|