diff --git a/backend/handlers/scheduling/time_blockers_test.go b/backend/handlers/scheduling/time_blockers_test.go index 476232c..54c3bfc 100644 --- a/backend/handlers/scheduling/time_blockers_test.go +++ b/backend/handlers/scheduling/time_blockers_test.go @@ -2027,6 +2027,179 @@ func TestCleanupOldReservations_EditRequest(t *testing.T) { } } +// --- Tests for CleanupOldReservations (Placeholder) --- + +// TestCleanupOldReservations_Placeholder verifies that placeholder reservations +// older than 24 hours are deleted, while recent ones are preserved. +func TestCleanupOldReservations_Placeholder(t *testing.T) { + t.Parallel() + ctx, tx := resetTestData(t) + + // Create old placeholder reservation (>24 hours old) + oldTime := clock.Now().Add(-25 * time.Hour) + _, err := tx.Exec(ctx, ` + INSERT INTO time_blockers (start_time, duration_minutes, description, created_at) + VALUES ($1, 60, 'RESERVATION:placeholder:bk123', $2) + `, oldTime, clock.Now().Add(-25*time.Hour)) + if err != nil { + t.Fatalf("failed to create old placeholder reservation: %v", err) + } + + // Create recent placeholder reservation (<24 hours old) + recentTime := clock.Now().Add(-12 * time.Hour) + _, err = tx.Exec(ctx, ` + INSERT INTO time_blockers (start_time, duration_minutes, description, created_at) + VALUES ($1, 60, 'RESERVATION:placeholder:bk456', $2) + `, recentTime, clock.Now().Add(-12*time.Hour)) + if err != nil { + t.Fatalf("failed to create recent placeholder reservation: %v", err) + } + + // Run cleanup + _, err = CleanupOldReservations(ctx) + if err != nil { + t.Fatalf("CleanupOldReservations failed: %v", err) + } + + // Verify old placeholder was deleted + var oldCount int + err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM time_blockers WHERE description = 'RESERVATION:placeholder:bk123'").Scan(&oldCount) + if err != nil { + t.Fatalf("failed to check old placeholder: %v", err) + } + if oldCount != 0 { + t.Error("expected old placeholder reservation (25h) to be deleted") + } + + // Verify recent placeholder still exists + var recentCount int + err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM time_blockers WHERE description = 'RESERVATION:placeholder:bk456'").Scan(&recentCount) + if err != nil { + t.Fatalf("failed to check recent placeholder: %v", err) + } + if recentCount != 1 { + t.Error("expected recent placeholder reservation (12h) to be preserved") + } +} + +// TestCleanupOldReservations_HolidayPlaceholder verifies that holiday placeholder +// reservations older than 24 hours are deleted, while recent ones are preserved. +func TestCleanupOldReservations_HolidayPlaceholder(t *testing.T) { + t.Parallel() + ctx, tx := resetTestData(t) + + // Create old holiday_placeholder reservation (>24 hours old) + oldTime := clock.Now().Add(-25 * time.Hour) + _, err := tx.Exec(ctx, ` + INSERT INTO time_blockers (start_time, duration_minutes, description, created_at) + VALUES ($1, 60, 'RESERVATION:holiday_placeholder:hld123', $2) + `, oldTime, clock.Now().Add(-25*time.Hour)) + if err != nil { + t.Fatalf("failed to create old holiday_placeholder reservation: %v", err) + } + + // Create recent holiday_placeholder reservation (<24 hours old) + recentTime := clock.Now().Add(-12 * time.Hour) + _, err = tx.Exec(ctx, ` + INSERT INTO time_blockers (start_time, duration_minutes, description, created_at) + VALUES ($1, 60, 'RESERVATION:holiday_placeholder:hld456', $2) + `, recentTime, clock.Now().Add(-12*time.Hour)) + if err != nil { + t.Fatalf("failed to create recent holiday_placeholder reservation: %v", err) + } + + // Run cleanup + _, err = CleanupOldReservations(ctx) + if err != nil { + t.Fatalf("CleanupOldReservations failed: %v", err) + } + + // Verify old placeholder was deleted + var oldCount int + err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM time_blockers WHERE description = 'RESERVATION:holiday_placeholder:hld123'").Scan(&oldCount) + if err != nil { + t.Fatalf("failed to check old holiday_placeholder: %v", err) + } + if oldCount != 0 { + t.Error("expected old holiday_placeholder reservation (25h) to be deleted") + } + + // Verify recent placeholder still exists + var recentCount int + err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM time_blockers WHERE description = 'RESERVATION:holiday_placeholder:hld456'").Scan(&recentCount) + if err != nil { + t.Fatalf("failed to check recent holiday_placeholder: %v", err) + } + if recentCount != 1 { + t.Error("expected recent holiday_placeholder reservation (12h) to be preserved") + } +} + +// TestCleanupOldReservations_MixedPlaceholders verifies that both placeholder +// and holiday_placeholder patterns are cleaned up together alongside other types. +func TestCleanupOldReservations_MixedPlaceholders(t *testing.T) { + t.Parallel() + ctx, tx := resetTestData(t) + + // Old placeholder (>24 hours) + _, err := tx.Exec(ctx, ` + INSERT INTO time_blockers (start_time, duration_minutes, description, created_at) + VALUES ($1, 60, 'RESERVATION:placeholder:bkOld', $2) + `, clock.Now().Add(-25*time.Hour), clock.Now().Add(-25*time.Hour)) + if err != nil { + t.Fatalf("failed to create old placeholder: %v", err) + } + + // Old holiday_placeholder (>24 hours) + _, err = tx.Exec(ctx, ` + INSERT INTO time_blockers (start_time, duration_minutes, description, created_at) + VALUES ($1, 60, 'RESERVATION:holiday_placeholder:hldOld', $2) + `, clock.Now().Add(-25*time.Hour), clock.Now().Add(-25*time.Hour)) + if err != nil { + t.Fatalf("failed to create old holiday_placeholder: %v", err) + } + + // Old user reservation (>1 hour) — should also be deleted + oldUserTime := clock.Now().Add(-2 * time.Hour) + userID, err := fixtures.CreateTestUser(tx) + if err != nil { + t.Fatalf("failed to create user: %v", err) + } + _, err = tx.Exec(ctx, ` + INSERT INTO time_blockers (start_time, duration_minutes, description, created_by, created_at) + VALUES ($1, 60, 'RESERVATION:user:test', $2, $3) + `, oldUserTime, userID, clock.Now().Add(-2*time.Hour)) + if err != nil { + t.Fatalf("failed to create old user reservation: %v", err) + } + + // Count before cleanup + var countBefore int + err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM time_blockers").Scan(&countBefore) + if err != nil { + t.Fatalf("failed to count before: %v", err) + } + if countBefore != 3 { + t.Errorf("expected 3 blockers before cleanup, got %d", countBefore) + } + + // Run cleanup + _, err = CleanupOldReservations(ctx) + if err != nil { + t.Fatalf("CleanupOldReservations failed: %v", err) + } + + // All 3 should be deleted + var countAfter int + err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM time_blockers").Scan(&countAfter) + if err != nil { + t.Fatalf("failed to count after: %v", err) + } + if countAfter != 0 { + t.Errorf("expected 0 blockers after cleanup (all 3 expired), got %d", countAfter) + } +} + // --- Tests for CleanupExpiredDeposits --- func TestCleanupExpiredDeposits_ExpiredConfirmed(t *testing.T) { diff --git a/frontend/src/lib/components/account/EditRequestModal.svelte b/frontend/src/lib/components/account/EditRequestModal.svelte index a823de5..06bedd8 100644 --- a/frontend/src/lib/components/account/EditRequestModal.svelte +++ b/frontend/src/lib/components/account/EditRequestModal.svelte @@ -3,6 +3,7 @@ import { SvelteDate, SvelteMap } from 'svelte/reactivity'; import { CalendarDate, getLocalTimeZone, type DateValue } from '@internationalized/date'; import { toast } from 'svelte-sonner'; + import { extractErrorMessage } from '$lib/utils/toast-safe'; import * as Modal from '$lib/components/ui/dialog'; import { Button } from '$lib/components/ui/button'; import * as Textarea from '$lib/components/ui/textarea'; @@ -724,7 +725,7 @@ onSubmitted(); } else { const text = await response.text(); - toast.error(text || 'Failed to submit edit/reschedule request'); + toast.error(extractErrorMessage(text) || 'Failed to submit edit/reschedule request'); } } catch { toast.error('Network error'); diff --git a/frontend/src/lib/components/account/UserBookingModal.svelte b/frontend/src/lib/components/account/UserBookingModal.svelte index 33c21b6..cd76e3c 100644 --- a/frontend/src/lib/components/account/UserBookingModal.svelte +++ b/frontend/src/lib/components/account/UserBookingModal.svelte @@ -6,6 +6,7 @@ import { ensureBusinessInfo, getBusinessInfo } from '$lib/stores/businessInfo.svelte'; import { SvelteDate } from 'svelte/reactivity'; import { toast } from 'svelte-sonner'; + import { extractErrorMessage } from '$lib/utils/toast-safe'; import * as Modal from '$lib/components/ui/dialog'; import { Button } from '$lib/components/ui/button'; import { Input } from '$lib/components/ui/input'; @@ -231,7 +232,7 @@ pendingEditRequest = editData.edit_request || null; } else { const text = await bookingResp.text(); - toast.error('Failed to load booking: ' + text); + toast.error('Failed to load booking: ' + extractErrorMessage(text)); open = false; } } catch (err) { @@ -361,7 +362,7 @@ ${hasVAT ? `
VAT is included at ${biz?.default_vat_rate ?? 20} open = false; } else { const text = await response.text(); - toast.error('Failed to cancel: ' + text); + toast.error('Failed to cancel: ' + extractErrorMessage(text)); } } catch { toast.error('Network error'); diff --git a/frontend/src/lib/components/admin/ApprovalModal.svelte b/frontend/src/lib/components/admin/ApprovalModal.svelte index 147a93b..2353936 100644 --- a/frontend/src/lib/components/admin/ApprovalModal.svelte +++ b/frontend/src/lib/components/admin/ApprovalModal.svelte @@ -2,7 +2,7 @@ import { SvelteDate } from 'svelte/reactivity'; import { apiFetch } from '$lib/utils/api'; import { toast } from 'svelte-sonner'; - import { sanitizeText } from '$lib/utils/toast-safe'; + import { extractErrorMessage, sanitizeText } from '$lib/utils/toast-safe'; import { formatDateTime, formatDuration } from '$lib/utils/format'; import { formatUserName } from '$lib/utils/nameDisplay'; import { parseWallClockDate } from '$lib/utils/timeSlots'; @@ -335,7 +335,7 @@ onApproved(); } else { const text = await response.text(); - toast.error('Failed to confirm: ' + sanitizeText(text), { id: loadingToast }); + toast.error('Failed to confirm: ' + sanitizeText(extractErrorMessage(text)), { id: loadingToast }); } } catch { toast.error('Network error confirming booking', { id: loadingToast }); @@ -363,7 +363,7 @@ onApproved(); } else { const text = await response.text(); - toast.error('Failed to decline: ' + sanitizeText(text), { id: loadingToast }); + toast.error('Failed to decline: ' + sanitizeText(extractErrorMessage(text)), { id: loadingToast }); } } catch { toast.error('Network error declining booking', { id: loadingToast }); diff --git a/frontend/src/lib/components/admin/BookingCreateModal.svelte b/frontend/src/lib/components/admin/BookingCreateModal.svelte index c58d355..a6818f5 100644 --- a/frontend/src/lib/components/admin/BookingCreateModal.svelte +++ b/frontend/src/lib/components/admin/BookingCreateModal.svelte @@ -1,6 +1,7 @@