- {[
- { label: 'Full Name', value: m.customerName },
- { label: 'Email', value: m.customerEmail },
- { label: 'Phone', value: m.customerPhone },
- { label: 'Address', value: m.customerAddress },
- { label: 'City', value: m.customerCity },
- { label: 'Country', value: m.customerCountry },
- { label: 'Postal Code', value: m.customerPostalCode },
- { label: 'Persona', value: m.persona },
- { label: 'Color', value: m.color },
- ].map(({ label, value }) => value ? (
-
-
{label}
-
- {label === 'Color' && (
-
- )}
- {value}
+
+
+ {/* Left: all info sections */}
+
+
+ {/* Shipping details */}
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Configuration */}
+
+
+
+
+
Color
+ {m.color ? (
+
+
+ {m.color}
+
+ ) :
—
}
-
- ) : null)}
+
+
+
+ {/* Price Breakdown */}
+
+ {priceLines.length > 0 ? (
+
+ {priceLines.map((line, i) => (
+
+ {line.label}
+
+ AED {new Intl.NumberFormat('en-AE').format(line.price)}
+
+
+ ))}
+
+ Total
+
+ {formatAmount(order.amount, order.currency)}
+
+
+
+ ) : (
+
+ Total (legacy order)
+
+ {formatAmount(order.amount, order.currency)}
+
+
+ )}
+
+
+ {/* Payment ID */}
+
+ Payment ID: {order.id}
+
- {/* Robot snapshot */}
- {snapshot === 'loading' && (
-
Loading image…
- )}
- {snapshot && snapshot !== 'loading' && snapshot !== 'none' && (
-
-
Robot Configuration
- {/* eslint-disable-next-line @next/next/no-img-element */}
+ {/* Right: snapshot */}
+
+
Robot Snapshot
+ {snapshot === 'loading' && (
+
Loading…
+ )}
+ {snapshot && snapshot !== 'loading' && snapshot !== 'none' && (
+ /* eslint-disable-next-line @next/next/no-img-element */

-
- )}
+ )}
+ {snapshot === 'none' && (
+
No snapshot
+ )}
+
)}
);
}
+function SectionBox({ title, children }: { title: string; children: React.ReactNode }) {
+ return (
+
+ );
+}
+
+function InfoGrid({ children }: { children: React.ReactNode }) {
+ return (
+
{children}
+ );
+}
+
+function InfoField({ label, value }: { label: string; value?: string | null }) {
+ if (!value) return null;
+ return (
+
+ );
+}
+
function StatCard({ label, value }: { label: string; value: string }) {
return (
diff --git a/src/app/api/admin/orders/route.ts b/src/app/api/admin/orders/route.ts
index 7722e56..9028a51 100644
--- a/src/app/api/admin/orders/route.ts
+++ b/src/app/api/admin/orders/route.ts
@@ -1,7 +1,24 @@
import { NextResponse } from 'next/server';
+import { cookies } from 'next/headers';
+import { jwtVerify } from 'jose';
import { prisma } from '@/lib/prisma';
+async function verifyAdmin() {
+ const cookieStore = await cookies();
+ const token = cookieStore.get('admin_token')?.value;
+ if (!token) return false;
+ const jwtSecret = process.env.ADMIN_JWT_SECRET;
+ if (!jwtSecret) return false;
+ try {
+ await jwtVerify(token, new TextEncoder().encode(jwtSecret));
+ return true;
+ } catch {
+ return false;
+ }
+}
+
export async function GET(request: Request) {
+ if (!(await verifyAdmin())) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const { searchParams } = new URL(request.url);
const limit = Math.min(parseInt(searchParams.get('limit') ?? '50', 10), 200);
@@ -27,6 +44,7 @@ export async function GET(request: Request) {
customerPostalCode: o.customerPostalCode ?? '',
persona: o.persona ?? '',
color: o.color ?? '',
+ priceItems: o.priceItems ?? '',
},
}));
diff --git a/src/app/api/admin/sync-orders/route.ts b/src/app/api/admin/sync-orders/route.ts
new file mode 100644
index 0000000..c69eea8
--- /dev/null
+++ b/src/app/api/admin/sync-orders/route.ts
@@ -0,0 +1,66 @@
+import { NextResponse } from 'next/server';
+import { cookies } from 'next/headers';
+import { jwtVerify } from 'jose';
+import Stripe from 'stripe';
+import { prisma } from '@/lib/prisma';
+
+const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ apiVersion: '2026-03-25.dahlia' as any,
+});
+
+async function verifyAdmin() {
+ const cookieStore = await cookies();
+ const token = cookieStore.get('admin_token')?.value;
+ if (!token) return false;
+ const jwtSecret = process.env.ADMIN_JWT_SECRET;
+ if (!jwtSecret) return false;
+ try {
+ await jwtVerify(token, new TextEncoder().encode(jwtSecret));
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+// POST /api/admin/sync-orders/
+// Fetches recent PaymentIntents from Stripe and upserts them into the DB
+export async function POST() {
+ if (!(await verifyAdmin())) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+
+ try {
+ const list = await stripe.paymentIntents.list({ limit: 100 });
+ let synced = 0;
+
+ for (const pi of list.data) {
+ if (pi.status !== 'succeeded' && pi.status !== 'canceled') continue;
+ const m = pi.metadata ?? {};
+ const data = {
+ amount: pi.amount,
+ currency: pi.currency,
+ status: pi.status,
+ customerName: m.customerName ?? null,
+ customerEmail: m.customerEmail ?? pi.receipt_email ?? null,
+ customerPhone: m.customerPhone ?? null,
+ customerAddress: m.customerAddress ?? null,
+ customerCity: m.customerCity ?? null,
+ customerCountry: m.customerCountry ?? null,
+ customerPostalCode: m.customerPostalCode ?? null,
+ persona: m.persona ?? null,
+ color: m.color ?? null,
+ priceItems: m.priceItems ?? null,
+ };
+ await prisma.order.upsert({
+ where: { paymentIntentId: pi.id },
+ create: { paymentIntentId: pi.id, ...data },
+ update: data,
+ });
+ synced++;
+ }
+
+ return NextResponse.json({ synced });
+ } catch (err) {
+ const message = err instanceof Error ? err.message : 'Sync failed';
+ return NextResponse.json({ error: message }, { status: 500 });
+ }
+}
diff --git a/src/app/api/orders/save/route.ts b/src/app/api/orders/save/route.ts
new file mode 100644
index 0000000..4eaa375
--- /dev/null
+++ b/src/app/api/orders/save/route.ts
@@ -0,0 +1,60 @@
+import { NextResponse } from 'next/server';
+import Stripe from 'stripe';
+import { prisma } from '@/lib/prisma';
+
+const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ apiVersion: '2026-03-25.dahlia' as any,
+});
+
+export async function POST(request: Request) {
+ let body: Record
;
+ try {
+ body = await request.json();
+ } catch {
+ return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 });
+ }
+
+ const { paymentIntentId } = body;
+ if (!paymentIntentId || typeof paymentIntentId !== 'string') {
+ return NextResponse.json({ error: 'Missing paymentIntentId' }, { status: 400 });
+ }
+
+ // Verify with Stripe that this PaymentIntent actually succeeded — prevents spoofing
+ let pi: Stripe.PaymentIntent;
+ try {
+ pi = await stripe.paymentIntents.retrieve(paymentIntentId);
+ } catch {
+ return NextResponse.json({ error: 'Invalid paymentIntentId' }, { status: 400 });
+ }
+
+ if (pi.status !== 'succeeded') {
+ return NextResponse.json({ error: `Payment not succeeded (status: ${pi.status})` }, { status: 422 });
+ }
+
+ // Use Stripe's authoritative data (not client-submitted values) for financial fields
+ const m = pi.metadata ?? {};
+ const data = {
+ amount: pi.amount,
+ currency: pi.currency,
+ status: pi.status,
+ customerName: (body.customerName as string | null) ?? m.customerName ?? null,
+ customerEmail: (body.customerEmail as string | null) ?? m.customerEmail ?? null,
+ customerPhone: (body.customerPhone as string | null) ?? m.customerPhone ?? null,
+ customerAddress: (body.customerAddress as string | null) ?? m.customerAddress ?? null,
+ customerCity: (body.customerCity as string | null) ?? m.customerCity ?? null,
+ customerCountry: (body.customerCountry as string | null) ?? m.customerCountry ?? null,
+ customerPostalCode: (body.customerPostalCode as string | null) ?? m.customerPostalCode ?? null,
+ persona: (body.persona as string | null) ?? m.persona ?? null,
+ color: (body.color as string | null) ?? m.color ?? null,
+ priceItems: (body.priceItems as string | null) ?? m.priceItems ?? null,
+ };
+
+ await prisma.order.upsert({
+ where: { paymentIntentId },
+ create: { paymentIntentId, ...data },
+ update: data,
+ });
+
+ return NextResponse.json({ saved: true });
+}
diff --git a/src/components/PricingEngine.tsx b/src/components/PricingEngine.tsx
index dcd7c86..63709af 100644
--- a/src/components/PricingEngine.tsx
+++ b/src/components/PricingEngine.tsx
@@ -32,10 +32,16 @@ export function PricingEngine() {
const handleProceed = () => {
const store = orderStore.getState();
+ const lineItems: { label: string; price: number }[] = [
+ { label: 'G1 Robot Base', price: basePrice },
+ ...(personaPrice > 0 ? [{ label: personaLabel, price: personaPrice }] : []),
+ ...(colorPrice > 0 ? [{ label: 'Custom Color', price: colorPrice }] : []),
+ ];
store.setOrderTotal(total);
store.setConfigSummary(
persona === 'none' ? 'Default' : personaLabel,
- primaryColor
+ primaryColor,
+ lineItems,
);
store.setStep('shipping');
};
diff --git a/src/components/checkout/ReviewStep.tsx b/src/components/checkout/ReviewStep.tsx
index 2a2ca8b..023b7d2 100644
--- a/src/components/checkout/ReviewStep.tsx
+++ b/src/components/checkout/ReviewStep.tsx
@@ -46,8 +46,43 @@ export function ReviewStep() {
return;
}
- // Payment succeeded — upload snapshot
+ // Payment succeeded — save order to DB and upload snapshot
const paymentIntentId = orderStore.getState().payment.paymentIntentId;
+ const s = orderStore.getState().shipping;
+ const configSummary = orderStore.getState().personaSummary;
+ const colorVal = orderStore.getState().colorSummary;
+ const priceItems = orderStore.getState().priceItems;
+ const total = orderStore.getState().orderTotal;
+
+ // Retrieve the resolved PaymentIntent to get final status + amount
+ const pi = await stripe.retrievePaymentIntent(clientSecret);
+ const piStatus = pi.paymentIntent?.status ?? 'succeeded';
+ const piAmount = pi.paymentIntent?.amount ?? Math.round(total * 100);
+
+ // Save order to DB directly (covers local dev where webhook can't reach localhost)
+ if (paymentIntentId) {
+ fetch('/api/orders/save/', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ paymentIntentId,
+ amount: piAmount,
+ currency: 'aed',
+ status: piStatus,
+ customerName: s.name,
+ customerEmail: s.email,
+ customerPhone: s.phone,
+ customerAddress: s.address,
+ customerCity: s.city,
+ customerCountry: s.country,
+ customerPostalCode: s.postalCode,
+ persona: configSummary,
+ color: colorVal,
+ priceItems: JSON.stringify(priceItems),
+ }),
+ }).catch(() => {});
+ }
+
if (snapshotDataUrl && paymentIntentId) {
fetch('/api/snapshots/', {
method: 'POST',
diff --git a/src/store/useOrderStore.ts b/src/store/useOrderStore.ts
index 21f8377..f2cce86 100644
--- a/src/store/useOrderStore.ts
+++ b/src/store/useOrderStore.ts
@@ -20,6 +20,11 @@ export interface PaymentInfo {
errorMessage: string;
}
+export interface PriceLineItem {
+ label: string;
+ price: number;
+}
+
export interface OrderState {
step: CheckoutStep;
shipping: ShippingInfo;
@@ -28,6 +33,7 @@ export interface OrderState {
orderTotal: number;
personaSummary: string;
colorSummary: string;
+ priceItems: PriceLineItem[];
}
export interface OrderActions {
@@ -35,7 +41,7 @@ export interface OrderActions {
setShipping: (shipping: ShippingInfo) => void;
setPayment: (payment: Partial) => void;
setOrderTotal: (total: number) => void;
- setConfigSummary: (persona: string, color: string) => void;
+ setConfigSummary: (persona: string, color: string, priceItems?: PriceLineItem[]) => void;
createPaymentIntent: () => Promise;
placeOrder: () => void;
resetOrder: () => void;
@@ -68,6 +74,7 @@ const defaultState: OrderState = {
orderTotal: 0,
personaSummary: '',
colorSummary: '',
+ priceItems: [],
};
function generateOrderId(): string {
@@ -89,13 +96,14 @@ export const orderStore = createStore((set) => ({
setOrderTotal: (total: number) => set({ orderTotal: total }),
- setConfigSummary: (persona: string, color: string) => set({
+ setConfigSummary: (persona: string, color: string, priceItems: PriceLineItem[] = []) => set({
personaSummary: persona,
colorSummary: color,
+ priceItems,
}),
createPaymentIntent: async (): Promise => {
- const { orderTotal, personaSummary, colorSummary, shipping } = orderStore.getState();
+ const { orderTotal, personaSummary, colorSummary, shipping, priceItems } = orderStore.getState();
try {
const res: Response = await fetch('/api/create-payment-intent/', {
method: 'POST',
@@ -107,6 +115,7 @@ export const orderStore = createStore((set) => ({
metadata: {
persona: personaSummary,
color: colorSummary,
+ priceItems: JSON.stringify(priceItems),
customerName: shipping.name,
customerEmail: shipping.email,
customerPhone: shipping.phone,