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-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 {
|
interface SaleorLineItem {
|
||||||
id: string;
|
id: string;
|
||||||
productName: string;
|
productName: string;
|
||||||
variantName?: string;
|
variantName?: string;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
quantityUnit?: string;
|
|
||||||
totalPrice: {
|
totalPrice: {
|
||||||
gross: {
|
gross: {
|
||||||
amount: number;
|
amount: number;
|
||||||
@@ -84,6 +121,66 @@ const LANGUAGE_CODE_MAP: Record<string, string> = {
|
|||||||
FR: "fr",
|
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 {
|
function getCustomerLanguage(order: SaleorOrder): string {
|
||||||
if (order.languageCode && LANGUAGE_CODE_MAP[order.languageCode]) {
|
if (order.languageCode && LANGUAGE_CODE_MAP[order.languageCode]) {
|
||||||
return 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("Headers:", { event, domain, apiUrl, hasSignature: !!signature });
|
||||||
console.log("Payload:", JSON.stringify(body).substring(0, 500));
|
console.log("Payload:", JSON.stringify(body).substring(0, 500));
|
||||||
|
|
||||||
// Handle Saleor subscription payload format (array of events)
|
// Handle Saleor legacy webhook payload format (array with snake_case fields)
|
||||||
let order = null;
|
let orderPayload: SaleorOrderPayload | null = null;
|
||||||
if (Array.isArray(body)) {
|
if (Array.isArray(body) && body.length > 0) {
|
||||||
// Find the order in the array
|
// Legacy format: array with order objects directly
|
||||||
const orderEvent = body.find((item: any) => item.order);
|
orderPayload = body[0] as SaleorOrderPayload;
|
||||||
if (orderEvent) {
|
} else if (body.data && Array.isArray(body.data)) {
|
||||||
order = orderEvent.order;
|
// Subscription format: { data: [...] }
|
||||||
}
|
orderPayload = body.data[0] as SaleorOrderPayload;
|
||||||
} else if (body.order) {
|
|
||||||
order = body.order;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (order) {
|
if (!orderPayload) {
|
||||||
console.log("Order ID:", order.id);
|
console.error("No order found in webhook payload");
|
||||||
console.log("Order number:", order.number);
|
return NextResponse.json({ error: "No order in payload" }, { status: 400 });
|
||||||
console.log("User email:", order.userEmail);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("Order ID:", orderPayload.id);
|
||||||
|
console.log("Order number:", orderPayload.number);
|
||||||
|
console.log("User email:", orderPayload.user_email);
|
||||||
|
|
||||||
if (!event) {
|
if (!event) {
|
||||||
return NextResponse.json({ error: "Missing saleor-event header" }, { status: 400 });
|
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" });
|
return NextResponse.json({ success: true, message: "Event not supported" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!order) {
|
// Convert snake_case payload to camelCase
|
||||||
console.error("No order found in webhook payload");
|
const order = convertPayloadToOrder(orderPayload);
|
||||||
return NextResponse.json({ error: "No order in payload" }, { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
await handleSaleorWebhook(normalizedEvent, { order });
|
await handleSaleorWebhook(normalizedEvent, { order });
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user