import refreshTokenHelper from "../../../utils/refreshTokenHelper" import errorHandlingHelper from "../../../utils/errorHandlingHelper" import fetchHelper from "../../../utils/fetchHelper" // GET /api/shopify/stocks?orderSourceId=123&variantIds=1,2,3 // Fetches current stock from Shopify for given SKUs // Uses REST API: fetches all products to build SKU->inventory_item_id map, then fetches inventory levels const handleFunc = async (event: any, authToken: any = null) => { const token = authToken ?? await getTokenHelper(event) const query = getQuery(event) const orderSourceId = query.orderSourceId as string const variantIdsParam = query.variantIds as string if(!orderSourceId) { return { status: 400, message: 'orderSourceId is required' } } if(!variantIdsParam) { return { status: 400, message: 'variantIds is required' } } const skus = variantIdsParam.split(',').map(id => id.trim()).filter(Boolean) if(skus.length === 0) { return { status: 400, message: 'No valid SKUs provided' } } // Load order source to verify it's Shopify and get credentials const os: any = await fetchHelper(event, `models/c_ordersource/${orderSourceId}`, 'GET', token, null) if(!os) { return { status: 404, message: 'Order Source not found' } } if(os?.Marketplace?.identifier !== 'shopify') { return { status: 400, message: 'Selected Order Source is not Shopify' } } // Get Shopify credentials from order source const shopUrl = os?.marketplace_url || '' const accessToken = os?.marketplace_token || os?.marketplace_secret || '' if(!shopUrl) { return { status: 400, message: 'No marketplace_url configured for this Order Source' } } if(!accessToken) { return { status: 400, message: 'No marketplace_token or marketplace_secret configured for this Order Source' } } // Get Shopify location ID (optional - if not set, will get all locations) const locationId = os?.Shopify_Location_ID || null try { // Normalize shop URL let host = shopUrl.trim() if(!/^https?:\/\//i.test(host)) host = `https://${host}` host = host.replace(/\/$/, '') if(!host.includes('.myshopify.com') && !host.includes('/admin')) { const shopName = host.replace(/^https?:\/\//i, '').split('.')[0] host = `https://${shopName}.myshopify.com` } const apiBase = `${host}/admin/api/2024-01` const authHeaders = { 'X-Shopify-Access-Token': accessToken, 'Content-Type': 'application/json', 'Accept': 'application/json' } const stocks: Record = {} const inventoryItemIds: Record = {} const errors: Record = {} // Initialize all requested SKUs with 0 for(const sku of skus) { stocks[sku] = 0 } // Create a Set for fast SKU lookup const skuSet = new Set(skus.map(s => s.toLowerCase())) // Step 1: Fetch ALL products with their variants to build SKU -> inventory_item_id map // Using since_id pagination (cursor-based) as page-based is deprecated // See: https://shopify.dev/docs/api/admin-rest/usage/pagination console.log('Fetching Shopify products to build SKU map...') const skuToInventoryItemMap: Record = {} let sinceId = 0 let hasMore = true const limit = 250 // Max allowed by Shopify let pageNum = 1 while(hasMore) { try { // Use since_id for cursor-based pagination const productsUrl = `${apiBase}/products.json?limit=${limit}&since_id=${sinceId}` console.log(`Fetching products (since_id=${sinceId})...`) const productsRes: any = await $fetch(productsUrl, { method: 'GET', headers: authHeaders }) const products = productsRes?.products || [] console.log(`Got ${products.length} products on page ${pageNum}`) if(products.length === 0) { hasMore = false break } // Build SKU -> inventory_item_id map for(const product of products) { const variants = product.variants || [] for(const variant of variants) { const variantSku = String(variant.sku || '').toLowerCase() if(variantSku && skuSet.has(variantSku)) { skuToInventoryItemMap[variantSku] = { inventoryItemId: String(variant.inventory_item_id), variantId: String(variant.id) } } } // Update since_id to the last product's ID for next page if(product.id) { sinceId = product.id } } // Check if we should continue (less than limit means last page) if(products.length < limit) { hasMore = false } else { pageNum++ // Safety limit if(pageNum > 100) hasMore = false } } catch (err: any) { console.error(`Failed to fetch products (since_id=${sinceId}):`, err?.message || err) hasMore = false } } console.log(`Built SKU map with ${Object.keys(skuToInventoryItemMap).length} entries`) // Step 2: Fetch all locations const locationsMap: Record = {} try { const locationsUrl = `${apiBase}/locations.json` const locationsRes: any = await $fetch(locationsUrl, { method: 'GET', headers: authHeaders }) if(locationsRes?.locations && Array.isArray(locationsRes.locations)) { for(const loc of locationsRes.locations) { locationsMap[String(loc.id)] = { id: String(loc.id), name: loc.name || `Location ${loc.id}`, active: loc.active ?? true } } console.log(`Found ${Object.keys(locationsMap).length} Shopify locations`) } } catch (locErr: any) { console.log('Could not fetch locations:', locErr?.message) } // Step 3: Fetch inventory levels for all inventory_item_ids (from ALL locations) const inventoryItemIdsToFetch = Object.values(skuToInventoryItemMap).map(v => v.inventoryItemId) // Map: inventoryItemId -> { total: number, byLocation: { locationId: { qty, name } } } const inventoryLevelsMap: Record }> = {} if(inventoryItemIdsToFetch.length > 0) { // Shopify allows max 50 inventory_item_ids per request const batchSize = 50 for(let i = 0; i < inventoryItemIdsToFetch.length; i += batchSize) { const batch = inventoryItemIdsToFetch.slice(i, i + batchSize) try { // Fetch from all locations (no location_ids filter) const inventoryUrl = `${apiBase}/inventory_levels.json?inventory_item_ids=${batch.join(',')}` console.log(`Fetching inventory levels for ${batch.length} items...`) const inventoryRes: any = await $fetch(inventoryUrl, { method: 'GET', headers: authHeaders }) const levels = inventoryRes?.inventory_levels || [] for(const level of levels) { const itemId = String(level.inventory_item_id) const locId = String(level.location_id) const qty = Number(level.available || 0) const locName = locationsMap[locId]?.name || `Location ${locId}` if(!inventoryLevelsMap[itemId]) { inventoryLevelsMap[itemId] = { total: 0, byLocation: {} } } inventoryLevelsMap[itemId].total += qty inventoryLevelsMap[itemId].byLocation[locId] = { qty, name: locName } } } catch (invErr: any) { console.error(`Failed to fetch inventory levels:`, invErr?.message || invErr) } } } console.log(`Got inventory levels for ${Object.keys(inventoryLevelsMap).length} items`) // Step 4: Map back to SKUs with location breakdown const stocksByLocation: Record> = {} for(const sku of skus) { const skuLower = sku.toLowerCase() const mapping = skuToInventoryItemMap[skuLower] if(mapping) { inventoryItemIds[sku] = mapping.inventoryItemId const invData = inventoryLevelsMap[mapping.inventoryItemId] stocks[sku] = invData?.total ?? 0 if(invData?.byLocation && Object.keys(invData.byLocation).length > 0) { stocksByLocation[sku] = invData.byLocation } } else { errors[sku] = 'SKU not found in Shopify' } } return { status: 200, stocks, stocksByLocation, // Per-SKU breakdown by location inventoryItemIds, locations: locationsMap, // All available locations errors: Object.keys(errors).length > 0 ? errors : undefined, skuCount: skus.length, matchedCount: Object.keys(skuToInventoryItemMap).length } } catch (err: any) { console.error('Shopify stocks error:', err) const data = errorHandlingHelper(err?.data ?? err, err?.data ?? err) return { status: Number(data?.status || 500), message: data?.message || 'Failed to fetch Shopify stocks' } } } export default defineEventHandler(async (event) => { try { return await handleFunc(event) } catch (err: any) { try { const authToken = await refreshTokenHelper(event) return await handleFunc(event, authToken) } catch (error: any) { const data = errorHandlingHelper(err?.data ?? err, error?.data ?? error) if([401, 402, 403, 407].includes(Number(data.status))) { //@ts-ignore setCookie(event, 'user', null) } return data } } })