diff --git a/frontend/src/lib/components/payments/PaymentModal.svelte b/frontend/src/lib/components/payments/PaymentModal.svelte index 8b3e4c7..1f415f7 100644 --- a/frontend/src/lib/components/payments/PaymentModal.svelte +++ b/frontend/src/lib/components/payments/PaymentModal.svelte @@ -7,6 +7,8 @@ import type { Booking, BookingService, BookingDiscount } from '$lib/types/booking'; import { authStore } from '$lib/stores/auth.svelte'; + const LOYALTY_DISCOUNT_RATE = 0.1; + interface Props { booking: Booking; onClose: () => void; @@ -45,6 +47,20 @@ let paymentResult = $state(null); let error = $state(null); + let stamps = $state(booking.user?.loyalty_stamps ?? 0); + let useLoyalty = $state(false); + + let loyaltyEligible = $derived( + stamps >= 10 && + !(booking.discounts ?? []).some((d: BookingDiscount) => d.discount_source === 'loyalty') && + booking.total_amount > 0 && + booking.amount_paid === 0 + ); + + let loyaltyDiscount = $derived( + useLoyalty ? Math.round(booking.total_amount * 100 * LOYALTY_DISCOUNT_RATE) : 0 + ); + let customerBalance = $state(0); let loadingCustomerBalance = $state(false); let giftCardPaymentAmount = $state(''); @@ -218,6 +234,21 @@ }).format(value); } + async function applyLoyaltyRedemption(): Promise { + if (!useLoyalty) return; + const res = await fetch(`/api/admin/bookings/${booking.id}/apply-redemption`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${authStore.currentToken}` + } + }); + if (!res.ok) { + const errData = await res.text(); + throw new Error(errData || 'Failed to apply loyalty discount'); + } + } + async function handleCardPayment() { const finalAmount = totalDue; @@ -230,6 +261,8 @@ error = null; try { + await applyLoyaltyRedemption(); + const response = await fetch(`/api/admin/bookings/${booking.id}/payment`, { method: 'POST', headers: { @@ -237,7 +270,7 @@ Authorization: `Bearer ${authStore.currentToken}` }, body: JSON.stringify({ - amount: Math.round(finalAmount * 100), + amount: Math.round(finalAmount * 100) - loyaltyDiscount, payment_type: 'full', tip_enabled: tipEnabled }) @@ -357,19 +390,23 @@ } async function handleCashPayment() { - if (cashAmountNum < totalDue) { + const cashDue = totalDue - loyaltyDiscount / 100; + + if (cashAmountNum < cashDue) { toast.error('Cash amount must cover the total'); return; } - const tipAmount = extraAsTip ? cashAmountNum - totalDue : 0; + const tipAmount = extraAsTip ? cashAmountNum - cashDue : 0; status = 'cash-confirming'; error = null; try { + await applyLoyaltyRedemption(); + const body: Record = { - amount: Math.round(totalDue * 100), + amount: Math.round(cashDue * 100), payment_type: 'full', payment_method: 'cash' }; @@ -474,7 +511,9 @@ return; } - let payAmountCents = Math.round(totalDue * 100); + const giftDue = totalDue - loyaltyDiscount / 100; + + let payAmountCents = Math.round(giftDue * 100); if (useAccountBalance) { const parsedAmt = parseFloat(giftCardPaymentAmount); if (isNaN(parsedAmt) || parsedAmt <= 0) { @@ -492,6 +531,8 @@ error = null; try { + await applyLoyaltyRedemption(); + const body: any = { amount: payAmountCents, payment_type: 'full', @@ -566,6 +607,8 @@ error = null; try { + await applyLoyaltyRedemption(); + const response = await fetch(`/api/admin/bookings/${booking.id}/payment`, { method: 'POST', headers: { @@ -573,7 +616,7 @@ Authorization: `Bearer ${authStore.currentToken}` }, body: JSON.stringify({ - amount: Math.round(totalDue * 100), + amount: Math.round(totalDue * 100) - loyaltyDiscount, payment_type: 'full', payment_method: 'saved_card', saved_card_id: selectedSavedCardId @@ -663,6 +706,24 @@ + {#if loyaltyEligible} +
+
+ + +
+
+ {/if} + {#if booking.discounts && booking.discounts.length > 0}
diff --git a/frontend/src/lib/components/payments/UserPaymentModal.svelte b/frontend/src/lib/components/payments/UserPaymentModal.svelte index 55d59ff..a7d6567 100644 --- a/frontend/src/lib/components/payments/UserPaymentModal.svelte +++ b/frontend/src/lib/components/payments/UserPaymentModal.svelte @@ -12,6 +12,8 @@ import { SvelteDate } from 'svelte/reactivity'; import PolicyPopover from '$lib/components/ui/policyPopover.svelte'; import { authStore } from '$lib/stores/auth.svelte'; + const LOYALTY_DISCOUNT_RATE = 0.1; + interface Props { booking: Booking; onClose: () => void; @@ -242,11 +244,12 @@ import { SvelteDate } from 'svelte/reactivity'; let loyaltyEligible = $derived( stamps >= 10 && !(booking.discounts ?? []).some((d) => d.discount_source === 'loyalty') && - booking.total_amount > 0 + booking.total_amount > 0 && + booking.amount_paid === 0 ); let loyaltyDiscount = $derived( - useLoyalty ? Math.round(booking.total_amount * 100 * 0.1) : 0 + useLoyalty ? Math.round(booking.total_amount * 100 * LOYALTY_DISCOUNT_RATE) : 0 ); // Deposit policy warning text — dynamic based on booking state @@ -731,7 +734,7 @@ import { SvelteDate } from 'svelte/reactivity';