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:
parent
c2b5168a04
commit
b7fa2a21a9
@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user