/** * Amazon-FBA relabel orders (C_Order.isRelabelByFulfillmentProvider). * * Amazon FBA sometimes requires a different barcode on the product than the * merchant's own EAN/SKU. Such orders carry the required code per position in * C_OrderLine.printLabelCode. These helpers implement: * - the completion gate (every product line must have its code before 'CO') * - the automatic warehouse ticket so staff relabel the articles * * The order and its lines are fetched with SEPARATE requests (single-level * $expand only) — deep/nested $expand is limited in the iDempiere REST API. */ import fetchHelper from './fetchHelper' export const isRelabelOrder = (order: any) => (order?.isRelabelByFulfillmentProvider ?? order?.IsRelabelByFulfillmentProvider) === true // Limited roles ('c') don't receive the custom relabel columns from iDempiere // (column security on c_order / c_orderline — same behavior as Strapi_Product_* // on m_product), and their role may not write into the HQ org at all. Every // relabel read/write therefore prefers the service token and only falls back // to the caller's token when none is configured. export const relabelServiceToken = (fallbackToken: any) => { const config = useRuntimeConfig() return (config.api as any)?.idempieretoken || fallbackToken } export async function fetchRelabelOrder(event: any, token: any, orderId: any): Promise { return await fetchHelper(event, `models/c_order/${orderId}`, 'GET', relabelServiceToken(token), null) } export async function fetchRelabelOrderLines(event: any, token: any, orderId: any): Promise { const filter = encodeURIComponent(`C_Order_ID eq ${orderId}`) const expand = encodeURIComponent('m_product_id($select=Name,SKU,Value)') const res: any = await fetchHelper(event, `models/c_orderline?$filter=${filter}&$expand=${expand}`, 'GET', relabelServiceToken(token), null) return res?.records || [] } // Product lines that count for the relabel rule (charge-only lines are exempt). export const relabelProductLines = (lines: any[]) => (lines || []).filter((l: any) => l?.IsActive !== false && l?.M_Product_ID?.id && Number(l?.QtyEntered ?? l?.QtyOrdered ?? 0) > 0) export const getRelabelMissingLines = (lines: any[]) => relabelProductLines(lines).filter((l: any) => !String(l?.printLabelCode ?? l?.PrintLabelCode ?? '').trim()) export const relabelMissingMessage = (missing: any[]) => 'FBA-Relabel: Etiketten-Code fehlt auf Position(en): ' + missing .map((l: any) => `#${l?.Line || '?'} ${l?.M_Product_ID?.SKU || l?.M_Product_ID?.identifier || ''}`.trim()) .join(', ') /** * Creates the warehouse relabel ticket after a relabel order completed. * Owned by DEFAULT_ORG_ID (LogShip HQ), related to the order-org's linked * business partner and the order itself. Entirely fail-soft — a ticket * failure must never fail the completion. */ const TICKET_SUMMARY_PREFIX = 'Relabel FBA articles manually' export async function createRelabelTicket(event: any, token: any, order: any, lines: any[]) { try { // The ticket is owned by the HQ org and reads cross-org data — a limited // role's token can do neither, so use the service token throughout. const writeToken = relabelServiceToken(token) // Idempotent: a re-completion (e.g. the CO→RE→CO reserved-qty flow) must // not create a second ticket for the same order. try { const existing: any = await fetchHelper(event, `models/r_request?$filter=${encodeURIComponent(`C_Order_ID eq ${Number(order?.id)}`)}`, 'GET', writeToken, null) if((existing?.records || []).some((r: any) => String(r?.Summary ?? r?.summary ?? '').startsWith(TICKET_SUMMARY_PREFIX))) { return true } } catch { /* dedupe check is best-effort */ } let orgPartnerId: any = null try { const orgId = order?.AD_Org_ID?.id if (orgId) { const orgRes: any = await fetchHelper(event, `models/ad_org/${orgId}?$select=C_BPartner_ID`, 'GET', writeToken, null) orgPartnerId = orgRes?.C_BPartner_ID?.id || null } } catch { /* partner link on the ticket is optional */ } const details = relabelProductLines(lines).map((l: any) => `SKU ${l?.M_Product_ID?.SKU || l?.M_Product_ID?.Value || '-'}` + ` | Code ${String(l?.printLabelCode ?? l?.PrintLabelCode ?? '').trim() || '-'}` + ` | Menge ${l?.QtyEntered ?? l?.QtyOrdered ?? 0}` ).join('\n') const salesRepId = getCookie(event, 'logship_user_id') await fetchHelper(event, 'models/r_request', 'POST', writeToken, { AD_Org_ID: { id: Number(process.env.DEFAULT_ORG_ID || '1000000'), tableName: 'AD_Org' }, isActive: true, summary: `${TICKET_SUMMARY_PREFIX} – Auftrag ${order?.DocumentNo || order?.id || ''}\n\n${details}`, isEscalated: false, dateLastAction: new Date().toISOString().replace(/\.\d{3}Z$/, 'Z'), processed: false, isSelfService: false, isInvoiced: false, qtySpent: 0, qtyInvoiced: 0, Priority: { id: '5' }, PriorityUser: { id: '5' }, DueType: { id: '5' }, NextAction: { id: 'F' }, ConfidentialType: { id: 'I' }, ConfidentialTypeEntry: { id: 'I' }, R_RequestType_ID: { id: 1000001, tableName: 'R_RequestType' }, C_Order_ID: { id: Number(order?.id), tableName: 'C_Order' }, ...(salesRepId ? { SalesRep_ID: { id: Number(salesRepId), tableName: 'AD_User' }, AD_User_ID: { id: Number(salesRepId), tableName: 'AD_User' } } : {}), ...(orgPartnerId ? { C_BPartner_ID: { id: orgPartnerId, tableName: 'C_BPartner' } } : {}), tableName: 'R_Request' }) return true } catch (err: any) { console.error('[relabel] ticket creation failed (non-fatal):', err?.data ?? err) return false } }