import { defineSisHandler } from "../../../../utils/listings/routeHelper" import { listListings } from "../../../../utils/listings/listingStore" import { enrichWithProducts } from "../../../../utils/sisMatching" import { amazonDomainForOrderSource } from "../../../../utils/amazonSpApi" import { marketplaceKeyOf, marketplaceLabel, supportsListings } from "../../../../utils/listings/adapters" import { shopifyStoreHandle } from "../../../../utils/listings/adapters/shopify" import { shopwareUrls } from "../../../../utils/listings/adapters/shopware" import { string } from 'alga-js' // GET /api/sales/sis/listings?orderSourceId=1,2,3&includeInactive=1 — one or several order sources // GET /api/sales/sis/listings?all=1 — every order source with a listings adapter // The persisted listings (CUST_MarketplaceListing) joined with the internal product data // (price list price, qty on hand, BOM components). iDempiere only — no marketplace call. // `sources` carries the per-source marketplace / link data the page needs for mixed lists. const sourceInfo = (os: any) => { const marketplace = marketplaceKeyOf(os) const shopDomain = String(os?.marketplace_url || os?.Marketplace_URL || '').replace(/^https?:\/\//i, '').replace(/\/.*$/, '') return { id: Number(os.id), name: os.Name || '', orgId: Number(os?.AD_Org_ID?.id) || 0, orgName: os?.AD_Org_ID?.identifier || '', marketplace, marketplaceName: marketplaceLabel(marketplace), supportsListings: supportsListings(os), marketplaceDomain: amazonDomainForOrderSource(os), shop: marketplace.startsWith('shopify') ? { storeHandle: shopifyStoreHandle(os), domain: shopDomain } : marketplace === 'shopware' ? { baseUrl: shopwareUrls(os).baseUrl } : null } } export default defineSisHandler(async ({ event, token, f }) => { const q = getQuery(event) const all = q.all === '1' || q.all === 'true' const ids = String(q.orderSourceId || '').split(',').map(s => Number(s.trim())).filter(n => Number.isFinite(n) && n > 0) if (!all && !ids.length) return { status: 400, message: 'orderSourceId (comma separated) or all=1 is required' } let sources: any[] = [] if (all) { const res: any = await f(`models/c_ordersource?$filter=${string.urlEncode('isActive eq true')}&$orderby=${string.urlEncode('Name asc')}&$top=500`) sources = (res?.records || []).filter((os: any) => supportsListings(os)) } else { for (const id of ids) { const os: any = await f(`models/c_ordersource/${id}`) if (os?.id) sources.push(os) } if (!sources.length) return { status: 404, message: 'Order source not found' } } const infos = Object.fromEntries(sources.map(os => [String(os.id), sourceInfo(os)])) const activeOnly = q.includeInactive !== '1' const listings: any[] = [] for (const os of sources) listings.push(...await listListings(f, Number(os.id), { activeOnly, top: 5000 })) // product enrichment is org-scoped (return locators) → one call per org const config: any = useRuntimeConfig(event) const priceListId = config.public?.pricelistid || '101' const enrich = new Map() const byOrg = new Map() for (const l of listings) { if (!l.productId) continue const orgId = infos[String(l.orderSourceId)]?.orgId || 0 byOrg.set(orgId, [...(byOrg.get(orgId) || []), l.productId]) } for (const [orgId, productIds] of byOrg) for (const [pid, e] of await enrichWithProducts(event, token, orgId, productIds, priceListId)) enrich.set(pid, e) const rows = listings.map(l => { const p = l.productId ? enrich.get(l.productId) : null const src = infos[String(l.orderSourceId)] return { ...l, orderSourceName: src?.name || l.orderSourceName || '', marketplace: src?.marketplace || 'unknown', productName: p?.productName || '', productValue: p?.productValue || '', productAsin: p?.asin || null, strapiProductDocumentId: p?.strapiProductDocumentId || null, isBom: p?.isBom || false, // BOM: components carry the stock, not the bundle → the FIRST component's qty is the row's // internal qty (all components with qty per bundle + stock go to the tooltip). bomComponents: p?.bomComponents || [], bomOwnQtyOnHand: p?.isBom ? (p?.internalQtyOnHand ?? null) : null, internalPrice: p?.internalPrice ?? null, internalQtyOnHand: p?.bomComponents?.[0] ? p.bomComponents[0].onHand : (p?.internalQtyOnHand ?? null), returnQtyOnHand: p?.bomComponents?.[0] ? p.bomComponents[0].returnOnHand : (p?.returnQtyOnHand ?? 0) } }) const counts = { total: rows.length, matched: rows.filter(r => r.productId).length, unmatched: rows.filter(r => !r.productId).length, queued: rows.filter(r => r.syncStatus === 'queued' || r.syncStatus === 'sending').length, sent: rows.filter(r => r.syncStatus === 'sent').length, errors: rows.filter(r => r.syncStatus === 'error').length, delisted: rows.filter(r => r.listingStatus === 'delisted').length } const first = infos[String(sources[0]?.id)] return { status: 200, all, sources: infos, // single-source fields kept for callers that pass one id orderSourceId: first?.id ?? null, orgId: first?.orgId ?? null, orderSourceName: first?.name || '', marketplace: first?.marketplace, marketplaceName: first?.marketplaceName, supportsListings: first?.supportsListings, marketplaceDomain: first?.marketplaceDomain, shop: first?.shop ?? null, rows, counts } })