// 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;