diff --git a/routes/manageProducts.js b/routes/manageProducts.js index 6948407..3db53df 100755 --- a/routes/manageProducts.js +++ b/routes/manageProducts.js @@ -55,8 +55,16 @@ function extractFirstJsonObject(text) { // Fetch all products for a brand from Turn14 // --------------------------------------------------------------------------- const GetAllProductsOfBranch = async (brandId, turn14accessToken, shop, jobId) => { + console.log(`\nšŸ“¦ [TURN14-FETCH] ========================================`); + console.log(`šŸ“¦ [TURN14-FETCH] Fetching products for brand ID: ${brandId}`); + console.log(`šŸ“¦ [TURN14-FETCH] Shop: ${shop}`); + console.log(`šŸ“¦ [TURN14-FETCH] Job ID: ${jobId}`); + console.log(`šŸ“¦ [TURN14-FETCH] Token (first 20 chars): ${turn14accessToken?.slice(0, 20)}...`); + console.log(`šŸ“¦ [TURN14-FETCH] URL: https://turn14.data4autos.com/v1/items/brandallitemswithfitment/${brandId}`); + try { appendJobLog(jobId, `[FETCH] Fetching products for brand ${brandId} from Turn14...`); + const fetchStart = Date.now(); const res = await fetch( `https://turn14.data4autos.com/v1/items/brandallitemswithfitment/${brandId}`, { @@ -66,15 +74,34 @@ const GetAllProductsOfBranch = async (brandId, turn14accessToken, shop, jobId) = }, } ); + + console.log(`šŸ“¦ [TURN14-FETCH] HTTP status: ${res.status} ${res.statusText}`); + console.log(`šŸ“¦ [TURN14-FETCH] Response time: ${Date.now() - fetchStart}ms`); + + if (!res.ok) { + console.error(`āŒ [TURN14-FETCH] Non-200 response: ${res.status}`); + } + const res_data = await res.json(); const data = res_data.items || []; const fitmentTags = res_data.fitmentTags || []; + + console.log(`šŸ“¦ [TURN14-FETCH] Raw items in response: ${data.length}`); + console.log(`šŸ“¦ [TURN14-FETCH] FitmentTags count: ${fitmentTags.length}`); + const validItems = Array.isArray(data) ? data.filter(item => item && item.id && item.attributes) : []; + + console.log(`šŸ“¦ [TURN14-FETCH] Valid items (have id + attributes): ${validItems.length}`); + console.log(`šŸ“¦ [TURN14-FETCH] Sample item IDs: ${validItems.slice(0, 5).map(i => i.id).join(', ')}`); + console.log(`šŸ“¦ [TURN14-FETCH] ========================================\n`); + appendJobLog(jobId, `[FETCH-OK] Found ${validItems.length} products for brand ${brandId}`); return { items: validItems, fitmentTags }; } catch (err) { + console.error(`āŒ [TURN14-FETCH] Exception fetching brand ${brandId}: ${err.message}`); + console.error(`āŒ [TURN14-FETCH] Stack: ${err.stack}`); appendJobLog(jobId, `[FETCH-FAIL] Error fetching brand ${brandId}: ${err.message}`); return null; } @@ -89,6 +116,30 @@ const AddProductToStore = async (shop, accessToken, product, jobId, locationId) const item = product; const attrs = item.attributes; + const productLabel = attrs.product_name || attrs.part_number || `Item ${item.id}`; + + console.log(`\nšŸ›’ [PRODUCT-START] ============================================`); + console.log(`šŸ›’ [PRODUCT-START] Product: ${productLabel}`); + console.log(`šŸ›’ [PRODUCT-START] Part Number: ${attrs.part_number || 'N/A'}`); + console.log(`šŸ›’ [PRODUCT-START] Mfr Part Number: ${attrs.mfr_part_number || 'N/A'}`); + console.log(`šŸ›’ [PRODUCT-START] Turn14 Item ID: ${item.id}`); + console.log(`šŸ›’ [PRODUCT-START] Brand: ${attrs.brand || 'N/A'}`); + console.log(`šŸ›’ [PRODUCT-START] Category: ${attrs.category || 'N/A'}`); + console.log(`šŸ›’ [PRODUCT-START] Subcategory: ${attrs.subcategory || 'N/A'}`); + console.log(`šŸ›’ [PRODUCT-START] Price: ${attrs.price || 'N/A'}`); + console.log(`šŸ›’ [PRODUCT-START] Compare Price: ${attrs.compare_price || 'N/A'}`); + console.log(`šŸ›’ [PRODUCT-START] Purchase Cost: ${attrs.purchase_cost || 'N/A'}`); + console.log(`šŸ›’ [PRODUCT-START] Barcode: ${attrs.barcode || 'N/A'}`); + console.log(`šŸ›’ [PRODUCT-START] Images count: ${(attrs.files || []).filter(f => f.type === 'Image').length}`); + console.log(`šŸ›’ [PRODUCT-START] Shop: ${SHOP}`); + console.log(`šŸ›’ [PRODUCT-START] Location ID: ${locationId || 'NOT SET'}`); + + // Inventory quantity + const inventoryData = attrs.inventorydata?.inventory || {}; + const totalQuantity = Object.values(inventoryData).reduce((sum, val) => sum + val, 0); + console.log(`šŸ›’ [PRODUCT-START] Inventory data: ${JSON.stringify(inventoryData)}`); + console.log(`šŸ›’ [PRODUCT-START] Total quantity: ${totalQuantity}`); + // SEO (stubbed — returns empty so we use defaults) let parsed = { seo_title: '', seo_description: '' }; const { seo_title, seo_description } = parsed; @@ -102,6 +153,10 @@ const AddProductToStore = async (shop, accessToken, product, jobId, locationId) const convertedFitment = {}; for (const key in globalUniqueFitmentMap) convertedFitment[key] = Array.from(globalUniqueFitmentMap[key]); const allFitmentTags = Array.from(new Set(Object.values(convertedFitment).flat())); + console.log(`šŸ”– [PRODUCT-TAGS] Fitment makes: ${convertedFitment.make?.join(', ') || 'none'}`); + console.log(`šŸ”– [PRODUCT-TAGS] Fitment models: ${convertedFitment.model?.slice(0, 10).join(', ') || 'none'}${convertedFitment.model?.length > 10 ? ` (+${convertedFitment.model.length - 10} more)` : ''}`); + console.log(`šŸ”– [PRODUCT-TAGS] Fitment years: ${convertedFitment.year?.slice(0, 5).join(', ') || 'none'}${convertedFitment.year?.length > 5 ? ` (+${convertedFitment.year.length - 5} more)` : ''}`); + console.log(`šŸ”– [PRODUCT-TAGS] Total fitment tags: ${allFitmentTags.length}`); const client = axios.create({ baseURL: `https://${SHOP}/admin/api/2025-10/graphql.json`, @@ -109,38 +164,46 @@ const AddProductToStore = async (shop, accessToken, product, jobId, locationId) }); try { - const inventoryData = attrs.inventorydata?.inventory || {}; - const totalQuantity = Object.values(inventoryData).reduce((sum, val) => sum + val, 0); - - // Collections + // ── STEP 1: Collections ────────────────────────────────────────────────── 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))); + + console.log(`šŸ“ [COLLECTIONS] Titles to find/create: [${collectionTitles.join(' | ')}]`); + console.log(`šŸ“ [COLLECTIONS] Total collections needed: ${collectionTitles.length}`); + const collectionIds = []; for (const title of collectionTitles) { + console.log(`šŸ“ [COLLECTIONS] Looking up: "${title}"`); const lookupResp = await client.post('', { query: `query { collections(first: 1, query: "title:\\"${title}\\" AND collection_type:manual") { nodes { id } } }` }); const existing = lookupResp.data.data.collections.nodes; if (existing.length) { + console.log(`šŸ“ [COLLECTIONS] āœ… Found existing: ${existing[0].id}`); collectionIds.push(existing[0].id); continue; } + console.log(`šŸ“ [COLLECTIONS] āž• Creating new collection: "${title}"`); const createResp = await client.post('', { query: `mutation collectionCreate($input: CollectionInput!) { collectionCreate(input: $input) { collection { id } userErrors { field message } } }`, variables: { input: { title } }, }); const createData = createResp.data.data.collectionCreate; if (createData.userErrors.length) { + console.error(`šŸ“ [COLLECTIONS] āŒ Failed to create "${title}": ${JSON.stringify(createData.userErrors)}`); throw new Error(`Could not create collection "${title}": ${createData.userErrors.map(e => e.message).join(', ')}`); } + console.log(`šŸ“ [COLLECTIONS] āœ… Created: ${createData.collection.id}`); collectionIds.push(createData.collection.id); } - // Tags + console.log(`šŸ“ [COLLECTIONS] Done. ${collectionIds.length} collection IDs collected`); + + // ── STEP 2: Build tags ─────────────────────────────────────────────────── const productTags = [ attrs.category, ...subcats, @@ -152,8 +215,9 @@ const AddProductToStore = async (shop, accessToken, product, jobId, locationId) attrs.units_per_sku && `${attrs.units_per_sku} per SKU`, attrs.barcode, ].filter(Boolean).map(t => t.trim()); + console.log(`šŸ·ļø [TAGS] Total product tags: ${productTags.length} — ${productTags.slice(0, 10).join(', ')}${productTags.length > 10 ? '...' : ''}`); - // Media + // ── STEP 3: Media ──────────────────────────────────────────────────────── const mediaInputs = (attrs.files || []) .filter(f => f.type === 'Image' && f.url) .map(file => ({ @@ -161,28 +225,46 @@ const AddProductToStore = async (shop, accessToken, product, jobId, locationId) mediaContentType: 'IMAGE', alt: `${attrs.product_name} — ${file.media_content}`, })); + console.log(`šŸ–¼ļø [MEDIA] Image inputs: ${mediaInputs.length}`); + if (mediaInputs.length > 0) { + console.log(`šŸ–¼ļø [MEDIA] First image URL: ${mediaInputs[0].originalSource}`); + } - // Description + // ── STEP 4: 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(`šŸ“ [DESC] Market descriptions found: ${marketDescs.length}`); + console.log(`šŸ“ [DESC] Description length: ${(descriptionHtml || '').length} chars`); const handle = slugify(item.id); + console.log(`šŸ”— [HANDLE] Generated handle: "${handle}"`); - // Dedup check + // ── STEP 5: Dedup check ────────────────────────────────────────────────── + console.log(`šŸ” [DEDUP] Checking if handle "${handle}" already exists in Shopify...`); const searchRes = await client.post('', { query: `query { products(first: 1, query: "handle:${handle}") { nodes { id handle } } }` }); const exists = searchRes.data?.data?.products?.nodes?.length > 0; if (exists) { + const existingId = searchRes.data.data.products.nodes[0].id; + console.log(`šŸ” [DEDUP] āš ļø DUPLICATE — handle "${handle}" exists as ${existingId} — SKIPPING`); appendJobLog(jobId, `[SKIP] ${attrs.part_number} — handle "${handle}" already exists`); return { action: 'skipped', handle, product: attrs.product_name || attrs.part_number }; } + console.log(`šŸ” [DEDUP] āœ… No duplicate found — proceeding to create`); + + // ── STEP 6: Create product ─────────────────────────────────────────────── + console.log(`šŸ†• [CREATE] Creating product in Shopify...`); + console.log(`šŸ†• [CREATE] Title: "${attrs.product_name}"`); + console.log(`šŸ†• [CREATE] Vendor: "${attrs.brand}"`); + console.log(`šŸ†• [CREATE] Product type: "${attrs.category}"`); + console.log(`šŸ†• [CREATE] Collections to join: ${collectionIds.length}`); + console.log(`šŸ†• [CREATE] Media: ${mediaInputs.length} images`); - // Create product const createProdRes = await client.post('', { query: ` mutation ProductCreate($product: ProductCreateInput!, $media: [CreateMediaInput!]) { @@ -212,8 +294,10 @@ const AddProductToStore = async (shop, accessToken, product, jobId, locationId) const prodErrs = createProdRes.data.data?.productCreate?.userErrors || []; if (prodErrs.length) { + console.error(`šŸ†• [CREATE] āŒ productCreate userErrors: ${JSON.stringify(prodErrs)}`); const taken = prodErrs.some(e => /already in use/i.test(e.message)); if (taken) { + console.log(`šŸ†• [CREATE] āš ļø Duplicate handle — SKIPPING`); appendJobLog(jobId, `[SKIP] ${attrs.part_number} — duplicate handle`); return { action: 'skipped', handle, product: attrs.product_name || attrs.part_number }; } @@ -222,12 +306,20 @@ const AddProductToStore = async (shop, accessToken, product, jobId, locationId) const createdProduct = createProdRes.data.data.productCreate.product; const variantNode = createdProduct.variants?.nodes?.[0]; - if (!variantNode) return null; + console.log(`šŸ†• [CREATE] āœ… Product created! Shopify Product ID: ${createdProduct.id}`); + console.log(`šŸ†• [CREATE] Variant ID: ${variantNode?.id || 'NOT FOUND'}`); + console.log(`šŸ†• [CREATE] Inventory Item ID: ${variantNode?.inventoryItem?.id || 'NOT FOUND'}`); + + if (!variantNode) { + console.error(`šŸ†• [CREATE] āŒ No variant node returned — cannot continue`); + return null; + } const variantId = variantNode.id; const inventoryItemId = variantNode.inventoryItem?.id; - // Pricing + // ── STEP 7: Pricing ────────────────────────────────────────────────────── + console.log(`šŸ’° [PRICING] Reading pricing config from Shopify metafield...`); const pricingConfigRes = await client.post('', { query: `query { shop { metafield(namespace: "turn14", key: "pricing_config") { value } } }` }); @@ -239,8 +331,14 @@ const AddProductToStore = async (shop, accessToken, product, jobId, locationId) const p = JSON.parse(pricingMf.value); priceType = p.priceType || 'map'; percentage = Number(p.percentage) || 0; - } catch {} + console.log(`šŸ’° [PRICING] Config found — priceType: "${priceType}", percentage: ${percentage}%`); + } catch (e) { + console.warn(`šŸ’° [PRICING] Failed to parse pricing config JSON: ${e.message}`); + } + } else { + console.log(`šŸ’° [PRICING] No pricing config metafield found — using defaults (MAP, 0%)`); } + const baseprice = parseFloat(attrs.price) || 0; let price = baseprice; if (priceType === 'percentage') price = baseprice + (baseprice * (percentage / 100)); @@ -249,7 +347,15 @@ const AddProductToStore = async (shop, accessToken, product, jobId, locationId) const barcode = attrs.barcode || ''; const weightValue = parseFloat(attrs.dimensions?.[0]?.weight) || 0; - // Variant update + console.log(`šŸ’° [PRICING] Base MAP price: $${baseprice}`); + console.log(`šŸ’° [PRICING] Final price: $${price.toFixed(2)} (${priceType === 'percentage' ? `${percentage}% markup` : 'MAP as-is'})`); + console.log(`šŸ’° [PRICING] Compare at price: ${comparePrice ? `$${comparePrice}` : 'not set'}`); + console.log(`šŸ’° [PRICING] Purchase cost: $${attrs.purchase_cost || 0}`); + console.log(`šŸ’° [PRICING] Barcode: "${barcode || 'none'}"`); + console.log(`šŸ’° [PRICING] Weight: ${weightValue} lbs`); + + // ── STEP 8: Variant update ─────────────────────────────────────────────── + console.log(`šŸ”§ [VARIANT] Updating variant — SKU: "${attrs.part_number}", price: $${price.toFixed(2)}`); const bulkRes = await client.post('', { query: ` mutation UpdateProductVariant($productId: ID!, $variants: [ProductVariantsBulkInput!]!) { @@ -271,23 +377,40 @@ const AddProductToStore = async (shop, accessToken, product, jobId, locationId) }, }); const bulkErrs = bulkRes.data.data.productVariantsBulkUpdate.userErrors; - if (bulkErrs.length) throw new Error(`Bulk update errors: ${bulkErrs.map(e => e.message).join(', ')}`); + if (bulkErrs.length) { + console.error(`šŸ”§ [VARIANT] āŒ Bulk update errors: ${JSON.stringify(bulkErrs)}`); + throw new Error(`Bulk update errors: ${bulkErrs.map(e => e.message).join(', ')}`); + } + const updatedVariant = bulkRes.data.data.productVariantsBulkUpdate.productVariants?.[0]; + console.log(`šŸ”§ [VARIANT] āœ… Variant updated — price: $${updatedVariant?.price}, SKU: ${updatedVariant?.inventoryItem?.sku}`); - // Publish + // ── STEP 9: Publish ────────────────────────────────────────────────────── + console.log(`šŸ“¢ [PUBLISH] Fetching publications...`); const publicationsRes = await client.post('', { query: `query { publications(first: 10) { edges { node { id name } } } }` }); - const onlineStorePub = publicationsRes.data.data.publications.edges.find(p => p.node.name === 'Online Store'); + const allPubs = publicationsRes.data.data.publications.edges; + console.log(`šŸ“¢ [PUBLISH] Available publications: ${allPubs.map(p => p.node.name).join(', ')}`); + + const onlineStorePub = allPubs.find(p => p.node.name === 'Online Store'); if (onlineStorePub) { + console.log(`šŸ“¢ [PUBLISH] Publishing to Online Store (pub ID: ${onlineStorePub.node.id})...`); const publishRes = await client.post('', { query: `mutation($id: ID!, $publicationId: ID!) { publishablePublish(id: $id, input: { publicationId: $publicationId }) { publishable { ... on Product { id } } userErrors { field message } } }`, variables: { id: createdProduct.id, publicationId: onlineStorePub.node.id }, }); const publishErrs = publishRes.data.data.publishablePublish.userErrors; - if (publishErrs.length) throw new Error(`Publish errors: ${publishErrs.map(e => e.message).join(', ')}`); + if (publishErrs.length) { + console.error(`šŸ“¢ [PUBLISH] āŒ Publish errors: ${JSON.stringify(publishErrs)}`); + throw new Error(`Publish errors: ${publishErrs.map(e => e.message).join(', ')}`); + } + console.log(`šŸ“¢ [PUBLISH] āœ… Published to Online Store`); + } else { + console.warn(`šŸ“¢ [PUBLISH] āš ļø "Online Store" publication not found — product not published`); } - // Inventory + // ── STEP 10: Inventory cost ────────────────────────────────────────────── + console.log(`šŸ“¦ [INVENTORY] Updating inventory item — cost: $${parseFloat(attrs.purchase_cost) || 0}, tracked: true`); const invRes = await client.post('', { query: ` mutation InventoryItemUpdate($id: ID!, $input: InventoryItemInput!) { @@ -300,14 +423,24 @@ const AddProductToStore = async (shop, accessToken, product, jobId, locationId) variables: { id: inventoryItemId, input: { cost: parseFloat(attrs.purchase_cost) || 0, tracked: true } }, }); const invErrs = invRes.data.data.inventoryItemUpdate.userErrors; - if (invErrs.length) throw new Error(`Inventory update errors: ${invErrs.map(e => e.message).join(', ')}`); + if (invErrs.length) { + console.error(`šŸ“¦ [INVENTORY] āŒ inventoryItemUpdate errors: ${JSON.stringify(invErrs)}`); + throw new Error(`Inventory update errors: ${invErrs.map(e => e.message).join(', ')}`); + } + const invItem = invRes.data.data.inventoryItemUpdate.inventoryItem; + console.log(`šŸ“¦ [INVENTORY] āœ… Cost set to $${invItem?.unitCost?.amount || 0} ${invItem?.unitCost?.currencyCode || 'USD'}, tracked: ${invItem?.tracked}`); + // ── STEP 11: Activate inventory at location ────────────────────────────── if (locationId) { + console.log(`šŸ“ [INVENTORY-LOC] Activating inventory at location: ${locationId}`); await client.post('', { query: `mutation ActivateInventoryItem($inventoryItemId: ID!, $locationId: ID!) { inventoryActivate(inventoryItemId: $inventoryItemId, locationId: $locationId) { inventoryLevel { id } userErrors { field message } } }`, variables: { inventoryItemId, locationId }, }); + console.log(`šŸ“ [INVENTORY-LOC] āœ… Inventory activated at location`); + // ── STEP 12: Set quantity ──────────────────────────────────────────── + console.log(`šŸ“ [INVENTORY-QTY] Setting quantity to ${totalQuantity} at location ${locationId}`); await client.post('', { query: ` mutation InventorySet($input: InventorySetQuantitiesInput!) { @@ -327,9 +460,16 @@ const AddProductToStore = async (shop, accessToken, product, jobId, locationId) }, }, }); + console.log(`šŸ“ [INVENTORY-QTY] āœ… Quantity set to ${totalQuantity}`); + } else { + console.warn(`šŸ“ [INVENTORY-LOC] āš ļø No locationId on token record — inventory NOT activated or set`); } - // SEO update + // ── STEP 13: SEO ───────────────────────────────────────────────────────── + const finalSeoTitle = seo_title || `${attrs.product_name} | Auto Parts`; + const finalSeoDesc = seo_description || `Find high-quality ${attrs.product_name} built for reliability and performance.`; + console.log(`šŸ” [SEO] Setting SEO title: "${finalSeoTitle}"`); + console.log(`šŸ” [SEO] Setting SEO description (${finalSeoDesc.length} chars)`); await client.post('', { query: ` mutation ProductUpdate($product: ProductUpdateInput!) { @@ -343,17 +483,33 @@ const AddProductToStore = async (shop, accessToken, product, jobId, locationId) product: { id: createdProduct.id, seo: { - title: seo_title || `${attrs.product_name} | Auto Parts`, - description: seo_description || `Find high-quality ${attrs.product_name} built for reliability and performance.`, + title: finalSeoTitle, + description: finalSeoDesc, }, }, }, }); + console.log(`šŸ” [SEO] āœ… SEO fields updated`); + + console.log(`āœ… [PRODUCT-DONE] ============================================`); + console.log(`āœ… [PRODUCT-DONE] SUCCESS: "${productLabel}"`); + console.log(`āœ… [PRODUCT-DONE] Shopify ID: ${createdProduct.id}`); + console.log(`āœ… [PRODUCT-DONE] Handle: ${handle}`); + console.log(`āœ… [PRODUCT-DONE] Part#: ${attrs.part_number}`); + console.log(`āœ… [PRODUCT-DONE] Price: $${price.toFixed(2)}`); + console.log(`āœ… [PRODUCT-DONE] Qty: ${totalQuantity}`); + console.log(`āœ… [PRODUCT-DONE] ============================================\n`); appendJobLog(jobId, `[PRODUCT-OK] Created: ${attrs.product_name} (${attrs.part_number})`); return { action: 'created', productId: createdProduct.id, handle, product: attrs.product_name }; } catch (err) { + console.error(`āŒ [PRODUCT-FAIL] ============================================`); + console.error(`āŒ [PRODUCT-FAIL] FAILED: "${productLabel}"`); + console.error(`āŒ [PRODUCT-FAIL] Part#: ${attrs.part_number || 'N/A'}`); + console.error(`āŒ [PRODUCT-FAIL] Error: ${err.message}`); + console.error(`āŒ [PRODUCT-FAIL] Stack: ${err.stack}`); + console.error(`āŒ [PRODUCT-FAIL] ============================================\n`); appendJobLog(jobId, `[PRODUCT-FAIL] ${attrs.product_name || attrs.part_number}: ${err.message}`); return { action: 'failed', product: attrs.product_name || attrs.part_number, error: err.message }; } @@ -365,6 +521,16 @@ const AddProductToStore = async (shop, accessToken, product, jobId, locationId) router.post('/', async (req, res) => { const { shop, brandID, brandName, turn14accessToken, productCount, selectedProductIds } = req.body; + console.log(`\nšŸš€ [JOB-REQUEST] ================================================`); + console.log(`šŸš€ [JOB-REQUEST] POST /manageproducts received`); + console.log(`šŸš€ [JOB-REQUEST] Shop: ${shop}`); + console.log(`šŸš€ [JOB-REQUEST] Brand ID: ${brandID}`); + console.log(`šŸš€ [JOB-REQUEST] Brand Name: ${brandName}`); + console.log(`šŸš€ [JOB-REQUEST] Selected product IDs count: ${Array.isArray(selectedProductIds) ? selectedProductIds.length : 'NOT ARRAY'}`); + console.log(`šŸš€ [JOB-REQUEST] Selected IDs: ${JSON.stringify(selectedProductIds?.slice(0, 10))}${selectedProductIds?.length > 10 ? `... (+${selectedProductIds.length - 10} more)` : ''}`); + console.log(`šŸš€ [JOB-REQUEST] Turn14 token (first 20): ${turn14accessToken?.slice(0, 20)}...`); + console.log(`šŸš€ [JOB-REQUEST] ================================================\n`); + if (!shop) return res.status(400).json({ error: 'Missing shop' }); if (!turn14accessToken) return res.status(400).json({ error: 'Missing turn14accessToken' }); if (!brandID) return res.status(400).json({ error: 'Missing brandID' }); @@ -379,6 +545,10 @@ router.post('/', async (req, res) => { totalSelected: selectedProductIds.length, }); + console.log(`šŸ†” [JOB-CREATED] Job ID: ${job.id}`); + console.log(`šŸ†” [JOB-CREATED] Brand: ${brandName || brandID}`); + console.log(`šŸ†” [JOB-CREATED] Total to import: ${selectedProductIds.length}`); + log(shop, `[JOB] Created job ${job.id} for brand ${brandID} — ${selectedProductIds.length} products selected`); res.json({ processId: job.id, jobId: job.id, status: 'started' }); @@ -386,23 +556,54 @@ router.post('/', async (req, res) => { // Run async — do not await (async () => { try { + console.log(`\nāš™ļø [JOB-RUN] ====================================================`); + console.log(`āš™ļø [JOB-RUN] Starting async import for job ${job.id}`); + updateJob(job.id, { status: 'fetching_products', step: 'fetching_products', detail: `Fetching products for brand ${brandName || brandID} from Turn14...` }); + // ── Get Shopify token for this shop ────────────────────────────────── + console.log(`šŸ”‘ [JOB-TOKEN] Looking up Shopify token for shop: ${shop}`); const tokenRecord = getToken(shop); - if (!tokenRecord) throw new Error('No token stored for shop — re-authenticate'); + if (!tokenRecord) { + console.error(`šŸ”‘ [JOB-TOKEN] āŒ No token found for shop "${shop}" in tokens.json`); + throw new Error('No token stored for shop — re-authenticate'); + } + console.log(`šŸ”‘ [JOB-TOKEN] āœ… Token found`); + console.log(`šŸ”‘ [JOB-TOKEN] Token (first 20): ${tokenRecord.accessToken?.slice(0, 20)}...`); + console.log(`šŸ”‘ [JOB-TOKEN] Scope: ${tokenRecord.scope || 'N/A'}`); + console.log(`šŸ”‘ [JOB-TOKEN] Saved at: ${tokenRecord.savedAt || 'N/A'}`); + console.log(`šŸ”‘ [JOB-TOKEN] Location ID: ${tokenRecord.locationId || 'NOT SET āš ļø'}`); + console.log(`šŸ”‘ [JOB-TOKEN] Fulfillment Service: ${tokenRecord.fulfillmentService ? JSON.stringify(tokenRecord.fulfillmentService).slice(0, 80) : 'NOT SET'}`); const locationId = tokenRecord.locationId || null; const accessToken = tokenRecord.accessToken; - // Step 1: Fetch products from Turn14 + if (!locationId) { + console.warn(`āš ļø [JOB-TOKEN] locationId is NULL — inventory will NOT be set for products`); + } + + // ── Step 1: Fetch products from Turn14 ─────────────────────────────── + console.log(`\nšŸ“¦ [JOB-STEP-1] Fetching all products for brand ${brandID} from Turn14...`); const products_res = await GetAllProductsOfBranch(brandID, turn14accessToken, shop, job.id); - if (!products_res) throw new Error(`Failed to fetch products from Turn14 for brand ${brandID}`); + if (!products_res) { + console.error(`āŒ [JOB-STEP-1] Turn14 fetch returned null — aborting job`); + throw new Error(`Failed to fetch products from Turn14 for brand ${brandID}`); + } const allItems = products_res.items; + console.log(`šŸ“¦ [JOB-STEP-1] Total items from Turn14: ${allItems.length}`); - // Step 2: Filter to selected IDs + // ── Step 2: Filter to selected IDs ─────────────────────────────────── + console.log(`\nšŸ”½ [JOB-STEP-2] Filtering to selected ${selectedProductIds.length} product IDs...`); const products = allItems.filter(item => selectedProductIds.includes(item.id)); const total = products.length; + console.log(`šŸ”½ [JOB-STEP-2] Matched ${total} out of ${allItems.length} Turn14 items`); + + if (total === 0) { + console.warn(`āš ļø [JOB-STEP-2] No matching products found! Check if selected IDs exist in brand ${brandID}`); + console.warn(`āš ļø [JOB-STEP-2] Selected IDs: ${JSON.stringify(selectedProductIds)}`); + console.warn(`āš ļø [JOB-STEP-2] Available IDs (first 10): ${allItems.slice(0, 10).map(i => i.id).join(', ')}`); + } updateJob(job.id, { status: 'importing', @@ -412,10 +613,14 @@ router.post('/', async (req, res) => { }); appendJobLog(job.id, `[IMPORT-START] Importing ${total} products for brand ${brandName || brandID}`); + console.log(`\nšŸ [JOB-STEP-3] Starting product-by-product import (${total} products)...`); + console.log(`āš™ļø [JOB-STEP-3] ====================================================\n`); + let created = 0, skipped = 0, failed = 0; for (let i = 0; i < products.length; i++) { if (isJobCancelled(job.id)) { + console.log(`šŸ›‘ [JOB-CANCEL] Job ${job.id} cancelled at product ${i + 1}/${total}`); appendJobLog(job.id, '[CANCEL] Import cancelled by user'); finishJob(job.id, 'cancelled'); return; @@ -426,6 +631,13 @@ router.post('/', async (req, res) => { const productLabel = attrs.product_name || attrs.part_number || `Item ${item.id}`; const partNum = attrs.part_number || ''; + console.log(`\nšŸ“‹ [PRODUCT-LOOP] ─────────────────────────────────────────`); + console.log(`šŸ“‹ [PRODUCT-LOOP] Product ${i + 1} of ${total}`); + console.log(`šŸ“‹ [PRODUCT-LOOP] Name: ${productLabel}`); + console.log(`šŸ“‹ [PRODUCT-LOOP] Part#: ${partNum}`); + console.log(`šŸ“‹ [PRODUCT-LOOP] Turn14 ID: ${item.id}`); + console.log(`šŸ“‹ [PRODUCT-LOOP] Running totals → created:${created} skipped:${skipped} failed:${failed}`); + updateJob(job.id, { currentProduct: { name: productLabel, partNumber: partNum, number: i + 1, total }, detail: `Importing product ${i + 1}/${total}: ${productLabel}`, @@ -434,11 +646,19 @@ router.post('/', async (req, res) => { const result = await AddProductToStore(shop, accessToken, item, job.id, locationId); - if (result?.action === 'created') created++; - else if (result?.action === 'skipped') skipped++; - else if (result?.action === 'failed') { + if (result?.action === 'created') { + created++; + console.log(`šŸ“‹ [PRODUCT-LOOP] āœ… CREATED — total created so far: ${created}`); + } else if (result?.action === 'skipped') { + skipped++; + console.log(`šŸ“‹ [PRODUCT-LOOP] ā­ļø SKIPPED — total skipped so far: ${skipped}`); + } else if (result?.action === 'failed') { failed++; + console.error(`šŸ“‹ [PRODUCT-LOOP] āŒ FAILED (${result?.error}) — total failed so far: ${failed}`); recordProductError(job.id, { index: i + 1, product: productLabel, error: result?.error || 'unknown error' }); + } else { + console.warn(`šŸ“‹ [PRODUCT-LOOP] āš ļø NULL result returned for ${productLabel}`); + failed++; } if (result) recordProductResult(job.id, result); @@ -446,11 +666,12 @@ router.post('/', async (req, res) => { const processed = i + 1; const successRate = processed > 0 ? Number((((processed - failed) / processed) * 100).toFixed(1)) : 0; + console.log(`šŸ“‹ [PRODUCT-LOOP] Progress: ${processed}/${total} (${successRate}% success rate)`); + updateJob(job.id, { liveStats: { total, processed, created, skipped, failed, remaining: total - processed, successRate, label: `${processed}/${total}` }, }); - // Emit structured line every 5 products for dashboard if (processed % 5 === 0 || processed === total) { appendJobLog(job.id, `[STATS] total=${total} processed=${processed} created=${created} skipped=${skipped} failed=${failed} rate=${successRate}`); } @@ -459,9 +680,27 @@ router.post('/', async (req, res) => { updateJob(job.id, { currentProduct: null }); appendJobLog(job.id, `[IMPORT-DONE] Finished: ${created} created, ${skipped} skipped, ${failed} failed`); finishJob(job.id, 'done'); + + console.log(`\nšŸŽ‰ [JOB-DONE] ====================================================`); + console.log(`šŸŽ‰ [JOB-DONE] Job ${job.id} COMPLETED`); + console.log(`šŸŽ‰ [JOB-DONE] Shop: ${shop}`); + console.log(`šŸŽ‰ [JOB-DONE] Brand: ${brandName || brandID}`); + console.log(`šŸŽ‰ [JOB-DONE] Total selected: ${total}`); + console.log(`šŸŽ‰ [JOB-DONE] āœ… Created: ${created}`); + console.log(`šŸŽ‰ [JOB-DONE] ā­ļø Skipped: ${skipped}`); + console.log(`šŸŽ‰ [JOB-DONE] āŒ Failed: ${failed}`); + console.log(`šŸŽ‰ [JOB-DONE] Success rate: ${total > 0 ? ((created / total) * 100).toFixed(1) : 0}%`); + console.log(`šŸŽ‰ [JOB-DONE] ====================================================\n`); + log(shop, `[JOB] ${job.id} completed — created=${created} skipped=${skipped} failed=${failed}`); } catch (err) { + console.error(`\nšŸ’„ [JOB-ERROR] ====================================================`); + console.error(`šŸ’„ [JOB-ERROR] Job ${job.id} CRASHED`); + console.error(`šŸ’„ [JOB-ERROR] Shop: ${shop}`); + console.error(`šŸ’„ [JOB-ERROR] Error: ${err.message}`); + console.error(`šŸ’„ [JOB-ERROR] Stack: ${err.stack}`); + console.error(`šŸ’„ [JOB-ERROR] ====================================================\n`); appendJobLog(job.id, `[ERROR] ${err.message}`); updateJob(job.id, { status: 'error', step: 'error', detail: err.message, currentProduct: null }); finishJob(job.id, 'error'); @@ -479,17 +718,13 @@ router.get('/status/:processId', (req, res) => { const s = job.liveStats; - // Legacy-compatible shape + full job for new dashboard res.json({ - // Legacy fields (managebrand.jsx polling) status: job.status, detail: job.detail, progress: s.total > 0 ? Math.round((s.processed / s.total) * 100) : 0, current: job.currentProduct, stats: { total: s.total, processed: s.processed, remaining: s.remaining }, results: job.results, - - // Full job object for dashboard job, }); });