From 8aaf2f1dcb58cfe770b24274c507aa189cad5ce7 Mon Sep 17 00:00:00 2001 From: chris Date: Tue, 30 Jun 2026 10:01:05 -0400 Subject: [PATCH] Improve error emails and invoice mailto with full order details Admin error email now includes: Square error code, category, and detail string; full customer name/email/phone; fulfillment type, address, date/time, and delivery instructions; per-item options, colors, and notes; itemised price breakdown with delivery and estimated tax; idempotency key for debugging. Customer invoice mailto now includes: full name/email/phone; per-item options, colors, and notes; delivery date, address, and instructions; itemised subtotal/delivery/tax/total breakdown. Co-Authored-By: Claude Sonnet 4.6 --- estore/src/app/api/checkout/route.ts | 46 ++++++++++++++++++++++----- estore/src/components/PaymentForm.tsx | 35 ++++++++++++++++---- 2 files changed, 66 insertions(+), 15 deletions(-) diff --git a/estore/src/app/api/checkout/route.ts b/estore/src/app/api/checkout/route.ts index 5620bee..a6a4f9b 100644 --- a/estore/src/app/api/checkout/route.ts +++ b/estore/src/app/api/checkout/route.ts @@ -515,21 +515,51 @@ export async function POST(req: NextRequest) { if (li.note) parts.push(` Note: ${li.note}`) return parts.join('\n') }).join('\n') - const fulfillmentStr = deliveryAddress - ? `Delivery to ${deliveryAddress}${deliverySlotISO ? ` at ${new Date(deliverySlotISO).toLocaleString('en-US', { timeZone: 'America/New_York', dateStyle: 'medium', timeStyle: 'short' })}` : ''}` - : pickupSlotISO ? `Pickup at ${new Date(pickupSlotISO).toLocaleString('en-US', { timeZone: 'America/New_York', dateStyle: 'medium', timeStyle: 'short' })}` : 'Unknown' + + const fmtET = (iso: string) => + new Date(iso).toLocaleString('en-US', { timeZone: 'America/New_York', dateStyle: 'full', timeStyle: 'short' }) + + const fulfillmentLines = deliveryAddress + ? [ + `Type: Delivery`, + `Address: ${deliveryAddress}`, + deliverySlotISO ? `Date/time: ${fmtET(deliverySlotISO)}` : null, + deliveryNotes ? `Instructions: ${deliveryNotes}` : null, + ].filter(Boolean) + : pickupSlotISO + ? [`Type: Pickup`, `Date/time: ${fmtET(pickupSlotISO)}`] + : [`Type: Unknown`] + + const itemSubtotal = lineItems.reduce((s, li) => s + li.priceCents * li.quantity, 0) + const estTax = Math.round(itemSubtotal * 0.0635) + const estTotal = itemSubtotal + (deliveryCents ?? 0) + estTax + + const squareDetail = squareErrors?.[0]?.detail ?? '' + const squareCat = squareErrors?.[0]?.category ?? '' + await sendAdminErrorAlert({ subject: 'Checkout error', message: [ - err instanceof Error ? err.message : String(err), + `Error code: ${code || '(none)'}`, + squareCat ? `Category: ${squareCat}` : null, + squareDetail ? `Detail: ${squareDetail}` : null, '', - `Customer: ${customerName ?? '(unknown)'} <${customerEmail ?? '(none)'}> ${customerPhone ?? ''}`, - `${fulfillmentStr}`, + `Customer: ${customerName ?? '(unknown)'}`, + `Email: ${customerEmail ?? '(none)'}`, + `Phone: ${customerPhone ?? '(none)'}`, + '', + ...fulfillmentLines, '', 'Order:', itemLines, - ].join('\n'), - context: { code: code || '(none)' }, + '', + `Items: $${(itemSubtotal / 100).toFixed(2)}`, + deliveryCents ? `Delivery: $${(deliveryCents / 100).toFixed(2)}` : null, + `Est. tax: $${(estTax / 100).toFixed(2)}`, + `Est. total: $${(estTotal / 100).toFixed(2)}`, + idempotencyKey ? `\nIdempotency key: ${idempotencyKey}` : null, + ].filter(Boolean).join('\n'), + context: {}, }) } catch { /* best effort */ } })() diff --git a/estore/src/components/PaymentForm.tsx b/estore/src/components/PaymentForm.tsx index a517f40..c833681 100644 --- a/estore/src/components/PaymentForm.tsx +++ b/estore/src/components/PaymentForm.tsx @@ -82,6 +82,9 @@ const SDK_URL = function buildMailtoLink(payload: CheckoutPayload): string { const fmtCents = (c: number) => `$${(c / 100).toFixed(2)}` + const fmtDate = (iso: string) => + new Date(iso).toLocaleString('en-US', { dateStyle: 'full', timeStyle: 'short' }) + const items = payload.lineItems .map((li) => { const lines = [` • ${li.quantity}× ${li.name} — ${fmtCents(li.priceCents * li.quantity)}`] @@ -91,25 +94,43 @@ function buildMailtoLink(payload: CheckoutPayload): string { return lines.join('\n') }) .join('\n') - const fulfillment = payload.deliverySlotISO - ? `Delivery: ${new Date(payload.deliverySlotISO).toLocaleString('en-US', { dateStyle: 'medium', timeStyle: 'short' })} to ${payload.deliveryAddress ?? ''}` + + const fulfillmentLines: string[] = payload.deliverySlotISO + ? [ + `Delivery date: ${fmtDate(payload.deliverySlotISO)}`, + `Address: ${payload.deliveryAddress ?? '(not set)'}`, + ...(payload.deliveryNotes ? [`Instructions: ${payload.deliveryNotes}`] : []), + ] : payload.pickupSlotISO - ? `Pickup: ${new Date(payload.pickupSlotISO).toLocaleString('en-US', { dateStyle: 'medium', timeStyle: 'short' })}` - : 'Fulfillment: not set' + ? [`Pickup: ${fmtDate(payload.pickupSlotISO)}`] + : [`Fulfillment: not set`] + + const itemSubtotal = payload.lineItems.reduce((s, li) => s + li.priceCents * li.quantity, 0) + const deliveryCents = payload.deliveryCents ?? 0 + const estTax = Math.round(itemSubtotal * 0.0635) + + const priceLines = [ + `Items: ${fmtCents(itemSubtotal)}`, + deliveryCents ? `Delivery: ${fmtCents(deliveryCents)}` : null, + `Est. tax: ${fmtCents(estTax)}`, + `Total: ${fmtCents(payload.grandTotal)}`, + ].filter(Boolean).join('\n') + const body = [ `Hi Beach Party Balloons,`, ``, `I tried to place an order online but ran into a payment error. Could you please send me an invoice?`, ``, - `Name: ${payload.customerFirstName} ${payload.customerLastName}`, + `Name: ${payload.customerFirstName} ${payload.customerLastName}`, `Email: ${payload.customerEmail}`, `Phone: ${payload.customerPhone}`, ``, `Order:`, items, ``, - fulfillment, - `Total: ${fmtCents(payload.grandTotal)}`, + ...fulfillmentLines, + ``, + priceLines, ].join('\n') const addr = atob('aW5mb0BiZWFjaHBhcnR5YmFsbG9vbnMuY29t')