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:
@@ -1,10 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { apiFetch } from '$lib/utils/api';
|
||||
import { ensureBusinessInfo, getBusinessInfo } from '$lib/stores/businessInfo.svelte';
|
||||
import { SvelteDate } from 'svelte/reactivity';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { formatDuration } from '$lib/utils/format';
|
||||
import { formatUserName } from '$lib/utils/nameDisplay';
|
||||
import { parseWallClockDate } from '$lib/utils/timeSlots';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
@@ -106,19 +106,19 @@
|
||||
const minutesUntilClosing = $derived.by(() => {
|
||||
if (!closingTime) return 0;
|
||||
const [ch, cm] = closingTime.split(':').map(Number);
|
||||
const now = new SvelteDate();
|
||||
const closing = new SvelteDate(now.getFullYear(), now.getMonth(), now.getDate(), ch, cm);
|
||||
const now = new Date();
|
||||
const closing = new Date(now.getFullYear(), now.getMonth(), now.getDate(), ch, cm);
|
||||
return Math.max(0, Math.floor((closing.getTime() - now.getTime()) / 60000));
|
||||
});
|
||||
|
||||
function calculateTimes() {
|
||||
const now = new SvelteDate();
|
||||
const now = new Date();
|
||||
|
||||
if (currentAppointment) {
|
||||
isInProgress = currentAppointment.status === 'in_progress';
|
||||
const startTime = new SvelteDate(currentAppointment.start_time);
|
||||
const startTime = parseWallClockDate(currentAppointment.start_time);
|
||||
|
||||
const endTime = new SvelteDate(
|
||||
const endTime = new Date(
|
||||
startTime.getTime() + currentAppointment.duration_minutes * 60 * 1000
|
||||
);
|
||||
|
||||
@@ -137,15 +137,15 @@
|
||||
|
||||
let rawFreeMinutes = 0;
|
||||
if (nextAppointment) {
|
||||
const nextStart = new SvelteDate(nextAppointment.start_time);
|
||||
const nextStart = parseWallClockDate(nextAppointment.start_time);
|
||||
const gapMs = nextStart.getTime() - endTime.getTime();
|
||||
rawFreeMinutes = Math.max(0, Math.floor(gapMs / 60000));
|
||||
}
|
||||
|
||||
if (closingTime) {
|
||||
const [ch, cm] = closingTime.split(':').map(Number);
|
||||
const today = new SvelteDate();
|
||||
const closing = new SvelteDate(
|
||||
const today = new Date();
|
||||
const closing = new Date(
|
||||
today.getFullYear(),
|
||||
today.getMonth(),
|
||||
today.getDate(),
|
||||
@@ -325,14 +325,14 @@
|
||||
</Card.Title>
|
||||
{#if activeAppointment}
|
||||
<Card.Description class="text-base">
|
||||
{new SvelteDate(activeAppointment.start_time).toLocaleTimeString('en-US', {
|
||||
{parseWallClockDate(activeAppointment.start_time).toLocaleTimeString('en-US', {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true
|
||||
})}
|
||||
-
|
||||
{new SvelteDate(
|
||||
new SvelteDate(activeAppointment.start_time).getTime() +
|
||||
{new Date(
|
||||
parseWallClockDate(activeAppointment.start_time).getTime() +
|
||||
activeAppointment.duration_minutes * 60 * 1000
|
||||
).toLocaleTimeString('en-US', {
|
||||
hour: 'numeric',
|
||||
@@ -439,13 +439,13 @@
|
||||
|
||||
{#if summary.summary_scope === 'week'}
|
||||
<div class="text-xs text-gray-500">
|
||||
Summary: {new SvelteDate(summary.summary_start_date).toLocaleDateString('en-US', {
|
||||
Summary: {parseWallClockDate(summary.summary_start_date).toLocaleDateString('en-US', {
|
||||
weekday: 'short',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})}
|
||||
–
|
||||
{new SvelteDate(summary.summary_end_date).toLocaleDateString('en-US', {
|
||||
{parseWallClockDate(summary.summary_end_date).toLocaleDateString('en-US', {
|
||||
weekday: 'short',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
@@ -552,7 +552,7 @@
|
||||
<div>
|
||||
<h4 class="mb-2 text-sm font-semibold text-gray-700">
|
||||
New bookings made since{summary.summary_scope === 'week'
|
||||
? ` opening ${new SvelteDate(summary.summary_start_date).toLocaleDateString('en-US', { weekday: 'long' })}`
|
||||
? ` opening ${parseWallClockDate(summary.summary_start_date).toLocaleDateString('en-US', { weekday: 'long' })}`
|
||||
: ' closing yesterday'}
|
||||
</h4>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@@ -573,13 +573,13 @@
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-gray-700">This week so far</h4>
|
||||
<div class="text-xs text-gray-500">
|
||||
{new SvelteDate(weekSummary.summary_start_date).toLocaleDateString('en-US', {
|
||||
{parseWallClockDate(weekSummary.summary_start_date).toLocaleDateString('en-US', {
|
||||
weekday: 'short',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})}
|
||||
–
|
||||
{new SvelteDate(weekSummary.summary_end_date).toLocaleDateString('en-US', {
|
||||
{parseWallClockDate(weekSummary.summary_end_date).toLocaleDateString('en-US', {
|
||||
weekday: 'short',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
|
||||
Reference in New Issue
Block a user