import { date } from 'alga-js' import refreshTokenHelper from "../../../../utils/refreshTokenHelper" import getTokenHelper from "../../../../utils/getTokenHelper" import errorHandlingHelper from "../../../../utils/errorHandlingHelper" import fetchHelper from "../../../../utils/fetchHelper" import laravelHelper from "../../../../utils/laravelHelper" import strapiHelper from "../../../../utils/strapiHelper" import { commissionTableFkPatch } from "../../../../utils/commissionTable" import { dpdGetAuth, dpdStoreOrders, extractOrderSourceDpdCreds, xmlEscape, kgTo10GramUnits, toCm, buildVolumeString, iso2ToCustomsOrigin, type DpdCredentials } from "../../../../utils/dpdHelper" // EU country codes used to decide whether the international/customs block is required const EU_COUNTRIES = ['AT','BE','BG','HR','CY','CZ','DK','EE','FI','FR','DE','GR','HU','IE','IT','LV','LT','LU','MT','NL','PL','PT','RO','SK','SI','ES','SE'] // ---------------------------------------------------------------------------- // Strapi: save customs document to order attachments (mirrors DHL flow) // ---------------------------------------------------------------------------- const saveCustomsDocToOrder = async (event: any, orderId: number, orderUid: string, customsDocBase64: string, shipmentNo: string) => { const config = useRuntimeConfig() try { let strapiAttachmentId: number | null = null const existingResponse: any = await strapiHelper(event, `ad-attachments?filters[AD_Table_ID][$eq]=259&filters[Record_ID][$eq]=${orderId}`, 'GET', null) if(existingResponse?.data?.[0]?.id) { strapiAttachmentId = existingResponse.data[0].id } else { const newAttachmentResp: any = await strapiHelper(event, `ad-attachments`, 'POST', { data: { AD_Table_ID: 259, Record_ID: orderId, Record_UU: orderUid } }) if(newAttachmentResp?.data?.id) strapiAttachmentId = newAttachmentResp.data.id } if(strapiAttachmentId) { const timestamp = date.now().replace(/[-:]/g, '_').replace(' ', '-') const fileName = `CN23-DPD-${shipmentNo}-${timestamp}.pdf` const pdfBuffer = Buffer.from(customsDocBase64, 'base64') const FormData = (await import('form-data')).default const formData = new FormData() formData.append('field', 'attachment') formData.append('ref', 'api::ad-attachment.ad-attachment') formData.append('refId', String(strapiAttachmentId)) formData.append('files', pdfBuffer, { filename: fileName, contentType: 'application/pdf' }) formData.append('fileInfo', JSON.stringify({ caption: `CN23 Customs Document - DPD Shipment ${shipmentNo}`, alternativeText: `CN23-DPD-${shipmentNo}`, name: fileName })) const uploadResponse = await $fetch(`${config.api.strapi}/upload`, { method: 'POST', headers: { Authorization: `Bearer ${config.api.strapitoken}`, ...formData.getHeaders() }, body: formData }) return { success: true, strapiAttachmentId, uploadResponse } } return { success: false, error: 'Could not get or create attachment record' } } catch(error: any) { console.error('[CN23 DPD] Error saving customs document to Strapi:', error) return { success: false, error: error.message || 'Unknown error' } } } // ---------------------------------------------------------------------------- // Build the inner XML for storeOrders (single-parcel variant) // ---------------------------------------------------------------------------- interface BuildOrderArgs { refNo: string depot: string customerNumber: string product: 'CL' | 'E830' | 'E12' | 'E18' | 'IE2' | 'PL' | 'MAIL' | 'MAX' sender: { name1: string; name2?: string; street: string; houseNo: string; country: string; zipCode: string; city: string; email?: string; phone?: string; } recipient: { name1: string; name2?: string; street: string; houseNo: string; country: string; zipCode: string; city: string; state?: string; contact?: string; email?: string; phone?: string; comment?: string; } parcelWeightKg: number parcelDimsCm?: { length: number; width: number; height: number } predictEmail?: string shipperEori?: string // optional, goes on commercialInvoiceConsignor when set customs?: { invoiceNo?: string currency: string totalValue: number // total customs value (in invoice currency) incoterm: 'DAP' | 'DDP' | 'EXW' reasonForExport?: '01' | '02' | '03' // 01 Sale (default), 02 Return, 03 Gift items: Array<{ description: string quantity: number value: number // unit value × quantity in invoice currency weightKg: number hsCode?: string countryOfOrigin?: string // ISO alpha-2 }> } } function buildOrderXml(a: BuildOrderArgs): string { const parts: string[] = [] parts.push(` `) parts.push(` ${xmlEscape(a.refNo.substring(0, 35))}`) parts.push(` ${xmlEscape(a.depot)}`) parts.push(` ${a.product}`) // sender parts.push(` `) parts.push(` ${xmlEscape(a.sender.name1.substring(0, 35))}`) if (a.sender.name2) parts.push(` ${xmlEscape(a.sender.name2.substring(0, 35))}`) parts.push(` ${xmlEscape(a.sender.street.substring(0, 35))}`) parts.push(` ${xmlEscape(a.sender.houseNo.substring(0, 8))}`) parts.push(` ${xmlEscape(a.sender.country.substring(0, 2).toUpperCase())}`) parts.push(` ${xmlEscape(a.sender.zipCode.substring(0, 9))}`) parts.push(` ${xmlEscape(a.sender.city.substring(0, 35))}`) parts.push(` ${xmlEscape(a.customerNumber.substring(0, 17))}`) // DPD XSD order: customerNumber, contact, phone, mobile, fax, email, comment, ... if (a.sender.phone) parts.push(` ${xmlEscape(a.sender.phone.substring(0, 30))}`) if (a.sender.email) parts.push(` ${xmlEscape(a.sender.email)}`) parts.push(` `) // recipient (addressWithType) parts.push(` `) parts.push(` ${xmlEscape(a.recipient.name1.substring(0, 35))}`) if (a.recipient.name2) parts.push(` ${xmlEscape(a.recipient.name2.substring(0, 35))}`) parts.push(` ${xmlEscape(a.recipient.street.substring(0, 35))}`) if (a.recipient.houseNo) parts.push(` ${xmlEscape(a.recipient.houseNo.substring(0, 8))}`) if (a.recipient.state) parts.push(` ${xmlEscape(a.recipient.state.substring(0, 2).toUpperCase())}`) parts.push(` ${xmlEscape(a.recipient.country.substring(0, 2).toUpperCase())}`) parts.push(` ${xmlEscape(a.recipient.zipCode.substring(0, 9))}`) parts.push(` ${xmlEscape(a.recipient.city.substring(0, 35))}`) // XSD order after city: gln, customerNumber, contact, phone, mobile, fax, email, comment, iaccount. // contact is a printed label line (35); comment is data-only (70). if (a.recipient.contact) parts.push(` ${xmlEscape(a.recipient.contact.substring(0, 35))}`) // Phone must precede email per the DPD XSD if (a.recipient.phone) parts.push(` ${xmlEscape(a.recipient.phone.substring(0, 30))}`) if (a.recipient.email) parts.push(` ${xmlEscape(a.recipient.email)}`) if (a.recipient.comment) parts.push(` ${xmlEscape(a.recipient.comment.substring(0, 70))}`) parts.push(` `) // softwareVersion is V4_5-only; omitted for the V4_4 production endpoint. parts.push(` `) // single parcel parts.push(` `) parts.push(` ${kgTo10GramUnits(a.parcelWeightKg)}`) if (a.parcelDimsCm && a.parcelDimsCm.length && a.parcelDimsCm.width && a.parcelDimsCm.height) { parts.push(` ${buildVolumeString(a.parcelDimsCm.length, a.parcelDimsCm.width, a.parcelDimsCm.height)}`) } parts.push(` `) // productAndServiceData parts.push(` `) parts.push(` consignment`) if (a.predictEmail) { parts.push(` `) parts.push(` 1`) parts.push(` ${xmlEscape(a.predictEmail)}`) parts.push(` DE`) parts.push(` `) } if (a.customs) { const c = a.customs // customsAmount: invoice currency total, two decimals without separator (14.00 → 1400) const customsAmount = Math.round(c.totalValue * 100) parts.push(` `) // parcelType must be first per V4_4 XSD (false = regular parcel, not documents) parts.push(` false`) parts.push(` ${customsAmount}`) parts.push(` ${xmlEscape(c.currency.substring(0, 3).toUpperCase())}`) parts.push(` ${customsAmount}`) parts.push(` ${xmlEscape(c.currency.substring(0, 3).toUpperCase())}`) // V4_4-only element between customsCurrencyExport and customsPaper. // 02 = sender pays duties + taxes (matches DDP incoterm), 01 = recipient pays. parts.push(` ${c.incoterm === 'DDP' ? '02' : '01'}`) parts.push(` A`) // A = Commercial invoice parts.push(` true`) if (c.invoiceNo) parts.push(` ${xmlEscape(c.invoiceNo.substring(0, 20))}`) parts.push(` ${Math.min(99, c.items.length)}`) // reasonForExport is V4_5-only; omitted for V4_4 production endpoint. // customsTerms above already conveys who-pays-duties info to DPD. // V4_4: EORI is carried in commercialInvoiceConsignorVatNumber, which // is a sibling of commercialInvoiceConsignor (not a child of it). if (a.shipperEori) { parts.push(` ${xmlEscape(String(a.shipperEori).substring(0, 20))}`) parts.push(` `) parts.push(` ${xmlEscape(a.sender.name1.substring(0, 35))}`) if (a.sender.name2) parts.push(` ${xmlEscape(a.sender.name2.substring(0, 35))}`) parts.push(` ${xmlEscape(a.sender.street.substring(0, 35))}`) parts.push(` ${xmlEscape(a.sender.houseNo.substring(0, 8))}`) parts.push(` ${xmlEscape(a.sender.country.substring(0, 2).toUpperCase())}`) parts.push(` ${xmlEscape(a.sender.zipCode.substring(0, 9))}`) parts.push(` ${xmlEscape(a.sender.city.substring(0, 35))}`) parts.push(` ${xmlEscape(a.customerNumber.substring(0, 17))}`) parts.push(` `) } // incoterm + paymentTerm are V4_5-only; for V4_4 the who-pays info is // already conveyed via emitted earlier in this block. for (let i = 0; i < c.items.length && i < 99; i++) { const it = c.items[i] parts.push(` `) parts.push(` ${i + 1}`) parts.push(` ${Math.max(1, Math.round(it.quantity))}`) // V4_4 has no dedicated HS code field — embed it in the description // so it appears on the customs paperwork DPD generates. const desc = it.hsCode ? `${it.description} [HS: ${it.hsCode}]` : it.description parts.push(` ${xmlEscape(String(desc).substring(0, 200))}`) parts.push(` ${Math.round(it.value * 100)}`) if (it.countryOfOrigin) { parts.push(` ${iso2ToCustomsOrigin(it.countryOfOrigin)}`) } parts.push(` ${kgTo10GramUnits(it.weightKg)}`) parts.push(` ${kgTo10GramUnits(it.weightKg)}`) // unitPrice is V4_5-only; the per-line total is already in customsAmountLine. parts.push(` `) } parts.push(` `) } parts.push(` `) return ` \n${parts.join('\n')}\n ` } function buildStoreOrdersInner(orderXml: string): string { return ` PDF A6 false ${orderXml}` } // ---------------------------------------------------------------------------- // M_Inout persistence + marketplace ack (mirrors DHL handleInoutFunc 1:1) // ---------------------------------------------------------------------------- const handleInoutFunc = async (event: any, authToken: any = null, option: any) => { const tIF0 = Date.now() let data: any = {} const token = authToken ?? await getTokenHelper(event) const trackingPayload: any = { TrackingNo: option.tracking_number, DPD_Shipment_Code: option.shipment_code, DPD_Label_Base64: option.label_base64, IsCommissioned: true, shipping_date: date.now('', '', {timeZone: 'UTC'}).replace(' ', 'T')+'Z', shipping_service_name: option.shippingService, tableName: 'M_Inout' } if(option.paket_type) trackingPayload['M_Paket_Type_ID'] = option.paket_type // Link the commissioning table (Tisch) so the correct camera's video can be // resolved later. No-op when the printer is unknown/unmapped. Object.assign(trackingPayload, await commissionTableFkPatch(event, option.shippingPrinter, token)) const tIFPatch = Date.now() const resp: any = await fetchHelper(event, 'models/m_inout/'+option.inout_id, 'PUT', token, trackingPayload) const tIFPut1 = Date.now() if(resp) { data['shipment'] = resp data['status'] = 200 data['message'] = '' if(resp?.IsCommissionedConfirmed !== true) { // The marketplace delivery-confirmation call (Shopify/Amazon/PlentyOne/eBay/ // JTL/Shopware) is external API traffic that has nothing to do with the label // — live-traced on 2026-09-03 to consistently cost ~8.8-8.9s while the DPD // SOAP call + CUPS print pipeline together take under 1s. Run it detached so // it can't hold up printing. Failures are logged; the existing resubmit/ // manual-retry flow covers re-running it if needed. Nothing in the frontend // response handling reads the per-marketplace result fields, so nothing is // lost by not awaiting this. confirmMarketplaceDelivery(event, token, option, trackingPayload).catch((err: any) => { console.error('[DPD Commission] background marketplace ack failed:', err?.message || err) }) } } console.log(`[DPD TIMING] handleInout patch=${tIFPatch - tIF0}ms put1=${tIFPut1 - tIFPatch}ms (marketplace ack backgrounded)`) return data } // Delivery confirmation to the order's marketplace — deliberately not awaited by // handleInoutFunc (see comment there). Never throws past its own try/catch per // marketplace branch; the outer .catch() on the caller is just a last-resort net. const confirmMarketplaceDelivery = async (event: any, token: any, option: any, trackingPayload: any) => { const tStart = Date.now() const resp2: any = await fetchHelper(event, 'models/m_inout/'+option.inout_id+'?$expand=c_order_id,m_inoutline,c_bpartner_location_id,ad_org_id', 'GET', token, null) const tGet2 = Date.now() const formatOrderNumber = () => { const documentNo = resp2?.C_Order_ID?.DocumentNo ?? '0' const companyName = resp2?.AD_Org_ID?.companyname ?? '' const firstWord = companyName.trim().split(/\s+/)[0] || '' return firstWord ? `${documentNo}-${firstWord}` : documentNo } if(!resp2?.C_Order_ID?.C_OrderSource_ID?.id) { console.log(`[DPD TIMING] background get2=${tGet2 - tStart}ms noOrderSource`) return } const resp3: any = await fetchHelper(event, 'models/c_ordersource/'+resp2.C_Order_ID.C_OrderSource_ID.id, 'GET', token, null) const tGet3 = Date.now() console.log('[DEBUG DPD] Order Source Info:', { orderSourceId: resp2.C_Order_ID.C_OrderSource_ID.id, orderSourceValue: resp3?.value, marketplaceIdentifier: resp3?.Marketplace?.identifier, hasShopware6: !!resp2?.C_Order_ID?.shopware6_order_id, hasShopify: !!resp2?.C_Order_ID?.shopify_order_id, hasAmazon: !!resp2?.C_Order_ID?.amazon_order_id, hasPlentyone: !!resp2?.C_Order_ID?.plentyone_order_id, hasJTLFFN: !!(resp2?.C_Order_ID?.ExternalOrderId && resp3?.Marketplace?.identifier === 'jtl-ffn'), hasEbay: !!(resp2?.C_Order_ID?.ebay_order_id) }) const mailPayload = { email: option.customerEmail, isSentCustomTrackingMail: option.isSentCustomTrackingMail, orderNumber: formatOrderNumber(), name: resp2?.C_Order_ID?.C_BPartner?.identifier ?? 'Mr/Ms', company: 'LogYou GmbH', carrier: option.shippingService ?? 'DPD', lines: resp2?.m_inoutline?.filter((i: any) => i.M_Product_ID && !i.C_Charge_ID).map((i: any) => ({ description: i.M_Product_ID.identifier, quantity: i.QtyEntered })) ?? [], address1: resp2?.C_Order_ID?.C_BPartner_Location_ID?.C_Location_ID?.Address1 ?? '', address2: resp2?.C_Order_ID?.C_BPartner_Location_ID?.C_Location_ID?.Address2 ?? '', city: resp2?.C_Order_ID?.C_BPartner_Location_ID?.C_Location_ID?.City ?? '', country: resp2?.C_Order_ID?.C_BPartner_Location_ID?.C_Location_ID?.C_Country_ID?.identifier ?? '', postal: resp2?.C_Order_ID?.C_BPartner_Location_ID?.C_Location_ID?.Postal ?? '' } const markConfirmed = async () => { await fetchHelper(event, 'models/m_inout/'+option.inout_id, 'PUT', token, { IsCommissionedConfirmed: true, ack_commissioned_laravel: true, tableName: 'M_Inout' }) } if(resp2?.C_Order_ID?.shopware6_order_id && resp3?.marketplace_url) { try { const resp4: any = await laravelHelper(event, 'sales/orders/mark-shopware-order-delivery', 'POST', { marketplace_url: resp3.marketplace_url, marketplace_key: resp3.marketplace_key, marketplace_secret: resp3.marketplace_secret, id: resp2.C_Order_ID.shopware6_order_id, trackingCodes: [option.tracking_number], mail: mailPayload }) if(resp4) await markConfirmed() } catch(err: any) { console.error('[DPD Commission] shopware ack failed:', err?.data ?? err?.message ?? err) } } if(resp2?.C_Order_ID?.shopify_order_id && resp3?.marketplace_url) { try { const resp4: any = await laravelHelper(event, 'sales/orders/mark-shopify-order-delivery', 'POST', { orderSource: resp3, id: resp2.C_Order_ID.shopify_order_id, trackingCodes: { number: option.tracking_number, url: option.tracking_url, company: 'DPD' }, mail: mailPayload }) if(resp4) await markConfirmed() } catch(err: any) { console.error('[DPD Commission] shopify ack failed:', err?.data ?? err?.message ?? err) } } if(resp2?.C_Order_ID?.amazon_order_id && resp3?.marketplace_url) { try { const resp4: any = await laravelHelper(event, 'sales/orders/mark-amazon-order-delivery', 'POST', { orderSource: resp3, id: resp2.C_Order_ID.amazon_order_id, details: { shippingDate: trackingPayload.shipping_date, carrierCode: 'DPD', shippingMethod: 'Paket', referenceId: resp2?.DocumentNo ?? option.inout_id }, trackingCodes: { number: option.tracking_number, url: option.tracking_url }, mail: mailPayload }) if(resp4) await markConfirmed() } catch(err: any) { console.error('[DPD Commission] amazon ack failed:', err?.data ?? err?.message ?? err) } } if(resp2?.C_Order_ID?.plentyone_order_id && resp3?.marketplace_url) { try { const plentyOneWeight = resp2?.m_inoutline?.reduce((acc: any, prev: any) => Number(acc['Weight'] ?? 0) + Number(prev['Weight'] ?? 0), 0) const resp4: any = await laravelHelper(event, 'sales/orders/mark-plentyone-order-delivery', 'POST', { orderSource: resp3, id: resp2.C_Order_ID.plentyone_order_id, details: { shippingDate: trackingPayload.shipping_date, carrierCode: 'DPD', shippingMethod: 'Paket', referenceId: resp2?.DocumentNo ?? option.inout_id, weight: plentyOneWeight ?? 0 }, trackingCodes: { number: option.tracking_number, url: option.tracking_url }, mail: mailPayload }) if(resp4) await markConfirmed() } catch(err: any) { console.error('[DPD Commission] plentyone ack failed:', err?.data ?? err?.message ?? err) } } if(resp2?.C_Order_ID?.ExternalOrderId && resp3?.Marketplace?.identifier === 'jtl-ffn' && resp3?.marketplace_url) { try { const resp4: any = await laravelHelper(event, 'sales/orders/mark-jtl-order-delivery', 'POST', { orderSource: resp3, id: resp2.C_Order_ID.ExternalOrderId, details: { shippingDate: trackingPayload.shipping_date, carrierCode: 'DPD', shippingMethod: 'Paket', referenceId: resp2?.DocumentNo ?? option.inout_id, weight: 0 }, trackingCodes: { number: option.tracking_number, url: option.tracking_url }, mail: mailPayload }) if(resp4) await markConfirmed() } catch(err: any) { console.error('[DPD Commission] jtl ack failed:', err?.data ?? err?.message ?? err) } } if(resp2?.C_Order_ID?.ebay_order_id) { try { const resp4: any = await laravelHelper(event, 'sales/orders/mark-ebay-order-delivery', 'POST', { orderSource: resp3, id: resp2.C_Order_ID.ebay_order_id, details: { shippingDate: trackingPayload.shipping_date, carrierCode: 'DPD', shippingMethod: 'Paket', referenceId: resp2?.DocumentNo ?? option.inout_id }, trackingCodes: { number: option.tracking_number, url: option.tracking_url }, mail: mailPayload }) if(resp4) await markConfirmed() } catch(err: any) { console.error('[DPD Commission] ebay ack failed:', err?.data ?? err?.message ?? err) } } console.log(`[DPD TIMING] background get2=${tGet2 - tStart}ms get3=${tGet3 - tGet2}ms marketplace=${Date.now() - tGet3}ms`) } // ---------------------------------------------------------------------------- // Main handler // ---------------------------------------------------------------------------- const handleFunc = async (event: any) => { const tStart = Date.now() let data: any = {} const body = await readBody(event) let customCreds: Partial | null = null let customerEmail = 'fulfillcustomer@logyou.de' let isSentCustomTrackingMail = false const tokenEmail = await getTokenHelper(event) const respInout: any = await fetchHelper(event, 'models/m_inout/'+body.inOutId+'?$expand=c_order_id,ad_org_id', 'GET', tokenEmail, null) if(respInout?.C_Order_ID?.C_OrderSource_ID?.id) { const respOrderSource: any = await fetchHelper(event, 'models/c_ordersource/'+respInout.C_Order_ID.C_OrderSource_ID.id, 'GET', tokenEmail, null) isSentCustomTrackingMail = respOrderSource?.isSentCustomTrackingMail ?? false if(body.email) { customerEmail = (respOrderSource?.isExcludetrackingmail ? 'fulfillcustomer@logyou.de' : body.email) } const extracted = extractOrderSourceDpdCreds(respOrderSource) if (extracted) { if (extracted.missing.length > 0) { console.warn('[DPD Commission] OrderSource missing fields:', extracted.missing, { id: respOrderSource?.id, Name: respOrderSource?.Name }) return { status: 400, message: `OrderSource ${respOrderSource?.id} (${respOrderSource?.Name || '?'}) is missing required DPD field(s): ${extracted.missing.join(', ')}. Open Settings → Order Sources → ${respOrderSource?.Name || respOrderSource?.id} and fill them in.` } } customCreds = extracted.creds } } let customerPhone = '+49 987654321' if(body.telephone) customerPhone = body.telephone // Format order number with first word of company name and sanitize special characters const companyName = respInout?.AD_Org_ID?.companyname ?? '' const firstWord = companyName.trim().split(/\s+/)[0]?.replace(/[^a-zA-Z0-9]/g, '') || '' const baseOrderNumber = body.orderNumber ?? body.inOutUId.replaceAll('-', '') const formattedOrderNumber = firstWord ? `${baseOrderNumber}-${firstWord}` : baseOrderNumber // Fetch sender email from the organization's linked C_BPartner (matches DHL) let orgSenderEmail: string = '' const orgId = respInout?.AD_Org_ID?.id if (orgId) { try { const orgRes: any = await fetchHelper(event, `models/ad_org/${orgId}?$select=C_BPartner_ID`, 'GET', tokenEmail, null) const orgBpartnerId = orgRes?.C_BPartner_ID?.id if (orgBpartnerId) { const partnerRes: any = await fetchHelper(event, `models/c_bpartner/${orgBpartnerId}?$select=freight_service_sender_mail`, 'GET', tokenEmail, null) orgSenderEmail = String(partnerRes?.freight_service_sender_mail ?? '').trim() } } catch (err: any) { console.error(`[DPD] Error fetching org sender email:`, err?.message || err) } } const tPreflightDone = Date.now() // Authenticate to DPD to obtain depot let auth, creds try { const r = await dpdGetAuth(event, customCreds) auth = r.auth creds = r.creds } catch(loginErr: any) { data['status'] = 500 data['message'] = loginErr?.message || 'DPD authentication failed' return data } const tAuthDone = Date.now() const country = String(body.country ?? '').toUpperCase().trim() const isInternational = !EU_COUNTRIES.includes(country) const enableCustoms = body.enableCN23 === true || (isInternational && Array.isArray(body.parcelItems) && body.parcelItems.length > 0) // Build the order XML const totalCustomsValue = (body.parcelItems || []).reduce((s: number, it: any) => s + Number(it.value || 0) * Number(it.quantity || 1), 0) const customs = enableCustoms ? { invoiceNo: body.orderNumber ? String(body.orderNumber).substring(0, 20) : undefined, currency: body.totalOrderValueCurrency || 'EUR', totalValue: totalCustomsValue || 0, incoterm: (body.customsIncoterm === 'DDP' || body.customsIncoterm === 'EXW' ? body.customsIncoterm : 'DAP') as 'DAP' | 'DDP' | 'EXW', reasonForExport: (['01', '02', '03'].includes(body.customsReasonForExport) ? body.customsReasonForExport : '01') as '01' | '02' | '03', items: (body.parcelItems || []).map((it: any) => ({ description: it.description || '', quantity: Number(it.quantity || 1), value: Number(it.value || 0) * Number(it.quantity || 1), weightKg: Number(it.weight || 0.1), // taricCode = M_Product.taricCode fallback for labels created without the // customs modal normalizing it into hsCode hsCode: it.hsCode || it.taricCode || undefined, countryOfOrigin: it.countryOfOrigin || undefined })) } : undefined const recipientName2 = String(body.name2 ?? '').trim() const recipientAddr2 = String(body.address2 ?? '').trim() const orderXml = buildOrderXml({ refNo: formattedOrderNumber, depot: auth.depot, customerNumber: creds.customerNumber, product: 'CL', shipperEori: body.shipperEori ? String(body.shipperEori).trim() : undefined, sender: { name1: respInout?.AD_Org_ID?.companyname && respInout?.AD_Org_ID !== 1000000 ? respInout?.AD_Org_ID?.companyname : 'LogYou GmbH', name2: respInout?.AD_Org_ID?.companyname && respInout?.AD_Org_ID !== 1000000 ? 'c/o LogYou GmbH' : undefined, street: 'Mühlenweg', houseNo: '4', country: 'DE', zipCode: '35510', city: 'Butzbach', email: orgSenderEmail || 'info@logyou.de', phone: '+4960339160570' }, recipient: { name1: String(body.name ?? '').trim(), // DPD has no name3 (DHL puts address2 there). The only extra printed lines are // name2 and contact (35 each): address2 -> name2 when no company/c/o is set, // otherwise the company keeps name2 and address2 prints on the contact line. // comment is not printed; it only mirrors address2 into DPD's data record. name2: recipientName2 || recipientAddr2 || undefined, contact: (recipientName2 && recipientAddr2) ? recipientAddr2 : undefined, street: String(body.address ?? '').trim(), houseNo: String(body.houseNumber || '').trim(), country: country, zipCode: String(body.postalCode ?? '').trim(), city: String(body.city ?? '').trim(), state: body.countryState ? String(body.countryState).trim() : undefined, email: customerEmail, phone: customerPhone, comment: recipientAddr2 || undefined }, parcelWeightKg: parseFloat(Number(body.weight ?? 0).toFixed(2)) + 0.25, // 0.25kg packaging (matches DHL) parcelDimsCm: (body.length && body.width && body.height) ? { length: toCm(body.length), width: toCm(body.width), height: toCm(body.height) } : undefined, predictEmail: customerEmail && /@/.test(customerEmail) ? customerEmail : undefined, customs }) let dpdResult try { const { result } = await dpdStoreOrders(event, customCreds, buildStoreOrdersInner(orderXml)) dpdResult = result } catch(dpdErr: any) { const detail = dpdErr?.dpdFault ? `${dpdErr.dpdFault.code}: ${dpdErr.dpdFault.message}` : (dpdErr?.message || 'DPD API Error') console.error('[DPD Commission] DPD API rejected shipment:', detail) data['status'] = 400 data['message'] = detail data['dpd_errors'] = [detail] data['debug'] = { payload: orderXml.substring(0, 2000) } return data } const tSoapDone = Date.now() // Check for per-shipment faults in a 200 response if (dpdResult.faults && dpdResult.faults.length > 0) { const detail = dpdResult.faults.map(f => `${f.code}: ${f.message}`).join(' | ') console.error('[DPD Commission] DPD returned faults:', detail) data['status'] = 400 data['message'] = detail data['dpd_errors'] = dpdResult.faults.map(f => `${f.code}: ${f.message}`) return data } if (!dpdResult.parcelLabelNumbers.length || !dpdResult.labelPdfBase64) { data['status'] = 500 data['message'] = 'DPD response missing tracking number or label' return data } const trackingNo = dpdResult.parcelLabelNumbers[0] const labelB64 = dpdResult.labelPdfBase64 // Shape the response like DHL so the frontend's existing label-print flow works data['dpd_parcel'] = { shipmentNo: dpdResult.mpsId || trackingNo, parcelLabelNumber: trackingNo, mpsId: dpdResult.mpsId, label: { b64: labelB64 } } // Persist label + tracking to M_Inout (with retry on token expiry, mirrors DHL) try { const res2 = await handleInoutFunc(event, null, { inout_id: body.inOutId, tracking_number: trackingNo, tracking_url: '', shipment_code: dpdResult.mpsId || trackingNo, label_base64: labelB64, paket_type: body.paketType, shippingService: body.shippingService || 'DPD', shippingPrinter: body.shippingPrinter, customerEmail: customerEmail, isSentCustomTrackingMail: isSentCustomTrackingMail }) data = { ...data, ...res2 } } catch(err: any) { try { const authToken: any = await refreshTokenHelper(event) const res3 = await handleInoutFunc(event, authToken, { inout_id: body.inOutId, tracking_number: trackingNo, tracking_url: '', shipment_code: dpdResult.mpsId || trackingNo, label_base64: labelB64, paket_type: body.paketType, shippingService: body.shippingService || 'DPD', shippingPrinter: body.shippingPrinter, customerEmail: customerEmail, isSentCustomTrackingMail: isSentCustomTrackingMail }) data = { ...data, ...res3 } } catch(error: any) { data = errorHandlingHelper(err?.data ?? err, error?.data ?? error) } } const tInoutDone = Date.now() console.log(`[DPD TIMING] inOutId=${body.inOutId} preflight=${tPreflightDone - tStart}ms auth=${tAuthDone - tPreflightDone}ms soap=${tSoapDone - tAuthDone}ms inout=${tInoutDone - tSoapDone}ms total=${tInoutDone - tStart}ms`) return data } export default defineEventHandler(async (event) => { let data: any = {} try { data = await handleFunc(event) } catch(err: any) { data = errorHandlingHelper(err?.data ?? err, err?.data ?? err) } return data })