import refreshTokenHelper from "../../utils/refreshTokenHelper" import errorHandlingHelper from "../../utils/errorHandlingHelper" import fetchHelper from "../../utils/fetchHelper" // GET /api/shopify/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 > shop primary location > first active. 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') { return { status: 400, message: 'Selected Order Source is not Shopify' } } 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' } } 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 locations: Record = {} let locationsQueryError = '' // Preferred: locations.json (requires the read_locations scope) try { const locationsRes: any = await $fetch(`${apiBase}/locations.json`, { method: 'GET', headers: authHeaders }) for(const loc of (locationsRes?.locations || [])) { locations[String(loc.id)] = { id: String(loc.id), name: loc.name || `Location ${loc.id}`, active: loc.active ?? true } } } catch (locErr: any) { locationsQueryError = locErr?.data?.errors || locErr?.message || String(locErr) console.log('shopify: locations.json failed, trying inventory-level fallback:', locationsQueryError) } // Fallback: derive location IDs from inventory levels of a sample of products. // inventory_levels.json needs only read_inventory and returns plain location_id // values (names are not available without read_locations). if(Object.keys(locations).length === 0) { try { const productsRes: any = await $fetch(`${apiBase}/products.json?limit=50&fields=id,variants`, { method: 'GET', headers: authHeaders }) const itemIds: string[] = [] for(const product of (productsRes?.products || [])) { for(const variant of (product.variants || [])) { if(variant.inventory_item_id) itemIds.push(String(variant.inventory_item_id)) if(itemIds.length >= 50) break } if(itemIds.length >= 50) break } if(itemIds.length > 0) { const inventoryRes: any = await $fetch(`${apiBase}/inventory_levels.json?inventory_item_ids=${itemIds.join(',')}`, { method: 'GET', headers: authHeaders }) for(const level of (inventoryRes?.inventory_levels || [])) { const locId = String(level.location_id) if(!locations[locId]) { locations[locId] = { id: locId, name: `Location ${locId}`, active: true } } } } if(Object.keys(locations).length > 0) { console.log(`shopify: Derived ${Object.keys(locations).length} location(s) from inventory levels`) } } catch (scanErr: any) { console.log('shopify: inventory-level location fallback failed:', scanErr?.message) if(!locationsQueryError) locationsQueryError = scanErr?.data?.errors || scanErr?.message || String(scanErr) } } if(Object.keys(locations).length === 0) { return { status: 400, message: `No Shopify locations accessible${locationsQueryError ? ` — ${typeof locationsQueryError === 'object' ? JSON.stringify(locationsQueryError) : locationsQueryError}` : ''}. Grant the app the read_locations scope, or set Shopify_Location_ID on the order source.`, locations: {}, defaultLocationId: '' } } // The shop's primary location is the natural default when none is configured let primaryLocationId = '' try { const shopRes: any = await $fetch(`${apiBase}/shop.json?fields=primary_location_id`, { method: 'GET', headers: authHeaders }) if(shopRes?.shop?.primary_location_id) { primaryLocationId = String(shopRes.shop.primary_location_id) } } catch (shopErr: any) { console.log('shopify: Could not fetch shop primary location:', shopErr?.message) } // 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 } } const active = Object.values(locations).filter((l) => l.active) const defaultFromOrderSource = !!(osLocationId && locations[osLocationId]) let defaultLocationId = defaultFromOrderSource ? osLocationId : '' if(!defaultLocationId && primaryLocationId && locations[primaryLocationId]?.active) { defaultLocationId = primaryLocationId } if(!defaultLocationId) { defaultLocationId = active[0]?.id || '' } return { status: 200, locations, defaultLocationId, defaultFromOrderSource } } catch (err: any) { console.error('shopify 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 } } })