import { string } from 'alga-js' import refreshTokenHelper from "../../utils/refreshTokenHelper" import errorHandlingHelper from "../../utils/errorHandlingHelper" import fetchHelper from "../../utils/fetchHelper" import getReturnLocatorIds from "../../utils/returnLocatorHelper" // Batch variant of /api/local/qty-on-hand. Resolves many products' local qty-on-hand // in a few chunked iDempiere queries instead of one request per product (the FFN stock // page would otherwise fire ~900 requests for kfp). Returns a map keyed by jfsku. // Body: { items: [{ jfsku, merchantSku }] } // Response: { status, results: { [jfsku]: { productId, qtyOnHand, returnQtyOnHand } } } const CHUNK = 40 const sumSellableQty = (storages: any[], returnLocatorIds: Set) => { let qtyOnHand = 0 let returnQtyOnHand = 0 if (!Array.isArray(storages)) return { qtyOnHand, returnQtyOnHand } for (const row of storages) { const qty = Number(row?.QtyOnHand || 0) const locator = row?.M_Locator_ID const locatorId = locator?.id ?? locator const isReturn = locatorId !== undefined && locatorId !== null && returnLocatorIds.has(Number(locatorId)) if (isReturn) returnQtyOnHand += qty else qtyOnHand += qty } return { qtyOnHand, returnQtyOnHand } } const chunk = (arr: T[], n: number): T[][] => { const out: T[][] = [] for (let i = 0; i < arr.length; i += n) out.push(arr.slice(i, i + n)) return out } const handleFunc = async (event: any, authToken: any = null) => { const token = authToken ?? await getTokenHelper(event) const body = await readBody(event) const items: any[] = Array.isArray(body?.items) ? body.items : [] if (items.length === 0) return { status: 400, message: 'items array is required', results: {} } const productSelect = string.urlEncode('m_product_id,Name,SKU,jfsku,AD_Org_ID') const storageExpand = string.urlEncode('M_Storage($expand=M_Locator_ID)') // De-duplicate the lookup keys. const jfskus = [...new Set(items.map(i => String(i?.jfsku || '').trim()).filter(Boolean))] const skuByMissing = new Map() // merchantSku -> intended jfsku (for fallback) for (const i of items) { const jf = String(i?.jfsku || '').trim() const sku = String(i?.merchantSku || '').trim() if (jf && sku) skuByMissing.set(sku, jf) } const results: Record = {} const foundJfskus = new Set() const products: any[] = [] // Pass 1: match by jfsku in chunks. for (const group of chunk(jfskus, CHUNK)) { const orParts = group.map(j => `jfsku eq '${j.replace(/'/g, "''")}'`).join(' or ') const filter = string.urlEncode(`(${orParts})`) const res: any = await fetchHelper( event, `models/m_product?$select=${productSelect}&$expand=${storageExpand}&$filter=${filter}&$top=${group.length}`, 'GET', token, null ) for (const p of (res?.records || [])) { if (p?.jfsku) foundJfskus.add(String(p.jfsku)) products.push(p) } } // Pass 2: SKU fallback for any input jfsku not matched above (some products only map by SKU). const missingSkus = [...skuByMissing.entries()] .filter(([, jf]) => !foundJfskus.has(jf)) .map(([sku]) => sku) const skuToProduct = new Map() for (const group of chunk([...new Set(missingSkus)], CHUNK)) { const orParts = group.map(s => `SKU eq '${s.replace(/'/g, "''")}'`).join(' or ') const filter = string.urlEncode(`(${orParts})`) const res: any = await fetchHelper( event, `models/m_product?$select=${productSelect}&$expand=${storageExpand}&$filter=${filter}&$top=${group.length}`, 'GET', token, null ) for (const p of (res?.records || [])) { if (p?.SKU) skuToProduct.set(String(p.SKU), p) products.push(p) } } // Resolve the return-locator set once per distinct org (helper is per-org cached). const orgIds = [...new Set(products.map(p => p?.AD_Org_ID?.id ?? p?.AD_Org_ID ?? null))] const returnLocByOrg: Record> = {} for (const oid of orgIds) { returnLocByOrg[String(oid)] = await getReturnLocatorIds(event, token, oid) } const computeFor = (p: any) => { const oid = p?.AD_Org_ID?.id ?? p?.AD_Org_ID ?? null const { qtyOnHand, returnQtyOnHand } = sumSellableQty(p?.M_Storage || [], returnLocByOrg[String(oid)] || new Set()) return { productId: Number(p?.m_product_id || p?.id), qtyOnHand, returnQtyOnHand } } // Map jfsku-matched products. for (const p of products) { if (p?.jfsku) results[String(p.jfsku)] = computeFor(p) } // Map SKU-fallback products back onto their intended jfsku key. for (const [sku, jf] of skuByMissing.entries()) { if (foundJfskus.has(jf)) continue const p = skuToProduct.get(sku) if (p) results[jf] = computeFor(p) } return { status: 200, results } } 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, results: {} } } } })