Files
Crussell/backend/db/db_dev.go
T
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

93 lines
2.2 KiB
Go

//go:build dev
package db
import (
"context"
"fmt"
"os"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
"github.com/jackc/pgx/v5/pgxpool"
)
var Conn *PoolProxy
func Connect() error {
// Connect to Postgres inside Docker network
dsn := fmt.Sprintf(
"postgres://%s:%s@%s:5432/%s?sslmode=disable&require_auth=scram-sha-256",
getEnv("POSTGRES_USER"),
getEnv("POSTGRES_PASSWORD"),
getEnv("POSTGRES_HOST"),
getEnv("POSTGRES_DB"),
)
poolCfg, err := pgxpool.ParseConfig(dsn)
if err != nil {
return err
}
poolCfg.ConnConfig.RuntimeParams["timezone"] = "UTC"
// Scan TIMESTAMPTZ into time.Time in UTC, never the host's local timezone.
// Without this, pgx scans into time.Local, so the JSON offset in API
// responses silently depends on the deployment host's TZ (e.g. +01:00 on a
// London host, Z on a UTC Docker host) — the instant is the same but the
// documented "backend emits UTC" invariant would be violated.
// The codec is registered as a NEW *Type rather than mutating the Type
// returned by TypeForOID: every connection's type map shares the same
// *Type pointers with the package default map, so mutating .Codec on the
// shared Type would be a data race when concurrent connections establish.
poolCfg.AfterConnect = func(_ context.Context, conn *pgx.Conn) error {
tzType, ok := conn.TypeMap().TypeForOID(pgtype.TimestamptzOID)
if !ok {
return nil
}
conn.TypeMap().RegisterType(&pgtype.Type{
Codec: &pgtype.TimestamptzCodec{ScanLocation: time.UTC},
Name: tzType.Name,
OID: tzType.OID,
})
return nil
}
pool, err := pgxpool.NewWithConfig(context.Background(), poolCfg)
if err != nil {
return err
}
Conn = NewPoolProxy(pool)
err = testDB()
if err != nil {
return err
}
return nil
}
func testDB() error {
ctx := context.Background()
conn, err := Conn.Acquire(ctx)
if err != nil {
return err
}
defer conn.Release()
row := conn.QueryRow(ctx, "SELECT 1")
var result int
err = row.Scan(&result)
if err != nil {
return err
}
return nil
}
func getEnv(key string) string {
if val := os.Getenv(key); val != "" {
return val
}
// Return empty string instead of fatal error - allows tests to run without prod env vars
return ""
}