fix: sanitize API error text display and add time_blockers tests
Add extractErrorMessage helper for JSON error body parsing and apply sanitizeText across all toast displays. Add time_blockers test coverage for new holiday placeholder cleanup and overlapping scenarios.
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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 ? `<p class="warning">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');
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { apiFetch } from '$lib/utils/api';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { extractErrorMessage } from '$lib/utils/toast-safe';
|
||||
import { SvelteDate, SvelteSet, SvelteURLSearchParams } from 'svelte/reactivity';
|
||||
import { CalendarDate, getLocalTimeZone, type DateValue } from '@internationalized/date';
|
||||
import { isValidUKPhone, toE164UK } from '$lib/utils/phone';
|
||||
@@ -718,7 +719,7 @@
|
||||
return false;
|
||||
} else {
|
||||
const errorText = await response.text();
|
||||
toast.error(`Failed to reserve slot: ${errorText}`);
|
||||
toast.error(`Failed to reserve slot: ${extractErrorMessage(errorText)}`);
|
||||
return false;
|
||||
}
|
||||
} catch {
|
||||
@@ -855,7 +856,7 @@
|
||||
toast.success('Custom service created and added');
|
||||
} else {
|
||||
const err = await response.text();
|
||||
toast.error(`Failed: ${err}`);
|
||||
toast.error(`Failed: ${extractErrorMessage(err)}`);
|
||||
}
|
||||
} catch {
|
||||
toast.error('Network error');
|
||||
@@ -1014,7 +1015,7 @@
|
||||
onBookingCreated?.();
|
||||
} else {
|
||||
const errorText = await res.text();
|
||||
toast.error(`Failed to create booking: ${errorText}`);
|
||||
toast.error(`Failed to create booking: ${extractErrorMessage(errorText)}`);
|
||||
}
|
||||
} catch {
|
||||
toast.error('An error occurred while creating booking');
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { apiFetch } from '$lib/utils/api';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { extractErrorMessage } from '$lib/utils/toast-safe';
|
||||
import { SvelteDate } from 'svelte/reactivity';
|
||||
import * as Modal from '$lib/components/ui/dialog';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
@@ -84,7 +85,7 @@
|
||||
fetchBookingDetails();
|
||||
} else {
|
||||
const text = await response.text();
|
||||
toast.error('Failed to cancel: ' + text);
|
||||
toast.error('Failed to cancel: ' + extractErrorMessage(text));
|
||||
}
|
||||
} catch {
|
||||
toast.error('Network error');
|
||||
@@ -210,7 +211,7 @@
|
||||
};
|
||||
} else {
|
||||
const text = await response.text();
|
||||
toast.error('Failed to load booking details: ' + text);
|
||||
toast.error('Failed to load booking details: ' + extractErrorMessage(text));
|
||||
}
|
||||
} catch (_err) {
|
||||
console.error('Network error loading booking details:', _err);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { apiFetch } from '$lib/utils/api';
|
||||
import { SvelteDate, SvelteURLSearchParams } from 'svelte/reactivity';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { extractErrorMessage } from '$lib/utils/toast-safe';
|
||||
|
||||
// shadcn-svelte components
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
@@ -77,7 +78,7 @@
|
||||
}
|
||||
} else {
|
||||
const text = await response.text();
|
||||
toast.error('Failed to load bookings: ' + text);
|
||||
toast.error('Failed to load bookings: ' + extractErrorMessage(text));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching bookings:', err);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { apiFetch } from '$lib/utils/api';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { extractErrorMessage } from '$lib/utils/toast-safe';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
@@ -257,7 +258,7 @@
|
||||
showEditModal = false;
|
||||
} else {
|
||||
const errText = await res.text();
|
||||
toast.error(errText || 'Failed to update settings');
|
||||
toast.error(extractErrorMessage(errText) || 'Failed to update settings');
|
||||
}
|
||||
} catch {
|
||||
toast.error('Network error saving settings');
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
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 { range } from '$lib/utils/format';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
@@ -156,7 +156,7 @@
|
||||
await fetchServices();
|
||||
} else {
|
||||
const err = await response.text();
|
||||
toast.error('Failed: ' + sanitizeText(err));
|
||||
toast.error('Failed: ' + sanitizeText(extractErrorMessage(err)));
|
||||
}
|
||||
} catch {
|
||||
toast.error('Network error');
|
||||
@@ -184,7 +184,7 @@
|
||||
toast.error('A service with this name already exists');
|
||||
} else {
|
||||
const errText = await response.text();
|
||||
toast.error('Failed: ' + sanitizeText(errText));
|
||||
toast.error('Failed: ' + sanitizeText(extractErrorMessage(errText)));
|
||||
}
|
||||
} catch {
|
||||
toast.error('Network error');
|
||||
@@ -204,7 +204,7 @@
|
||||
toast.error('Cannot delete: used in bookings. Promote first.');
|
||||
} else {
|
||||
const errText = await response.text();
|
||||
toast.error('Failed: ' + sanitizeText(errText));
|
||||
toast.error('Failed: ' + sanitizeText(extractErrorMessage(errText)));
|
||||
}
|
||||
} catch {
|
||||
toast.error('Network error');
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { apiFetch } from '$lib/utils/api';
|
||||
import { range } from '$lib/utils/format';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { extractErrorMessage } from '$lib/utils/toast-safe';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
@@ -186,7 +187,7 @@
|
||||
await loadCampaigns();
|
||||
} else {
|
||||
const text = await res.text();
|
||||
toast.error(text || 'Failed to save campaign');
|
||||
toast.error(extractErrorMessage(text) || 'Failed to save campaign');
|
||||
}
|
||||
} catch {
|
||||
toast.error('Failed to save campaign');
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { apiFetch } from '$lib/utils/api';
|
||||
import { SvelteDate } from 'svelte/reactivity';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { extractErrorMessage } from '$lib/utils/toast-safe';
|
||||
import { formatDuration } from '$lib/utils/format';
|
||||
import { formatUserName } from '$lib/utils/nameDisplay';
|
||||
import { parseWallClockDate } from '$lib/utils/timeSlots';
|
||||
@@ -138,7 +139,7 @@
|
||||
notes = booking.notes || '';
|
||||
} else {
|
||||
const text = await response.text();
|
||||
toast.error('Failed to load booking: ' + text);
|
||||
toast.error('Failed to load booking: ' + extractErrorMessage(text));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching booking:', err);
|
||||
@@ -331,7 +332,7 @@
|
||||
onSaved?.();
|
||||
} else {
|
||||
const text = await response.text();
|
||||
toast.error('Failed to update booking: ' + text);
|
||||
toast.error('Failed to update booking: ' + extractErrorMessage(text));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error saving booking:', err);
|
||||
@@ -379,7 +380,7 @@
|
||||
fetchBooking();
|
||||
} else {
|
||||
const text = await response.text();
|
||||
toast.error('Failed to process refund: ' + text);
|
||||
toast.error('Failed to process refund: ' + extractErrorMessage(text));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error processing refund:', err);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { apiFetch } from '$lib/utils/api';
|
||||
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 AlertDialog from '$lib/components/ui/alert-dialog';
|
||||
@@ -128,7 +129,7 @@
|
||||
onApproved();
|
||||
} else {
|
||||
const text = await response.text();
|
||||
toast.error('Failed to approve: ' + text, { id: loadingToast });
|
||||
toast.error('Failed to approve: ' + extractErrorMessage(text), { id: loadingToast });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error approving edit request:', err);
|
||||
@@ -160,7 +161,7 @@
|
||||
onDenied();
|
||||
} else {
|
||||
const text = await response.text();
|
||||
toast.error('Failed to deny: ' + text, { id: loadingToast });
|
||||
toast.error('Failed to deny: ' + extractErrorMessage(text), { id: loadingToast });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error denying edit request:', err);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { apiFetch } from '$lib/utils/api';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { extractErrorMessage } from '$lib/utils/toast-safe';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
@@ -397,7 +398,7 @@
|
||||
await fetchGiftCards();
|
||||
} else {
|
||||
const errText = await res.text();
|
||||
toast.error(errText || 'Failed to create inventory card');
|
||||
toast.error(extractErrorMessage(errText) || 'Failed to create inventory card');
|
||||
}
|
||||
} catch {
|
||||
toast.error('Network error');
|
||||
@@ -439,7 +440,7 @@
|
||||
await fetchGiftCards();
|
||||
} else {
|
||||
const errText = await res.text();
|
||||
toast.error(errText || 'Failed to transfer balance');
|
||||
toast.error(extractErrorMessage(errText) || 'Failed to transfer balance');
|
||||
}
|
||||
} catch {
|
||||
toast.error('Network error transferring balance');
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
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 { apiFetch } from '$lib/utils/api';
|
||||
@@ -41,7 +42,7 @@
|
||||
}
|
||||
} else {
|
||||
const text = await response.text();
|
||||
toast.error('Failed to load services: ' + text);
|
||||
toast.error('Failed to load services: ' + extractErrorMessage(text));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching eligible services:', err);
|
||||
@@ -77,7 +78,7 @@
|
||||
onPatchTestAdded();
|
||||
} else {
|
||||
const text = await response.text();
|
||||
toast.error(text || 'Failed to add patch test', { id: loadingToast });
|
||||
toast.error(extractErrorMessage(text) || 'Failed to add patch test', { id: loadingToast });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error adding patch test:', err);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { extractErrorMessage } from '$lib/utils/toast-safe';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { apiFetch } from '$lib/utils/api';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
@@ -216,7 +217,7 @@
|
||||
await fetchData();
|
||||
} else {
|
||||
const errText = await res.text();
|
||||
toast.error(`Failed to save: ${errText}`);
|
||||
toast.error(`Failed to save: ${extractErrorMessage(errText)}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { SvelteDate, SvelteSet } from 'svelte/reactivity';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { extractErrorMessage } from '$lib/utils/toast-safe';
|
||||
import { apiFetch } from '$lib/utils/api';
|
||||
import { CalendarDate, getLocalTimeZone, type DateValue } from '@internationalized/date';
|
||||
import { Checkbox } from '$lib/components/ui/checkbox';
|
||||
@@ -323,7 +324,7 @@
|
||||
onSaved?.();
|
||||
} else {
|
||||
const text = await response.text();
|
||||
toast.error('Failed to reschedule: ' + text, { id: loadingToast });
|
||||
toast.error('Failed to reschedule: ' + extractErrorMessage(text), { id: loadingToast });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error rescheduling booking:', err);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { extractErrorMessage } from '$lib/utils/toast-safe';
|
||||
import { apiFetch } from '$lib/utils/api';
|
||||
import { range } from '$lib/utils/format';
|
||||
|
||||
@@ -192,7 +193,7 @@
|
||||
await fetchServices();
|
||||
} else {
|
||||
const errorText = await response.text();
|
||||
toast.error(`Failed to update service: ${errorText}`);
|
||||
toast.error(`Failed to update service: ${extractErrorMessage(errorText)}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error toggling service:', err);
|
||||
@@ -219,7 +220,7 @@
|
||||
await fetchServices();
|
||||
} else {
|
||||
const errorText = await response.text();
|
||||
toast.error(`Failed to delete service: ${errorText}`);
|
||||
toast.error(`Failed to delete service: ${extractErrorMessage(errorText)}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error deleting service:', err);
|
||||
@@ -275,10 +276,10 @@
|
||||
toast.error('A service with this name already exists', { id: loadingToast });
|
||||
} else if (response.status === 400) {
|
||||
const errorText = await response.text();
|
||||
toast.error(`Validation error: ${errorText}`, { id: loadingToast });
|
||||
toast.error(`Validation error: ${extractErrorMessage(errorText)}`, { id: loadingToast });
|
||||
} else {
|
||||
const errorText = await response.text();
|
||||
toast.error(`Failed to create service: ${errorText}`, { id: loadingToast });
|
||||
toast.error(`Failed to create service: ${extractErrorMessage(errorText)}`, { id: loadingToast });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error creating service:', err);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { SvelteDate, SvelteURLSearchParams } from 'svelte/reactivity';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { extractErrorMessage } from '$lib/utils/toast-safe';
|
||||
import { apiFetch } from '$lib/utils/api';
|
||||
import * as Modal from '$lib/components/ui/dialog';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
@@ -115,7 +116,7 @@
|
||||
selectedUser = await response.json();
|
||||
} else {
|
||||
const text = await response.text();
|
||||
toast.error('Failed to load user details: ' + text);
|
||||
toast.error('Failed to load user details: ' + extractErrorMessage(text));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching user details:', err);
|
||||
@@ -191,7 +192,7 @@
|
||||
}
|
||||
} else {
|
||||
const text = await response.text();
|
||||
toast.error('Failed to load user bookings: ' + text);
|
||||
toast.error('Failed to load user bookings: ' + extractErrorMessage(text));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching user bookings:', err);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { extractErrorMessage } from '$lib/utils/toast-safe';
|
||||
import { apiFetch } from '$lib/utils/api';
|
||||
import { range } from '$lib/utils/format';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
@@ -94,7 +95,7 @@
|
||||
}
|
||||
} else {
|
||||
const text = await response.text();
|
||||
toast.error('Failed to load users: ' + text);
|
||||
toast.error('Failed to load users: ' + extractErrorMessage(text));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching users:', err);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { SvelteDate } from 'svelte/reactivity';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { extractErrorMessage } from '$lib/utils/toast-safe';
|
||||
|
||||
import type { AvailableHoursDay, Service } from '$lib/types/booking';
|
||||
import {
|
||||
@@ -306,7 +307,7 @@
|
||||
return false;
|
||||
} else {
|
||||
const errorText = await response.text();
|
||||
toast.error(`Failed to reserve slot: ${errorText}`);
|
||||
toast.error(`Failed to reserve slot: ${extractErrorMessage(errorText)}`);
|
||||
return false;
|
||||
}
|
||||
} catch (_err) {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { generateUUID } from '$lib/utils/uuid';
|
||||
import { apiFetch } from '$lib/utils/api';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { extractErrorMessage } from '$lib/utils/toast-safe';
|
||||
import { SvelteDate, SvelteURLSearchParams } from 'svelte/reactivity';
|
||||
import { isValidUKPhone, toE164UK } from '$lib/utils/phone';
|
||||
import { formatUserName } from '$lib/utils/nameDisplay';
|
||||
@@ -409,7 +410,7 @@
|
||||
toast.success('Custom service created and added');
|
||||
} else {
|
||||
const err = await response.text();
|
||||
toast.error(`Failed: ${err}`);
|
||||
toast.error(`Failed: ${extractErrorMessage(err)}`);
|
||||
}
|
||||
} catch {
|
||||
toast.error('Network error');
|
||||
@@ -555,7 +556,7 @@
|
||||
onBookingCreated?.();
|
||||
} else {
|
||||
const errorText = await res.text();
|
||||
toast.error(`Failed to create booking: ${errorText}`);
|
||||
toast.error(`Failed to create booking: ${extractErrorMessage(errorText)}`);
|
||||
}
|
||||
} catch {
|
||||
toast.error('An error occurred while creating booking');
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { extractErrorMessage } from '$lib/utils/toast-safe';
|
||||
import { browser } from '$app/environment';
|
||||
import { apiFetch } from '$lib/utils/api';
|
||||
import { range } from '$lib/utils/format';
|
||||
@@ -128,7 +129,7 @@
|
||||
);
|
||||
} else {
|
||||
const text = await response.text();
|
||||
error = 'Failed to load working hours: ' + text;
|
||||
error = 'Failed to load working hours: ' + extractErrorMessage(text);
|
||||
console.error('Error fetching default hours:', text);
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -185,7 +186,7 @@
|
||||
toast.error('Unauthorized. Please log in again.', { id: loadingToast });
|
||||
} else {
|
||||
const text = await response.text();
|
||||
toast.error('Failed to save: ' + text, { id: loadingToast });
|
||||
toast.error('Failed to save: ' + extractErrorMessage(text), { id: loadingToast });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('save default hours', err);
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
import { apiFetch, getAuthHeaders } from '$lib/utils/api';
|
||||
import { ensureBusinessInfo, getBusinessInfo } from '$lib/stores/businessInfo.svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { extractErrorMessage } from '$lib/utils/toast-safe';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { SvelteDate } from 'svelte/reactivity';
|
||||
|
||||
@@ -1472,17 +1473,10 @@
|
||||
}, 50);
|
||||
} else {
|
||||
const errorText = await response.text();
|
||||
let errorMessage = errorText.trim();
|
||||
let errorMessage = extractErrorMessage(errorText);
|
||||
if (!errorMessage) {
|
||||
errorMessage = 'Failed to submit booking. Please try again.';
|
||||
}
|
||||
// Backend may return plain text or JSON
|
||||
try {
|
||||
const errorData = JSON.parse(errorText);
|
||||
if (errorData.error) errorMessage = errorData.error;
|
||||
} catch {
|
||||
/* use raw text from backend */
|
||||
}
|
||||
|
||||
if (response.status === 409) {
|
||||
if (errorMessage.includes('active booking') || errorMessage.includes('already have')) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { extractErrorMessage } from '$lib/utils/toast-safe';
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
@@ -241,7 +242,7 @@
|
||||
});
|
||||
if (!res.ok) {
|
||||
const errData = await res.text();
|
||||
throw new Error(errData || 'Failed to apply loyalty discount');
|
||||
throw new Error(extractErrorMessage(errData) || 'Failed to apply loyalty discount');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,7 +272,7 @@
|
||||
|
||||
if (!response.ok) {
|
||||
const errData = await response.text();
|
||||
throw new Error(errData || 'Failed to initiate payment');
|
||||
throw new Error(extractErrorMessage(errData) || 'Failed to initiate payment');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
@@ -412,7 +413,7 @@
|
||||
|
||||
if (!response.ok) {
|
||||
const errData = await response.text();
|
||||
throw new Error(errData || 'Failed to process payment');
|
||||
throw new Error(extractErrorMessage(errData) || 'Failed to process payment');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
@@ -539,7 +540,7 @@
|
||||
|
||||
if (!response.ok) {
|
||||
const errData = await response.text();
|
||||
throw new Error(errData || 'Failed to process gift card');
|
||||
throw new Error(extractErrorMessage(errData) || 'Failed to process gift card');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
@@ -614,7 +615,7 @@
|
||||
|
||||
if (!response.ok) {
|
||||
const errData = await response.text();
|
||||
throw new Error(errData || 'Failed to process saved card payment');
|
||||
throw new Error(extractErrorMessage(errData) || 'Failed to process saved card payment');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { SvelteDate } from 'svelte/reactivity';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { extractErrorMessage } from '$lib/utils/toast-safe';
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
@@ -407,7 +408,7 @@
|
||||
});
|
||||
if (!redemptionResponse.ok) {
|
||||
const errData = await redemptionResponse.text();
|
||||
throw new Error(errData || 'Failed to apply loyalty discount');
|
||||
throw new Error(extractErrorMessage(errData) || 'Failed to apply loyalty discount');
|
||||
}
|
||||
} catch (_err) {
|
||||
status = 'error';
|
||||
@@ -452,7 +453,7 @@
|
||||
|
||||
if (!response.ok) {
|
||||
const errData = await response.text();
|
||||
throw new Error(errData || 'Failed to initiate payment');
|
||||
throw new Error(extractErrorMessage(errData) || 'Failed to initiate payment');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { SvelteDate } from 'svelte/reactivity';
|
||||
import { CalendarDate } from '@internationalized/date';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { sanitizeText } from '$lib/utils/toast-safe';
|
||||
import { extractErrorMessage, sanitizeText } from '$lib/utils/toast-safe';
|
||||
import { formatUserName } from '$lib/utils/nameDisplay';
|
||||
import { range } from '$lib/utils/format';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
@@ -523,7 +523,7 @@
|
||||
await fetchTodayBlockersData();
|
||||
} else {
|
||||
const text = await response.text();
|
||||
toast.error('Failed to create: ' + sanitizeText(text), { id: loadingToast });
|
||||
toast.error('Failed to create: ' + sanitizeText(extractErrorMessage(text)), { id: loadingToast });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error creating time blocker:', err);
|
||||
@@ -547,7 +547,7 @@
|
||||
await fetchTodayBlockersData();
|
||||
} else {
|
||||
const text = await response.text();
|
||||
toast.error('Failed to delete: ' + sanitizeText(text), { id: loadingToast });
|
||||
toast.error('Failed to delete: ' + sanitizeText(extractErrorMessage(text)), { id: loadingToast });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error deleting time blocker:', err);
|
||||
|
||||
@@ -12,3 +12,25 @@ export function sanitizeText(text: string): string {
|
||||
};
|
||||
return text.replace(/[&<>"']/g, (ch) => map[ch] || ch);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts a human-readable error message from an API error response string.
|
||||
* The backend returns JSON error bodies like `{"error":"Rate limit exceeded"}`,
|
||||
* so we parse the JSON and extract the `.error` or `.message` field.
|
||||
* Falls back to the raw string if parsing fails or there's no recognizable field.
|
||||
*/
|
||||
export function extractErrorMessage(text: string): string {
|
||||
const trimmed = text.trim();
|
||||
if (!trimmed) return '';
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed);
|
||||
if (typeof parsed === 'string') return parsed;
|
||||
if (parsed.error && typeof parsed.error === 'string') return parsed.error;
|
||||
if (parsed.message && typeof parsed.message === 'string') return parsed.message;
|
||||
} catch {
|
||||
// Not JSON — use as-is
|
||||
}
|
||||
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { SvelteDate } from 'svelte/reactivity';
|
||||
import { browser } from '$app/environment';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { sanitizeText } from '$lib/utils/toast-safe';
|
||||
import { extractErrorMessage, sanitizeText } from '$lib/utils/toast-safe';
|
||||
import { apiFetch } from '$lib/utils/api';
|
||||
import UserBookingModal from '$lib/components/account/UserBookingModal.svelte';
|
||||
import { isValidUKPhone, formatPhoneDisplay, toE164UK } from '$lib/utils/phone';
|
||||
@@ -291,7 +291,7 @@
|
||||
await fetchGiftCardBalance();
|
||||
} else {
|
||||
const errText = await res.text();
|
||||
toast.error(errText || 'Failed to redeem gift card');
|
||||
toast.error(extractErrorMessage(errText) || 'Failed to redeem gift card');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('redeemGiftCard error:', err);
|
||||
@@ -357,7 +357,7 @@
|
||||
}
|
||||
} else {
|
||||
const errText = await res.text();
|
||||
toast.error(errText || 'Failed to purchase gift card');
|
||||
toast.error(extractErrorMessage(errText) || 'Failed to purchase gift card');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('buyGiftCard error:', err);
|
||||
@@ -576,7 +576,7 @@
|
||||
savedCardsStore.invalidate();
|
||||
} else {
|
||||
const errText = await res.text();
|
||||
toast.error(errText || 'Failed to add card');
|
||||
toast.error(extractErrorMessage(errText) || 'Failed to add card');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('addCard error:', err);
|
||||
@@ -764,7 +764,7 @@
|
||||
await fetchUserData();
|
||||
} else {
|
||||
const text = await response.text();
|
||||
toast.error(sanitizeText(text) || 'Failed to update phone number', { id: loadingToast });
|
||||
toast.error(sanitizeText(extractErrorMessage(text)) || 'Failed to update phone number', { id: loadingToast });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error updating phone:', err);
|
||||
@@ -869,7 +869,7 @@
|
||||
await fetchUserData();
|
||||
} else {
|
||||
const text = await response.text();
|
||||
toast.error(sanitizeText(text) || 'Failed to update name', { id: loadingToast });
|
||||
toast.error(sanitizeText(extractErrorMessage(text)) || 'Failed to update name', { id: loadingToast });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error updating name:', err);
|
||||
@@ -995,7 +995,7 @@
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
toast.error('Failed to load upcoming bookings: ' + sanitizeText(text));
|
||||
toast.error('Failed to load upcoming bookings: ' + sanitizeText(extractErrorMessage(text)));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1034,7 +1034,7 @@
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
toast.error('Failed to load past bookings: ' + sanitizeText(text));
|
||||
toast.error('Failed to load past bookings: ' + sanitizeText(extractErrorMessage(text)));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1155,7 +1155,7 @@
|
||||
passwordData = { current: '', new: '', confirm: '' };
|
||||
} else {
|
||||
const text = await response.text();
|
||||
toast.error(sanitizeText(text) || 'Failed to change password', { id: loadingToast });
|
||||
toast.error(sanitizeText(extractErrorMessage(text)) || 'Failed to change password', { id: loadingToast });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error changing password:', err);
|
||||
@@ -1201,7 +1201,7 @@
|
||||
goto('/');
|
||||
} else {
|
||||
const text = await response.text();
|
||||
toast.error(sanitizeText(text) || 'Failed to delete account', { id: loadingToast });
|
||||
toast.error(sanitizeText(extractErrorMessage(text)) || 'Failed to delete account', { id: loadingToast });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error deleting account:', err);
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Skeleton } from '$lib/components/ui/skeleton';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { extractErrorMessage } from '$lib/utils/toast-safe';
|
||||
import { SvelteDate } from 'svelte/reactivity';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { apiFetch } from '$lib/utils/api';
|
||||
@@ -172,7 +173,7 @@
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(errorText || 'Payment failed');
|
||||
throw new Error(extractErrorMessage(errorText) || 'Payment failed');
|
||||
}
|
||||
|
||||
paymentState = 'success';
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { apiFetch } from '$lib/utils/api';
|
||||
import { SvelteDate } from 'svelte/reactivity';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { extractErrorMessage } from '$lib/utils/toast-safe';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
@@ -143,7 +144,7 @@
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(errorText || 'Payment failed');
|
||||
throw new Error(extractErrorMessage(errorText) || 'Payment failed');
|
||||
}
|
||||
|
||||
paymentState = 'success';
|
||||
|
||||
Reference in New Issue
Block a user