import axios from 'axios'; import https from 'https'; import fs from 'fs'; import readline from 'readline'; import path from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // ─── CONFIG ───────────────────────────────────────────────────────────────────── const SHOP = 'veloxautomotive.myshopify.com'; const ACCESS_TOKEN = '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'; const API_VERSION = '2023-10'; const TURN14_ACCESS_TOKEN = '4d82c2d2a2ddb59a6da68afb722f50dd43f4640b'; // ─── HELPERS ──────────────────────────────────────────────────────────────────── function timestamp() { const now = new Date(); const yyyy = now.getFullYear(); const MM = String(now.getMonth() + 1).padStart(2, '0'); const dd = String(now.getDate()).padStart(2, '0'); const hh = String(now.getHours()).padStart(2, '0'); const mm = String(now.getMinutes()).padStart(2, '0'); const ss = String(now.getSeconds()).padStart(2, '0'); return `${yyyy}${MM}${dd}_${hh}${mm}${ss}`; } // ─── GLOBALS ───────────────────────────────────────────────────────────────────── const desiredInventoryByHandle = {}; const exportDir = path.join(path.dirname(new URL(import.meta.url).pathname), 'exports'); // ─── 1. START BULK EXPORT ──────────────────────────────────────────────────────── async function startBulkExport() { console.log('[1] Starting bulk export…'); const mutation = ` mutation { bulkOperationRunQuery( query: """ { products { edges { node { id handle variants { edges { node { id inventoryItem { id } } } } } } } } """ ) { bulkOperation { id status } userErrors { field message } } } `; const resp = await axios.post( `https://${SHOP}/admin/api/${API_VERSION}/graphql.json`, { query: mutation }, { headers: { 'X-Shopify-Access-Token': ACCESS_TOKEN } } ); const err = resp.data.data.bulkOperationRunQuery.userErrors; if (err.length) { console.error('[1] ❌ Errors starting export:', err); throw new Error('Error starting export'); } console.log('[1] ✔️ Export kicked off:', resp.data.data.bulkOperationRunQuery.bulkOperation); } // ─── 2. POLL UNTIL READY ───────────────────────────────────────────────────────── async function pollUntilReady() { console.log('[2] Polling for bulk-export completion…'); const query = `{ currentBulkOperation { status url errorCode } }`; while (true) { const resp = await axios.post( `https://${SHOP}/admin/api/${API_VERSION}/graphql.json`, { query }, { headers: { 'X-Shopify-Access-Token': ACCESS_TOKEN } } ); const op = resp.data.data.currentBulkOperation; console.log(`[2] Status: ${op.status}`); if (op.status === 'COMPLETED') { console.log('[2] ✔️ Completed. URL:', op.url); return op.url; } if (op.status === 'FAILED') { console.error('[2] ❌ Failed with code:', op.errorCode); throw new Error('Bulk operation failed'); } await new Promise(r => setTimeout(r, 5000)); } } // ─── 3. DOWNLOAD FILE ──────────────────────────────────────────────────────────── async function downloadToFile(fileUrl) { const fname = path.join(exportDir, `${timestamp()}_bulk_export.ndjson`); console.log('[3] Downloading export to', fname); const fileStream = fs.createWriteStream(fname); await new Promise((resolve, reject) => { https.get(fileUrl, res => { res.pipe(fileStream); fileStream.on('finish', () => { console.log('[3] ✔️ Download complete'); fileStream.close(resolve); }); fileStream.on('error', err => { console.error('[3] ❌ Download error:', err); reject(err); }); }); }); return fname; } // ─── 4. PARSE HANDLE MAP ───────────────────────────────────────────────────────── // async function buildHandleMap(ndjsonPath) { // console.log('[4] Parsing NDJSON and building handle map…'); // const handleMap = {}; // let totalLines = 0; // const rl = readline.createInterface({ // input: fs.createReadStream(ndjsonPath), // crlfDelay: Infinity // }); // desiredInventoryByHandle["84485"] = 2000; // for await (const line of rl) { // totalLines++; // try { // const rec = JSON.parse(line); // const h = rec.handle; // if (desiredInventoryByHandle.hasOwnProperty(h)) { // const invtdataline = JSON.parse(line + 1); // handleMap[h] = { // variantId: invtdataline.id, // inventoryItemId: invtdataline.inventoryItem.id // }; // } // } catch (e) { // console.error('[4] ⚠️ JSON parse error on line', totalLines, e); // } // } // console.log(handleMap) // console.log(`[4] ✔️ Parsed ${totalLines} lines. Matched handles:`, Object.keys(handleMap)); // const mapPath = path.join(exportDir, `${timestamp()}_handle_map.json`); // fs.writeFileSync(mapPath, JSON.stringify(handleMap, null, 2)); // console.log('[4] ✔️ Saved handle→inventory map to', mapPath); // return handleMap; // } async function buildHandleMap(ndjsonPath) { console.log('[4] Parsing NDJSON and building handle map…'); desiredInventoryByHandle["84485"] = 2000; const handleMap = {}; const productIdToHandle = {}; let totalLines = 0; const rl = readline.createInterface({ input: fs.createReadStream(ndjsonPath), crlfDelay: Infinity }); for await (const line of rl) { totalLines++; try { const rec = JSON.parse(line); // It's a product line if (rec.handle && rec.id) { productIdToHandle[rec.id] = rec.handle; } // It's a variant line if (rec.__parentId && rec.inventoryItem && rec.inventoryItem.id) { const handle = productIdToHandle[rec.__parentId]; if (handle && desiredInventoryByHandle.hasOwnProperty(handle)) { if (!handleMap[handle]) handleMap[handle] = []; handleMap[handle].push({ variantId: rec.id, inventoryItemId: rec.inventoryItem.id }); } } } catch (e) { console.error('[4] ⚠️ JSON parse error on line', totalLines, e); } } console.log(`[4] ✔️ Parsed ${totalLines} lines. Matched handles: ${Object.keys(handleMap).length}`); // Save map to disk if needed const mapPath = path.join(exportDir, `${timestamp()}_handle_map.json`); fs.writeFileSync(mapPath, JSON.stringify(handleMap, null, 2)); console.log('[4] ✔️ Saved handle→inventory map to', mapPath); return handleMap; } function writeBulkInventoryJSONL(handleMap, desiredInventoryByHandle, outputPath) { console.log('[5] Creating JSONL file for bulk inventory update…'); const stream = fs.createWriteStream(outputPath); let lineCount = 0; for (const [handle, items] of Object.entries(handleMap)) { const qty = desiredInventoryByHandle[handle]; if (qty == null) continue; items.forEach(({ inventoryItemId }) => { const line = JSON.stringify({ input: { inventoryItemId, availableDelta: qty } }); stream.write(line + '\n'); lineCount++; }); } stream.end(); console.log(`[5] ✔️ Wrote ${lineCount} inventory adjustments to ${outputPath}`); } // ─── 5. BUILD BULK MUTATION ─────────────────────────────────────────────────────── function makeBulkUpdateMutation(handleMap) { const filePath = path.join(__dirname, 'exports', 'bulk_inventory.jsonl'); // Create the JSONL file instead of building a GraphQL mutation string writeBulkInventoryJSONL(handleMap, desiredInventoryByHandle, filePath); console.log('[6] Building bulk update mutation…'); const ops = []; for (const [handle, items] of Object.entries(handleMap)) { const qty = desiredInventoryByHandle[handle]; items.forEach(({ inventoryItemId }) => { ops.push(` inventoryAdjustQuantity( input: {inventoryItemId: "${inventoryItemId}", availableDelta: ${qty}} ) { inventoryLevel { id available } userErrors { field message } } `); }); } const full = ` mutation { bulkOperationRunMutation( mutation: """ mutation { ${ops.join('\n')} } """ ) { bulkOperation { id status } userErrors { field message } } } `; console.log('[6] ✔️ Mutation built with', ops.length, 'operations'); console.log(full) return full; } // ─── FETCH LOCATIONS (OPTIONAL DEBUGGING) ──────────────────────────────────────── async function fetchAllLocations(pageSize = 250) { const endpoint = `https://${SHOP}/admin/api/${API_VERSION}/graphql.json`; let hasNextPage = true; let cursor = null; const allLocations = []; while (hasNextPage) { const query = ` query ($first: Int!, $after: String) { locations(first: $first, after: $after) { pageInfo { hasNextPage } edges { cursor node { id name } } } } `; const variables = { first: pageSize, after: cursor }; const resp = await axios.post( endpoint, { query, variables }, { headers: { 'X-Shopify-Access-Token': ACCESS_TOKEN } } ); const locs = resp.data.data.locations; locs.edges.forEach(edge => { allLocations.push(edge.node); }); hasNextPage = locs.pageInfo.hasNextPage; cursor = hasNextPage ? locs.edges[locs.edges.length - 1].cursor : null; } console.log(`✅ Retrieved ${allLocations.length} locations`); return allLocations; } // ─── MAIN ───────────────────────────────────────────────────────────────────────── try { const turn14Res = await fetch(`https://turn14.data4autos.com/v1/inventory/allupdates`, { headers: { Authorization: `Bearer ${TURN14_ACCESS_TOKEN}`, "Content-Type": "application/json", }, }); const turn14Data = await turn14Res.json(); for (const item of turn14Data) { desiredInventoryByHandle[item.id] = item.totalQuantity; } console.log('✅ Inventory fetched from Turn14:', Object.keys(desiredInventoryByHandle).length, 'items'); //console.log(desiredInventoryByHandle); if (!fs.existsSync(exportDir)) { console.log('Creating exports directory at', exportDir); fs.mkdirSync(exportDir); } await fetchAllLocations(20); await startBulkExport(); const url = await pollUntilReady(); const ndjsonPath = await downloadToFile(url); const handleMap = await buildHandleMap(ndjsonPath); const mutation = makeBulkUpdateMutation(handleMap); console.log('[5] Kicking off inventory update…'); const resp = await axios.post( `https://${SHOP}/admin/api/${API_VERSION}/graphql.json`, { query: mutation }, { headers: { 'X-Shopify-Access-Token': ACCESS_TOKEN } } ); console.log('🚧 Full response:', JSON.stringify(resp.data, null, 2)); console.log(resp.data.data) const errs = resp.data.data.bulkOperationRunMutation.userErrors; if (errs.length) { console.error('[5] ❌ Inventory bulk update errors:', errs); } else { console.log('[5] ✔️ Inventory bulk update kickstarted successfully'); } } catch (err) { console.error('🚨 Uncaught error:', err); process.exit(1); }