'002 ΕΙΔΙΚΗ ΔΙΑΧΕΙΡΗΣΗ', 'timed_delivery' => '003 ΠΡΟΚΑΘΟΡΙΣΜΕΝΗ ΩΡΑ', 'saturday_delivery' => '004 ΕΠΙΔΟΣΗ ΣΑΒΒΑΤΟΥ', ]; public function __construct(private readonly AreaResolver $areaResolver) {} public function render(Shipment $shipment): string { return $this->renderMany(collect([$shipment])); } /** * Several labels in one PDF (the Pending Vouchers screen's "Print * selected"). Each label view is a full page — a single `.page` block * inside — so the pages are stacked under the first view's * (the styles are the same for every label) with a page break * between them. * * @param Collection $shipments */ public function renderMany(Collection $shipments): string { $paperSize = config('elta.label_paper_size', 'a4'); $head = null; $styles = []; $pages = []; foreach ($shipments as $shipment) { $data = $this->data($shipment); // A6 prints every parcel on the same label; on A4 the extra // parcels of a multi-parcel send use ELTA's own separate, smaller // report (sydetaE1.rdlc) — so one PDF can mix two layouts, and // each layout's styles are carried into the shared . $view = match (true) { $paperSize === 'a6' => 'core::shipping.carriers.elta.label-a6', $data['isChild'] => 'core::shipping.carriers.elta.label-a4-child', default => 'core::shipping.carriers.elta.label-a4', }; $html = view($view, $data)->render(); $viewHead = Str::before($html, '.*?#s', $viewHead, $matches); $styles[$view] = implode('', $matches[0]); $pages[] = Str::beforeLast(Str::after(Str::after($html, ''), ''); } $html = Str::before($head, '' .Str::afterLast($head, '') .''.implode('', $pages).''; $pdf = Pdf::loadHTML($html); // A6 = 104mm x 148mm, per RDLCPrinter.cs's own DeviceInfo // override for printer_size==2 (1mm ≈ 2.8346pt) — note this is // ELTA's own thermal-label size, not the ISO A6 (105x148mm). A // 4-value paper array plus an orientation string together confuse // dompdf into doubling the canvas and silently overflowing to a // second blank page; the array alone is already portrait (height > // width), so no orientation argument. $paperSize === 'a6' ? $pdf->setPaper([0, 0, 294.80, 419.53]) : $pdf->setPaper('a4'); return $pdf->output(); } /** * @return array */ private function data(Shipment $shipment): array { $order = $shipment->order; $address = $order->shippingAddress; $paperSize = config('elta.label_paper_size', 'a4'); $piece = $this->pieceOf($shipment); $master = $piece['master'] ?? null; // A child parcel's label details (stations, service, issue time, // weight) are the main voucher's. $meta = array_merge( $master?->meta?->toArray() ?? [], array_filter($shipment->meta?->toArray() ?? [], fn ($value) => $value !== null), ); // Issuing a pending voucher (PELVG01NEW1) returns the real // destination station and service; vouchers issued before that flow // existed don't have them, so fall back to the postcode lookup. $station = filled($meta['rec_station'] ?? null) ? ['code' => $meta['rec_station'], 'name' => $meta['rec_station_t'] ?? ''] : (fn ($s) => ['code' => $s->code, 'name' => $s->name])($this->areaResolver->resolve($address->postcode)); $isChild = $piece !== null && $piece['number'] > 1; $masterVoucher = $isChild ? ($master?->tracking_reference ?? $shipment->parent_reference) : null; $services = $isChild ? [] : (array) ($meta['services'] ?? []); $surcharges = $this->surchargeSlots($services); $cod = $isChild ? $this->childCodFields(($master?->meta['cod_amount'] ?? null) > 0) : $this->codFields(isset($meta['cod_amount']) ? (float) $meta['cod_amount'] : null); $dateTime = (string) ($meta['date_time'] ?? ''); $occurredAt = strlen($dateTime) >= 12 ? Carbon::createFromFormat('YmdHi', substr($dateTime, 0, 12)) : now()->inDisplayTimezone(); $voucherNo = $shipment->tracking_reference; $ocrLine = $isChild ? '' : (string) ($meta['ocr_line'] ?? ''); $packageCount = (int) ($piece['of'] ?? $meta['package_count'] ?? 1); $pieceLabel = sprintf('%03d/%03d', $piece['number'] ?? 1, $packageCount); $generator = new BarcodeGeneratorPNG(); return [ 'sender_name' => config('elta.sender_name'), 'sender_address' => config('elta.sender_address'), 'sender_postcode' => config('elta.sender_postcode'), 'sender_area' => config('elta.sender_area'), 'sender_phone' => config('elta.sender_phone'), 'apost_code' => config('elta.apost_code'), 'recipient_name' => trim("{$address->first_name} {$address->last_name}"), 'recipient_address' => $address->line_one, 'recipient_postcode' => $address->postcode, 'recipient_area' => $address->city, 'recipient_phone' => $address->contact_phone, // Mirrors Sydeta.cs::print_vg()'s own dataRow["sender_1..5"] / // dataRow["rec_1..5"] construction: name on its own line, then // address, then "TK: ΤΗΛ:" — each of // the RDLC's rectangle8/rectangle9 boxes renders these as 3 // separate textbox lines (not 5; sender_2/sender_3 and // rec_2/rec_3 only appear when a line's text exceeds the // report's 40-char wrap width, which our own data never hits // in practice for a name/address/contact triple). 'sender_lines' => [ 'Κωδικός:'.config('elta.apost_code'), config('elta.sender_name'), config('elta.sender_address'), 'TK:'.config('elta.sender_postcode').' '.config('elta.sender_area').' ΤΗΛ:'.config('elta.sender_phone'), ], 'recipient_lines' => [ trim("{$address->first_name} {$address->last_name}"), $address->line_one, 'TK:'.$address->postcode.' '.$address->city, 'ΤΗΛ: '.$address->contact_phone, ], 'date' => $occurredAt->format('d/m/Y'), 'time' => $occurredAt->format('H:i'), 'voucher_no' => $voucherNo, 'barcode_voucher' => $this->barcodeDataUri($generator, $voucherNo), 'weight' => $meta['weight'] ?? null, 'package_count' => $packageCount, // Τεμάχια: "001/002" on the main voucher, "002/002" on the second // parcel (print_vg() / print_child()). The A4 child report shows // the piece in its own big box (periexomeno) and just the total // count under Τεμάχια. 'package_label' => $packageCount > 1 ? ($isChild && $paperSize !== 'a6' ? (string) $packageCount : $pieceLabel) : '001', 'piece_label' => $pieceLabel, 'multiPiece' => $packageCount > 1, 'polaplo' => match (true) { $isChild => 'ΠΟΛΛΑΠΛΗ ΑΠΟΣΤΟΛΗ - MASTER : '.$masterVoucher, $packageCount > 1 => '* ΠΟΛΛΑΠΛΗ ΑΠΟΣΤΟΛΗ *', default => null, }, 'isChild' => $isChild, 'master_voucher' => $masterVoucher, // The "copy" field: "ΠΟΛΛΑΠΛH" on an A6 child, the master's // voucher (*NZ…GR*) under "ΜASTER ΣΥΔΕΤΑ" on an A4 child. 'copy' => $isChild ? ($paperSize === 'a6' ? 'ΠΟΛΛΑΠΛH' : '*'.$masterVoucher.'*') : null, 'station_apo' => config('elta.origin_station_code'), 'station_pros' => $station['code'], 'station_pros_title' => $station['name'], 'service_code' => $meta['rec_srv'] ?? '', 'service_name' => $meta['rec_srv_t'] ?? '', // print_child() doesn't set these, so a child's cells stay empty. 'xreosi' => $isChild ? null : 'ΧΡΕΩΣΗ ΑΠΟΣΤΟΛΕΑ ΠΙΣΤΩΣΗ', 'siimvasi' => $isChild ? null : '131775-9', 'cod_amount' => $meta['cod_amount'] ?? null, 'ocr_line' => $ocrLine, // The A6 layout's 3-cell row shows a 13-digit reference derived // from the OCR line, not the barcode's own voucher text — // confirmed against the real sample: ">14260009814363<..." // yields reference "1426000981436" (13 chars after the '>'). 'ocr_reference' => strlen($ocrLine) >= 14 ? substr($ocrLine, 1, 13) : null, // The reference we send ELTA as pel_ref_no (order reference; // older vouchers were sent the order id). 'order_reference' => $isChild ? null : (string) $order->reference, // sydetaE.rdlc's "Πρόσθετες Υπηρεσίες" (sur_1..4): the ticked // extra services, in ELTA's own wording and slots. 'sur_1' => $surcharges[1], 'sur_2' => $surcharges[2], 'sur_3' => $surcharges[3], 'sur_4' => null, ...$cod, // ogos_2: "LxWxH = volume", only when dimensions were given — // we never send any, so the box stays empty like the client's. 'volumetric_weight' => $meta['volumetric_weight'] ?? null, 'eltaLogo' => $this->logoDataUri(), ]; } /** * The client's own sur_1..3 placement (Sydeta.cs::print_vg()): 002 is * always sur_1; 003 is always sur_2; 004 is sur_3 after 003, else * sur_2. Slots can stay empty (003 alone leaves sur_1 blank). * * @param array $services ExtraService values * @return array{1: ?string, 2: ?string, 3: ?string} */ private function surchargeSlots(array $services): array { $has = fn (string $service) => in_array($service, $services, true); $slots = [1 => null, 2 => null, 3 => null]; if ($has('special_handling')) { $slots[1] = self::SERVICE_TEXT['special_handling']; } if ($has('timed_delivery')) { $slots[2] = self::SERVICE_TEXT['timed_delivery']; if ($has('saturday_delivery')) { $slots[3] = self::SERVICE_TEXT['saturday_delivery']; } } elseif ($has('saturday_delivery')) { $slots[2] = self::SERVICE_TEXT['saturday_delivery']; } return $slots; } /** * The cash-on-delivery texts exactly as the client prints them * (Sydeta.cs::print_vg(), cash branch): antik_1 "ΑΝΤΙΚΑΤΑΒΟΛΗ 17.00" * heads the COD column and the A6 bottom banner, then "* ΑΝΑΛΥΣΗ *" and * the cash line ("17.00 MΕΤΡΗΤΑ" — the amount as sent in pel_ant_poso, * the client's own Latin "M" kept). antik_minima / apodiksi / antik_poso * are the A4 payment stub's. We only ever send cash (no cheques), so * antik_4..7 stay empty. * * @return array */ private function codFields(?float $amount): array { if (! $amount) { return [ 'antik_1' => null, 'antik_2' => null, 'antik_3' => null, 'antik_4' => null, 'antik_5' => null, 'antik_6' => null, 'antik_7' => null, 'antik_lines' => [], 'antik_minima' => null, 'apodiksi' => null, 'antik_poso' => '0.00', ]; } $sent = number_format($amount, 2, '.', ''); $lines = [ 'antik_1' => 'ΑΝΤΙΚΑΤΑΒΟΛΗ '.$sent, 'antik_2' => '* ΑΝΑΛΥΣΗ *', 'antik_3' => $sent.' MΕΤΡΗΤΑ', 'antik_4' => null, 'antik_5' => null, 'antik_6' => null, 'antik_7' => null, ]; return [ ...$lines, 'antik_lines' => array_values(array_filter($lines)), 'antik_minima' => '* ΠΡΟΣΟΧΗ ΑΝΤΙΚΑΤΑΒΟΛΗ *', 'apodiksi' => '* Απόδειξη Είσπαξης *', 'antik_poso' => $sent, ]; } /** * A child parcel's COD text (print_child()): only the warning, never the * amount — the cash is collected against the main voucher. * * @return array */ private function childCodFields(bool $cod): array { $warning = $cod ? '* ΠΡΟΣΟΧΗ ΑΝΤΙΚΑΤΑΒΟΛΗ *' : null; return [ 'antik_1' => $warning, 'antik_2' => null, 'antik_3' => null, 'antik_4' => null, 'antik_5' => null, 'antik_6' => null, 'antik_7' => null, 'antik_lines' => array_filter([$warning]), 'antik_minima' => $warning, 'apodiksi' => null, 'antik_poso' => '0.00', ]; } /** * Which parcel of a multi-parcel send this is: ['number' => k, 'of' => * n, 'master' => main voucher's Shipment|null], or null for a single * parcel. Parcels created by createShipment() carry it in meta; * children issued before that (only parent_reference set) are numbered * by their order among their siblings, after the main voucher. * * @return array{number: int, of: int, master: ?Shipment}|null */ private function pieceOf(Shipment $shipment): ?array { $meta = $shipment->meta?->toArray() ?? []; if (isset($meta['piece'], $meta['pieces'])) { $master = isset($meta['master_shipment_id']) ? Shipment::find($meta['master_shipment_id']) : null; return ['number' => (int) $meta['piece'], 'of' => (int) $meta['pieces'], 'master' => $master]; } if (blank($shipment->parent_reference)) { return null; } $master = Shipment::where('tracking_reference', $shipment->parent_reference)->first(); $siblings = Shipment::where('parent_reference', $shipment->parent_reference)->orderBy('id')->pluck('id'); return [ 'number' => $siblings->search($shipment->id) + 2, 'of' => (int) ($master?->meta['package_count'] ?? $siblings->count() + 1), 'master' => $master, ]; } /** * PNG over SVG — dompdf's inline support doesn't reliably * render the barcode generator's -based bars (confirmed: the * SVG output rendered as literal text in a live test), while a * base64 PNG is unambiguous. */ private function barcodeDataUri(BarcodeGeneratorPNG $generator, string $payload): string { $png = $generator->getBarcode($payload, BarcodeGeneratorPNG::TYPE_CODE_128); return 'data:image/png;base64,'.base64_encode($png); } /** * Embedded as a base64 data URI rather than referenced by URL — * resources/logos/ is only copied into a consuming app's public/ on * vendor:publish, which isn't guaranteed to have run, and dompdf * needs either a real filesystem path or a working absolute URL. */ private function logoDataUri(): string { $path = __DIR__.'/../../../../resources/logos/elta-courier-logo.png'; return 'data:image/png;base64,'.base64_encode(file_get_contents($path)); } }