import dhlHelper from "../../../../utils/dhlHelper" import errorHandlingHelper from "../../../../utils/errorHandlingHelper" import getDhlMainCredential from "../../../../utils/dhlMainCredential" const toCm = (val: any): number => { if (val === null || val === undefined || val === '') return 0 const raw = String(val).trim() if (raw === '' || raw === '0') return 0 const hasDecimal = raw.includes('.') || raw.includes(',') const numeric = parseFloat(raw.replace(',', '.')) if (isNaN(numeric) || numeric <= 0) return 0 const cm = hasDecimal ? numeric * 100 : numeric / 10 return parseFloat(cm.toFixed(2)) } export default defineEventHandler(async (event) => { const data: any = {} const body: any = await readBody(event) const country = String(body.country ?? '').toUpperCase().trim() const countryIso3 = String(body.countryIso3 ?? '').toUpperCase().trim() // Normalize parcels: accept either body.parcels (array) for multi-parcel mode, // or fall back to the flat weight/length/width/height for single-parcel mode. const rawParcels: any[] = Array.isArray(body.parcels) && body.parcels.length > 0 ? body.parcels : [{ weight: body.weight, length: body.length, width: body.width, height: body.height }] const parcels = rawParcels .map(p => ({ weightKg: parseFloat(p.weight ?? 0) || 0, length: p.length, width: p.width, height: p.height })) .filter(p => p.weightKg > 0) if(!body.name || !body.address || !body.postalCode || !body.city || !country || !countryIso3 || parcels.length === 0) { return { status: 400, message: 'Missing required fields: name, address, postalCode, city, country, and at least one parcel with weight > 0' } } const isDE = country === 'DE' const extraWeight = 0.25 const baseRefNo = `MANUAL-${Date.now()}` // Optional shipper EORI (used on the CN23 customs form for non-EU shipments). // Only attached when the user supplied one and the destination is non-EU. const shipperEoriRaw = String(body.shipperEori || '').trim() 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'] const isNonEUDestination = !EU_COUNTRIES.includes(country) // DHL accepts "EORI/" formatted refs (matches the existing // commission flow's hard-coded value). For manual we trust what the user // typed; if they entered a raw EORI we prefix it automatically. const shipperCustomsRef = isNonEUDestination && shipperEoriRaw ? (shipperEoriRaw.toUpperCase().startsWith('EORI') ? shipperEoriRaw : `EORI${shipperEoriRaw}/0000`) : '' // Customs declaration (parallel to DPD). DHL maps reason for export to: // 01 Sale → COMMERCIAL_GOODS / OTHER (we keep OTHER for flexibility) // 02 Return → RETURN_OF_GOODS // 03 Gift → GIFT const customsInput: any = body.customs || {} const reasonCode = ['01', '02', '03'].includes(customsInput.reasonForExport) ? customsInput.reasonForExport : '01' const dhlExportType = reasonCode === '02' ? 'RETURN_OF_GOODS' : (reasonCode === '03' ? 'GIFT' : 'OTHER') const customsCurrency = String(customsInput.currency || 'EUR').toUpperCase().substring(0, 3) // Customs items derived from per-parcel customs fields when the user filled them in const customsItems = (Array.isArray(body.parcels) ? body.parcels : []) .map((p: any) => ({ description: String(p.description || '').trim(), hsCode: String(p.hsCode || '').trim(), countryOfOrigin: String(p.countryOfOrigin || '').trim().toUpperCase(), quantity: Math.max(1, parseInt(p.quantity || 1) || 1), value: parseFloat(p.value) || 0, weightKg: parseFloat(p.weight ?? 0) || 0 })) .filter((it: any) => it.description && it.value > 0) const totalCustomsValue = customsItems.reduce((s: number, it: any) => s + it.value * it.quantity, 0) // Billing numbers come from the DHL shipper record (Materials → Shippers), // with the historical literals as final fallback. const mainCred = await getDhlMainCredential(event) // Build one DHL shipment per parcel. Each shipment gets a unique refNo // suffix so DHL can distinguish them in their billing/tracking. const shipments = parcels.map((p, i) => { // Kleinpaket eligibility is per-parcel const meetsV62KPCriteria = isDE && p.weightKg < 1 const product = meetsV62KPCriteria ? 'V62KP' : (isDE ? 'V01PAK' : 'V53WPAK') const billingNumber = meetsV62KPCriteria ? (mainCred.billingKp || '63807218926201') : (isDE ? (mainCred.billingDe || '63807218920101') : (mainCred.billingInt || '63807218925301')) const refNo = parcels.length > 1 ? `${baseRefNo}-P${i + 1}` : baseRefNo return { product, billingNumber, refNo, shipper: { name1: 'LogYou GmbH', addressStreet: 'Mühlenweg', addressHouse: '4', postalCode: '35510', city: 'Butzbach', country: 'DEU', email: 'info@logyou.de', phone: '+4960339160570' }, consignee: { name1: String(body.name).trim(), addressStreet: String(body.address).trim(), addressHouse: String(body.houseNumber ?? '').trim(), postalCode: String(body.postalCode).trim(), city: String(body.city).trim(), country: countryIso3, email: String(body.email ?? '').trim(), phone: String(body.phone ?? '').trim() }, details: { dim: { uom: 'cm', height: toCm(p.height), length: toCm(p.length), width: toCm(p.width) }, weight: { uom: 'kg', value: parseFloat((p.weightKg + extraWeight).toFixed(2)) } }, // Customs declaration for non-EU shipments. Built from the customs + // per-parcel items the user filled in. shipperCustomsRef is the EORI. ...(isNonEUDestination && customsItems.length > 0 ? { customs: { exportType: dhlExportType, ...(shipperCustomsRef ? { shipperCustomsRef } : {}), postalCharges: { currency: customsCurrency, value: 0 }, invoiceNo: baseRefNo, items: customsItems.map(it => ({ itemDescription: it.description.substring(0, 256), packagedQuantity: it.quantity, itemValue: { currency: customsCurrency, value: it.value }, itemWeight: { uom: 'kg', value: it.weightKg }, ...(it.hsCode ? { hsCode: it.hsCode.substring(0, 11) } : {}), ...(it.countryOfOrigin ? { countryOfOrigin: it.countryOfOrigin.substring(0, 3) } : {}) })) } } : {}) } }) try { const res: any = await dhlHelper(event, 'shipping/v2/orders?printFormat=910-300-400', 'POST', { profile: 'STANDARD_GRUPPENPROFIL', shipments }) if(res?.items?.[0]?.label?.b64) { const first = res.items[0] const allItems = (res.items || []).filter((it: any) => it?.shipmentNo) const trackingNumbers = allItems.map((it: any) => it.shipmentNo) data['status'] = 200 data['message'] = '' data['label_b64'] = first.label.b64 // first label for preview data['tracking_number'] = first.shipmentNo // first for backwards compat data['tracking_numbers'] = trackingNumbers // all parcels data['parcel_count'] = trackingNumbers.length data['shipment_code'] = first.shipmentNo data['routing_code'] = first.routingCode data['product'] = shipments[0].product data['billingNumber'] = shipments[0].billingNumber // Per-parcel labels for multi-parcel mode (so frontend can print all of them) data['parcels'] = allItems.map((it: any) => ({ tracking_number: it.shipmentNo, label_b64: it?.label?.b64 || '', routing_code: it.routingCode || '' })) } else { data['status'] = 500 data['message'] = res?.status?.detail ?? 'DHL label was not created' } } catch(dhlErr: any) { const dhlData = dhlErr?.data const itemErrors: string[] = [] if(dhlData?.items && Array.isArray(dhlData.items)) { for(const item of dhlData.items) { if(item?.validationMessages && Array.isArray(item.validationMessages)) { for(const vm of item.validationMessages) { itemErrors.push(`${vm.property || ''}: ${vm.validationMessage || vm.message || ''}`.trim()) } } if(item?.sstatus?.detail) { itemErrors.push(item.sstatus.detail) } } } const errorDetail = itemErrors.length > 0 ? itemErrors.join(' | ') : (dhlData?.status?.detail ?? dhlData?.detail ?? dhlErr?.message ?? 'DHL API Error') data['status'] = dhlData?.status?.statusCode ?? dhlErr?.statusCode ?? 400 data['message'] = errorDetail data['dhl_errors'] = itemErrors } return data })