const axios = require('axios'); const https = require('https'); const fs = require('fs'); const readline = require('readline'); const path = require('path'); // ─── 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(__dirname, '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 }); for await (const line of rl) { totalLines++; try { const rec = JSON.parse(line); const h = rec.handle; if (desiredInventoryByHandle.hasOwnProperty(h)) { handleMap[h] = rec.variants.edges.map(v => ({ variantId: v.node.id, inventoryItemId: v.node.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)); 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; } // ─── 5. BUILD BULK MUTATION ─────────────────────────────────────────────────────── function makeBulkUpdateMutation(handleMap) { console.log('[5] 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('[5] ✔️ Mutation built with', ops.length, 'operations'); 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 ───────────────────────────────────────────────────────────────────────── async function main() { try { // Fetch inventory from Turn14 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 } } ); 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); } } main();