diff --git a/estore/src/app/api/invoice-request/route.ts b/estore/src/app/api/invoice-request/route.ts
new file mode 100644
index 0000000..1e5ae8b
--- /dev/null
+++ b/estore/src/app/api/invoice-request/route.ts
@@ -0,0 +1,45 @@
+import { NextRequest, NextResponse } from 'next/server'
+import type { EmailLineItem } from '@/lib/notify'
+
+interface InvoiceRequestBody {
+ customerName: string
+ customerEmail: string
+ customerPhone: string
+ lineItems: EmailLineItem[]
+ fulfillment: 'delivery' | 'pickup'
+ slotISO?: string
+ address?: string
+ deliveryNotes?: string
+ deliveryCents?: number
+ grandTotal: number
+}
+
+export async function POST(req: NextRequest) {
+ let body: InvoiceRequestBody
+ try {
+ body = (await req.json()) as InvoiceRequestBody
+ } catch {
+ return NextResponse.json({ error: 'Invalid request body' }, { status: 400 })
+ }
+
+ const { customerName, customerEmail, customerPhone, lineItems, grandTotal } = body
+
+ if (
+ typeof customerName !== 'string' || !customerName.trim() ||
+ typeof customerEmail !== 'string' || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(customerEmail) ||
+ typeof customerPhone !== 'string' || !customerPhone.trim() ||
+ !Array.isArray(lineItems) || lineItems.length === 0 ||
+ typeof grandTotal !== 'number'
+ ) {
+ return NextResponse.json({ error: 'Missing or invalid required fields' }, { status: 400 })
+ }
+
+ try {
+ const { sendInvoiceRequest } = await import('@/lib/notify')
+ await sendInvoiceRequest(body)
+ return NextResponse.json({ success: true })
+ } catch (err) {
+ console.error('[invoice-request] Failed to send:', err)
+ return NextResponse.json({ error: 'Failed to send — please email us directly.' }, { status: 500 })
+ }
+}
diff --git a/estore/src/components/PaymentForm.tsx b/estore/src/components/PaymentForm.tsx
index c833681..f3f2b02 100644
--- a/estore/src/components/PaymentForm.tsx
+++ b/estore/src/components/PaymentForm.tsx
@@ -147,6 +147,7 @@ export default function PaymentForm({ payload, onSuccess, onError, active }: Pro
const [applePayReady, setApplePayReady] = useState(false)
const [submitting, setSubmitting] = useState(false)
const [error, setError] = useState('')
+ const [invoiceState, setInvoiceState] = useState<'idle' | 'sending' | 'sent' | 'failed'>('idle')
const cardRef = useRef(null)
const googlePayRef = useRef(null)
@@ -331,6 +332,32 @@ export default function PaymentForm({ payload, onSuccess, onError, active }: Pro
)
}
+ async function handleSendInvoice() {
+ setInvoiceState('sending')
+ try {
+ const res = await fetch(`${BASE}/api/invoice-request`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ customerName: `${payload.customerFirstName} ${payload.customerLastName}`.trim(),
+ customerEmail: payload.customerEmail,
+ customerPhone: payload.customerPhone,
+ lineItems: payload.lineItems,
+ fulfillment: payload.deliverySlotISO ? 'delivery' : 'pickup',
+ slotISO: payload.deliverySlotISO ?? payload.pickupSlotISO,
+ address: payload.deliveryAddress,
+ deliveryNotes: payload.deliveryNotes,
+ deliveryCents: payload.deliveryCents,
+ grandTotal: payload.grandTotal,
+ }),
+ })
+ if (!res.ok) throw new Error('send failed')
+ setInvoiceState('sent')
+ } catch {
+ setInvoiceState('failed')
+ }
+ }
+
const hasWallet = googlePayReady || applePayReady
return (
@@ -392,14 +419,36 @@ export default function PaymentForm({ payload, onSuccess, onError, active }: Pro
{error}
- If this keeps happening, you can{' '}
-
- email us your order details
-
- {' '}and we'll send you an invoice.
+ {invoiceState === 'sent' ? (
+
+ ✓ Sent! Check your email at {payload.customerEmail} — we'll be in touch shortly.
+
+ ) : invoiceState === 'failed' ? (
+ <>
+ Couldn't send automatically.{' '}
+
+ Email us directly
+ {' '}
+ and we'll invoice you.
+ >
+ ) : (
+ <>
+ If this keeps happening,{' '}
+
+ {' '}and we'll invoice you.
+ >
+ )}
)}
diff --git a/estore/src/lib/notify.ts b/estore/src/lib/notify.ts
index ef29616..a0b3c2a 100644
--- a/estore/src/lib/notify.ts
+++ b/estore/src/lib/notify.ts
@@ -107,6 +107,7 @@ async function send(params: {
to: string
subject: string
text: string
+ replyTo?: string
attachments?: Array<{ filename: string; content: string; contentType: string }>
}): Promise {
const from = process.env.ALERT_EMAIL_FROM ?? 'shop@beachpartyballoons.com'
@@ -122,6 +123,7 @@ async function send(params: {
await transporter.sendMail({
from,
to: params.to,
+ replyTo: params.replyTo,
subject: params.subject,
text: params.text,
attachments: params.attachments,
@@ -453,6 +455,89 @@ export async function sendBookingRequest(params: {
}
}
+export async function sendInvoiceRequest(params: {
+ customerName: string
+ customerEmail: string
+ customerPhone: string
+ lineItems: EmailLineItem[]
+ fulfillment: 'delivery' | 'pickup'
+ slotISO?: string
+ address?: string
+ deliveryNotes?: string
+ deliveryCents?: number
+ grandTotal: number // cents
+}): Promise {
+ const to = process.env.ALERT_EMAIL_TO ?? 'info@beachpartyballoons.com'
+ const from = process.env.ALERT_EMAIL_FROM ?? 'shop@beachpartyballoons.com'
+ const firstName = params.customerName.split(' ')[0] || params.customerName
+
+ const fmtD = (iso: string) =>
+ new Date(iso).toLocaleString('en-US', { timeZone: 'America/New_York', dateStyle: 'full', timeStyle: 'short' })
+ const fmt$ = (c: number) => `$${(c / 100).toFixed(2)}`
+
+ const fulfillmentLines = [
+ `Type: ${params.fulfillment === 'delivery' ? 'Delivery' : 'Pickup'}`,
+ params.slotISO ? `Date/time: ${fmtD(params.slotISO)}` : null,
+ params.address ? `Address: ${params.address}` : null,
+ params.deliveryNotes ? `Notes: ${params.deliveryNotes}` : null,
+ ].filter((l): l is string => l !== null)
+
+ const itemsBlock = formatLineItems(params.lineItems)
+ const itemSubtotal = params.lineItems.reduce((s: number, li: EmailLineItem) => s + li.priceCents * li.quantity, 0)
+ const estTax = Math.round(itemSubtotal * 0.0635)
+
+ const priceLines = [
+ `Items: ${fmt$(itemSubtotal)}`,
+ params.deliveryCents ? `Delivery: ${fmt$(params.deliveryCents)}` : null,
+ `Est. tax: ${fmt$(estTax)}`,
+ `Total: ${fmt$(params.grandTotal)}`,
+ ].filter((l): l is string => l !== null)
+
+ // ── Alert email to shop (reply-to set to customer so you can reply directly) ─
+ await send({
+ to,
+ replyTo: params.customerEmail,
+ subject: `📋 Invoice request — ${params.customerName}`,
+ text: [
+ `Invoice request from ${params.customerName}`,
+ `Reply to this email to contact the customer directly.`,
+ '',
+ `Customer: ${params.customerName}`,
+ `Email: ${params.customerEmail}`,
+ `Phone: ${params.customerPhone}`,
+ '',
+ ...fulfillmentLines,
+ '',
+ itemsBlock,
+ '',
+ ...priceLines,
+ ].join('\n'),
+ })
+
+ // ── Confirmation to customer ──────────────────────────────────────────────────
+ await send({
+ to: params.customerEmail,
+ replyTo: from,
+ subject: `We received your request — Beach Party Balloons`,
+ text: [
+ `Hi ${firstName},`,
+ '',
+ `We got your order details and will send you an invoice shortly.`,
+ '',
+ itemsBlock,
+ '',
+ ...fulfillmentLines,
+ '',
+ ...priceLines,
+ '',
+ `Questions? Just reply to this email or call us any time.`,
+ '',
+ `— Beach Party Balloons`,
+ `beachpartyballoons.com`,
+ ].join('\n'),
+ })
+}
+
export async function sendAdminErrorAlert(params: {
subject: string
message: string