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.
96 lines
3.7 KiB
Go
96 lines
3.7 KiB
Go
package clock
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestNow_ReturnsUTC(t *testing.T) {
|
|
t.Parallel()
|
|
now := Now()
|
|
if now.Location() != time.UTC {
|
|
t.Errorf("clock.Now() returned time in %v, expected UTC", now.Location())
|
|
}
|
|
// UTC is a fixed-offset zone: the zone abbreviation and offset must both be
|
|
// the canonical UTC values so SQL TIMESTAMPTZ comparisons and duration math
|
|
// never drift.
|
|
name, off := now.Zone()
|
|
if name != "UTC" || off != 0 {
|
|
t.Errorf("clock.Now() returned zone %q offset %d, expected UTC offset 0", name, off)
|
|
}
|
|
}
|
|
|
|
// TestLondon_Location_IsEuropeLondon proves the single London location used for
|
|
// wall-clock business decisions is the IANA Europe/London zone (which carries
|
|
// the full DST rule table for GMT<->BST).
|
|
func TestLondon_Location_IsEuropeLondon(t *testing.T) {
|
|
t.Parallel()
|
|
if got := London.String(); got != "Europe/London" {
|
|
t.Errorf("clock.London.String() = %q, expected %q", got, "Europe/London")
|
|
}
|
|
}
|
|
|
|
// TestLondon_DSTTransitions pins the two 2026 Europe/London transitions:
|
|
//
|
|
// Spring forward (2026-03-29 01:00 GMT -> 02:00 BST):
|
|
// - 2026-03-29 00:30 UTC = 01:30 GMT, offset +0 (still winter time)
|
|
// - 2026-03-29 01:00 UTC = 02:00 BST, offset +1 (the transition instant)
|
|
// Fall back (2026-10-25 02:00 BST -> 01:00 GMT):
|
|
// - 2026-10-25 00:30 UTC = 01:30 BST, offset +1 (still summer time)
|
|
// - 2026-10-25 01:00 UTC = 01:00 GMT, offset +0 (the ambiguous hour, which
|
|
// Go resolves to the SECOND occurrence)
|
|
//
|
|
// These instants are the boundary of every London wall-clock decision the app
|
|
// makes (closing hours, default-hours effective dates, today summaries), so
|
|
// clock.London must get them exactly right.
|
|
func TestLondon_DSTTransitions(t *testing.T) {
|
|
t.Parallel()
|
|
cases := []struct {
|
|
name string
|
|
utc time.Time
|
|
wantWall string // London wall-clock, "2006-01-02 15:04"
|
|
wantZone string // "GMT" or "BST"
|
|
wantOffsetH int
|
|
}{
|
|
{"spring before", time.Date(2026, 3, 29, 0, 30, 0, 0, time.UTC), "2026-03-29 00:30", "GMT", 0},
|
|
{"spring transition", time.Date(2026, 3, 29, 1, 0, 0, 0, time.UTC), "2026-03-29 02:00", "BST", 1},
|
|
{"autumn before", time.Date(2026, 10, 25, 0, 30, 0, 0, time.UTC), "2026-10-25 01:30", "BST", 1},
|
|
{"autumn ambiguous->GMT", time.Date(2026, 10, 25, 1, 0, 0, 0, time.UTC), "2026-10-25 01:00", "GMT", 0},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
l := tc.utc.In(London)
|
|
if got := l.Format("2006-01-02 15:04"); got != tc.wantWall {
|
|
t.Errorf("%s UTC in London = %q, expected wall-clock %q", tc.utc.Format(time.RFC3339), got, tc.wantWall)
|
|
}
|
|
zone, off := l.Zone()
|
|
if zone != tc.wantZone {
|
|
t.Errorf("%s UTC in London zone = %q, expected %q", tc.utc.Format(time.RFC3339), zone, tc.wantZone)
|
|
}
|
|
if off != tc.wantOffsetH*3600 {
|
|
t.Errorf("%s UTC in London offset = %ds, expected %dh", tc.utc.Format(time.RFC3339), off, tc.wantOffsetH)
|
|
}
|
|
})
|
|
}
|
|
|
|
// Sanity: the two wall-clock times on the autumn transition day share the
|
|
// same 01:xx wall hour (the ambiguous hour) but at different UTC instants —
|
|
// proving Go resolves the fold to the second occurrence when converting the
|
|
// UTC instant back to London.
|
|
fold1 := time.Date(2026, 10, 25, 0, 30, 0, 0, time.UTC).In(London).Format("15:04") // 01:30 BST
|
|
fold2 := time.Date(2026, 10, 25, 1, 30, 0, 0, time.UTC).In(London).Format("15:04") // 01:30 GMT
|
|
if fold1 != "01:30" || fold2 != "01:30" {
|
|
t.Errorf("expected both sides of the autumn fold to show 01:30, got %q and %q", fold1, fold2)
|
|
}
|
|
}
|
|
|
|
func TestNow_IsReasonable(t *testing.T) {
|
|
t.Parallel()
|
|
now := Now()
|
|
// Check it's within the last minute (reasonable)
|
|
since := time.Since(now)
|
|
if since < 0 || since > time.Minute {
|
|
t.Errorf("clock.Now() returned unreasonable time: %v ago", since)
|
|
}
|
|
}
|