2026-04-13 05:23:25 +00:00

1110 lines
35 KiB
JavaScript
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// routes/manageBrands.js
const express = require('express');
const axios = require('axios');
const { v4: uuid } = require('uuid');
const { getToken } = require('../tokenStore');
const { log } = require('../logger');
const crypto = require('crypto');
const router = express.Router();
const seo_llm_client = axios.create({
baseURL: "https://llm.thedomainnest.com", // 👈 change this
headers: {
"Content-Type": "application/json",
},
timeout: 0,
});
// Simple in-memory process tracker
const processes = {};
function slugify(str) {
return str
.toString()
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
}
const GetAllProductsOfBranch = async (brandId, turn14accessToken, shop, procId, productCount) => {
var AllProductsOfBrans = [];
try {
log(shop, `🔍 [${procId}] Fetching products for brand ${brandId}`);
// const res = await fetch(`https://turn14.data4autos.com/v1/items/brandallitems/${brandId}`, {
const res = await fetch(`https://turn14.data4autos.com/v1/items/brandallitemswithfitment/${brandId}`, {
headers: {
Authorization: `Bearer ${turn14accessToken}`,
"Content-Type": "application/json",
},
});
const res_data = await res.json();
const data = res_data.items || [];
const fitmentTags = res_data.fitmentTags || [];
// Ensure we have an array of valid items
const validItems = Array.isArray(data)
? data.filter(item => item && item.id && item.attributes)
: [];
AllProductsOfBrans = validItems;
log(shop, `📦 [${procId}] Found ${AllProductsOfBrans.length} products for brand ${brandId}`);
const items = Array.isArray(AllProductsOfBrans) ? AllProductsOfBrans : [];
log(shop, `📝 [${procId}] Processing ${items.length} sample products`);
return { items, fitmentTags };
} catch (err) {
log(shop, `❌ [${procId}] Error fetching items: ${err.message}`);
return null;
}
}
function extractFirstJsonObject(text) {
if (typeof text !== "string") return text;
// remove code fences if any
let s = text.trim()
.replace(/^```json\s*/i, "")
.replace(/^```\s*/i, "")
.replace(/```$/i, "")
.trim();
// grab first {...} block
const start = s.indexOf("{");
const end = s.lastIndexOf("}");
if (start === -1 || end === -1 || end <= start) return null;
s = s.slice(start, end + 1);
// common LLM JSON mistakes quick-fix:
// 1) seo_description:" -> "seo_description":
s = s.replace(/(\{|,)\s*(seo_title|seo_description)\s*:/g, '$1"$2":');
// 2) "seo_description:" -> "seo_description":
s = s.replace(/"seo_description\s*:\s*/g, '"seo_description":');
return s;
}
const AddProductToStore = async (shop, accessToken, product, procId, fulfillmentServiceId, locationId) => {
var results = [];
const {
product_name,
descriptions,
brand,
category,
subcategory, part_number, price
} = product.attributes;
var seo_product_json_data = {
product_name,
descriptions,
brand,
category,
subcategory, part_number, price
}
const requestBody = {
message: `Use the following product JSON as the only source of truth. Create:
1) seo_title (max 70 characters min 60 characters)
2) seo_description (max 160 characters min 140 characters)
Product JSON:
${JSON.stringify(seo_product_json_data)}`,
mode: "quality",
// system_prompt: `You are an SEO metadata generator for automotive performance parts.
// OUTPUT RULES (STRICT):
// - Output ONLY valid JSON. No markdown. No comments. No extra keys.
// - Output schema exactly: {"seo_title":"","seo_description":""}
// - seo_title MUST be <= 70 characters and >= 60 characters.
// - seo_description MUST be <= 160 characters and >= 140 characters.
// - Use natural, high-intent wording. No keyword stuffing.
// - Must include: Brand + part type + key fitment + finish when available.
// - If space allows, include ONE technical hook from the data.
// - Avoid price, shipping, hype, or compliance claims.
// - Do NOT copy product_name verbatim; rephrase for uniqueness.
// - Title and description must be clearly different.
// FINAL CHECK:
// - JSON parses correctly.
// - Length limits respected.
// - No text outside JSON.`,
system_prompt: `You are an SEO metadata generator for automotive performance parts.
OUTPUT RULES (STRICT):
- Output ONLY valid JSON. No markdown. No comments. No extra keys.
- Output schema exactly: {"seo_title":"","seo_description":""}
- seo_title MUST be <= 70 characters and >= 60 characters.
- seo_description MUST be <= 160 characters and >= 140 characters.
- Use natural, high-intent wording. No keyword stuffing.
- Must include: Brand + part type + key fitment + finish when available.
- If space allows, include ONE technical hook from the data.
- Avoid price, shipping, hype, or compliance claims.
- Do NOT copy product_name verbatim; rephrase for uniqueness.
- Title and description must be clearly different.
ANTI-REPETITION RULES (MANDATORY):
- NEVER start seo_description with any of these phrases (case-insensitive):
"Upgrade your", "Boost your", "Take your", "Enhance your", "Transform your",
"Elevate your", "Improve your", "Unlock", "Experience", "Introducing"
- Do NOT use the phrase "upgrade your" anywhere in the description.
- Do NOT reuse the same opening 3 words across different products in the same session.
- Avoid generic filler like: "wheel game", "next level", "top-notch", "ultimate", "perfect for".
DESCRIPTION OPENING STYLE (MUST CHOOSE ONE PER PRODUCT):
Pick ONE of the following opening patterns and write the description accordingly:
1) Fitment-first: "For [vehicle/fitment], this [part type]..."
2) Spec-first: "[Key size/spec] [part type] from [Brand]..."
3) Feature-first: "Built with [feature], this [part type]..."
4) Finish-first: "[Finish] [part type] that adds..."
5) Use-case-first: "Ideal for [track/street/OE replacement], this [part type]..."
CONSISTENCY & QUALITY:
- Keep grammar clean and professional.
- Keep it product-focused (no marketing fluff).
- If key fitment is missing, omit fitment entirely (do NOT guess).
- If finish is missing, omit finish entirely (do NOT invent).
FINAL CHECK:
- JSON parses correctly.
- Length limits respected.
- No text outside JSON.`
,
session_id: crypto.randomUUID(),
image_base64: null,
file_name: null,
file_base64: null
};
// const seo_data_from_llm = await seo_llm_client.post(`/chat-json`, requestBody);
// const rawReply = seo_data_from_llm.data.reply;
// let parsed = {};
// try {
// const extracted = extractFirstJsonObject(rawReply);
// if (!extracted) throw new Error("No JSON object found in reply");
// parsed = typeof extracted === "string" ? JSON.parse(extracted) : extracted;
// } catch (e) {
// console.error("Failed to parse SEO JSON:", e);
// parsed = { seo_title: "", seo_description: "" }; // fallback
// }
parsed = { seo_title: "", seo_description: "" };
const { seo_title, seo_description } = parsed;
console.log("SEO TITLE FROM LLM -", seo_title);
console.log("SEO DESCRIPTION FROM LLM -", seo_description);
const SHOP = shop;
const ACCESS_TOKEN = accessToken;
const item = product;
const attrs = item.attributes;
const globalUniqueFitmentMap = {
make: new Set(),
model: new Set(),
year: new Set(),
drive: new Set(),
baseModel: new Set()
};
// Loop over all processed items
const tags = item.attributes?.fitmmentTags;
for (const key in globalUniqueFitmentMap) {
if (tags[key]) {
tags[key].forEach(value => {
globalUniqueFitmentMap[key].add(value);
});
}
}
// Convert sets to arrays
const convertedGlobalUniqueFitmentMap = {};
for (const key in globalUniqueFitmentMap) {
convertedGlobalUniqueFitmentMap[key] = Array.from(globalUniqueFitmentMap[key]);
}
const fitmentTags = convertedGlobalUniqueFitmentMap;
const allFitmentTagsSet = new Set();
for (const arr of Object.values(convertedGlobalUniqueFitmentMap)) {
arr.forEach(val => allFitmentTagsSet.add(val));
}
const allFitmentTags = Array.from(allFitmentTagsSet);
// Now allFitmentTags is a flat array of unique values
log(shop, `All Fitment Tags for ${attrs.product_name || attrs.part_number}: ${JSON.stringify(allFitmentTags, null, 2)}`);
log(shop, `Fitment Tags for ${attrs.product_name || attrs.part_number}: ${JSON.stringify(fitmentTags, null, 2)}`);
try {
var inventoryData = attrs.inventorydata.inventory
const totalQuantity = Object.values(inventoryData).reduce((sum, val) => sum + val, 0);
//console.log(totalQuantity, "1234567890")
const client = axios.create({
// baseURL: `https://${SHOP}/admin/api/${API_VERSION}/graphql.json`,
baseURL: `https://${SHOP}/admin/api/2025-10/graphql.json`,
headers: {
'X-Shopify-Access-Token': ACCESS_TOKEN,
'Content-Type': 'application/json',
},
});
const client_2024_01 = axios.create({
// baseURL: `https://${SHOP}/admin/api/${API_VERSION}/graphql.json`,
baseURL: `https://${SHOP}/admin/api/2024-01/graphql.json`,
headers: {
'X-Shopify-Access-Token': ACCESS_TOKEN,
'Content-Type': 'application/json',
},
});
log(shop, `🛒 [${procId}] Processing product: ${attrs.product_name || attrs.part_number}`);
// Build and normalize collection titles
const category = attrs.category;
const subcategory = attrs.subcategory || "";
const brand = attrs.brand;
const subcats = subcategory
.split(/[,\/]/)
.map((s) => s.trim())
.filter(Boolean);
const collectionTitles = Array.from(
new Set([category, ...subcats, brand, ...allFitmentTags].filter(Boolean))
);
// Find or create collections, collect their IDs
const collectionIds = [];
for (const title of collectionTitles) {
log(shop, `🏷️ [${procId}] Handling collection: ${title}`);
// 1. Query existing manual collection by title
const lookupQuery = `
query {
collections(first: 1, query: "title:\\"${title}\\" AND collection_type:manual") {
nodes { id }
}
}
`;
const lookupResp = await client.post('', { query: lookupQuery });
const existing = lookupResp.data.data.collections.nodes;
if (existing.length) {
log(shop, `✅ [${procId}] Found existing collection: ${title}`);
collectionIds.push(existing[0].id);
continue;
}
// 2. Otherwise, create it
log(shop, ` [${procId}] Creating new collection: ${title}`);
const createMutation = `
mutation collectionCreate($input: CollectionInput!) {
collectionCreate(input: $input) {
collection { id }
userErrors { field message }
}
}
`;
const createResp = await client.post('', {
query: createMutation,
variables: { input: { title } }
});
const createData = createResp.data.data.collectionCreate;
if (createData.userErrors.length) {
throw new Error(
`Could not create collection "${title}": ` +
createData.userErrors.map(e => e.message).join(', ')
);
}
const newId = createData.collection.id;
log(shop, `✨ [${procId}] Created collection: ${title} (ID: ${newId})`);
collectionIds.push(newId);
}
// Build tags
const tags = [
attrs.category,
...subcats,
...allFitmentTags,
attrs.brand,
attrs.part_number,
attrs.mfr_part_number,
attrs.price_group,
attrs.units_per_sku && `${attrs.units_per_sku} per SKU`,
attrs.barcode
].filter(Boolean).map((t) => t.trim());
// Prepare media inputs
const mediaInputs = (attrs.files || [])
.filter((f) => f.type === "Image" && f.url)
.map((file) => ({
originalSource: file.url,
mediaContentType: "IMAGE",
alt: `${attrs.product_name}${file.media_content}`,
}));
// Pick the longest "Market Description" or fallback to part_description
const marketDescs = (attrs.descriptions || [])
.filter((d) => d.type === "Market Description")
.map((d) => d.description);
const descriptionHtml = marketDescs.length
? marketDescs.reduce((a, b) => (b.length > a.length ? b : a))
: attrs.part_description;
log(shop, `🔄 [${procId}] Creating product: ${attrs.product_name}`);
const handle = slugify(item.id)
// const handle = slugify(item.id + "-" + (attrs.mfr_part_number || attrs.product_name))
const searchRes = await client.post('', {
query: `
query {
products(first: 1, query: "handle:${handle}") {
nodes { id handle }
}
}
`
});
// console.log(`[AddProductToStore] Search result for handle "${handle}":`, searchRes.data.data.products);
const exists = searchRes.data?.data?.products?.nodes?.length > 0;
if (exists) {
log(shop, `⏭️ [${procId}] Skipping duplicate product: ${attrs.part_number}`);
results.push({ skippedHandle: attrs.part_number, reason: "handle in use" });
return null;
} else {
const createProdRes = await client.post('', {
query: `
mutation ProductCreate($product: ProductCreateInput!, $media: [CreateMediaInput!]) {
productCreate(product: $product, media: $media) {
product {
id
variants(first: 1) {
nodes {
id
inventoryItem { id }
price
compareAtPrice
barcode
}
}
}
userErrors { field message }
}
}
`,
variables: {
product: {
title: attrs.product_name,
descriptionHtml: descriptionHtml,
vendor: attrs.brand,
productType: attrs.category,
handle: handle,
tags,
collectionsToJoin: collectionIds,
status: "ACTIVE",
},
media: mediaInputs,
},
});
const createProdJson = createProdRes.data;
const prodErrs = createProdJson.data?.productCreate?.userErrors || [];
if (prodErrs.length) {
const taken = prodErrs.some(e => /already in use/i.test(e.message));
if (taken) {
log(shop, `⏭️ [${procId}] Skipping duplicate product: ${attrs.part_number}`);
results.push({ skippedHandle: attrs.part_number, reason: "handle in use" });
return null;
}
throw new Error(`ProductCreate errors: ${prodErrs.map(e => e.message).join(", ")}`);
}
const product = createProdJson.data.productCreate.product;
const variantNode = product.variants?.nodes?.[0];
if (!variantNode) {
log(shop, `⚠️ [${procId}] No variant found for product: ${product.id}`);
return null;
}
const variantId = variantNode.id;
const inventoryItemId = variantNode.inventoryItem?.id;
console.log("Invemtory Item ID : ", inventoryItemId)
// Bulk-update variant (price, compare-at, barcode)
const baseprice = parseFloat(attrs.price) || 0;
const pricingConfigRes = await client.post('', {
query: `
query {
shop {
metafield(namespace: "turn14", key: "pricing_config") {
value
}
}
}
`
});
let priceType = 'map';
let percentage = 0;
const pricingMf = pricingConfigRes.data?.data?.shop?.metafield;
if (pricingMf?.value) {
try {
const parsed = JSON.parse(pricingMf.value);
priceType = parsed.priceType || 'map';
percentage = Number(parsed.percentage) || 0;
} catch (err) {
console.error('Failed to parse pricing_config metafield:', err);
}
}
// 2) Apply your price calculation using the metafield values
let price = baseprice;
if (priceType === 'percentage') {
price = baseprice + (baseprice * (percentage / 100));
}
log(shop, `📢 [${procId}] Calculated price: ${price} (type: ${priceType}, percentage: ${percentage})`);
const comparePrice = parseFloat(attrs.compare_price) || null;
const barcode = attrs.barcode || "";
log(shop, `💲 [${procId}] Updating pricing for variant: ${variantId}`);
const weightValue = parseFloat(attrs.dimensions?.[0]?.weight) || 0;
const bulkRes_new = await client.post('', {
query: `
mutation UpdateProductVariant(
$productId: ID!,
$variants: [ProductVariantsBulkInput!]!
) {
productVariantsBulkUpdate(productId: $productId, variants: $variants) {
productVariants {
id
price
compareAtPrice
barcode
inventoryItem {
sku
measurement {
weight {
value
unit
}
}
tracked
}
}
userErrors {
field
message
}
}
}
`,
variables: {
productId: product.id,
variants: [{
id: variantId,
price,
...(comparePrice !== null && { compareAtPrice: comparePrice }),
...(barcode && { barcode }),
inventoryItem: {
sku: attrs.part_number,
measurement: {
weight: { value: weightValue, unit: "POUNDS" }
},
// tracke d: true
}
}]
},
});
// const bulkRes = await client.post('', {
// query: `
// mutation UpdateProductVariant(
// $productId: ID!,
// $variants: [ProductVariantsBulkInput!]!
// ) {
// productVariantsBulkUpdate(productId: $productId, variants: $variants) {
// productVariants {
// id
// price
// compareAtPrice
// barcode
// inventoryItem {
// measurement {
// weight {
// value
// unit
// }
// }
// tracked
// }
// }
// userErrors {
// field
// message
// }
// }
// }
// `,
// variables: {
// productId: product.id,
// variants: [{
// id: variantId,
// price,
// ...(comparePrice !== null && { compareAtPrice: comparePrice }),
// ...(barcode && { barcode }),
// // sku: attrs.part_number,
// inventoryItem: {
// measurement: {
// weight: { value: weightValue, unit: "POUNDS" }
// },
// // tracked: true
// }
// }]
// },
// });
log(shop, `🔄 [${procId}] Bulk updating variant: ${variantId}`);
//console.warn(JSON.stringify(bulkRes.data, null, 2))
const bulkJson = bulkRes_new.data;
const bulkErrs = bulkJson.data.productVariantsBulkUpdate.userErrors;
if (bulkErrs.length) {
throw new Error(`Bulk update errors: ${bulkErrs.map(e => e.message).join(", ")}`);
}
// Fetch the Online Store publication ID
log(shop, `📢 [${procId}] Publishing product to Online Store`);
const publicationsRes = await client.post('', {
query: `
query {
publications(first: 10) {
edges {
node {
id
name
}
}
}
}
`
});
const publicationsJson = publicationsRes.data;
const onlineStorePublication = publicationsJson.data.publications.edges.find(
pub => pub.node.name === 'Online Store'
);
const onlineStorePublicationId = onlineStorePublication ? onlineStorePublication.node.id : null;
if (onlineStorePublicationId) {
const publishRes = await client.post('', {
query: `
mutation($id: ID!, $publicationId: ID!) {
publishablePublish(id: $id, input: { publicationId: $publicationId }) {
publishable {
... on Product {
id
title
status
}
}
userErrors { field message }
}
}
`,
variables: {
id: product.id,
publicationId: onlineStorePublicationId,
},
});
const publishJson = publishRes.data;
const publishErrs = publishJson.data.publishablePublish.userErrors;
if (publishErrs.length) {
throw new Error(`Publish errors: ${publishErrs.map(e => e.message).join(", ")}`);
}
log(shop, `🌐 [${procId}] Published product to Online Store`);
} else {
throw new Error("Online Store publication not found.");
}
const costPerItem = parseFloat(attrs.purchase_cost) || 0;
log(shop, `📦 [${procId}] Updating inventory for product`);
const invRes = await client.post('', {
query: `
mutation InventoryItemUpdate($id: ID!, $input: InventoryItemInput!) {
inventoryItemUpdate(id: $id, input: $input) {
inventoryItem {
id
sku
unitCost {
amount
currencyCode
}
tracked
measurement {
weight {
value
unit
}
}
}
userErrors {
field
message
}
}
}
`,
variables: {
id: inventoryItemId,
input: {
cost: parseFloat(attrs.purchase_cost) || 0,
tracked: true
}
}
});
const invJson = invRes.data;
//console.log(JSON.stringify(invJson, null, 2))
const invErrs = invJson.data.inventoryItemUpdate.userErrors;
if (invErrs.length) {
throw new Error(`Inventory update errors: ${invErrs.map(e => e.message).join(", ")}`);
}
const activateInventoryMutation = `
mutation ActivateInventoryItem($inventoryItemId: ID!, $locationId: ID!) {
inventoryActivate(inventoryItemId: $inventoryItemId, locationId: $locationId) {
inventoryLevel {
id
quantities(names: ["available"]) {
name
quantity
}
item { id }
location { id }
}
userErrors {
field
message
}
}
}
`;
const activateInventoryVariables = {
inventoryItemId: inventoryItemId, // your inventory item ID
locationId: locationId
};
const activateInventoryRes = await client.post('', {
query: activateInventoryMutation,
variables: activateInventoryVariables
});
log('Activate Inventory:', JSON.stringify(activateInventoryRes.data, null, 2));
const mutation = `
mutation InventorySet($input: InventorySetQuantitiesInput!) {
inventorySetQuantities(input: $input) {
inventoryAdjustmentGroup {
createdAt
reason
referenceDocumentUri
changes {
name
delta
}
}
userErrors {
field
message
}
}
}
`;
const variables = {
input: {
name: "available",
reason: "correction",
referenceDocumentUri: "logistics://some.warehouse/take/2023-01-23T13:14:15Z",
ignoreCompareQuantity: true,
quantities: [
{
inventoryItemId: inventoryItemId,
locationId: locationId,
quantity: totalQuantity,
compareQuantity: 1
}
]
}
};
console.log("Variables for Setting Inventory : ", totalQuantity)
var setInventoryRes
try {
console.log("newwww")
setInventoryRes = await client.post('', {
query: mutation,
variables: variables
});
} catch (error) {
if (error.setInventoryRes) {
console.error('Error:', error.setInventoryRes.data);
} else {
console.error('Error:', error.message);
}
}
const setInventoryData = setInventoryRes.data.inventorySetQuantities;
if (setInventoryData?.userErrors.length) {
throw new Error(
"Inventory update errors: " +
setInventoryData?.userErrors.map(e => e.message).join(", ")
);
}
// const updatedProduct = prodRes.data;
const prodRes = await client.post('', {
query: `
mutation ProductUpdate($product: ProductUpdateInput!) {
productUpdate(product: $product) {
product {
id
title
seo {
title
description
}
}
userErrors {
field
message
}
}
}
`,
variables: {
product: {
id: product.id, // e.g. "gid://shopify/Product/1234567890"
seo: {
title: seo_title || `${product.title} | Performance Auto Parts`,
description:
seo_description ||
`Find high-quality ${product.title} built for reliability and performance. Trusted automotive brands and precision engineering.`
}
// ...other fields as needed
}
}
});
const prodJson = prodRes.data;
const prodErrsseo = prodJson.data.productUpdate.userErrors;
if (prodErrs.length) {
throw new Error(`Product update errors: ${prodErrs.map(e => e.message).join(", ")}`);
}
// Get the updated inventory item from the response
const updatedInventoryItem = invJson.data.inventoryItemUpdate.inventoryItem;
// Collect results
results.push({
productId: product.id,
variant: {
id: variantId,
price: variantNode.price,
compareAtPrice: variantNode.compareAtPrice,
sku: updatedInventoryItem.sku || attrs.part_number || '',
barcode: variantNode.barcode || attrs.barcode || '',
weight: updatedInventoryItem?.measurement?.weight?.value || 0,
weightUnit: updatedInventoryItem?.measurement?.weight?.unit || 'kg',
},
collections: collectionTitles,
tags,
});
log(shop, `✅ [${procId}] Successfully processed product: ${attrs.product_name}`);
return results;
}
} catch (err) {
log(shop, `❌ [${procId}] Error processing product: ${err.message}`);
results.push({
error: `Failed to process item ${item.id}: ${err.message}`,
product: attrs.product_name || attrs.part_number || 'Unknown'
});
return results;
}
}
const GetAllProductsandAddToStore = async (shop, accessToken, brandId, turn14accessToken, procId, tokens, selectedProductIds, productCount) => {
const fulfillmentServiceTokens = tokens.fulfillmentService || {}
const fulfillmentServiceId = fulfillmentServiceTokens.id || null;
// const locationId = fulfillmentServiceTokens.location ? fulfillmentServiceTokens.location.id : null;
const locationId = tokens.locationId ? tokens.locationId : null;
console.log("Custom Location ID to Store : ", locationId)
log(shop, `🔍 [${procId}] Fetching products for brand ${brandId}`);
const products_res = await GetAllProductsOfBranch(brandId, turn14accessToken, shop, procId, productCount);
const items = products_res ? products_res.items : [];
// Update total products count
const results = [];
const products_filter = items.filter(item => {
return selectedProductIds.includes(item.id);
});
const products = Array.isArray(products_filter) ? products_filter : [];
//const products = Array.isArray(products_filter) ? products_filter.slice(0, 11) : [];
processes[procId].totalProducts = products.length;
processes[procId].processedProducts = 0;
log(shop, `🔄 [${procId}] Processing ${products.length} products after the filter`);
if (!products) {
log(shop, `⚠️ [${procId}] No products found for brand ${brandId}`);
return [];
}
for (const [index, item] of products.entries()) {
try {
// Update current product being processed
const attrs = item.attributes;
processes[procId].currentProduct = {
name: attrs.product_name || attrs.part_number || 'Unknown',
number: index + 1,
total: products.length
};
processes[procId].status = `processing (${index + 1}/${products.length})`;
const res = await AddProductToStore(shop, accessToken, item, procId, fulfillmentServiceId, locationId);
if (res) results.push(...res);
// Update processed count
processes[procId].processedProducts = index + 1;
processes[procId].detail = `Processed ${index + 1} of ${products.length} products`;
} catch (err) {
log(shop, `⚠️ [${procId}] Error processing product ${index + 1}: ${err.message}`);
results.push({
error: `Failed to process product ${index + 1}: ${err.message}`,
product: item.attributes.product_name || item.attributes.part_number || 'Unknown'
});
}
}
// Clear current product when done
processes[procId].currentProduct = null;
log(shop, `✅ [${procId}] Completed processing ${results.length} products`);
return results;
}
router.post('/', async (req, res) => {
const { shop, brandID, turn14accessToken, productCount, selectedProductIds } = req.body;
const procId = uuid();
processes[procId] = {
status: 'started',
detail: null,
totalProducts: 0,
processedProducts: 0,
currentProduct: null,
results: []
};
log(shop, `🔔 [${procId}] Starting product import for brand ${brandID}`);
res.json({ processId: procId, status: 'started' });
(async () => {
try {
processes[procId].status = 'fetching_products';
log(shop, `🔍 [${procId}] Fetching token for shop`);
// 1. Get token
if (!turn14accessToken) throw new Error('No Turn14 access token provided');
if (!brandID) throw new Error('No brand ID provided');
if (!shop) throw new Error('No shop provided');
if (!selectedProductIds) throw new Error('No selected product IDs provided');
log(shop, `Selected Product IDs: ${selectedProductIds}`);
// console.log("Selected Product IDs:", selectedProductIds);
if (!Array.isArray(selectedProductIds) || selectedProductIds.length === 0) {
throw new Error('Selected product IDs must be a non-empty array');
}
log(shop, `🔍 [${procId}] Fetching products for brand ${brandID}`);
const tokenRecord = getToken(shop);
if (!tokenRecord) throw new Error('No token for shop');
processes[procId].status = 'importing_products';
processes[procId].detail = 'Starting product import';
const importResults = await GetAllProductsandAddToStore(shop, tokenRecord.accessToken, brandID, turn14accessToken, procId, tokenRecord, selectedProductIds, productCount);
log(shop, `✅ [${procId}] Successfully imported ${importResults.length} products`);
processes[procId].status = 'done';
processes[procId].detail = `Imported ${importResults.length} products`;
processes[procId].results = importResults;
} catch (err) {
processes[procId].status = 'error';
processes[procId].detail = err.message;
log(shop, `❌ [${procId}] Error: ${err.message}`);
}
})();
});
router.get('/status/:processId', (req, res) => {
const info = processes[req.params.processId];
if (!info) return res.status(404).json({ error: 'Not found' });
const response = {
status: info.status,
detail: info.detail,
progress: info.totalProducts > 0
? Math.round((info.processedProducts / info.totalProducts) * 100)
: 0,
current: info.currentProduct,
stats: {
total: info.totalProducts,
processed: info.processedProducts,
remaining: info.totalProducts - info.processedProducts
},
results: info.results || []
};
res.json(response);
});
module.exports = router;