// 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 router = express.Router(); const API_VERSION = '2024-01'; // 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.slice(0, 3) : []; //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; } } const AddProductToStore = async (shop, accessToken, product, procId, fulfillmentServiceId, locationId) => { var results = []; 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/2024-01/graphql.json`, headers: { 'X-Shopify-Access-Token': ACCESS_TOKEN, 'Content-Type': 'application/json', }, }); const client_new = axios.create({ // baseURL: `https://${SHOP}/admin/api/${API_VERSION}/graphql.json`, baseURL: `https://${SHOP}/admin/api/2025-07/graphql.json`, headers: { 'X-Shopify-Access-Token': ACCESS_TOKEN, 'Content-Type': 'application/json', }, }); const client_2510 = 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', }, }); 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 { // Proceed with productCreate mutation // const createProdRes = await client.post('', { // query: ` // mutation($prod: ProductInput!, $media: [CreateMediaInput!]) { // productCreate(input: $prod, media: $media) { // product { // id // variants(first: 1) { // nodes { // id // inventoryItem { id } // price // compareAtPrice // barcode // } // } // } // userErrors { field message } // } // } // `, // variables: { // prod: { // title: attrs.product_name, // descriptionHtml: descriptionHtml, // vendor: attrs.brand, // productType: attrs.category, // handle: handle, // tags, // collectionsToJoin: collectionIds, // status: "ACTIVE", // }, // media: mediaInputs, // }, // }); const createProdRes = await client_2510.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; // 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 = await client.post('', { query: ` mutation UpdateProductVariant( $productId: ID!, $variants: [ProductVariantsBulkInput!]! ) { productVariantsBulkUpdate(productId: $productId, variants: $variants) { productVariants { id price compareAtPrice barcode sku 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}`); const bulkJson = bulkRes.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($id: ID!, $input: InventoryItemUpdateInput!) { inventoryItemUpdate(id: $id, input: $input) { inventoryItem { id sku unitCost { amount } tracked measurement { weight { value unit } } } userErrors { field message } } } `, variables: { id: inventoryItemId, input: { cost: parseFloat(attrs.purchase_cost) || 0, tracked: true } } }); const invJson = invRes.data; const invErrs = invJson.data.inventoryItemUpdate.userErrors; if (invErrs.length) { throw new Error(`Inventory update errors: ${invErrs.map(e => e.message).join(", ")}`); } console.log("Invemtory ID : ", inventoryItemId) console.log("Location ID : ", locationId) log(shop, `⚙️ [${procId}] Assigning variant to fulfillment service`); // const assignVariantMutation = ` // mutation AssignVariantToFulfillmentService($variantId: ID!, $fulfillmentServiceId: ID!) { // productVariantUpdate(input: { // id: $variantId, // fulfillmentServiceId: $fulfillmentServiceId // }) { // productVariant { // id // fulfillmentService { // id // serviceName // } // } // userErrors { // field // message // } // } // } // `; // const assignVariantVariables = { // variantId: variantId, // your variant ID // fulfillmentServiceId: fulfillmentServiceId // your fulfillment service ID // }; // const assignVariantRes = await client.post('', { // query: assignVariantMutation, // variables: assignVariantVariables // }); // console.log('Assign Variant:', JSON.stringify(assignVariantRes.data, null, 2)); const assignVariantMutation = ` mutation ProductVariantsBulkUpdate($productId: ID!, $variants: [ProductVariantsBulkInput!]!) { productVariantsBulkUpdate(productId: $productId, variants: $variants) { productVariants { id title sku } userErrors { field message } } } `; console.log("Product ID : ", product.id) const assignVariantVariables = { productId: product.id, // Replace with your product ID variants: [ { id: inventoryItemId, // Replace with your variant ID fulfillmentServiceId: fulfillmentServiceId // Replace with your fulfillment service ID } ] }; const assignVariantRes = await client_new.post('', { query: assignVariantMutation, variables: assignVariantVariables }); console.log('Assign Variant:', JSON.stringify(assignVariantRes.data, null, 2)); // 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 // }); // console.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 } ] } }; var setInventoryRes try { // console.log("newwww") setInventoryRes = await client_new.post('', { query: mutation, variables: variables }); // Print the full setInventoryRes from Shopify console.log(JSON.stringify(setInventoryRes.data, null, 2)); } 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(", ") ); } // 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; 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 = items.filter(item => { return selectedProductIds.includes(item.id); }); // const globalUniqueFitmentMap = { // make: new Set(), // model: new Set(), // year: new Set(), // drive: new Set(), // baseModel: new Set() // }; // // Loop over all processed items // for (const item of products) { // const tags = item.attributes?.fitmmentTags; // if (!tags) continue; // 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: ${JSON.stringify(allFitmentTags, null, 2)}`); // log(shop, `Fitment Tags: ${JSON.stringify(fitmentTags, null, 2)}`); processes[procId].totalProducts = products.length; processes[procId].processedProducts = 0; log(shop, `🔄 [${procId}] Processing ${products.length} products`); 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;