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:
parent
84167c1ae8
commit
8aaf2f1dcb
@ -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 */ }
|
||||
})()
|
||||
|
||||
@ -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')
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user