diff --git a/src/app/api/webhooks/saleor/route.ts b/src/app/api/webhooks/saleor/route.ts index 7e1e138..189b272 100644 --- a/src/app/api/webhooks/saleor/route.ts +++ b/src/app/api/webhooks/saleor/route.ts @@ -16,12 +16,49 @@ interface SaleorWebhookHeaders { "saleor-api-url": string; } +// Saleor sends snake_case in webhook payloads +interface SaleorLineItemPayload { + id: string; + product_name: string; + variant_name?: string; + quantity: number; + total_price_gross_amount: string; + currency: string; +} + +interface SaleorAddressPayload { + first_name?: string; + last_name?: string; + street_address_1?: string; + street_address_2?: string; + city?: string; + postal_code?: string; + country?: string; + phone?: string; +} + +interface SaleorOrderPayload { + id: string; + number: number; + user_email: string; + first_name?: string; + last_name?: string; + billing_address?: SaleorAddressPayload; + shipping_address?: SaleorAddressPayload; + lines: SaleorLineItemPayload[]; + total_gross_amount: string; + shipping_price_gross_amount?: string; + currency: string; + language_code?: string; + metadata?: Record; +} + +// Internal camelCase interfaces for our code interface SaleorLineItem { id: string; productName: string; variantName?: string; quantity: number; - quantityUnit?: string; totalPrice: { gross: { amount: number; @@ -84,6 +121,66 @@ const LANGUAGE_CODE_MAP: Record = { FR: "fr", }; +// Convert Saleor snake_case payload to camelCase +function convertPayloadToOrder(payload: SaleorOrderPayload): SaleorOrder { + return { + id: payload.id, + number: String(payload.number), + userEmail: payload.user_email, + user: payload.first_name || payload.last_name ? { + firstName: payload.first_name, + lastName: payload.last_name, + email: payload.user_email, + } : undefined, + billingAddress: payload.billing_address ? { + firstName: payload.billing_address.first_name, + lastName: payload.billing_address.last_name, + streetAddress1: payload.billing_address.street_address_1, + streetAddress2: payload.billing_address.street_address_2, + city: payload.billing_address.city, + postalCode: payload.billing_address.postal_code, + country: payload.billing_address.country, + phone: payload.billing_address.phone, + } : undefined, + shippingAddress: payload.shipping_address ? { + firstName: payload.shipping_address.first_name, + lastName: payload.shipping_address.last_name, + streetAddress1: payload.shipping_address.street_address_1, + streetAddress2: payload.shipping_address.street_address_2, + city: payload.shipping_address.city, + postalCode: payload.shipping_address.postal_code, + country: payload.shipping_address.country, + phone: payload.shipping_address.phone, + } : undefined, + lines: payload.lines.map((line) => ({ + id: line.id, + productName: line.product_name, + variantName: line.variant_name, + quantity: line.quantity, + totalPrice: { + gross: { + amount: parseInt(line.total_price_gross_amount), + currency: line.currency, + }, + }, + })), + total: { + gross: { + amount: parseInt(payload.total_gross_amount), + currency: payload.currency, + }, + }, + shippingPrice: payload.shipping_price_gross_amount ? { + gross: { + amount: parseInt(payload.shipping_price_gross_amount), + currency: payload.currency, + }, + } : undefined, + languageCode: payload.language_code?.toUpperCase(), + metadata: payload.metadata ? Object.entries(payload.metadata).map(([key, value]) => ({ key, value })) : undefined, + }; +} + function getCustomerLanguage(order: SaleorOrder): string { if (order.languageCode && LANGUAGE_CODE_MAP[order.languageCode]) { return LANGUAGE_CODE_MAP[order.languageCode]; @@ -402,23 +499,24 @@ export async function POST(request: NextRequest) { console.log("Headers:", { event, domain, apiUrl, hasSignature: !!signature }); console.log("Payload:", JSON.stringify(body).substring(0, 500)); - // Handle Saleor subscription payload format (array of events) - let order = null; - if (Array.isArray(body)) { - // Find the order in the array - const orderEvent = body.find((item: any) => item.order); - if (orderEvent) { - order = orderEvent.order; - } - } else if (body.order) { - order = body.order; + // Handle Saleor legacy webhook payload format (array with snake_case fields) + let orderPayload: SaleorOrderPayload | null = null; + if (Array.isArray(body) && body.length > 0) { + // Legacy format: array with order objects directly + orderPayload = body[0] as SaleorOrderPayload; + } else if (body.data && Array.isArray(body.data)) { + // Subscription format: { data: [...] } + orderPayload = body.data[0] as SaleorOrderPayload; } - if (order) { - console.log("Order ID:", order.id); - console.log("Order number:", order.number); - console.log("User email:", order.userEmail); + if (!orderPayload) { + console.error("No order found in webhook payload"); + return NextResponse.json({ error: "No order in payload" }, { status: 400 }); } + + console.log("Order ID:", orderPayload.id); + console.log("Order number:", orderPayload.number); + console.log("User email:", orderPayload.user_email); if (!event) { return NextResponse.json({ error: "Missing saleor-event header" }, { status: 400 }); @@ -432,10 +530,8 @@ export async function POST(request: NextRequest) { return NextResponse.json({ success: true, message: "Event not supported" }); } - if (!order) { - console.error("No order found in webhook payload"); - return NextResponse.json({ error: "No order in payload" }, { status: 400 }); - } + // Convert snake_case payload to camelCase + const order = convertPayloadToOrder(orderPayload); await handleSaleorWebhook(normalizedEvent, { order });