import refreshTokenHelper from "../../utils/refreshTokenHelper" import errorHandlingHelper from "../../utils/errorHandlingHelper" import fetchHelper from "../../utils/fetchHelper" import { getShopifyNewAccessToken, shopifyNewGraphQL, parseShopifyGid } from "../../utils/shopifyNewAuthHelper" // GET /api/shopify-new/locations?orderSourceId=123 // Returns all Shopify locations for the shop plus a default location to preselect. // Default priority: Shopify_Location_ID on the order source > first active location // that ships inventory / fulfills online orders > first active location. // If the locations query is denied (missing read_locations scope), falls back to // deriving locations from inventory levels of a sample of variants. const handleFunc = async (event: any, authToken: any = null) => { const token = authToken ?? await getTokenHelper(event) const query = getQuery(event) const orderSourceId = query.orderSourceId as string if(!orderSourceId) { return { status: 400, message: 'orderSourceId is required' } } 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-new') { return { status: 400, message: 'Selected Order Source is not Shopify-New' } } try { const { accessToken, graphqlUrl } = await getShopifyNewAccessToken(os) const locations: Record = {} let locationsQueryError = '' // Preferred: the locations query (requires the read_locations scope) try { const locationsQuery = `{ locations(first: 50) { edges { node { id name isActive shipsInventory fulfillsOnlineOrders } } } }` const locRes = await shopifyNewGraphQL(graphqlUrl, accessToken, locationsQuery) for(const edge of (locRes?.data?.locations?.edges || [])) { const node = edge.node const numericId = parseShopifyGid(node.id) locations[numericId] = { id: numericId, name: node.name || `Location ${numericId}`, active: node.isActive ?? true, shipsInventory: node.shipsInventory ?? true, fulfillsOnlineOrders: node.fulfillsOnlineOrders ?? false } } } catch (locErr: any) { locationsQueryError = locErr?.message || String(locErr) console.log('shopify-new: locations query failed, trying inventory-level fallback:', locationsQueryError) } // Fallback: derive locations from the inventory levels of a sample of variants. // Needs only read_products + read_inventory, which the stock page requires anyway. if(Object.keys(locations).length === 0) { try { const scanQuery = `{ productVariants(first: 30) { edges { node { inventoryItem { inventoryLevels(first: 10) { edges { node { location { id name isActive } } } } } } } } }` const scanRes = await shopifyNewGraphQL(graphqlUrl, accessToken, scanQuery) for(const vEdge of (scanRes?.data?.productVariants?.edges || [])) { for(const lEdge of (vEdge?.node?.inventoryItem?.inventoryLevels?.edges || [])) { const loc = lEdge?.node?.location if(!loc?.id) continue const numericId = parseShopifyGid(loc.id) if(!locations[numericId]) { locations[numericId] = { id: numericId, name: loc.name || `Location ${numericId}`, active: loc.isActive ?? true, shipsInventory: true, fulfillsOnlineOrders: false } } } } if(Object.keys(locations).length > 0) { console.log(`shopify-new: Derived ${Object.keys(locations).length} location(s) from inventory levels`) } } catch (scanErr: any) { console.log('shopify-new: inventory-level location fallback failed:', scanErr?.message) if(!locationsQueryError) locationsQueryError = scanErr?.message || String(scanErr) } } if(Object.keys(locations).length === 0) { return { status: 400, message: `No Shopify locations accessible${locationsQueryError ? ` — ${locationsQueryError}` : ''}. Grant the app the read_locations scope, or set Shopify_Location_ID on the order source.`, locations: {}, defaultLocationId: '' } } // iDempiere returns the ColumnName casing verbatim — the column is shopify_location_id const osLocationRaw = os?.shopify_location_id ?? os?.Shopify_Location_ID const osLocationId = osLocationRaw ? String(osLocationRaw) : '' // A configured default always appears in the list, even if the (possibly // fallback-derived) location scan didn't surface it if(osLocationId && !locations[osLocationId]) { locations[osLocationId] = { id: osLocationId, name: `Location ${osLocationId}`, active: true, shipsInventory: true, fulfillsOnlineOrders: false } } const active = Object.values(locations).filter((l) => l.active) const defaultFromOrderSource = !!(osLocationId && locations[osLocationId]) let defaultLocationId = defaultFromOrderSource ? osLocationId : '' if(!defaultLocationId) { const preferred = active.find((l) => l.fulfillsOnlineOrders && l.shipsInventory) || active.find((l) => l.shipsInventory) || active[0] defaultLocationId = preferred?.id || '' } return { status: 200, locations, defaultLocationId, defaultFromOrderSource } } catch (err: any) { console.error('shopify-new locations error:', err?.message || err) return { status: Number(err?.status || err?.response?.status || 500), message: err?.message || 'Failed to fetch Shopify locations', locations: {}, defaultLocationId: '' } } } 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 } } })