Feature: Add ACS courier integration

ACS rate driver (live price quotes via ACS_Price_Calculation, cached postcode-to-station lookups) and fulfillment service (voucher creation, label printing, end-of-day pickup manifest). Adds a per-rate pricing_mode column so admins can choose live API pricing vs. a fixed price on ACS-driven shipping rates, surfaced via a custom Rates page.
This commit is contained in:
2026-07-19 00:50:51 +03:00
parent 3599329b57
commit 89255687a1
10 changed files with 691 additions and 0 deletions
@@ -0,0 +1,81 @@
<?php
namespace Modules\Core\Shipping\Carriers\Acs;
use Illuminate\Support\Facades\Cache;
use Modules\Core\Shipping\Carriers\Acs\Exceptions\AcsApiException;
class AreaResolver
{
public const CACHE_KEY = 'acs.areas';
public function __construct(private readonly AcsClient $client) {}
/**
* Resolve a Greek postcode to its ACS station/branch codes.
*
* Reads from the table warmed daily by WarmAcsAreaCacheJob. Falls
* back to a live lookup for that single postcode if the warmed cache is
* missing (e.g. the daily job never ran or failed) or doesn't contain it.
*/
public function resolve(string $postcode): AcsArea
{
$areas = Cache::get(self::CACHE_KEY);
if ($areas !== null && isset($areas[$postcode])) {
return $this->toArea($areas[$postcode]);
}
return $this->toArea($this->fetch($postcode));
}
/**
* Fetch and cache the full country's postcode-to-station map in one call.
*/
public function warmAll(): void
{
$areas = [];
foreach ($this->fetchAll() as $row) {
$areas[$row['Zip_Code']] = $row;
}
Cache::forever(self::CACHE_KEY, $areas);
}
private function fetch(string $postcode): array
{
$response = $this->client->call('ACS_Area_Find_By_Zip_Code', [
'Zip_Code' => $postcode,
'Show_Only_Inaccessible_Areas' => 0,
'Country' => 'GR',
])->throwIfError();
$area = $response->tableOutput['Table_Data'][0] ?? null;
if (! $area) {
throw new AcsApiException("No ACS area found for postcode {$postcode}");
}
return $area;
}
private function fetchAll(): array
{
$response = $this->client->call('ACS_Area_Find_By_Zip_Code', [
'Zip_Code' => null,
'Show_Only_Inaccessible_Areas' => 0,
'Country' => 'GR',
])->throwIfError();
return $response->tableOutput['Table_Data'] ?? [];
}
private function toArea(array $row): AcsArea
{
return new AcsArea(
stationId: $row['Station_ID'],
branchId: (int) $row['Branch_ID'],
);
}
}