import refreshTokenHelper from "../../../utils/refreshTokenHelper" import errorHandlingHelper from "../../../utils/errorHandlingHelper" import fetchHelper from "../../../utils/fetchHelper" // POST /api/shopify/stocks/sync // Syncs local qty on hand to Shopify via REST API // Body: { orderSourceId, variantId (SKU), inventoryItemId?, quantity, sku?, productId? } // // Shopify Inventory Level Set API: // POST /admin/api/2024-01/inventory_levels/set.json const handleFunc = async (event: any, authToken: any = null) => { const token = authToken ?? await getTokenHelper(event) const body = await readBody(event) const { orderSourceId, variantId, quantity, sku, productId, locationId: bodyLocationId } = body let { inventoryItemId } = body if(!orderSourceId) { return { status: 400, message: 'orderSourceId is required', success: false } } if(!variantId) { return { status: 400, message: 'variantId (SKU) is required', success: false } } if(quantity === undefined || quantity === null) { return { status: 400, message: 'quantity is required', success: false } } // 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', success: false } } if(os?.Marketplace?.identifier !== 'shopify') { return { status: 400, message: 'Selected Order Source is not Shopify', success: false } } // 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', success: false } } if(!accessToken) { return { status: 400, message: 'No marketplace_token or marketplace_secret configured for this Order Source', success: false } } // Get Shopify location ID (required for setting inventory) 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 targetQty = Number(quantity) const skuToFind = String(variantId).toLowerCase() // If inventoryItemId is provided, use it directly (from previous fetch) if(inventoryItemId) { console.log(`Using provided inventoryItemId: ${inventoryItemId} for SKU "${variantId}"`) } // If inventoryItemId is not provided, search for SKU in products using REST API if(!inventoryItemId) { console.log(`Searching for SKU "${skuToFind}" in Shopify products...`) // Fetch products and find matching SKU using since_id pagination let sinceId = 0 let found = false const limit = 250 let pageNum = 1 while(!found && pageNum <= 50) { try { const productsUrl = `${apiBase}/products.json?limit=${limit}&since_id=${sinceId}` const productsRes: any = await $fetch(productsUrl, { method: 'GET', headers: authHeaders }) const products = productsRes?.products || [] if(products.length === 0) break for(const product of products) { const variants = product.variants || [] for(const variant of variants) { if(String(variant.sku || '').toLowerCase() === skuToFind) { inventoryItemId = String(variant.inventory_item_id) console.log(`Found SKU "${skuToFind}" -> inventory_item_id: ${inventoryItemId}`) found = true break } } if(found) break // Update since_id to the last product's ID if(product.id) sinceId = product.id } if(products.length < limit) break pageNum++ } catch (err: any) { console.error(`Failed to fetch products (since_id=${sinceId}):`, err?.message) break } } } if(!inventoryItemId) { return { status: 400, success: false, message: `Could not find variant with SKU "${variantId}" in Shopify.`, variantId } } // Get location ID - prioritize: body param > order source config > fetch from API let effectiveLocationId = bodyLocationId || locationId if(!effectiveLocationId) { try { const locationsUrl = `${apiBase}/locations.json` const locationsRes: any = await $fetch(locationsUrl, { method: 'GET', headers: authHeaders }) if(locationsRes?.locations && locationsRes.locations.length > 0) { const activeLocation = locationsRes.locations.find((l: any) => l.active) || locationsRes.locations[0] effectiveLocationId = String(activeLocation.id) console.log('Using Shopify location:', effectiveLocationId) } } catch (locErr: any) { console.error('Failed to fetch Shopify locations:', locErr?.message) return { status: 400, success: false, message: 'Could not determine Shopify location. Please configure Shopify_Location_ID on the order source.', variantId } } } if(!effectiveLocationId) { return { status: 400, success: false, message: 'No Shopify location found. Please configure Shopify_Location_ID on the order source.', variantId } } // Set inventory level using REST API // POST /admin/api/2024-01/inventory_levels/set.json const setUrl = `${apiBase}/inventory_levels/set.json` const setBody = { location_id: Number(effectiveLocationId), inventory_item_id: Number(inventoryItemId), available: targetQty } console.log('Shopify inventory set request:', { url: setUrl, body: setBody }) const result: any = await $fetch(setUrl, { method: 'POST', headers: authHeaders, body: setBody }) console.log('Shopify inventory set response:', JSON.stringify(result).slice(0, 800)) const updatedStock = result?.inventory_level?.available ?? targetQty return { status: 200, success: true, message: `Stock set to ${updatedStock} for SKU "${variantId}" at location ${effectiveLocationId}`, variantId, inventoryItemId, quantity: targetQty, updatedStock, locationId: effectiveLocationId, shopifyResponse: result } } catch (err: any) { const errData = err?.data || err const errMsg = errData?.message || errData?.errors || err?.message || 'Failed to sync stock to Shopify' const errStatus = err?.status || err?.response?.status || errData?.status || 500 console.error('Shopify stock sync error:', { variantId, quantity, error: errData }) return { status: errStatus, success: false, message: typeof errMsg === 'object' ? JSON.stringify(errMsg) : errMsg, error: errData, variantId } } } 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, success: false } } } })