- 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
75 lines
2.6 KiB
Go
75 lines
2.6 KiB
Go
//go:build test && dev
|
|
|
|
package payments
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strconv"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// policyTSValue extracts the numeric value of a `NAME: value,` entry from the
|
|
// frontend's POLICY object literal (policy.ts), tolerating the tab-indented
|
|
// formatting the file uses. Returns "" when the name is absent.
|
|
func policyTSValue(t *testing.T, src, name string) string {
|
|
t.Helper()
|
|
re := regexp.MustCompile(`(?m)^\s*` + regexp.QuoteMeta(name) + `:\s*(\d+(?:\.\d+)?),`)
|
|
m := re.FindStringSubmatch(src)
|
|
if m == nil {
|
|
return ""
|
|
}
|
|
return m[1]
|
|
}
|
|
|
|
// TestPolicyTS_CrossCheck pins the frontend's single-source policy constants
|
|
// (frontend/src/lib/constants/policy.ts) to THIS package's refund_policy.go
|
|
// values by REACTING to drift: the test reads the .ts file and asserts each
|
|
// entry equals the backend constant, so a one-sided change on either side fails
|
|
// CI. The frontend's own vitest suite (policy.test.ts) pins the same values in
|
|
// the other direction, closing the drift loop both ways.
|
|
func TestPolicyTS_CrossCheck(t *testing.T) {
|
|
path := filepath.Join("..", "..", "..", "frontend", "src", "lib", "constants", "policy.ts")
|
|
data, err := os.ReadFile(path)
|
|
require.NoError(t, err, "policy.ts not found at %s (tests run from the package dir; repo layout is <repo>/backend/handlers/payments + <repo>/frontend)", path)
|
|
src := string(data)
|
|
|
|
cases := []struct {
|
|
name string
|
|
want string
|
|
}{
|
|
{"REQUIRED_DEPOSIT_PCT", "0.2"},
|
|
{"PROTECTED_DEPOSIT_MAX_PCT", "0.5"},
|
|
{"FULL_REFUND_THRESHOLD_HOURS", "72"},
|
|
{"PARTIAL_REFUND_THRESHOLD_HOURS", "24"},
|
|
{"NO_SHOW_THRESHOLD_HOURS", "24"},
|
|
{"DEPOSIT_ADVANCE_HOURS", "36"},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got := policyTSValue(t, src, tc.name)
|
|
require.NotEmpty(t, got, "policy.ts no longer declares %q — the constant may have been renamed or removed", tc.name)
|
|
require.Equal(t, tc.want, got, "%s drifted from backend/handlers/payments/refund_policy.go", tc.name)
|
|
})
|
|
}
|
|
|
|
// The backend has no named reschedule constants; the frontend values must
|
|
// track the refund tiers they were derived from.
|
|
for _, tc := range []struct {
|
|
name string
|
|
want string
|
|
}{
|
|
{"RESCHEDULE_BLOCK_HOURS_WITH_PAYMENTS", strconv.Itoa(int(FullRefundThreshold.Hours()))},
|
|
{"RESCHEDULE_BLOCK_HOURS_NO_PAYMENTS", strconv.Itoa(int(PartialRefundThreshold.Hours()))},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got := policyTSValue(t, src, tc.name)
|
|
require.NotEmpty(t, got, "policy.ts no longer declares %q", tc.name)
|
|
require.Equal(t, tc.want, got, "%s must track the backend refund threshold", tc.name)
|
|
})
|
|
}
|
|
}
|