fix(webhook): handle Saleor legacy webhook payload format with snake_case fields
Some checks failed
Build and Deploy / build (push) Has been cancelled
Some checks failed
Build and Deploy / build (push) Has been cancelled
Saleor sends webhook payloads as arrays with snake_case fields: - user_email instead of userEmail - billing_address instead of billingAddress - total_gross_amount instead of total.gross.amount - etc. Added convertPayloadToOrder() function to transform snake_case payload to camelCase format expected by our email templates.
This commit is contained in:
@@ -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<string, string>;
|
||||
}
|
||||
|
||||
// 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<string, string> = {
|
||||
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 });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user