import { string } from 'alga-js' import refreshTokenHelper from "../../utils/refreshTokenHelper" import forceLogoutHelper from "../../utils/forceLogoutHelper" import errorHandlingHelper from "../../utils/errorHandlingHelper" import fetchHelper from "../../utils/fetchHelper" import getTokenHelper from "../../utils/getTokenHelper" const handleFunc = async (event: any, authToken: any = null) => { let data: any = { records: [], 'row-count': 0 } const token = authToken ?? await getTokenHelper(event) const query = getQuery(event) const top = query['$top'] || 100 const skip = query['$skip'] || 0 const orgId = query['org_id'] as string const dateFrom = query['date_from'] as string const dateTo = query['date_to'] as string if (!orgId) { return { status: 400, message: 'Organization ID is required', records: [], 'row-count': 0 } } // Base filter: sales orders only, filtered by org let filterParts = ['isSOTrx eq true', `AD_Org_ID eq ${orgId}`] // Date range filter on DateOrdered if (dateFrom) { filterParts.push(`DateOrdered ge '${dateFrom}'`) } if (dateTo) { filterParts.push(`DateOrdered le '${dateTo}'`) } if (query.filter) { filterParts.push(query.filter as string) } const filterString = string.urlEncode(filterParts.join(' AND ')) // Fetch orders with order lines (including tax), order source, and shipments (with freight costs) const orderRes: any = await fetchHelper( event, `models/c_order?$filter=${filterString}&$expand=${string.urlEncode( 'c_orderline($expand=m_product_id($select=Name,Value,SKU,UPC),C_Tax_ID($select=Rate,Name)),' + 'c_ordersource_id($select=Name,Marketplace),' + 'm_inout($select=ext_freight_cost,ext_freight_total,int_freight_cost,int_freight_total,shipping_date,TrackingNo,DocStatus),' + 'C_BPartner_ID($select=Name),' + 'AD_Org_ID($select=Name),' + 'C_Currency_ID($select=ISO_Code)' )}&$orderby=${string.urlEncode('c_order_id desc')}&$top=${top}&$skip=${skip}`, 'GET', token, null ) // Fetch purchase prices from pricelist version 1000004 const priceRes: any = await fetchHelper( event, `models/m_productprice?$filter=${string.urlEncode('M_PriceList_Version_ID eq 1000004')}&$expand=M_Product_ID($select=Name,Value,SKU)&$top=10000`, 'GET', token, null ) // Build purchase price lookup map: product_id -> PriceStd const purchasePriceMap: Record = {} if (priceRes?.records) { for (const pp of priceRes.records) { if (pp.M_Product_ID?.id) { purchasePriceMap[pp.M_Product_ID.id] = pp.PriceStd || pp.PriceList || 0 } } } if (orderRes?.records) { // Flatten: one row per order line const rows: any[] = [] for (const order of orderRes.records) { const orderLines = order.c_orderline || [] const orderSource = order.c_ordersource_id || order.C_OrderSource_ID || null const inouts = order.m_inout || [] const isTaxIncluded = order.IsTaxIncluded === true || order.IsTaxIncluded === 'Y' // Determine marketplace let marketplace = '' if (orderSource) { const mp = orderSource.Marketplace if (typeof mp === 'object' && mp?.identifier) marketplace = mp.identifier else if (typeof mp === 'string') marketplace = mp else if (orderSource.Name) marketplace = orderSource.Name } // Shipment freight: take the max of ext vs int freight total across all shipments let extFreightTotal = 0 let intFreightTotal = 0 for (const inout of inouts) { extFreightTotal += inout.ext_freight_total || 0 intFreightTotal += inout.int_freight_total || 0 } const shippingCost = Math.max(extFreightTotal, intFreightTotal) // Derive tax multiplier from order header: GrandTotal / TotalLines // This is the most reliable source since C_Tax_ID expansion may not return Rate const orderGrandTotal = order.GrandTotal || 0 const orderTotalLines = order.TotalLines || orderLines.reduce((s: number, l: any) => s + (l.LineNetAmt || 0), 0) || 1 // When IsTaxIncluded: TotalLines already contains tax, GrandTotal also contains tax // When NOT IsTaxIncluded: TotalLines is net, GrandTotal is gross // So orderTaxMultiplier = GrandTotal / TotalLines gives us gross/net ratio const orderTaxMultiplier = (!isTaxIncluded && orderTotalLines > 0 && orderGrandTotal > 0) ? (orderGrandTotal / orderTotalLines) : (isTaxIncluded && orderTotalLines > 0 && orderGrandTotal > 0 && orderGrandTotal !== orderTotalLines) ? (orderGrandTotal / orderTotalLines) // some iDempiere configs: GrandTotal may still differ : 1.19 // fallback to 19% VAT let orderNetTotal = 0 // First pass: compute net and gross amounts per line, skipping charge lines (no product) // and aggregating lines with the same product within the same order const productAggMap: Record = {} for (const line of orderLines) { const productId = line.M_Product_ID?.id || line.m_product_id?.id // Skip charge lines (no product) — these are shipping/handling charges etc. if (!productId) continue const lineNetAmtRaw = line.LineNetAmt || 0 const qty = line.QtyEntered || line.QtyOrdered || 1 let lineNetAmt: number let lineGrossAmt: number if (isTaxIncluded) { lineGrossAmt = lineNetAmtRaw lineNetAmt = lineNetAmtRaw / orderTaxMultiplier } else { lineNetAmt = lineNetAmtRaw lineGrossAmt = lineNetAmtRaw * orderTaxMultiplier } // Aggregate by unique product within the same order if (productAggMap[productId]) { productAggMap[productId].qty += qty productAggMap[productId].lineNetAmt += lineNetAmt productAggMap[productId].lineGrossAmt += lineGrossAmt } else { productAggMap[productId] = { line, productId, qty, lineNetAmt, lineGrossAmt } } orderNetTotal += lineNetAmt } if (orderNetTotal === 0) orderNetTotal = 1 // avoid division by zero const taxRate = orderTaxMultiplier > 1 ? Math.round((orderTaxMultiplier - 1) * 10000) / 100 : 0 // Marketplace info const marketplaceLower = marketplace.toLowerCase() const isAmazon = marketplaceLower.includes('amazon') const isEbay = marketplaceLower.includes('ebay') const isMarketplace = isAmazon || isEbay // Amazon referral rate determined by orderGrandTotal thresholds let amazonReferralPct = 0 if (isAmazon) { if (orderGrandTotal <= 15) amazonReferralPct = 5 else if (orderGrandTotal <= 20) amazonReferralPct = 10 else amazonReferralPct = 15 } // Shipping cost: use actual if available, default 4.50 per order if missing const hasShippingFee = shippingCost > 0 const effectiveShippingCost = hasShippingFee ? shippingCost : 4.50 // Second pass: build rows for unique products, shipping only on first position const uniqueProducts = Object.values(productAggMap) let isFirstLine = true for (const agg of uniqueProducts) { const { line, productId, qty, lineNetAmt, lineGrossAmt } = agg const productName = line.M_Product_ID?.identifier || line.m_product_id?.Name || line.M_Product_ID?.Name || '' const productSKU = line.M_Product_ID?.SKU || line.m_product_id?.SKU || '' const productValue = line.M_Product_ID?.Value || line.m_product_id?.Value || '' const purchasePrice = productId ? (purchasePriceMap[productId] || 0) : 0 const salesPriceNet = qty > 0 ? (lineNetAmt / qty) : 0 const salesPriceGross = qty > 0 ? (lineGrossAmt / qty) : 0 // Shipping cost only on the first position of the order const lineShippingCost = isFirstLine ? effectiveShippingCost : 0 // Referral fee calculated per position based on line gross amount (incl. tax) let referralFee = 0 let referralPct = 0 if (isAmazon) { referralPct = amazonReferralPct if (orderGrandTotal > 45) { // Blended rate for >€45 orders, applied proportionally to this line's gross const blendedRate = ((45 * 0.15) + ((orderGrandTotal - 45) * 0.07)) / orderGrandTotal referralFee = lineGrossAmt * blendedRate } else { referralFee = lineGrossAmt * (referralPct / 100) } if (referralFee < 0.30 && uniqueProducts.length === 1) referralFee = 0.30 } else if (isEbay) { referralPct = lineGrossAmt <= 2000 ? 15 : 9 referralFee = lineGrossAmt * (referralPct / 100) } // Margin = line net amount - purchase cost - shipping - referral fee const totalPurchaseCost = purchasePrice * qty const margin = lineNetAmt - totalPurchaseCost - lineShippingCost - referralFee const marginPct = lineNetAmt > 0 ? (margin / lineNetAmt) * 100 : 0 isFirstLine = false rows.push({ // Order info orderId: order.id, documentNo: order.DocumentNo, dateOrdered: order.DateOrdered, organization: order.AD_Org_ID?.identifier || order.AD_Org_ID?.Name || '', organizationId: order.AD_Org_ID?.id, partner: order.C_BPartner_ID?.identifier || order.C_BPartner_ID?.Name || '', partnerId: order.C_BPartner_ID?.id, currency: order.C_Currency_ID?.ISO_Code || order.C_Currency_ID?.identifier || 'EUR', grandTotal: order.GrandTotal || 0, isTaxIncluded: isTaxIncluded, // Order source marketplace: marketplace, orderSourceName: orderSource?.Name || '', isAmazon: isAmazon, isEbay: isEbay, // Product info orderLineId: line.id, productId: productId, product: productName, productSKU: productSKU, productValue: productValue, qty: qty, // Prices salesPriceNet: salesPriceNet, salesPriceGross: salesPriceGross, lineNetAmt: lineNetAmt, lineGrossAmt: lineGrossAmt, taxRate: taxRate, purchasePrice: purchasePrice, totalPurchaseCost: totalPurchaseCost, // Shipping extFreightTotal: extFreightTotal, intFreightTotal: intFreightTotal, shippingCost: effectiveShippingCost, lineShippingCost: lineShippingCost, hasShippingFee: hasShippingFee, // Fees referralFee: referralFee, referralPct: referralPct, // Margin completeness: warn if shipping is estimated or marketplace fee unknown missingFees: !hasShippingFee || (isMarketplace ? false : (marketplace ? true : false)), // Margin margin: margin, marginPct: marginPct }) } } data = { status: 200, records: rows, 'row-count': orderRes['row-count'] || orderRes.records.length, 'line-count': rows.length } } return data } export default defineEventHandler(async (event) => { let data: any = {} try { data = await handleFunc(event) } catch (err: any) { try { let authToken: any = await refreshTokenHelper(event) data = await handleFunc(event, authToken) } catch (error: any) { data = errorHandlingHelper(err?.data ?? err, error?.data ?? error) forceLogoutHelper(event, data) } } return data })