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) // (iDempiere returns the ColumnName casing verbatim — the column is shopify_location_id) const locationId = os?.shopify_location_id ?? 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 > item's stocked location > locations API let effectiveLocationId = bodyLocationId || locationId let locationResolveError = '' if(!effectiveLocationId) { // Where is this item already stocked? inventory_levels.json needs only // read_inventory and returns plain location_id values — works even when // locations.json is denied (missing read_locations). try { const levelsRes: any = await $fetch(`${apiBase}/inventory_levels.json?inventory_item_ids=${inventoryItemId}`, { method: 'GET', headers: authHeaders }) const levels = levelsRes?.inventory_levels || [] if(levels.length > 0) { effectiveLocationId = String(levels[0].location_id) console.log(`Using item's stocked location: ${effectiveLocationId}`) } } catch (lvlErr: any) { locationResolveError = lvlErr?.data?.errors || lvlErr?.message || String(lvlErr) console.log('inventory_levels location lookup failed:', locationResolveError) } } 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) { locationResolveError = locErr?.data?.errors || locErr?.message || locationResolveError console.error('Failed to fetch Shopify locations:', locErr?.message) } } if(!effectiveLocationId) { const errText = typeof locationResolveError === 'object' ? JSON.stringify(locationResolveError) : locationResolveError return { status: 400, success: false, message: `Could not determine Shopify location${errText ? ` — ${errText}` : ''}. Select a location on the page, set Shopify_Location_ID on the order source, or grant the app the read_locations scope.`, 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 }) let result: any try { result = await $fetch(setUrl, { method: 'POST', headers: authHeaders, body: setBody }) } catch (setErr: any) { // 422 usually means the inventory item is not stocked/connected at that // location yet — connect it, then retry the set once. const setErrStatus = Number(setErr?.status || setErr?.response?.status || 0) if(setErrStatus !== 422) throw setErr console.log(`Connecting inventory item ${inventoryItemId} to location ${effectiveLocationId}, then retrying set...`) await $fetch(`${apiBase}/inventory_levels/connect.json`, { method: 'POST', headers: authHeaders, body: { location_id: Number(effectiveLocationId), inventory_item_id: Number(inventoryItemId), relocate_if_necessary: false } }) result = 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 } } } })