debug: add exhaustive console.log to entire product import pipeline

Logs every step for each product:
- Job request received (shop, brand, selected IDs)
- Shopify token lookup (token, scope, locationId, fulfillmentService)
- Turn14 fetch (URL, HTTP status, item count, response time)
- Per-product: all attributes, inventory, fitment tags
- Collections: each title looked up/created with IDs
- Dedup check: handle existence
- productCreate: title, vendor, collections, media count
- Pricing: metafield config, base price, final price, markup
- Variant update: SKU, price, weight
- Publish: available publications, Online Store result
- Inventory: cost, activate at location, quantity set
- SEO: title and description
- Job completion summary: created/skipped/failed counts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
MOHAN 2026-07-02 23:39:35 +05:30
parent c2b5168a04
commit b7fa2a21a9

View File

@ -55,8 +55,16 @@ function extractFirstJsonObject(text) {
// Fetch all products for a brand from Turn14 // Fetch all products for a brand from Turn14
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const GetAllProductsOfBranch = async (brandId, turn14accessToken, shop, jobId) => { 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 { try {
appendJobLog(jobId, `[FETCH] Fetching products for brand ${brandId} from Turn14...`); appendJobLog(jobId, `[FETCH] Fetching products for brand ${brandId} from Turn14...`);
const fetchStart = Date.now();
const res = await fetch( const res = await fetch(
`https://turn14.data4autos.com/v1/items/brandallitemswithfitment/${brandId}`, `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 res_data = await res.json();
const data = res_data.items || []; const data = res_data.items || [];
const fitmentTags = res_data.fitmentTags || []; 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) const validItems = Array.isArray(data)
? data.filter(item => item && item.id && item.attributes) ? 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}`); appendJobLog(jobId, `[FETCH-OK] Found ${validItems.length} products for brand ${brandId}`);
return { items: validItems, fitmentTags }; return { items: validItems, fitmentTags };
} catch (err) { } 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}`); appendJobLog(jobId, `[FETCH-FAIL] Error fetching brand ${brandId}: ${err.message}`);
return null; return null;
} }
@ -89,6 +116,30 @@ const AddProductToStore = async (shop, accessToken, product, jobId, locationId)
const item = product; const item = product;
const attrs = item.attributes; 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) // SEO (stubbed — returns empty so we use defaults)
let parsed = { seo_title: '', seo_description: '' }; let parsed = { seo_title: '', seo_description: '' };
const { seo_title, seo_description } = parsed; const { seo_title, seo_description } = parsed;
@ -102,6 +153,10 @@ const AddProductToStore = async (shop, accessToken, product, jobId, locationId)
const convertedFitment = {}; const convertedFitment = {};
for (const key in globalUniqueFitmentMap) convertedFitment[key] = Array.from(globalUniqueFitmentMap[key]); for (const key in globalUniqueFitmentMap) convertedFitment[key] = Array.from(globalUniqueFitmentMap[key]);
const allFitmentTags = Array.from(new Set(Object.values(convertedFitment).flat())); 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({ const client = axios.create({
baseURL: `https://${SHOP}/admin/api/2025-10/graphql.json`, baseURL: `https://${SHOP}/admin/api/2025-10/graphql.json`,
@ -109,38 +164,46 @@ const AddProductToStore = async (shop, accessToken, product, jobId, locationId)
}); });
try { try {
const inventoryData = attrs.inventorydata?.inventory || {}; // ── STEP 1: Collections ──────────────────────────────────────────────────
const totalQuantity = Object.values(inventoryData).reduce((sum, val) => sum + val, 0);
// Collections
const category = attrs.category || ''; const category = attrs.category || '';
const subcategory = attrs.subcategory || ''; const subcategory = attrs.subcategory || '';
const brand = attrs.brand || ''; const brand = attrs.brand || '';
const subcats = subcategory.split(/[,\/]/).map(s => s.trim()).filter(Boolean); const subcats = subcategory.split(/[,\/]/).map(s => s.trim()).filter(Boolean);
const collectionTitles = Array.from(new Set([category, ...subcats, brand, ...allFitmentTags].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 = []; const collectionIds = [];
for (const title of collectionTitles) { for (const title of collectionTitles) {
console.log(`📁 [COLLECTIONS] Looking up: "${title}"`);
const lookupResp = await client.post('', { const lookupResp = await client.post('', {
query: `query { collections(first: 1, query: "title:\\"${title}\\" AND collection_type:manual") { nodes { id } } }` query: `query { collections(first: 1, query: "title:\\"${title}\\" AND collection_type:manual") { nodes { id } } }`
}); });
const existing = lookupResp.data.data.collections.nodes; const existing = lookupResp.data.data.collections.nodes;
if (existing.length) { if (existing.length) {
console.log(`📁 [COLLECTIONS] ✅ Found existing: ${existing[0].id}`);
collectionIds.push(existing[0].id); collectionIds.push(existing[0].id);
continue; continue;
} }
console.log(`📁 [COLLECTIONS] Creating new collection: "${title}"`);
const createResp = await client.post('', { const createResp = await client.post('', {
query: `mutation collectionCreate($input: CollectionInput!) { collectionCreate(input: $input) { collection { id } userErrors { field message } } }`, query: `mutation collectionCreate($input: CollectionInput!) { collectionCreate(input: $input) { collection { id } userErrors { field message } } }`,
variables: { input: { title } }, variables: { input: { title } },
}); });
const createData = createResp.data.data.collectionCreate; const createData = createResp.data.data.collectionCreate;
if (createData.userErrors.length) { 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(', ')}`); 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); collectionIds.push(createData.collection.id);
} }
// Tags console.log(`📁 [COLLECTIONS] Done. ${collectionIds.length} collection IDs collected`);
// ── STEP 2: Build tags ───────────────────────────────────────────────────
const productTags = [ const productTags = [
attrs.category, attrs.category,
...subcats, ...subcats,
@ -152,8 +215,9 @@ const AddProductToStore = async (shop, accessToken, product, jobId, locationId)
attrs.units_per_sku && `${attrs.units_per_sku} per SKU`, attrs.units_per_sku && `${attrs.units_per_sku} per SKU`,
attrs.barcode, attrs.barcode,
].filter(Boolean).map(t => t.trim()); ].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 || []) const mediaInputs = (attrs.files || [])
.filter(f => f.type === 'Image' && f.url) .filter(f => f.type === 'Image' && f.url)
.map(file => ({ .map(file => ({
@ -161,28 +225,46 @@ const AddProductToStore = async (shop, accessToken, product, jobId, locationId)
mediaContentType: 'IMAGE', mediaContentType: 'IMAGE',
alt: `${attrs.product_name}${file.media_content}`, 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 || []) const marketDescs = (attrs.descriptions || [])
.filter(d => d.type === 'Market Description') .filter(d => d.type === 'Market Description')
.map(d => d.description); .map(d => d.description);
const descriptionHtml = marketDescs.length const descriptionHtml = marketDescs.length
? marketDescs.reduce((a, b) => (b.length > a.length ? b : a)) ? marketDescs.reduce((a, b) => (b.length > a.length ? b : a))
: attrs.part_description; : attrs.part_description;
console.log(`📝 [DESC] Market descriptions found: ${marketDescs.length}`);
console.log(`📝 [DESC] Description length: ${(descriptionHtml || '').length} chars`);
const handle = slugify(item.id); 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('', { const searchRes = await client.post('', {
query: `query { products(first: 1, query: "handle:${handle}") { nodes { id handle } } }` query: `query { products(first: 1, query: "handle:${handle}") { nodes { id handle } } }`
}); });
const exists = searchRes.data?.data?.products?.nodes?.length > 0; const exists = searchRes.data?.data?.products?.nodes?.length > 0;
if (exists) { 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`); appendJobLog(jobId, `[SKIP] ${attrs.part_number} — handle "${handle}" already exists`);
return { action: 'skipped', handle, product: attrs.product_name || attrs.part_number }; 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('', { const createProdRes = await client.post('', {
query: ` query: `
mutation ProductCreate($product: ProductCreateInput!, $media: [CreateMediaInput!]) { 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 || []; const prodErrs = createProdRes.data.data?.productCreate?.userErrors || [];
if (prodErrs.length) { if (prodErrs.length) {
console.error(`🆕 [CREATE] ❌ productCreate userErrors: ${JSON.stringify(prodErrs)}`);
const taken = prodErrs.some(e => /already in use/i.test(e.message)); const taken = prodErrs.some(e => /already in use/i.test(e.message));
if (taken) { if (taken) {
console.log(`🆕 [CREATE] ⚠️ Duplicate handle — SKIPPING`);
appendJobLog(jobId, `[SKIP] ${attrs.part_number} — duplicate handle`); appendJobLog(jobId, `[SKIP] ${attrs.part_number} — duplicate handle`);
return { action: 'skipped', handle, product: attrs.product_name || attrs.part_number }; 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 createdProduct = createProdRes.data.data.productCreate.product;
const variantNode = createdProduct.variants?.nodes?.[0]; 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 variantId = variantNode.id;
const inventoryItemId = variantNode.inventoryItem?.id; const inventoryItemId = variantNode.inventoryItem?.id;
// Pricing // ── STEP 7: Pricing ──────────────────────────────────────────────────────
console.log(`💰 [PRICING] Reading pricing config from Shopify metafield...`);
const pricingConfigRes = await client.post('', { const pricingConfigRes = await client.post('', {
query: `query { shop { metafield(namespace: "turn14", key: "pricing_config") { value } } }` 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); const p = JSON.parse(pricingMf.value);
priceType = p.priceType || 'map'; priceType = p.priceType || 'map';
percentage = Number(p.percentage) || 0; 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; const baseprice = parseFloat(attrs.price) || 0;
let price = baseprice; let price = baseprice;
if (priceType === 'percentage') price = baseprice + (baseprice * (percentage / 100)); 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 barcode = attrs.barcode || '';
const weightValue = parseFloat(attrs.dimensions?.[0]?.weight) || 0; 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('', { const bulkRes = await client.post('', {
query: ` query: `
mutation UpdateProductVariant($productId: ID!, $variants: [ProductVariantsBulkInput!]!) { 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; 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('', { const publicationsRes = await client.post('', {
query: `query { publications(first: 10) { edges { node { id name } } } }` 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) { if (onlineStorePub) {
console.log(`📢 [PUBLISH] Publishing to Online Store (pub ID: ${onlineStorePub.node.id})...`);
const publishRes = await client.post('', { const publishRes = await client.post('', {
query: `mutation($id: ID!, $publicationId: ID!) { publishablePublish(id: $id, input: { publicationId: $publicationId }) { publishable { ... on Product { id } } userErrors { field message } } }`, 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 }, variables: { id: createdProduct.id, publicationId: onlineStorePub.node.id },
}); });
const publishErrs = publishRes.data.data.publishablePublish.userErrors; 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('', { const invRes = await client.post('', {
query: ` query: `
mutation InventoryItemUpdate($id: ID!, $input: InventoryItemInput!) { 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 } }, variables: { id: inventoryItemId, input: { cost: parseFloat(attrs.purchase_cost) || 0, tracked: true } },
}); });
const invErrs = invRes.data.data.inventoryItemUpdate.userErrors; 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) { if (locationId) {
console.log(`📍 [INVENTORY-LOC] Activating inventory at location: ${locationId}`);
await client.post('', { await client.post('', {
query: `mutation ActivateInventoryItem($inventoryItemId: ID!, $locationId: ID!) { inventoryActivate(inventoryItemId: $inventoryItemId, locationId: $locationId) { inventoryLevel { id } userErrors { field message } } }`, query: `mutation ActivateInventoryItem($inventoryItemId: ID!, $locationId: ID!) { inventoryActivate(inventoryItemId: $inventoryItemId, locationId: $locationId) { inventoryLevel { id } userErrors { field message } } }`,
variables: { inventoryItemId, locationId }, 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('', { await client.post('', {
query: ` query: `
mutation InventorySet($input: InventorySetQuantitiesInput!) { 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('', { await client.post('', {
query: ` query: `
mutation ProductUpdate($product: ProductUpdateInput!) { mutation ProductUpdate($product: ProductUpdateInput!) {
@ -343,17 +483,33 @@ const AddProductToStore = async (shop, accessToken, product, jobId, locationId)
product: { product: {
id: createdProduct.id, id: createdProduct.id,
seo: { seo: {
title: seo_title || `${attrs.product_name} | Auto Parts`, title: finalSeoTitle,
description: seo_description || `Find high-quality ${attrs.product_name} built for reliability and performance.`, 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})`); appendJobLog(jobId, `[PRODUCT-OK] Created: ${attrs.product_name} (${attrs.part_number})`);
return { action: 'created', productId: createdProduct.id, handle, product: attrs.product_name }; return { action: 'created', productId: createdProduct.id, handle, product: attrs.product_name };
} catch (err) { } 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}`); 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 }; 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) => { router.post('/', async (req, res) => {
const { shop, brandID, brandName, turn14accessToken, productCount, selectedProductIds } = req.body; 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 (!shop) return res.status(400).json({ error: 'Missing shop' });
if (!turn14accessToken) return res.status(400).json({ error: 'Missing turn14accessToken' }); if (!turn14accessToken) return res.status(400).json({ error: 'Missing turn14accessToken' });
if (!brandID) return res.status(400).json({ error: 'Missing brandID' }); if (!brandID) return res.status(400).json({ error: 'Missing brandID' });
@ -379,6 +545,10 @@ router.post('/', async (req, res) => {
totalSelected: selectedProductIds.length, 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`); log(shop, `[JOB] Created job ${job.id} for brand ${brandID}${selectedProductIds.length} products selected`);
res.json({ processId: job.id, jobId: job.id, status: 'started' }); res.json({ processId: job.id, jobId: job.id, status: 'started' });
@ -386,23 +556,54 @@ router.post('/', async (req, res) => {
// Run async — do not await // Run async — do not await
(async () => { (async () => {
try { 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...` }); 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); 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 locationId = tokenRecord.locationId || null;
const accessToken = tokenRecord.accessToken; 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); 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; 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 products = allItems.filter(item => selectedProductIds.includes(item.id));
const total = products.length; 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, { updateJob(job.id, {
status: 'importing', status: 'importing',
@ -412,10 +613,14 @@ router.post('/', async (req, res) => {
}); });
appendJobLog(job.id, `[IMPORT-START] Importing ${total} products for brand ${brandName || brandID}`); 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; let created = 0, skipped = 0, failed = 0;
for (let i = 0; i < products.length; i++) { for (let i = 0; i < products.length; i++) {
if (isJobCancelled(job.id)) { 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'); appendJobLog(job.id, '[CANCEL] Import cancelled by user');
finishJob(job.id, 'cancelled'); finishJob(job.id, 'cancelled');
return; return;
@ -426,6 +631,13 @@ router.post('/', async (req, res) => {
const productLabel = attrs.product_name || attrs.part_number || `Item ${item.id}`; const productLabel = attrs.product_name || attrs.part_number || `Item ${item.id}`;
const partNum = attrs.part_number || ''; 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, { updateJob(job.id, {
currentProduct: { name: productLabel, partNumber: partNum, number: i + 1, total }, currentProduct: { name: productLabel, partNumber: partNum, number: i + 1, total },
detail: `Importing product ${i + 1}/${total}: ${productLabel}`, 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); const result = await AddProductToStore(shop, accessToken, item, job.id, locationId);
if (result?.action === 'created') created++; if (result?.action === 'created') {
else if (result?.action === 'skipped') skipped++; created++;
else if (result?.action === 'failed') { 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++; 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' }); 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); if (result) recordProductResult(job.id, result);
@ -446,11 +666,12 @@ router.post('/', async (req, res) => {
const processed = i + 1; const processed = i + 1;
const successRate = processed > 0 ? Number((((processed - failed) / processed) * 100).toFixed(1)) : 0; 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, { updateJob(job.id, {
liveStats: { total, processed, created, skipped, failed, remaining: total - processed, successRate, label: `${processed}/${total}` }, 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) { if (processed % 5 === 0 || processed === total) {
appendJobLog(job.id, `[STATS] total=${total} processed=${processed} created=${created} skipped=${skipped} failed=${failed} rate=${successRate}`); 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 }); updateJob(job.id, { currentProduct: null });
appendJobLog(job.id, `[IMPORT-DONE] Finished: ${created} created, ${skipped} skipped, ${failed} failed`); appendJobLog(job.id, `[IMPORT-DONE] Finished: ${created} created, ${skipped} skipped, ${failed} failed`);
finishJob(job.id, 'done'); 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}`); log(shop, `[JOB] ${job.id} completed — created=${created} skipped=${skipped} failed=${failed}`);
} catch (err) { } 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}`); appendJobLog(job.id, `[ERROR] ${err.message}`);
updateJob(job.id, { status: 'error', step: 'error', detail: err.message, currentProduct: null }); updateJob(job.id, { status: 'error', step: 'error', detail: err.message, currentProduct: null });
finishJob(job.id, 'error'); finishJob(job.id, 'error');
@ -479,17 +718,13 @@ router.get('/status/:processId', (req, res) => {
const s = job.liveStats; const s = job.liveStats;
// Legacy-compatible shape + full job for new dashboard
res.json({ res.json({
// Legacy fields (managebrand.jsx polling)
status: job.status, status: job.status,
detail: job.detail, detail: job.detail,
progress: s.total > 0 ? Math.round((s.processed / s.total) * 100) : 0, progress: s.total > 0 ? Math.round((s.processed / s.total) * 100) : 0,
current: job.currentProduct, current: job.currentProduct,
stats: { total: s.total, processed: s.processed, remaining: s.remaining }, stats: { total: s.total, processed: s.processed, remaining: s.remaining },
results: job.results, results: job.results,
// Full job object for dashboard
job, job,
}); });
}); });