From 3ccc0177166d3815ecf74e9cc45da6ac0df76118 Mon Sep 17 00:00:00 2001 From: Stephen Adamson Date: Mon, 25 May 2026 18:05:14 +0100 Subject: [PATCH] feat: user payment modal and tip route for online payments Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .../payments/UserPaymentModal.svelte | 756 ++++++++++++++++++ frontend/src/routes/tip/+page.svelte | 414 ++++++++++ 2 files changed, 1170 insertions(+) create mode 100644 frontend/src/lib/components/payments/UserPaymentModal.svelte create mode 100644 frontend/src/routes/tip/+page.svelte diff --git a/frontend/src/lib/components/payments/UserPaymentModal.svelte b/frontend/src/lib/components/payments/UserPaymentModal.svelte new file mode 100644 index 0000000..1859f50 --- /dev/null +++ b/frontend/src/lib/components/payments/UserPaymentModal.svelte @@ -0,0 +1,756 @@ + + + !open && handleClose()}> + + + Make a Payment + {#if booking.id} +
Booking ID: {booking.id}
+ {/if} +
+ + {#if status === 'idle' || status === 'processing' || status === 'error'} +
+ +
+
Services
+
+ {#each booking.services ?? [] as service, index (index)} +
+ {service.service_name || 'Unknown Service'} + + {service.override_price ? formatCurrency(Math.round(service.override_price * 100)) : service.price ? formatCurrency(Math.round(service.price * 100)) : '-'} + +
+ {/each} +
+
+ + +
+
+ Total + {formatCurrency(Math.round(booking.total_amount * 100))} +
+
+ Amount Paid + {formatCurrency(totalPaid)} +
+
+ Amount Remaining + + {formatCurrency(Math.round(amountRemaining * 100))} + +
+
+ + + {#if status === 'idle' && authStore.isAuthenticated} + {#if paymentMethodsLoading} +
Loading payment methods...
+ {:else if paymentMethods.length > 0 && !showNewCardForm} + +
+ {#if selectedPaymentMethod} + {#each paymentMethods as method (method.id)} + {#if method.id === selectedPaymentMethod} +
+
+
+ {method.brand} +
+
+ **** {method.last_4} + + {formatCardExpiry(method.exp_month, method.exp_year)} + +
+
+ Selected +
+ {/if} + {/each} + {/if} + {#if canSaveCards} + + {/if} + + {#if showCardList} +
+ {#if canSaveCards} +
{ + showNewCardForm = true; + showCardList = false; + selectedPaymentMethod = null; + }} + > +
Enter card details
+ + + +
+ {/if} + {#each paymentMethods as method (method.id)} +
{ + selectedPaymentMethod = method.id; + showNewCardForm = false; + showCardList = false; + }} + > +
+
+ {method.brand} +
+
+ **** {method.last_4} + + {formatCardExpiry(method.exp_month, method.exp_year)} + +
+
+ {#if selectedPaymentMethod === method.id} + Selected + {/if} +
+ {/each} +
+ {/if} +
+ {:else} + +
+

Card Details

+
+
+ + +
+
+
+ + +
+
+ + +
+
+ {#if canSaveCards} +
+ + +
+ {/if} +
+
+ {/if} + {/if} + + + {#if isScenarioA} +
+ +
+ + +
+ + + {#if payButtonError} +

{payButtonError}

+ {/if} + +
+ {/if} + + + {#if isScenarioB} +
+ +
+ + +
+ + + {#if paymentType === 'partial'} +
+ +
+ £ + +
+
+ {/if} + + + {#if payButtonError} +

{payButtonError}

+ {/if} + +
+ {/if} + + {#if status === 'error' && error} +
+

{error}

+
+ {/if} + + + +
+ {:else if status === 'polling'} + +
+
+

Processing payment...

+

This may take a few moments

+ +
+ {:else if status === 'success' && paymentResult} + +
+
+
+ + + +
+

Payment Successful

+
+ + +
+
+
+ Amount + + {formatCurrency(paymentResult.amount)} + +
+
+ Type + + {paymentResult.payment_type} + +
+ {#if paymentResult.card_brand} +
+ Card + + {paymentResult.card_brand} ****{paymentResult.card_last4} + +
+ {/if} +
+ Status + Completed +
+
+
+ + +
+ {/if} +
+
diff --git a/frontend/src/routes/tip/+page.svelte b/frontend/src/routes/tip/+page.svelte new file mode 100644 index 0000000..4d2d917 --- /dev/null +++ b/frontend/src/routes/tip/+page.svelte @@ -0,0 +1,414 @@ + + + + Leave a Tip - Crussell + + +
+ {#if loading} +
+
+ + +
+ + + + + + + +
+ {:else if error} + + + Something went wrong + + +

{error}

+
+ + +
+
+
+ {:else if booking} +
+

Leave a Tip

+

Show your appreciation for great service

+
+ + {#if paymentState === 'success'} + + +
+ + + +
+

Thank you!

+

Your generosity is greatly appreciated.

+ +
+
+ {:else} + + + Your last Appointment + + +
+ Date + {formatDate(booking.start_time)} +
+
+ Time + {formatTimeRange(booking.start_time, booking.duration_minutes ?? 0)} +
+
+ Subtotal + {formatPrice(subtotal)} +
+ {#if tipsPaid > 0} +
+ Tips + {formatPrice(tipsPaid)} +
+ {/if} + {#if booking.services && booking.services.length > 0} +
+
Services
+
+ {#each booking.services as service} +
+ {service.service_name} + {formatPrice(service.override_price ?? service.price)} +
+ {/each} +
+
+ {/if} +
+
+ + + + Choose Tip Amount + + +
+ {#each tipPercentages as tip (tip.pct)} + + {/each} +
+ +
+ +
+ £ + +
+
+
+
+ + {#if paymentState === 'error'} +
+

Payment failed. Please try again.

+ +
+ {/if} + + + +

Secure payment powered by Square

+ {/if} + {/if} +