mirror of
https://github.com/openshiporg/openship.git
synced 2026-06-19 07:35:55 +00:00
fix(openfront): update webhook handler to match new OpenFront payload structure
Updated the webhook handler to align with OpenFront's latest API response format: - Changed field mappings from `orderLineItems` to `lineItems` - Updated price fields from `unitPrice` to `moneyAmount.amount` - Fixed address structure to use `shippingAddress` object - Added fallback handling for missing fields and case-insensitive headers - Improved logging with detailed payload inspection These changes ensure proper order processing with the updated OpenFront webhook format.
This commit is contained in:
+59
-125
@@ -66,6 +66,7 @@ async function searchProductsFunction({
|
||||
image {
|
||||
url
|
||||
}
|
||||
imagePath
|
||||
}
|
||||
productVariants {
|
||||
id
|
||||
@@ -116,7 +117,7 @@ async function searchProductsFunction({
|
||||
const firstPrice = variant.prices[0];
|
||||
const firstImage = product.productImages[0];
|
||||
return {
|
||||
image: firstImage?.image?.url || null,
|
||||
image: getProductImageUrl(firstImage, platform.domain),
|
||||
title: `${product.title} - ${variant.title}`,
|
||||
productId: product.id,
|
||||
variantId: variant.id,
|
||||
@@ -158,6 +159,7 @@ async function getProductFunction({
|
||||
image {
|
||||
url
|
||||
}
|
||||
imagePath
|
||||
}
|
||||
productVariants(where: $variantId ? { id: { equals: $variantId } } : {}) {
|
||||
id
|
||||
@@ -190,7 +192,7 @@ async function getProductFunction({
|
||||
const firstPrice = variant.prices[0];
|
||||
const firstImage = product.productImages[0];
|
||||
const transformedProduct = {
|
||||
image: firstImage?.image?.url || null,
|
||||
image: getProductImageUrl(firstImage, platform.domain),
|
||||
title: `${product.title} - ${variant.title}`,
|
||||
productId: product.id,
|
||||
variantId: variant.id,
|
||||
@@ -205,8 +207,7 @@ async function getProductFunction({
|
||||
async function createPurchaseFunction({
|
||||
platform,
|
||||
cartItems,
|
||||
shipping,
|
||||
notes
|
||||
shipping
|
||||
}) {
|
||||
console.log(`\u{1F6D2} OpenFront Channel: Creating purchase with ${cartItems.length} items`);
|
||||
console.log(`\u{1F69A} OpenFront Channel: Ship to: ${shipping?.firstName} ${shipping?.lastName}`);
|
||||
@@ -506,70 +507,55 @@ async function oAuthCallbackFunction({
|
||||
function scopes() {
|
||||
return REQUIRED_SCOPES;
|
||||
}
|
||||
var import_graphql_request, getFreshAccessToken, createOpenFrontClient, REQUIRED_SCOPES;
|
||||
var import_graphql_request, getFreshAccessToken, createOpenFrontClient, getProductImageUrl, REQUIRED_SCOPES;
|
||||
var init_openfront = __esm({
|
||||
"features/integrations/channel/openfront.ts"() {
|
||||
"use strict";
|
||||
import_graphql_request = require("graphql-request");
|
||||
getFreshAccessToken = async (platform) => {
|
||||
const tokenCheckUrl = `${platform.domain}/api/oauth/check-token`;
|
||||
try {
|
||||
const checkResponse = await fetch(tokenCheckUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
client_id: platform.appKey,
|
||||
client_secret: platform.appSecret
|
||||
})
|
||||
});
|
||||
if (checkResponse.ok) {
|
||||
const { access_token, is_valid } = await checkResponse.json();
|
||||
if (is_valid) {
|
||||
return access_token;
|
||||
}
|
||||
if (platform.tokenExpiresAt && platform.refreshToken) {
|
||||
const expiresAt = typeof platform.tokenExpiresAt === "string" ? new Date(platform.tokenExpiresAt) : platform.tokenExpiresAt;
|
||||
if (expiresAt > /* @__PURE__ */ new Date()) {
|
||||
return platform.accessToken;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Token check failed, will refresh:", error);
|
||||
const tokenUrl = `${platform.domain}/api/oauth/token`;
|
||||
const formData = new URLSearchParams({
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: platform.refreshToken
|
||||
});
|
||||
const response = await fetch(tokenUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: formData
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error("Token refresh failed:", errorText);
|
||||
throw new Error(`Failed to refresh access token: ${response.statusText} - ${errorText}`);
|
||||
}
|
||||
const { access_token } = await response.json();
|
||||
return access_token;
|
||||
}
|
||||
const tokenUrl = `${platform.domain}/api/oauth/token`;
|
||||
console.log("\u{1F534} Attempting to refresh token:");
|
||||
console.log("\u{1F534} Token URL:", tokenUrl);
|
||||
console.log("\u{1F534} Client ID:", platform.appKey);
|
||||
console.log("\u{1F534} Client Secret length:", platform.appSecret?.length);
|
||||
console.log("\u{1F534} Refresh Token (first 10 chars):", platform.accessToken?.substring(0, 10));
|
||||
const formData = new URLSearchParams({
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: platform.accessToken
|
||||
// This is actually the refresh token stored in accessToken field
|
||||
});
|
||||
const response = await fetch(tokenUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: formData
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error("\u{1F534} Token refresh failed:", errorText);
|
||||
console.error("\u{1F534} Response status:", response.status);
|
||||
throw new Error(`Failed to refresh access token: ${response.statusText} - ${errorText}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
console.log("\u{1F7E2} Token refreshed successfully");
|
||||
return data.access_token;
|
||||
return platform.accessToken;
|
||||
};
|
||||
createOpenFrontClient = async (platform) => {
|
||||
const accessToken = platform.appKey && platform.appSecret ? await getFreshAccessToken(platform) : platform.accessToken;
|
||||
const freshAccessToken = await getFreshAccessToken(platform);
|
||||
return new import_graphql_request.GraphQLClient(
|
||||
`${platform.domain}/api/graphql`,
|
||||
// Fixed: removed hardcoded https://
|
||||
{
|
||||
headers: {
|
||||
"Authorization": `Bearer ${accessToken}`,
|
||||
"Authorization": `Bearer ${freshAccessToken}`,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
getProductImageUrl = (productImage, domain) => {
|
||||
if (productImage?.imagePath) {
|
||||
return `${domain}${productImage.imagePath}`;
|
||||
}
|
||||
return productImage?.image?.url || null;
|
||||
};
|
||||
REQUIRED_SCOPES = "read_products,write_products,read_orders,write_orders,read_fulfillments,write_fulfillments,read_webhooks,write_webhooks";
|
||||
}
|
||||
});
|
||||
@@ -1262,6 +1248,7 @@ async function searchProductsFunction3({
|
||||
image {
|
||||
url
|
||||
}
|
||||
imagePath
|
||||
}
|
||||
productVariants {
|
||||
id
|
||||
@@ -1317,7 +1304,7 @@ async function searchProductsFunction3({
|
||||
const firstPrice = variant.prices[0];
|
||||
const firstImage = product.productImages[0];
|
||||
return {
|
||||
image: firstImage?.image?.url || null,
|
||||
image: getProductImageUrl2(firstImage, platform.domain),
|
||||
title: `${product.title} - ${variant.title}`,
|
||||
productId: product.id,
|
||||
variantId: variant.id,
|
||||
@@ -1361,6 +1348,7 @@ async function getProductFunction3({
|
||||
image {
|
||||
url
|
||||
}
|
||||
imagePath
|
||||
}
|
||||
productVariants(where: { id: { equals: $variantId } }) {
|
||||
id
|
||||
@@ -1391,6 +1379,7 @@ async function getProductFunction3({
|
||||
image {
|
||||
url
|
||||
}
|
||||
imagePath
|
||||
}
|
||||
productVariants {
|
||||
id
|
||||
@@ -1424,7 +1413,7 @@ async function getProductFunction3({
|
||||
const firstPrice = variant.prices[0];
|
||||
const firstImage = product.productImages[0];
|
||||
const transformedProduct = {
|
||||
image: firstImage?.image?.url || null,
|
||||
image: getProductImageUrl2(firstImage, platform.domain),
|
||||
title: `${product.title} - ${variant.title}`,
|
||||
productId: product.id,
|
||||
variantId: variant.id,
|
||||
@@ -1546,7 +1535,7 @@ async function searchOrdersFunction({
|
||||
lineItemId: lineItem.id,
|
||||
name: lineItem.title,
|
||||
quantity: lineItem.quantity,
|
||||
image: lineItem.thumbnail || lineItem.productVariant?.product?.thumbnail || "",
|
||||
image: getProductImageUrl2({ imagePath: lineItem.thumbnail, image: { url: lineItem.productVariant?.product?.thumbnail } }, platform.domain) || "",
|
||||
price: lineItem.moneyAmount ? (lineItem.moneyAmount.amount / 100).toFixed(2) : "0.00",
|
||||
variantId: lineItem.productVariant?.id || "",
|
||||
productId: lineItem.productVariant?.product?.id || "",
|
||||
@@ -1687,18 +1676,12 @@ async function oAuthFunction3({
|
||||
platform,
|
||||
callbackUrl
|
||||
}) {
|
||||
console.log("\u{1F534} oAuthFunction START");
|
||||
console.log("\u{1F534} Platform domain:", platform.domain);
|
||||
console.log("\u{1F534} Platform appKey:", platform.appKey);
|
||||
console.log("\u{1F534} Callback URL:", callbackUrl);
|
||||
if (!platform.appKey) {
|
||||
throw new Error("OpenFront OAuth requires appKey in platform configuration");
|
||||
}
|
||||
const scopes5 = "read_products,write_products,read_orders,write_orders,read_customers,write_customers,read_webhooks,write_webhooks";
|
||||
const state = platform.state || Math.random().toString(36).substring(7);
|
||||
const openFrontAuthUrl = `${platform.domain}/dashboard/platform/apps?install=true&client_id=${platform.appKey}&scope=${encodeURIComponent(scopes5)}&redirect_uri=${encodeURIComponent(callbackUrl)}&state=${state}&response_type=code`;
|
||||
console.log("\u{1F534} Generated authUrl:", openFrontAuthUrl);
|
||||
console.log("\u{1F534} Returning object:", { authUrl: openFrontAuthUrl });
|
||||
return { authUrl: openFrontAuthUrl };
|
||||
}
|
||||
async function oAuthCallbackFunction3({
|
||||
@@ -1714,13 +1697,6 @@ async function oAuthCallbackFunction3({
|
||||
const tokenUrl = `${domain}/api/oauth/token`;
|
||||
const clientId = appKey || platform.appKey;
|
||||
const clientSecret = appSecret || platform.appSecret;
|
||||
console.log("\u{1F535} OPENSHIP TOKEN EXCHANGE:");
|
||||
console.log("\u{1F535} Domain:", domain);
|
||||
console.log("\u{1F535} TokenUrl:", tokenUrl);
|
||||
console.log("\u{1F535} ClientId:", clientId);
|
||||
console.log("\u{1F535} ClientSecret:", clientSecret);
|
||||
console.log("\u{1F535} Code:", code);
|
||||
console.log("\u{1F535} RedirectUri:", redirectUri);
|
||||
if (!clientId || !clientSecret) {
|
||||
throw new Error("OpenFront OAuth requires appKey and appSecret in platform configuration or as parameters");
|
||||
}
|
||||
@@ -1760,10 +1736,10 @@ async function createOrderWebhookHandler({
|
||||
}
|
||||
const lineItemsOutput = event.data?.orderLineItems?.map((item) => ({
|
||||
name: item.title,
|
||||
image: item.productVariant?.product?.productImages?.[0]?.image?.url || null,
|
||||
price: item.unitPrice / 100,
|
||||
// Convert from cents
|
||||
quantity: item.quantity,
|
||||
image: getProductImageUrl2(item.productVariant?.product?.productImages?.[0], platform.domain),
|
||||
price: item.unitPrice ? item.unitPrice / 100 : 0,
|
||||
// Convert from cents, handle null/undefined
|
||||
quantity: item.quantity || 0,
|
||||
productId: item.productVariant?.product?.id,
|
||||
variantId: item.productVariant?.id,
|
||||
sku: item.productVariant?.sku || "",
|
||||
@@ -1784,11 +1760,11 @@ async function createOrderWebhookHandler({
|
||||
country: orderData.countryCode,
|
||||
phone: orderData.phone,
|
||||
currency: orderData.currency?.code || "USD",
|
||||
totalPrice: orderData.total / 100,
|
||||
// Convert from cents
|
||||
subTotalPrice: (orderData.subtotal || orderData.total) / 100,
|
||||
totalDiscounts: (orderData.totalDiscounts || 0) / 100,
|
||||
totalTax: (orderData.totalTax || 0) / 100,
|
||||
totalPrice: orderData.total ? orderData.total / 100 : 0,
|
||||
// Convert from cents, handle null/undefined
|
||||
subTotalPrice: orderData.subtotal || orderData.total ? (orderData.subtotal || orderData.total) / 100 : 0,
|
||||
totalDiscounts: orderData.totalDiscounts ? orderData.totalDiscounts / 100 : 0,
|
||||
totalTax: orderData.totalTax ? orderData.totalTax / 100 : 0,
|
||||
status: "INPROCESS",
|
||||
linkOrder: true,
|
||||
matchOrder: true,
|
||||
@@ -1846,7 +1822,7 @@ async function addTrackingFunction2({
|
||||
});
|
||||
return result;
|
||||
}
|
||||
var import_graphql_request3, import_getBaseUrl, getFreshAccessToken2, createOpenFrontClient2, REQUIRED_SCOPES3;
|
||||
var import_graphql_request3, import_getBaseUrl, getFreshAccessToken2, createOpenFrontClient2, getProductImageUrl2, REQUIRED_SCOPES3;
|
||||
var init_openfront2 = __esm({
|
||||
"features/integrations/shop/openfront.ts"() {
|
||||
"use strict";
|
||||
@@ -1856,10 +1832,8 @@ var init_openfront2 = __esm({
|
||||
if (platform.tokenExpiresAt && platform.refreshToken) {
|
||||
const expiresAt = typeof platform.tokenExpiresAt === "string" ? new Date(platform.tokenExpiresAt) : platform.tokenExpiresAt;
|
||||
if (expiresAt > /* @__PURE__ */ new Date()) {
|
||||
console.log("\u{1F7E2} Using cached access token (not expired)");
|
||||
return platform.accessToken;
|
||||
}
|
||||
console.log("\u{1F7E1} Access token expired, refreshing with refresh token");
|
||||
const tokenUrl = `${platform.domain}/api/oauth/token`;
|
||||
const formData = new URLSearchParams({
|
||||
grant_type: "refresh_token",
|
||||
@@ -1872,54 +1846,12 @@ var init_openfront2 = __esm({
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error("\u{1F534} Token refresh failed:", errorText);
|
||||
throw new Error(`Failed to refresh access token: ${response.statusText} - ${errorText}`);
|
||||
}
|
||||
const { access_token } = await response.json();
|
||||
console.log("\u{1F7E2} Token refreshed successfully");
|
||||
return access_token;
|
||||
}
|
||||
if (platform.appKey && platform.appSecret) {
|
||||
console.log("\u{1F7E1} Using legacy token refresh flow");
|
||||
const tokenCheckUrl = `${platform.domain}/api/oauth/check-token`;
|
||||
try {
|
||||
const checkResponse = await fetch(tokenCheckUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
client_id: platform.appKey,
|
||||
client_secret: platform.appSecret
|
||||
})
|
||||
});
|
||||
if (checkResponse.ok) {
|
||||
const { access_token: access_token2, is_valid } = await checkResponse.json();
|
||||
if (is_valid) {
|
||||
return access_token2;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Token check failed, will refresh:", error);
|
||||
}
|
||||
const tokenUrl = `${platform.domain}/api/oauth/token`;
|
||||
const formData = new URLSearchParams({
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: platform.accessToken
|
||||
// Legacy: accessToken field contains refresh token
|
||||
});
|
||||
const response = await fetch(tokenUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: formData
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error("\u{1F534} Legacy token refresh failed:", errorText);
|
||||
console.error("Token refresh failed:", errorText);
|
||||
throw new Error(`Failed to refresh access token: ${response.statusText} - ${errorText}`);
|
||||
}
|
||||
const { access_token } = await response.json();
|
||||
return access_token;
|
||||
}
|
||||
console.log("\u{1F7E0} No refresh token available, using access token as-is");
|
||||
return platform.accessToken;
|
||||
};
|
||||
createOpenFrontClient2 = async (platform) => {
|
||||
@@ -1934,6 +1866,12 @@ var init_openfront2 = __esm({
|
||||
}
|
||||
);
|
||||
};
|
||||
getProductImageUrl2 = (productImage, domain) => {
|
||||
if (productImage?.imagePath) {
|
||||
return `${domain}${productImage.imagePath}`;
|
||||
}
|
||||
return productImage?.image?.url || null;
|
||||
};
|
||||
REQUIRED_SCOPES3 = "read_products,write_products,read_orders,write_orders,read_customers,write_customers,read_webhooks,write_webhooks";
|
||||
}
|
||||
});
|
||||
@@ -5057,8 +4995,6 @@ async function getShopWebhooks3(root, { shopId }, context) {
|
||||
where: { id: shopId },
|
||||
query: "id domain accessToken platform { id getWebhooksFunction }"
|
||||
});
|
||||
console.log("\u{1F534} getShopWebhooks - shopId:", shopId);
|
||||
console.log("\u{1F534} getShopWebhooks - shop result:", shop);
|
||||
if (!shop) {
|
||||
throw new Error("Shop not found");
|
||||
}
|
||||
@@ -5116,8 +5052,6 @@ async function searchShopOrders3(root, { shopId, searchEntry, take = 25, skip =
|
||||
}
|
||||
`
|
||||
});
|
||||
console.log("\u{1F534} searchShopOrders - shopId:", shopId);
|
||||
console.log("\u{1F534} searchShopOrders - shop result:", shop);
|
||||
if (!shop) {
|
||||
throw new Error("Shop not found");
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -769,43 +769,46 @@ export async function createOrderWebhookHandler({
|
||||
headers: Record<string, string>;
|
||||
}) {
|
||||
// Verify webhook authenticity using OpenFront's signature
|
||||
const signature = headers["x-openfront-webhook-signature"];
|
||||
const signature = headers["x-openfront-webhook-signature"] || headers["X-OpenFront-Webhook-Signature"];
|
||||
if (!signature) {
|
||||
console.error("Missing webhook signature. Available headers:", Object.keys(headers));
|
||||
throw new Error("Missing webhook signature");
|
||||
}
|
||||
|
||||
// Transform OpenFront order to Openship format
|
||||
const lineItemsOutput = event.data?.orderLineItems?.map((item: any) => ({
|
||||
const lineItemsOutput = event.data?.lineItems?.map((item: any) => ({
|
||||
name: item.title,
|
||||
image: getProductImageUrl(item.productVariant?.product?.productImages?.[0], platform.domain),
|
||||
price: item.unitPrice ? (item.unitPrice / 100) : 0, // Convert from cents, handle null/undefined
|
||||
image: getProductImageUrl(item.productVariant?.product?.productImages?.[0], platform.domain) || item.thumbnail,
|
||||
price: item.moneyAmount?.amount ? (item.moneyAmount.amount / 100) : 0, // Convert from cents to float
|
||||
quantity: item.quantity || 0,
|
||||
productId: item.productVariant?.product?.id,
|
||||
variantId: item.productVariant?.id,
|
||||
sku: item.productVariant?.sku || "",
|
||||
lineItemId: item.id,
|
||||
productId: item.productVariant?.product?.id?.toString(),
|
||||
variantId: item.productVariant?.id?.toString(),
|
||||
sku: item.productVariant?.sku || item.sku || "",
|
||||
lineItemId: item.id?.toString(),
|
||||
})) || [];
|
||||
|
||||
// Return Keystone-ready order data
|
||||
const orderData = event.data;
|
||||
const shippingAddress = orderData.shippingAddress || {};
|
||||
|
||||
return {
|
||||
orderId: orderData.id,
|
||||
orderName: orderData.orderNumber,
|
||||
email: orderData.customerEmail,
|
||||
firstName: orderData.firstName,
|
||||
lastName: orderData.lastName,
|
||||
streetAddress1: orderData.address1,
|
||||
streetAddress2: orderData.address2,
|
||||
city: orderData.city,
|
||||
state: orderData.state,
|
||||
zip: orderData.postalCode,
|
||||
country: orderData.countryCode,
|
||||
phone: orderData.phone,
|
||||
currency: orderData.currency?.code || "USD",
|
||||
totalPrice: orderData.total ? (orderData.total / 100) : 0, // Convert from cents, handle null/undefined
|
||||
subTotalPrice: (orderData.subtotal || orderData.total) ? ((orderData.subtotal || orderData.total) / 100) : 0,
|
||||
totalDiscounts: orderData.totalDiscounts ? (orderData.totalDiscounts / 100) : 0,
|
||||
totalTax: orderData.totalTax ? (orderData.totalTax / 100) : 0,
|
||||
orderId: orderData.id?.toString(),
|
||||
orderName: orderData.displayId ? `#${orderData.displayId}` : "",
|
||||
email: orderData.email || "",
|
||||
firstName: shippingAddress.firstName || "",
|
||||
lastName: shippingAddress.lastName || "",
|
||||
streetAddress1: shippingAddress.address1 || "",
|
||||
streetAddress2: shippingAddress.address2 || "",
|
||||
city: shippingAddress.city || "",
|
||||
state: shippingAddress.province || "",
|
||||
zip: shippingAddress.postalCode || "",
|
||||
country: shippingAddress.country?.iso2?.toUpperCase() || "",
|
||||
phone: shippingAddress.phone || "",
|
||||
currency: orderData.currency?.code?.toUpperCase() || "USD",
|
||||
totalPrice: orderData.rawTotal ? (orderData.rawTotal / 100) : 0, // rawTotal is in cents, convert to float
|
||||
subTotalPrice: parseFloat(orderData.subtotal?.replace(/[$,]/g, '') || '0'), // Parse formatted string to float
|
||||
totalDiscounts: parseFloat(orderData.discount?.replace(/[$,]/g, '') || '0'), // Parse formatted string to float
|
||||
totalTax: parseFloat(orderData.tax?.replace(/[$,]/g, '') || '0'), // Parse formatted string to float
|
||||
status: "INPROCESS",
|
||||
linkOrder: true,
|
||||
matchOrder: true,
|
||||
|
||||
Reference in New Issue
Block a user