//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))) }