Files
popertots 197d4c4b9b 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.
2026-08-22 00:34:49 +01:00

57 lines
2.3 KiB
Go

//go:build test
package scheduling
// Strict timezone/DST/midnight-boundary tests for the scheduling package's
// London-date derivation. All assertions use fixed time.Date instants.
import (
"testing"
"time"
"crussell/clock"
)
// TestScheduling_DefaultHours_DST_Midnight proves ApplyScheduledDefaultHours's
// "today" date (derived via LondonDateString) is the LONDON calendar date, not
// the UTC date. During BST, London dates begin at 23:00 UTC the previous day:
// both 2026-06-14 23:30 UTC (= 2026-06-15 00:30 BST) and 2026-06-15 00:30 UTC
// (= 2026-06-15 01:30 BST) fall on London date 2026-06-15 — a naive UTC date
// would report 2026-06-14 for the first instant and misdate the effective day
// of a staged default-hours change.
func TestScheduling_DefaultHours_DST_Midnight(t *testing.T) {
t.Parallel()
cases := []struct {
name string
utc time.Time
want string
}{
{"00:30 BST window", time.Date(2026, 6, 14, 23, 30, 0, 0, time.UTC), "2026-06-15"},
{"01:30 BST same day", time.Date(2026, 6, 15, 0, 30, 0, 0, time.UTC), "2026-06-15"},
{"GMT midnight is UTC midnight", time.Date(2026, 1, 14, 23, 30, 0, 0, time.UTC), "2026-01-14"},
{"GMT next day", time.Date(2026, 1, 15, 0, 30, 0, 0, time.UTC), "2026-01-15"},
{"autumn BST->GMT transition day", time.Date(2026, 10, 25, 0, 30, 0, 0, time.UTC), "2026-10-25"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := LondonDateString(tc.utc); got != tc.want {
t.Errorf("LondonDateString(%s UTC) = %q, expected London date %q", tc.utc.Format(time.RFC3339), got, tc.want)
}
})
}
// Direct proof of the DST window: the UTC date of the first instant differs
// from its London date — exactly the drift the helper must absorb.
boundary := time.Date(2026, 6, 14, 23, 30, 0, 0, time.UTC)
if boundary.Format("2006-01-02") == LondonDateString(boundary) {
t.Fatal("test setup invariant: 2026-06-14 23:30 UTC must straddle the London midnight boundary")
}
// The helper must be consistent with clock.London's own wall-clock for the
// same instant (the production seam ApplyScheduledDefaultHours now uses).
nowish := time.Date(2026, 6, 14, 23, 45, 0, 0, time.UTC)
if LondonDateString(nowish) != nowish.In(clock.London).Format("2006-01-02") {
t.Errorf("LondonDateString disagrees with the direct London conversion for %s", nowish.Format(time.RFC3339))
}
}