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 <noreply@anthropic.com>
This commit is contained in:
chris 2026-06-30 10:01:05 -04:00
parent 84167c1ae8
commit 8aaf2f1dcb
2 changed files with 66 additions and 15 deletions

View File

@ -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 */ }
})()

View File

@ -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')