Reverts the earlier per-rate pricing_mode column in favor of extending Lunar's existing charge_by field (cart_total/weight) with a third "live" option, gated by a SupportsLivePricing capability check on the driver. Adds a shared ResolvesFixedPricing trait so any carrier driver can fall back to Lunar's normal price-break resolution, matching the vendor ShipBy driver's own charge_by handling instead of introducing a separate mechanism. Also fixes an incorrect Get() path in the admin form that silently hid the new "live" option.
153 lines
5.0 KiB
PHP
153 lines
5.0 KiB
PHP
<?php
|
|
|
|
namespace Modules\Core\Shipping\Extensions;
|
|
|
|
use Filament\Forms\Components\Component;
|
|
use Filament\Forms\Components\Concerns\HasChildComponents;
|
|
use Filament\Forms\Components\Select;
|
|
use Filament\Forms\Form;
|
|
use Filament\Forms\Get;
|
|
use Filament\Tables\Columns\TextColumn;
|
|
use Filament\Tables\Table;
|
|
use Lunar\Admin\Support\Extending\ResourceExtension;
|
|
use Lunar\Shipping\Facades\Shipping;
|
|
use Modules\Core\Shipping\Contracts\SupportsLivePricing;
|
|
|
|
class ShippingMethodResourceExtension extends ResourceExtension
|
|
{
|
|
public function extendForm(Form $form): Form
|
|
{
|
|
return $form->schema(
|
|
$this->replaceChargeByField(
|
|
$this->replaceDriverField($form->getComponents())
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Extend the vendor's cart_total/weight charge_by Select with a third
|
|
* "live" option — only offered when the currently selected driver
|
|
* supports live pricing (see SupportsLivePricing). Picking it is what
|
|
* tells the driver to call its carrier API instead of resolving a
|
|
* price break.
|
|
*/
|
|
private function replaceChargeByField(array $components): array
|
|
{
|
|
return array_map(function (Component $component) {
|
|
if (method_exists($component, 'getName') && $component->getName() === 'charge_by') {
|
|
return $this->chargeBySelect();
|
|
}
|
|
|
|
if (in_array(HasChildComponents::class, class_uses_recursive($component), true)) {
|
|
$component->schema(
|
|
$this->replaceChargeByField($component->getChildComponents())
|
|
);
|
|
}
|
|
|
|
return $component;
|
|
}, $components);
|
|
}
|
|
|
|
private function chargeBySelect(): Select
|
|
{
|
|
return Select::make('charge_by')
|
|
->label('Charge by')
|
|
->options(function (Get $get) {
|
|
$options = [
|
|
'cart_total' => 'Cart Total',
|
|
'weight' => 'Weight',
|
|
];
|
|
|
|
// "charge_by" is nested inside a Group with
|
|
// ->statePath('data'), while "driver" sits one level up, at
|
|
// the form root. Note: an *absolute* path here would need to
|
|
// additionally account for the page's own form wrapper
|
|
// (EditRecord::getFormStatePath() === 'data'), which relative
|
|
// paths never cross — so "../driver" (relative) is the
|
|
// correct, page-independent way to reach it, not an
|
|
// absolute 'driver' string.
|
|
if ($this->driverSupportsLivePricing($get('../driver'))) {
|
|
$options['live'] = 'Live API pricing';
|
|
}
|
|
|
|
return $options;
|
|
})
|
|
->live();
|
|
}
|
|
|
|
private function driverSupportsLivePricing(?string $driver): bool
|
|
{
|
|
if (! $driver) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
return Shipping::driver($driver) instanceof SupportsLivePricing;
|
|
} catch (\InvalidArgumentException) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public function extendTable(Table $table): Table
|
|
{
|
|
return $table->columns(
|
|
array_map(function ($column) {
|
|
if (method_exists($column, 'getName') && $column->getName() === 'driver') {
|
|
return $this->driverColumn();
|
|
}
|
|
|
|
return $column;
|
|
}, $table->getColumns())
|
|
);
|
|
}
|
|
|
|
private function driverColumn(): TextColumn
|
|
{
|
|
return TextColumn::make('driver')
|
|
->label('Type')
|
|
->formatStateUsing(fn ($state) => $this->driverLabel($state));
|
|
}
|
|
|
|
private function driverLabel(string $key): string
|
|
{
|
|
$driver = collect(Shipping::getSupportedDrivers())->get($key);
|
|
|
|
return $driver?->name() ?? $key;
|
|
}
|
|
|
|
/**
|
|
* Recursively walk the form tree and replace the hardcoded driver
|
|
* Select (nested inside Section > Group) with one listing every
|
|
* registered driver, built-in or custom.
|
|
*
|
|
* @param array<Component> $components
|
|
* @return array<Component>
|
|
*/
|
|
private function replaceDriverField(array $components): array
|
|
{
|
|
return array_map(function (Component $component) {
|
|
if (method_exists($component, 'getName') && $component->getName() === 'driver') {
|
|
return $this->driverSelect();
|
|
}
|
|
|
|
if (in_array(HasChildComponents::class, class_uses_recursive($component), true)) {
|
|
$component->schema(
|
|
$this->replaceDriverField($component->getChildComponents())
|
|
);
|
|
}
|
|
|
|
return $component;
|
|
}, $components);
|
|
}
|
|
|
|
private function driverSelect(): Select
|
|
{
|
|
return Select::make('driver')
|
|
->label('Type')
|
|
->options(fn () => collect(Shipping::getSupportedDrivers())
|
|
->mapWithKeys(fn ($driver, $key) => [$key => $driver->name()]))
|
|
->default('flat-rate')
|
|
->live();
|
|
}
|
|
}
|