// 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 = '2023-10'; // 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) => { var AllProductsOfBrans = []; try { const res = await fetch(`https://turn14.data4autos.com/v1/items/brandallitems/${brandId}`, { headers: { Authorization: `Bearer ${turn14accessToken}`, "Content-Type": "application/json", }, }); const data = await res.json(); // Ensure we have an array of valid items const validItems = Array.isArray(data) ? data.filter(item => item && item.id && item.attributes) : []; AllProductsOfBrans = validItems console.log("AllProductsOfBrans", AllProductsOfBrans.length); const items = Array.isArray(AllProductsOfBrans) ? AllProductsOfBrans.slice(0, 5) : []; console.log("items", items.length); return items; } catch (err) { console.error("Error fetching items:", err); return null } } const AddProductToStore = async (shop, accessToken, product) => { var results = []; const SHOP = shop const ACCESS_TOKEN = accessToken const item = product; const client = axios.create({ baseURL: `https://${SHOP}/admin/api/${API_VERSION}/graphql.json`, headers: { 'X-Shopify-Access-Token': ACCESS_TOKEN, 'Content-Type': 'application/json', }, }); try { const attrs = item.attributes; // console.log("Processing item:", attrs); // 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].filter(Boolean)) ); // console.log("Collection Titles:", collectionTitles); // Find or create collections, collect their IDs const collectionIds = [] for (const title of collectionTitles) { // console.log(`Searching for 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) { console.log(`→ Found existing collection: ${existing[0].id}`); collectionIds.push(existing[0].id); continue; } // 2. Otherwise, create it //console.log(`→ No existing collection. Creating "${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; // console.log(`→ Created collection: ${newId}`); collectionIds.push(newId); } // Build tags const tags = [ attrs.category, ...subcats, 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()); // console.log("Tags:", tags); // 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}`, })); // console.log("Media inputs:", mediaInputs); // 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; // console.log("Description HTML:", descriptionHtml); // Create product + attach to collections + add media // const createProdRes = await admin.graphql(` // mutation($prod: ProductCreateInput!, $media: [CreateMediaInput!]) { // productCreate(product: $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: slugify(attrs.part_number || attrs.product_name), // tags, // collectionsToJoin: collectionIds, // status: "ACTIVE", // }, // media: mediaInputs, // }, // }); // const createProdRes = await client.post('', { // query: ` // mutation($prod: ProductCreateInput!, $media: [CreateMediaInput!]) { // productCreate(product: $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: slugify(item.id + "-" + attrs.mfr_part_number || attrs.product_name), // tags, // collectionsToJoin: collectionIds, // status: "ACTIVE", // }, // media: mediaInputs, // }, // }); // const createProdJson = createProdRes.data; // console.log("Create product response:", createProdJson); // const prodErrs = createProdJson.data?.productCreate?.userErrors || []; // if (prodErrs.length) { // const taken = prodErrs.some(e => /already in use/i.test(e.message)); // if (taken) { // 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 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: slugify(item.id + "-" + (attrs.mfr_part_number || attrs.product_name)), tags, collectionsToJoin: collectionIds, status: "ACTIVE", }, media: mediaInputs, }, }); const createProdJson = createProdRes.data; //console.log("Create product response:", createProdJson); const prodErrs = createProdJson.data?.productCreate?.userErrors || []; if (prodErrs.length) { const taken = prodErrs.some(e => /already in use/i.test(e.message)); if (taken) { 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) { console.error("Variant node is undefined for product:", product.id); return null } const variantId = variantNode.id; const inventoryItemId = variantNode.inventoryItem?.id; // Bulk-update variant (price, compare-at, barcode) const price = parseFloat(attrs.price) || 1000; const comparePrice = parseFloat(attrs.compare_price) || null; const barcode = attrs.barcode || ""; const bulkRes = await client.post('', { query: ` mutation($productId: ID!, $variants: [ProductVariantsBulkInput!]!) { productVariantsBulkUpdate(productId: $productId, variants: $variants) { productVariants { id price compareAtPrice barcode } userErrors { field message } } } `, variables: { productId: product.id, variants: [{ id: variantId, price, ...(comparePrice !== null && { compareAtPrice: comparePrice }), ...(barcode && { barcode }), }], }, }); 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 //console.log("Fetching Online Store publication ID..."); const publicationsRes = await client.post('', { query: ` query { publications(first: 10) { edges { node { id name } } } } ` }); const publicationsJson = publicationsRes.data; // console.log("Publications response:", publicationsJson); const onlineStorePublication = publicationsJson.data.publications.edges.find( pub => pub.node.name === 'Online Store' ); const onlineStorePublicationId = onlineStorePublication ? onlineStorePublication.node.id : null; if (onlineStorePublicationId) { // console.log("Publishing product to Online Store..."); // ▶︎ Replace admin.graphql(…) with: 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; console.log("Publish response:", publishJson); const publishErrs = publishJson.data.publishablePublish.userErrors; if (publishErrs.length) { throw new Error(`Publish errors: ${publishErrs.map(e => e.message).join(", ")}`); } } else { throw new Error("Online Store publication not found."); } // Update inventory item (SKU, cost & weight) const costPerItem = parseFloat(attrs.purchase_cost) || 0; const weightValue = parseFloat(attrs.dimensions?.[0]?.weight) || 0; // console.log("Updating inventory item..."); // const invRes = await client.post('', { // query: ` // mutation($id: ID!, $input: InventoryItemInput!) { // inventoryItemUpdate(id: $id, input: $input) { // inventoryItem { // id // sku // measurement { // weight { value unit } // } // } // userErrors { field message } // } // } // `, // variables: { // id: inventoryItemId, // input: { // sku: attrs.part_number, // cost: parseFloat(attrs.purchase_cost) || 0, // measurement: { // weight: { // value: parseFloat(attrs.dimensions?.[0]?.weight) || 0, // unit: "POUNDS" // } // } // } // } // }); // const invJson = invRes.data; // console.log("Inventory update response:", invJson); // const invErrs = invJson.data.inventoryItemUpdate.userErrors; // if (invErrs.length) { // throw new Error(`Inventory update errors: ${invErrs.map(e => e.message).join(", ")}`); // } // const inventoryItem = invJson.data.inventoryItemUpdate.inventoryItem; // console.log("Updating inventory item..."); // const invRes = await client.post('', { // query: ` // mutation($id: ID!, $input: InventoryItemUpdateInput!) { // inventoryItemUpdate(id: $id, input: $input) { // inventoryItem { // id // sku // measurement { // weight { value unit } // } // } // userErrors { field message } // } // } // `, // variables: { // id: inventoryItemId, // input: { // sku: attrs.part_number, // cost: parseFloat(attrs.purchase_cost) || 0, // measurement: { // weight: { // value: parseFloat(attrs.dimensions?.[0]?.weight) || 0, // unit: "POUNDS" // } // } // } // } // }); // const invJson = invRes.data; // console.log("Inventory update response:", invJson); // const invErrs = invJson.data.inventoryItemUpdate.userErrors; // if (invErrs.length) { // throw new Error(`Inventory update errors: ${invErrs.map(e => e.message).join(", ")}`); // } // const inventoryItem = invJson.data.inventoryItemUpdate.inventoryItem; console.log("Updating inventory item..."); const invRes = await client.post('', { query: ` mutation($id: ID!, $input: InventoryItemUpdateInput!) { inventoryItemUpdate(id: $id, input: $input) { inventoryItem { id unitCost { amount } tracked } userErrors { field message } } } `, variables: { id: inventoryItemId, input: { cost: parseFloat(attrs.purchase_cost) || 0, tracked: true } } }); const invJson = invRes.data; console.log("Inventory update response:", invJson); const invErrs = invJson.data.inventoryItemUpdate.userErrors; if (invErrs.length) { throw new Error(`Inventory update errors: ${invErrs.map(e => e.message).join(", ")}`); } const inventoryItem = invJson.data.inventoryItemUpdate.inventoryItem; // Collect results results.push({ productId: product.id, variant: { id: variantId, price: variantNode.price, compareAtPrice: variantNode.compareAtPrice, sku: inventoryItem.sku, barcode: variantNode.barcode, weight: inventoryItem?.measurement?.weight.value, weightUnit: inventoryItem?.measurement?.weight.unit, }, collections: collectionTitles, tags, }); return results; } catch (err) { console.error("Error in AddProductToStore:", err); results.push({ error: `Failed to process item ${item.id}: ${err.message}` }); return results; } } const GetAllProductsandAddToStore = async (shop, accessToken, brandId, turn14accessToken) => { const products = await GetAllProductsOfBranch(brandId, turn14accessToken); if (!products) { console.error("No products found or error fetching products."); return []; } const results = []; for (const item of products) { const res = await AddProductToStore(shop, accessToken, item); results.push(...res); } return results; } async function fetchAllCollections(shop, accessToken) { const adminUrl = `https://${shop}/admin/api/2023-10/graphql.json`; const headers = { 'X-Shopify-Access-Token': accessToken, 'Content-Type': 'application/json', }; let allCollections = []; let hasNextPage = true; let endCursor = null; const pageSize = 100; while (hasNextPage) { const fetchQuery = ` query GetCollections { collections(first: ${pageSize}${endCursor ? `, after: "${endCursor}"` : ''}) { edges { node { id title } cursor } pageInfo { hasNextPage endCursor } } } `; const fetchResp = await axios.post(adminUrl, { query: fetchQuery }, { headers }); const collections = fetchResp.data.data.collections.edges.map(e => e.node); allCollections = allCollections.concat(collections); hasNextPage = fetchResp.data.data.collections.pageInfo.hasNextPage; endCursor = fetchResp.data.data.collections.pageInfo.endCursor; } return allCollections; } router.post('/', async (req, res) => { const { shop, brandID, turn14accessToken } = req.body; const procId = uuid(); processes[procId] = { status: 'started', detail: null }; res.json({ processId: procId, status: 'started' }); (async () => { try { log(shop, `🔔 [${procId}] ManageBrands started`); processes[procId].status = 'fetching_collections'; // 1. Get token const tokenRecord = getToken(shop); if (!tokenRecord) throw new Error('No token for shop'); const allCollections = await GetAllProductsandAddToStore(shop, tokenRecord.accessToken, brandID, turn14accessToken); log(shop, `🔍 [${procId}] Fetchedd ${allCollections.length} existing collections`); } 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' }); res.json(info); }); module.exports = router;