Feat: Updates to fullfilment services, shipping views, timezones, and elta labels

This commit is contained in:
2026-09-30 19:47:38 +03:00
parent eadaf3a580
commit a8fe13e336
17 changed files with 653 additions and 45 deletions
+116 -18
View File
@@ -59,22 +59,37 @@ class EltaLabelRenderer
public function renderMany(Collection $shipments): string
{
$paperSize = config('elta.label_paper_size', 'a4');
$view = $paperSize === 'a6'
? 'core::shipping.carriers.elta.label-a6'
: 'core::shipping.carriers.elta.label-a4';
$head = null;
$styles = [];
$pages = [];
foreach ($shipments as $shipment) {
$html = view($view, $this->data($shipment))->render();
$data = $this->data($shipment);
$head ??= Str::before($html, '<body');
// 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 <head>.
$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, '<body');
$head ??= $viewHead;
preg_match_all('#<style>.*?</style>#s', $viewHead, $matches);
$styles[$view] = implode('', $matches[0]);
$pages[] = Str::beforeLast(Str::after(Str::after($html, '<body'), '>'), '</body>');
}
$html = $head
$html = Str::before($head, '<style>')
.implode('', $styles)
.'<style>.page { page-break-after: always; } .page:last-child { page-break-after: auto; }</style>'
.Str::afterLast($head, '</style>')
.'<body>'.implode('', $pages).'</body></html>';
$pdf = Pdf::loadHTML($html);
@@ -100,7 +115,16 @@ class EltaLabelRenderer
{
$order = $shipment->order;
$address = $order->shippingAddress;
$meta = $shipment->meta?->toArray() ?? [];
$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
@@ -109,18 +133,24 @@ class EltaLabelRenderer
? ['code' => $meta['rec_station'], 'name' => $meta['rec_station_t'] ?? '']
: (fn ($s) => ['code' => $s->code, 'name' => $s->name])($this->areaResolver->resolve($address->postcode));
$services = (array) ($meta['services'] ?? []);
$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 = $this->codFields(isset($meta['cod_amount']) ? (float) $meta['cod_amount'] : null);
$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();
: now()->inDisplayTimezone();
$voucherNo = $shipment->tracking_reference;
$ocrLine = (string) ($meta['ocr_line'] ?? '');
$packageCount = (int) ($meta['package_count'] ?? 1);
$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();
@@ -167,11 +197,25 @@ class EltaLabelRenderer
'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
? sprintf('001/%03d', $packageCount)
? ($isChild && $paperSize !== 'a6' ? (string) $packageCount : $pieceLabel)
: '001',
'piece_label' => $pieceLabel,
'multiPiece' => $packageCount > 1,
'polaplo' => $packageCount > 1 ? '* ΠΟΛΛΑΠΛΗ ΑΠΟΣΤΟΛΗ *' : null,
'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'],
@@ -180,8 +224,9 @@ class EltaLabelRenderer
'service_code' => $meta['rec_srv'] ?? '',
'service_name' => $meta['rec_srv_t'] ?? '',
'xreosi' => 'ΧΡΕΩΣΗ ΑΠΟΣΤΟΛΕΑ ΠΙΣΤΩΣΗ',
'siimvasi' => '131775-9',
// 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,
@@ -189,11 +234,11 @@ class EltaLabelRenderer
// 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) : '',
'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' => (string) $order->reference,
'order_reference' => $isChild ? null : (string) $order->reference,
// sydetaE.rdlc's "Πρόσθετες Υπηρεσίες" (sur_1..4): the ticked
// extra services, in ELTA's own wording and slots.
@@ -282,6 +327,59 @@ class EltaLabelRenderer
];
}
/**
* A child parcel's COD text (print_child()): only the warning, never the
* amount — the cash is collected against the main voucher.
*
* @return array<string, mixed>
*/
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 <svg> support doesn't reliably
* render the barcode generator's <rect>-based bars (confirmed: the