623 lines
25 KiB
PHP
623 lines
25 KiB
PHP
<?php
|
||
|
||
namespace Modules\Core\Shipping\Carriers\Elta;
|
||
|
||
use Carbon\CarbonInterface;
|
||
use Illuminate\Support\Carbon;
|
||
use Illuminate\Support\Collection;
|
||
use Illuminate\Support\Facades\Cache;
|
||
use Lunar\Models\Order;
|
||
use Modules\Core\Shipping\Carriers\Elta\Exceptions\EltaApiException;
|
||
use Modules\Core\Shipping\Contracts\CarrierFulfillmentInterface;
|
||
use Modules\Core\Shipping\Contracts\IssuesVoucherOnPrint;
|
||
use Modules\Core\Shipping\Contracts\SupportsBatchLabels;
|
||
use Modules\Core\Shipping\Contracts\SupportsExtraServices;
|
||
use Modules\Core\Shipping\Contracts\SupportsTracking;
|
||
use Modules\Core\Shipping\Contracts\SupportsVoucherListing;
|
||
use Modules\Core\Shipping\Contracts\SupportsVoucherLookup;
|
||
use Modules\Core\Shipping\DTOs\CarrierVoucher;
|
||
use Modules\Core\Shipping\DTOs\ShipmentRequest;
|
||
use Modules\Core\Shipping\DTOs\TrackingCheckpoint;
|
||
use Modules\Core\Shipping\Enums\ExtraService;
|
||
use Modules\Core\Shipping\Enums\TrackingStatus;
|
||
use Modules\Core\Shipping\Models\Shipment;
|
||
|
||
/**
|
||
* ELTA vouchers are created **pending** (pel_insert_flag "0", live-verified):
|
||
* ELTA stores the voucher with an internal id but no voucher number, and it
|
||
* can still be cancelled. Printing issues it (PELVG01NEW1 — what ELTA's own
|
||
* client does from its group-editing screen): that assigns the voucher
|
||
* number, OCR line, destination station and service, and from then on ELTA
|
||
* refuses to delete it ("Δεν Επιτρέπεται! Εχει Γίνει Εκτύπωση"). So a
|
||
* shipment has no tracking_reference until its label is printed.
|
||
*
|
||
* Not SupportsManifestBatching — ELTA has no manifest/pickup-list step.
|
||
*/
|
||
class EltaFulfillmentService implements CarrierFulfillmentInterface, IssuesVoucherOnPrint, SupportsBatchLabels, SupportsExtraServices, SupportsTracking, SupportsVoucherListing, SupportsVoucherLookup
|
||
{
|
||
/** PELMANIF2 / PELPARALVGNEW1 page through 100 rows at a time. */
|
||
private const MAX_PAGES = 50;
|
||
|
||
public function __construct(
|
||
private readonly EltaClient $client,
|
||
private readonly EltaLabelRenderer $labelRenderer,
|
||
) {}
|
||
|
||
/**
|
||
* ELTA surcharge codes (pel_sur_1..3), from its own client's checkboxes.
|
||
* Insurance isn't a surcharge — it's the amount in pel_asf_poso.
|
||
*/
|
||
public function extraServices(): array
|
||
{
|
||
return [
|
||
ExtraService::SaturdayDelivery->value => '004',
|
||
ExtraService::TimedDelivery->value => '003',
|
||
ExtraService::SpecialHandling->value => '002',
|
||
ExtraService::Insurance->value => 'pel_asf_poso',
|
||
];
|
||
}
|
||
|
||
public function createShipment(Order $order, ShipmentRequest $request): Shipment
|
||
{
|
||
$address = $order->shippingAddress;
|
||
$weight = $request->weight ?? 0.5;
|
||
$surcharges = $this->surchargeCodes($request);
|
||
|
||
$params = [
|
||
'pel_apost_code' => config('elta.apost_code'),
|
||
'pel_paral_name' => trim("{$address->first_name} {$address->last_name}"),
|
||
'pel_paral_address' => $address->line_one,
|
||
'pel_paral_area' => $address->city,
|
||
'pel_paral_tk' => $address->postcode,
|
||
'pel_paral_thl_1' => $address->contact_phone,
|
||
'pel_paral_thl_2' => '',
|
||
'pel_service' => '',
|
||
'pel_baros' => number_format($weight, 3, '.', ''),
|
||
'pel_baros_xyz' => '',
|
||
'pel_x' => '',
|
||
'pel_y' => '',
|
||
'pel_z' => '',
|
||
'pel_temaxia' => (string) $request->packageCount,
|
||
'pel_paral_sxolia' => '',
|
||
'pel_sur_1' => $surcharges[0] ?? '',
|
||
'pel_sur_2' => $surcharges[1] ?? '',
|
||
'pel_sur_3' => $surcharges[2] ?? '',
|
||
// pel_ant_poso is the cash-on-delivery amount; pel_ant_poso1..4
|
||
// are cheques (with dates) in ELTA's client — never used here.
|
||
'pel_ant_poso' => '',
|
||
'pel_ant_poso1' => '',
|
||
'pel_ant_poso2' => '',
|
||
'pel_ant_poso3' => '',
|
||
'pel_ant_poso4' => '',
|
||
'pel_ant_date1' => '',
|
||
'pel_ant_date2' => '',
|
||
'pel_ant_date3' => '',
|
||
'pel_ant_date4' => '',
|
||
'pel_asf_poso' => $request->has(ExtraService::Insurance) && $request->insuranceAmount
|
||
? number_format($request->insuranceAmount, 2, '.', '')
|
||
: '',
|
||
'pel_user' => config('elta.user_code'),
|
||
// Our order reference — comes back in ELTA's lists and tracking,
|
||
// which is how we find the pending voucher's id below and match
|
||
// vouchers to orders on the Carrier Vouchers screen.
|
||
'pel_ref_no' => (string) $order->reference,
|
||
// "0" = save without issuing (see this class's docblock).
|
||
'pel_insert_flag' => '0',
|
||
'pel_paral_code' => '',
|
||
'pel_retur_code' => '',
|
||
];
|
||
|
||
$codAmount = null;
|
||
|
||
if ($request->paymentMode === 'cod') {
|
||
$codAmount = $request->amountToCollect ?? $order->total->decimal;
|
||
$params['pel_ant_poso'] = number_format($codAmount, 2, '.', '');
|
||
}
|
||
|
||
$this->client->createVoucher($params)->throwIfError();
|
||
|
||
$pieces = max(1, $request->packageCount);
|
||
|
||
// PELVGNEW doesn't return the new voucher's id, so find it in the
|
||
// pending list by our reference. Saved even if that lookup fails —
|
||
// the voucher exists at ELTA either way, and printing retries it.
|
||
$master = Shipment::create([
|
||
'order_id' => $order->id,
|
||
'carrier' => 'elta',
|
||
'source' => Shipment::SOURCE_CREATED,
|
||
'tracking_reference' => null,
|
||
'meta' => [
|
||
'carrier_id' => $this->findPendingVoucherId((string) $order->reference),
|
||
'reference' => (string) $order->reference,
|
||
'weight' => $weight,
|
||
'package_count' => $pieces,
|
||
'cod_amount' => $codAmount,
|
||
'services' => $request->serviceValues(),
|
||
'insurance_amount' => $request->has(ExtraService::Insurance) ? $request->insuranceAmount : null,
|
||
...($pieces > 1 ? ['piece' => 1, 'pieces' => $pieces] : []),
|
||
],
|
||
]);
|
||
|
||
// One row per extra parcel straight away, so the order shows every
|
||
// parcel; ELTA gives them their own voucher numbers only when the
|
||
// main voucher is issued (issue()). They belong to the main voucher:
|
||
// no COD or services of their own.
|
||
for ($piece = 2; $piece <= $pieces; $piece++) {
|
||
Shipment::create([
|
||
'order_id' => $order->id,
|
||
'carrier' => 'elta',
|
||
'source' => Shipment::SOURCE_CREATED,
|
||
'tracking_reference' => null,
|
||
'meta' => [
|
||
'master_shipment_id' => $master->id,
|
||
'reference' => (string) $order->reference,
|
||
'piece' => $piece,
|
||
'pieces' => $pieces,
|
||
'weight' => $weight,
|
||
'package_count' => $pieces,
|
||
],
|
||
]);
|
||
}
|
||
|
||
return $master;
|
||
}
|
||
|
||
/**
|
||
* Like ELTA's own client: printing the main voucher of a multi-parcel
|
||
* send prints every parcel (main first, then each child); printing a
|
||
* child prints just that parcel. Either way the main voucher is issued
|
||
* first — that's what gives the children their numbers.
|
||
*/
|
||
public function printLabel(Shipment $shipment): string
|
||
{
|
||
$master = $this->masterOf($shipment);
|
||
|
||
$this->issue($master);
|
||
|
||
$labels = $shipment->is($master)
|
||
? collect([$master->refresh(), ...$this->childrenOf($master)])
|
||
: collect([$shipment->refresh()]);
|
||
|
||
$pdf = $this->labelRenderer->renderMany($labels);
|
||
|
||
Shipment::whereIn('id', $labels->pluck('id'))->update(['label_printed_at' => now()]);
|
||
|
||
return $pdf;
|
||
}
|
||
|
||
/**
|
||
* Several labels in one PDF. A selected main voucher brings its
|
||
* children along (as printLabel() does); each parcel is printed once,
|
||
* main voucher before its children.
|
||
*/
|
||
public function printLabels(Collection $shipments): string
|
||
{
|
||
$labels = collect();
|
||
|
||
foreach ($shipments as $shipment) {
|
||
$master = $this->masterOf($shipment);
|
||
$this->issue($master);
|
||
|
||
$group = $shipment->is($master)
|
||
? [$master->refresh(), ...$this->childrenOf($master)]
|
||
: [$shipment->refresh()];
|
||
|
||
foreach ($group as $label) {
|
||
$labels->put($label->id, $label);
|
||
}
|
||
}
|
||
|
||
$pdf = $this->labelRenderer->renderMany($labels->values());
|
||
|
||
Shipment::whereIn('id', $labels->keys())->update(['label_printed_at' => now()]);
|
||
|
||
return $pdf;
|
||
}
|
||
|
||
/**
|
||
* Pending (not yet printed) vouchers are deleted at ELTA. Issued ones
|
||
* can't be — ELTA only allows that before printing.
|
||
*/
|
||
public function cancelShipment(Shipment $shipment): void
|
||
{
|
||
$master = $this->masterOf($shipment);
|
||
|
||
if (! $shipment->is($master)) {
|
||
$piece = $shipment->meta['piece'] ?? null;
|
||
|
||
throw new EltaApiException(
|
||
'This is '.($piece ? "parcel {$piece}" : 'an extra parcel').' of a multi-parcel ELTA voucher — ELTA cancels the whole voucher, so cancel the main parcel instead.',
|
||
);
|
||
}
|
||
|
||
if (filled($shipment->tracking_reference)) {
|
||
throw new EltaApiException(
|
||
"ELTA voucher {$shipment->tracking_reference} is already printed, and ELTA only allows cancelling vouchers that haven't been printed yet. Contact ELTA to cancel it.",
|
||
);
|
||
}
|
||
|
||
$this->client->deleteVoucher(['pel_vg_id' => $this->pendingVoucherId($shipment)])->throwIfError();
|
||
|
||
$shipment->update(['cancelled_at' => now()]);
|
||
$this->childrenOf($shipment)->each->update(['cancelled_at' => now()]);
|
||
}
|
||
|
||
/**
|
||
* The main voucher's shipment for a child parcel, or the shipment
|
||
* itself. Children created by createShipment() point at it via
|
||
* meta.master_shipment_id; older ones only via parent_reference.
|
||
*/
|
||
private function masterOf(Shipment $shipment): Shipment
|
||
{
|
||
if ($id = $shipment->meta['master_shipment_id'] ?? null) {
|
||
return Shipment::find($id) ?? $shipment;
|
||
}
|
||
|
||
if (filled($shipment->parent_reference)) {
|
||
return Shipment::where('tracking_reference', $shipment->parent_reference)->first() ?? $shipment;
|
||
}
|
||
|
||
return $shipment;
|
||
}
|
||
|
||
/**
|
||
* @return Collection<int, Shipment> the main voucher's child parcels, in piece order
|
||
*/
|
||
private function childrenOf(Shipment $master): Collection
|
||
{
|
||
return Shipment::query()
|
||
->where('carrier', 'elta')
|
||
->where(fn ($query) => $query
|
||
->where('meta->master_shipment_id', $master->id)
|
||
->when($master->tracking_reference, fn ($q) => $q->orWhere('parent_reference', $master->tracking_reference)))
|
||
->orderBy('id')
|
||
->get()
|
||
->sortBy(fn (Shipment $child) => $child->meta['piece'] ?? PHP_INT_MAX)
|
||
->values();
|
||
}
|
||
|
||
public function trackShipment(Shipment $shipment): Collection
|
||
{
|
||
// ELTA tracks a multi-parcel send by its main voucher only — an
|
||
// extra parcel's own number is rejected ("Λάθος ΣΥΔΕΤΑ"). Its
|
||
// history is the main voucher's, which is tracked on its own row.
|
||
if (! $shipment->is($this->masterOf($shipment))) {
|
||
return collect();
|
||
}
|
||
|
||
$response = $this->client->getTracking([
|
||
'web_vg' => $shipment->tracking_reference,
|
||
'pel_code' => config('elta.apost_code'),
|
||
])->throwIfError();
|
||
|
||
$rows = array_filter(
|
||
(array) ($response->data['web_status'] ?? []),
|
||
fn (array $row) => filled($row['web_date_time'] ?? null),
|
||
);
|
||
|
||
// No explicit delivered-confirmation field like the old
|
||
// PELTT01's pod_name — pel_rec.a_rec_date_time is blank until
|
||
// delivery per the real client's own rendering, used as the
|
||
// delivered signal here; unconfirmed against a genuinely
|
||
// delivered parcel yet.
|
||
$isDelivered = filled(trim($response->data['pel_rec']['a_rec_date_time'] ?? '', ' /:'));
|
||
|
||
return collect(array_values($rows))->map(function (array $row, int $index) use ($rows, $isDelivered) {
|
||
$isLast = $index === count($rows) - 1;
|
||
$dateTime = $row['web_date_time'] ?? '';
|
||
|
||
return new TrackingCheckpoint(
|
||
status: $isLast && $isDelivered
|
||
? TrackingStatus::Delivered
|
||
: $this->guessStatusFromTitle($row['web_status_name'] ?? ''),
|
||
carrierStatus: $row['web_status_name'] ?? null,
|
||
message: $row['web_sxolia'] ?: ($row['web_status_name'] ?? null),
|
||
location: $row['web_station'] ?? null,
|
||
// ELTA reports Greek local time.
|
||
occurredAt: Carbon::createFromFormat('YmdHi', substr($dateTime, 0, 12), 'Europe/Athens')->setTimezone(config('app.timezone')),
|
||
meta: $row,
|
||
);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* PELMANIF2 (the client's "Πολλαπλή Αναζήτηση"): every issued voucher
|
||
* in the date range with its latest status. A second pass with
|
||
* status_flag 3 ("Προς Επιστροφή") marks the ones heading back to us.
|
||
*/
|
||
public function listVouchers(CarbonInterface $from, CarbonInterface $to): iterable
|
||
{
|
||
$returning = collect($this->searchVouchers($from, $to, '3'))->pluck('pel_col_1')->flip();
|
||
|
||
foreach ($this->searchVouchers($from, $to, '0') as $row) {
|
||
$number = trim($row['pel_col_1']);
|
||
|
||
yield new CarrierVoucher(
|
||
carrier: 'elta',
|
||
voucherNumber: $number,
|
||
reference: filled($row['pel_col_2'] ?? null) ? trim($row['pel_col_2']) : null,
|
||
recipientName: trim($row['pel_col_3'] ?? '') ?: null,
|
||
postcode: trim($row['pel_col_4'] ?? '') ?: null,
|
||
statusText: trim($row['pel_col_6'] ?? '') ?: null,
|
||
status: filled(trim($row['pel_col_6'] ?? '')) ? $this->guessStatusFromTitle(trim($row['pel_col_6'])) : null,
|
||
codAmount: (float) ($row['pel_col_8'] ?? 0) ?: null,
|
||
isReturn: $returning->has($row['pel_col_1']),
|
||
date: $this->dateFromListRow($row['pel_col_7'] ?? ''),
|
||
raw: $row,
|
||
);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* PELTTNEW01 (tracking) also returns the voucher's details (pel_rec):
|
||
* recipient, postcode, our reference, cash on delivery.
|
||
*/
|
||
public function lookupVoucher(string $voucherNumber): ?CarrierVoucher
|
||
{
|
||
$response = $this->client->getTracking([
|
||
'web_vg' => trim($voucherNumber),
|
||
'pel_code' => config('elta.apost_code'),
|
||
]);
|
||
|
||
$record = $response->data['pel_rec'] ?? [];
|
||
|
||
if ($response->hasError || blank($record['a_vg_3'] ?? null)) {
|
||
return null;
|
||
}
|
||
|
||
$statuses = array_values(array_filter(
|
||
(array) ($response->data['web_status'] ?? []),
|
||
fn (array $row) => filled($row['web_date_time'] ?? null),
|
||
));
|
||
|
||
return new CarrierVoucher(
|
||
carrier: 'elta',
|
||
voucherNumber: trim($record['a_vg_3']),
|
||
reference: trim($record['a_ref'] ?? '') ?: null,
|
||
recipientName: trim($record['a_rec_title'] ?? '') ?: null,
|
||
postcode: trim($record['a_rec_postal'] ?? '') ?: null,
|
||
phone: trim($record['a_rec_tel_1'] ?? '') ?: null,
|
||
statusText: filled($statuses) ? trim(end($statuses)['web_status_name'] ?? '') : null,
|
||
status: filled($statuses) ? $this->guessStatusFromTitle(trim(end($statuses)['web_status_name'] ?? '')) : null,
|
||
codAmount: (float) ($record['a_antik'] ?? 0) ?: null,
|
||
date: $this->dateFromListRow($record['a_sender_date'] ?? ''),
|
||
raw: $record,
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Issues a pending voucher (no-op once it has a number): stores the
|
||
* voucher number and what ELTA returns for the label, plus one shipment
|
||
* per child voucher of a multi-piece send.
|
||
*/
|
||
private function issue(Shipment $shipment): void
|
||
{
|
||
if (filled($shipment->tracking_reference)) {
|
||
return;
|
||
}
|
||
|
||
$response = $this->client->issueVoucher([
|
||
'pel_id' => $this->pendingVoucherId($shipment),
|
||
'sender_station' => $this->senderStation(),
|
||
])->throwIfError();
|
||
|
||
$data = $response->data;
|
||
$voucherNo = trim((string) ($data['vg_code'] ?? ''));
|
||
|
||
if ($voucherNo === '') {
|
||
throw new EltaApiException('ELTA issued the voucher but returned no voucher number.', $data);
|
||
}
|
||
|
||
$shipment->tracking_reference = $voucherNo;
|
||
$shipment->meta = array_merge($shipment->meta?->toArray() ?? [], [
|
||
'ocr_line' => $data['ocr_line'] ?? null,
|
||
'date_time' => $data['date_time'] ?? null,
|
||
'rec_station' => $data['rec_station'] ?? null,
|
||
'rec_station_t' => $data['rec_station_t'] ?? null,
|
||
'rec_srv' => $data['rec_srv'] ?? null,
|
||
'rec_srv_t' => $data['rec_srv_t'] ?? null,
|
||
'return_vg' => trim((string) ($data['return_vg'] ?? '')) ?: null,
|
||
'epitagh_vg' => trim((string) ($data['epitagh_vg'] ?? '')) ?: null,
|
||
]);
|
||
$shipment->save();
|
||
|
||
// The children's numbers, in ELTA's order (vg_child_no), go to the
|
||
// pending child rows in piece order; any parcel without a row (older
|
||
// shipments) gets one.
|
||
$pending = $this->childrenOf($shipment)->filter(fn (Shipment $child) => blank($child->tracking_reference))->values();
|
||
$pieces = (int) ($shipment->meta['package_count'] ?? 1);
|
||
$childNumbers = array_values(array_filter(array_map('trim', (array) ($data['vg_child_no'] ?? []))));
|
||
|
||
foreach ($childNumbers as $i => $childVoucherNo) {
|
||
$child = $pending->get($i) ?? new Shipment([
|
||
'order_id' => $shipment->order_id,
|
||
'carrier' => 'elta',
|
||
'source' => Shipment::SOURCE_CREATED,
|
||
'meta' => [
|
||
'master_shipment_id' => $shipment->id,
|
||
'reference' => $shipment->meta['reference'] ?? null,
|
||
'piece' => $i + 2,
|
||
'pieces' => max($pieces, count($childNumbers) + 1),
|
||
'weight' => $shipment->meta['weight'] ?? null,
|
||
'package_count' => max($pieces, count($childNumbers) + 1),
|
||
],
|
||
]);
|
||
|
||
$child->tracking_reference = $childVoucherNo;
|
||
$child->parent_reference = $voucherNo;
|
||
$child->save();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @return array<int, string>
|
||
*/
|
||
private function surchargeCodes(ShipmentRequest $request): array
|
||
{
|
||
$codes = $this->extraServices();
|
||
|
||
return collect($request->services)
|
||
->reject(fn (ExtraService $service) => $service === ExtraService::Insurance)
|
||
->map(fn (ExtraService $service) => $codes[$service->value] ?? null)
|
||
->filter()
|
||
->values()
|
||
->take(3)
|
||
->all();
|
||
}
|
||
|
||
private function pendingVoucherId(Shipment $shipment): string
|
||
{
|
||
$id = $shipment->meta['carrier_id'] ?? null;
|
||
|
||
if (blank($id) && $shipment->order) {
|
||
$id = $this->findPendingVoucherId((string) $shipment->order->reference);
|
||
|
||
if ($id) {
|
||
$shipment->meta = array_merge($shipment->meta?->toArray() ?? [], ['carrier_id' => $id]);
|
||
$shipment->save();
|
||
}
|
||
}
|
||
|
||
if (blank($id)) {
|
||
throw new EltaApiException("Couldn't find this shipment's pending voucher at ELTA.");
|
||
}
|
||
|
||
return $id;
|
||
}
|
||
|
||
/**
|
||
* The newest not-yet-issued voucher in ELTA's list carrying our
|
||
* reference. flag_1/flag_2 are the client's "Show all" / "All users"
|
||
* checkboxes — both "1", or even a just-created voucher is filtered out.
|
||
*/
|
||
private function findPendingVoucherId(string $reference): ?string
|
||
{
|
||
$inId = '0';
|
||
$match = null;
|
||
|
||
for ($page = 0; $page < self::MAX_PAGES; $page++) {
|
||
$rows = array_values(array_filter(
|
||
(array) ($this->client->listVouchers([
|
||
'pel_code' => config('elta.apost_code'),
|
||
'pel_user_code' => config('elta.user_code'),
|
||
'flag_1' => '1',
|
||
'flag_2' => '1',
|
||
'in_id' => $inId,
|
||
])->throwIfError()->data['vg_rec'] ?? []),
|
||
fn (array $row) => filled($row['pel_vg_id'] ?? null),
|
||
));
|
||
|
||
foreach ($rows as $row) {
|
||
if (trim($row['pel_ref_no'] ?? '') === $reference && blank(trim($row['pel_paral_vg'] ?? ''))
|
||
&& ($match === null || $row['pel_vg_id'] > $match)) {
|
||
$match = $row['pel_vg_id'];
|
||
}
|
||
}
|
||
|
||
if (count($rows) < 100) {
|
||
break;
|
||
}
|
||
|
||
$inId = (string) end($rows)['pel_vg_id'];
|
||
}
|
||
|
||
return $match;
|
||
}
|
||
|
||
/**
|
||
* @return array<int, array<string, string>>
|
||
*/
|
||
private function searchVouchers(CarbonInterface $from, CarbonInterface $to, string $statusFlag): array
|
||
{
|
||
$inId = '0';
|
||
$all = [];
|
||
|
||
for ($page = 0; $page < self::MAX_PAGES; $page++) {
|
||
$rows = array_values(array_filter(
|
||
(array) ($this->client->searchVouchers([
|
||
'pel_code' => config('elta.apost_code'),
|
||
'date_1' => $from->format('d/m/Y'),
|
||
'date_2' => $to->format('d/m/Y'),
|
||
'paral_code' => '',
|
||
'status_flag' => $statusFlag,
|
||
'in_id' => $inId,
|
||
])->throwIfError()->data['ag_pel_rec'] ?? []),
|
||
fn (array $row) => filled(trim($row['pel_col_1'] ?? '')),
|
||
));
|
||
|
||
array_push($all, ...$rows);
|
||
|
||
if (count($rows) < 100) {
|
||
break;
|
||
}
|
||
|
||
$inId = (string) end($rows)['pel_col_id'];
|
||
}
|
||
|
||
return $all;
|
||
}
|
||
|
||
/**
|
||
* ELTA dates in its lists look like "29/09/26 09:41 PEL CLIENT" or
|
||
* "29/09/2026 09:51".
|
||
*/
|
||
private function dateFromListRow(string $value): ?CarbonInterface
|
||
{
|
||
if (! preg_match('#(\d{2})/(\d{2})/(\d{2,4})#', $value, $m)) {
|
||
return null;
|
||
}
|
||
|
||
$year = strlen($m[3]) === 2 ? '20'.$m[3] : $m[3];
|
||
|
||
return Carbon::createFromDate((int) $year, (int) $m[2], (int) $m[1])->startOfDay();
|
||
}
|
||
|
||
/**
|
||
* The station ELTA needs when issuing a voucher: configured
|
||
* (ELTA_ORIGIN_STATION_CODE), or read from the login response once —
|
||
* ELTA returns the account's user_station there even though our
|
||
* account's login password is rejected.
|
||
*/
|
||
private function senderStation(): string
|
||
{
|
||
if (filled(config('elta.origin_station_code'))) {
|
||
return (string) config('elta.origin_station_code');
|
||
}
|
||
|
||
return (string) Cache::rememberForever('elta.user_station', fn () => $this->client->login([
|
||
'pel_code' => config('elta.apost_code'),
|
||
'user_code' => config('elta.user_code'),
|
||
'user_pass' => config('elta.user_pass'),
|
||
])->data['user_station'] ?? '');
|
||
}
|
||
|
||
/**
|
||
* web_status_name is free text with no structured status code — same
|
||
* limitation Acs\AcsFulfillmentService::guessStatusFromAction() has.
|
||
* Live-confirmed text seen so far: "ΔΗΜΙΟΥΡΓΙΑ ΣΥ.ΔΕ.ΤΑ. ΑΠΟ ΠΕΛΑΤΗ"
|
||
* (voucher created) — matched via a Pending-ish fallback below since
|
||
* it isn't yet in transit. Other statuses are best-guess substring
|
||
* matches to refine as more real tracking text is observed.
|
||
*/
|
||
/**
|
||
* ELTA's status texts come in unaccented capitals ("ΠΑΡΑΔΟΘΗΚΕ"), so
|
||
* they're lowercased and stripped of accents before matching.
|
||
*/
|
||
private function guessStatusFromTitle(string $title): TrackingStatus
|
||
{
|
||
$title = strtr(mb_strtolower($title), [
|
||
'ά' => 'α', 'έ' => 'ε', 'ή' => 'η', 'ί' => 'ι', 'ό' => 'ο', 'ύ' => 'υ', 'ώ' => 'ω',
|
||
'ϊ' => 'ι', 'ϋ' => 'υ', 'ΐ' => 'ι', 'ΰ' => 'υ',
|
||
]);
|
||
|
||
return match (true) {
|
||
str_contains($title, 'επιστροφ') => TrackingStatus::Returned,
|
||
str_contains($title, 'παραδοθηκε') => TrackingStatus::Delivered,
|
||
str_contains($title, 'διανομη') => TrackingStatus::OutForDelivery,
|
||
str_contains($title, 'παραλαβη') => TrackingStatus::CollectedFromSender,
|
||
str_contains($title, 'μεταφορα') || str_contains($title, 'διαμετακομιση') || str_contains($title, 'διακινηση') => TrackingStatus::InTransit,
|
||
default => TrackingStatus::Pending,
|
||
};
|
||
}
|
||
}
|