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:
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Core\Shipping\Filament\Pages;
|
||||
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Lunar\Shipping\Facades\Shipping;
|
||||
use Lunar\Shipping\Models\ShippingMethod;
|
||||
use Lunar\Shipping\Models\ShippingRate;
|
||||
use Lunar\Shipping\Filament\Resources\ShippingZoneResource\Pages\ManageShippingRates as BaseManageShippingRates;
|
||||
use Modules\Core\Shipping\Contracts\SupportsLivePricing;
|
||||
|
||||
/**
|
||||
* Bound in place of the vendor ManageShippingRates page via the container
|
||||
* (see ShippingServiceProvider), since that page has no extension hook of
|
||||
* its own. Every reference to the vendor class name — routes, sub-nav,
|
||||
* ShippingZoneResource::getPages() — is untouched; the container simply
|
||||
* hands back this subclass whenever the vendor class is resolved.
|
||||
*
|
||||
* Adds a per-rate "Pricing" toggle (live API vs. fixed price) for methods
|
||||
* whose driver supports live pricing (see SupportsLivePricing). Rates on
|
||||
* methods without live pricing behave exactly as the vendor page always did
|
||||
* — no toggle shown, price fields always required, vendor save logic used
|
||||
* as-is.
|
||||
*/
|
||||
class ManageShippingRates extends BaseManageShippingRates
|
||||
{
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
$form = parent::form($form);
|
||||
|
||||
return $form->schema(
|
||||
$this->insertPricingModeFieldAfterShippingMethod(
|
||||
$this->hidePriceFieldsWhenLive($form->getComponents())
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert the "Pricing" field immediately after "shipping_method_id" so
|
||||
* it reads as a pair, rather than appending it elsewhere in the form.
|
||||
*/
|
||||
private function insertPricingModeFieldAfterShippingMethod(array $components): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
foreach ($components as $component) {
|
||||
$result[] = $component;
|
||||
|
||||
if (method_exists($component, 'getName') && $component->getName() === 'shipping_method_id') {
|
||||
$result[] = $this->pricingModeField();
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function pricingModeField(): Select
|
||||
{
|
||||
return Select::make('pricing_mode')
|
||||
->label('Pricing')
|
||||
->options([
|
||||
'live' => 'Use live API pricing',
|
||||
'fixed' => 'Use a fixed price',
|
||||
])
|
||||
->default('live')
|
||||
->live()
|
||||
->visible(fn (Get $get) => static::methodHasLivePricing($get('shipping_method_id')))
|
||||
->columnSpan(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the vendor's price / price-break fields whenever this rate is
|
||||
* set to live pricing — they'd otherwise be dead configuration,
|
||||
* silently ignored by the driver. Only the base "price" field was ever
|
||||
* required by the vendor form; the "prices" repeater (price breaks) is
|
||||
* always optional, so its required() state is left untouched.
|
||||
*/
|
||||
private function hidePriceFieldsWhenLive(array $components): array
|
||||
{
|
||||
$isFixedOrNotLiveCapable = fn (Get $get) => ! static::methodHasLivePricing($get('shipping_method_id'))
|
||||
|| $get('pricing_mode') === 'fixed';
|
||||
|
||||
foreach ($components as $component) {
|
||||
if (! method_exists($component, 'getName')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($component->getName() === 'price') {
|
||||
$component->visible($isFixedOrNotLiveCapable)
|
||||
->required($isFixedOrNotLiveCapable)
|
||||
->dehydrated(true);
|
||||
}
|
||||
|
||||
if ($component->getName() === 'prices') {
|
||||
$component->visible($isFixedOrNotLiveCapable)
|
||||
->dehydrated(true);
|
||||
}
|
||||
}
|
||||
|
||||
return $components;
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
$table = parent::table($table);
|
||||
|
||||
return $table->columns(
|
||||
array_map(function ($column) {
|
||||
if (method_exists($column, 'getName') && $column->getName() === 'basePrices.0') {
|
||||
return TextColumn::make('basePrices.0')
|
||||
->label(__('lunarpanel.shipping::relationmanagers.shipping_rates.table.price.label'))
|
||||
->formatStateUsing(function ($state, ShippingRate $record) {
|
||||
if (static::methodHasLivePricing($record->shipping_method_id) && $record->pricing_mode === 'live') {
|
||||
return 'Live API pricing';
|
||||
}
|
||||
|
||||
return $state?->price->formatted;
|
||||
});
|
||||
}
|
||||
|
||||
return $column;
|
||||
}, $table->getColumns())
|
||||
);
|
||||
}
|
||||
|
||||
protected static function saveShippingRate(?ShippingRate $shippingRate = null, array $data = []): void
|
||||
{
|
||||
$isLive = static::methodHasLivePricing($data['shipping_method_id'] ?? $shippingRate?->shipping_method_id)
|
||||
&& ($data['pricing_mode'] ?? 'live') === 'live';
|
||||
|
||||
$shippingRate->pricing_mode = $isLive ? 'live' : 'fixed';
|
||||
$shippingRate->save();
|
||||
|
||||
if ($isLive) {
|
||||
return;
|
||||
}
|
||||
|
||||
parent::saveShippingRate($shippingRate, $data);
|
||||
}
|
||||
|
||||
protected static function methodHasLivePricing(ShippingMethod|int|string|null $method): bool
|
||||
{
|
||||
if (blank($method)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! $method instanceof ShippingMethod) {
|
||||
$method = ShippingMethod::find($method);
|
||||
}
|
||||
|
||||
if (! $method) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return Shipping::driver($method->driver) instanceof SupportsLivePricing;
|
||||
} catch (\InvalidArgumentException) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user