Gift-card rolling expiry, SvelteDate→Date purge, strict DST tests, UTC scan-location + settings legal floor
Gift-card rolling expiry (setting-driven, was dead config): - GetGiftCardExpiryMonths(): single source of truth (business_settings gift_card_expiry_months, fallback 24) shared by payment handlers and the CleanupExpiredGiftCards job (was hardcoded 24). - expiry_date now maintained on ALL 9 gift-card write sites (buy, topup, transfer, redeem, terminal payment, refund credit, till) so the refund-time guard at refunds.go actually fires. Schema default 12->24 + migration note; test-DB seed aligned. Stale "expiry_date IS NULL" test rewritten; new expired-card-rejected regression test. Frontend SvelteDate purge (docs' stated convention, wide): - All 180+ raw `new SvelteDate(...)` uses across routes/components replaced with parseWallClockDate (backend UTC ISO) or new Date (wall-clock constructors). SvelteDate imports removed. timeSlots.ts getDayWithOrdinal fixed. Zero SvelteDate references remain; svelte-check clean. Strict timezone/DST testing + QA fixes: - 8 new hermetic boundary tests: clock.DST transitions (both 2026 folds), closing-hours GMT vs BST, booking date-window midnight, refund-tier elapsed-time independence, deposit-window UTC-instant, scheduling LondonDateString midnight, today AT TIME ZONE window + UTC round-trip. - today.go summary date labels fixed to London wall-clock (were showing the previous UTC day during BST) + regression test. - pgx ScanLocation fixed to UTC via AfterConnect (was host-local -> JSON offsets depended on deployment TZ, contradicting the documented UTC invariant) + regression test. Registered as a new *Type to avoid a data race on the shared type map (caught by -race). Admin Business Settings (setting now functional => legal floor): - gift_card_expiry_months validation floor raised 1 -> 12 months (CMA/ Consumer Rights Act 2015 unfair-contract-term guidance) in endpoint + UI, with rolling-expiry semantics shown in both display and edit form. - 3 new expiry validation tests; 2 pre-existing message assertions updated. Full suite 25/25 + race clean via run-tests.sh lockfile; svelte-check 0 errors/warnings; production build succeeds.
This commit is contained in:
@@ -169,8 +169,12 @@ func UpdateBusinessSettings(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "voucher_type must be 'SPV' or 'MPV'", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if req.GiftCardExpiryMonths != nil && *req.GiftCardExpiryMonths < 1 {
|
||||
http.Error(w, "gift_card_expiry_months must be at least 1", http.StatusBadRequest)
|
||||
if req.GiftCardExpiryMonths != nil && *req.GiftCardExpiryMonths < 12 {
|
||||
// Legal floor, not arbitrary: UK Consumer Rights Act 2015 requires
|
||||
// expiry terms to be "fair and transparent", and CMA guidance flags
|
||||
// sub-12-month expiry windows as at risk of being an unfair contract
|
||||
// term. 24 months is the documented default (matches John Lewis, M&S).
|
||||
http.Error(w, "gift_card_expiry_months must be at least 12 (CMA guidance flags sub-12-month expiry as an unfair contract term; 24 is recommended)", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if req.DefaultVATRate != nil && (*req.DefaultVATRate < 0 || *req.DefaultVATRate > 100) {
|
||||
|
||||
@@ -725,6 +725,65 @@ func TestUpdateBusinessSettings_VoucherType_SPV(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestUpdateBusinessSettings_Expiry_RejectsSub12 rejects gift_card_expiry_months
|
||||
// below the legal floor. The setting now actually drives gift-card expiry (the
|
||||
// cleanup job + every expiry_date write), so a sub-12-month window — flagged by
|
||||
// CMA guidance as an unfair contract term under the Consumer Rights Act 2015 —
|
||||
// must not be settable.
|
||||
func TestUpdateBusinessSettings_Expiry_RejectsSub12(t *testing.T) {
|
||||
ctx, _ := testutils.SetupTestTx(t)
|
||||
|
||||
handler := http.HandlerFunc(UpdateBusinessSettings)
|
||||
body := UpdateBusinessSettingsRequest{
|
||||
GiftCardExpiryMonths: intPtr(6),
|
||||
}
|
||||
w := makeAdminRequest(handler, "PUT", "/api/admin/settings", body, ctx)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected status 400 for 6-month expiry, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestUpdateBusinessSettings_Expiry_RejectsZero rejects gift_card_expiry_months
|
||||
// of 0 (previously the only invalid value, now the legal floor subsumes it).
|
||||
func TestUpdateBusinessSettings_Expiry_RejectsZero(t *testing.T) {
|
||||
ctx, _ := testutils.SetupTestTx(t)
|
||||
|
||||
handler := http.HandlerFunc(UpdateBusinessSettings)
|
||||
body := UpdateBusinessSettingsRequest{
|
||||
GiftCardExpiryMonths: intPtr(0),
|
||||
}
|
||||
w := makeAdminRequest(handler, "PUT", "/api/admin/settings", body, ctx)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected status 400 for 0-month expiry, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestUpdateBusinessSettings_Expiry_Accepts24 verifies a 24-month window (the
|
||||
// documented default) is accepted and persisted.
|
||||
func TestUpdateBusinessSettings_Expiry_Accepts24(t *testing.T) {
|
||||
ctx, _ := testutils.SetupTestTx(t)
|
||||
|
||||
handler := http.HandlerFunc(UpdateBusinessSettings)
|
||||
body := UpdateBusinessSettingsRequest{
|
||||
GiftCardExpiryMonths: intPtr(24),
|
||||
}
|
||||
w := makeAdminRequest(handler, "PUT", "/api/admin/settings", body, ctx)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200 for 24-month expiry, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var s BusinessSettings
|
||||
if err := parseResponseBody(w, &s); err != nil {
|
||||
t.Fatalf("failed to parse response: %v", err)
|
||||
}
|
||||
if s.GiftCardExpiryMonths != 24 {
|
||||
t.Errorf("expected GiftCardExpiryMonths 24, got %d", s.GiftCardExpiryMonths)
|
||||
}
|
||||
}
|
||||
|
||||
// TestUpdateBusinessSettings_VoucherType_MPV accepts MPV.
|
||||
func TestUpdateBusinessSettings_VoucherType_MPV(t *testing.T) {
|
||||
ctx, _ := testutils.SetupTestTx(t)
|
||||
@@ -764,7 +823,7 @@ func TestUpdateBusinessSettings_NegativeExpiryMonths(t *testing.T) {
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
if w.Body.String() != "gift_card_expiry_months must be at least 1\n" {
|
||||
if !strings.Contains(w.Body.String(), "must be at least 12") {
|
||||
t.Errorf("unexpected error message: %s", w.Body.String())
|
||||
}
|
||||
}
|
||||
@@ -782,8 +841,8 @@ func TestUpdateBusinessSettings_ExpiryMonths_Negative(t *testing.T) {
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected status 400 for negative expiry, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
if w.Body.String() != "gift_card_expiry_months must be at least 1\n" {
|
||||
t.Errorf("unexpected error message: %s", w.Body.String())
|
||||
if !strings.Contains(w.Body.String(), "must be at least 12") {
|
||||
t.Errorf("expected legal-floor error message, got: %s", w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user