From d34e4505267dd3ae9da2460fb418ae09a2677ef1 Mon Sep 17 00:00:00 2001 From: Konstantinos Arvanitakis Date: Sun, 19 Jul 2026 02:24:10 +0300 Subject: [PATCH] Fix: Move carrier live-pricing choice onto ShippingMethod.charge_by 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. --- ...d_pricing_mode_to_shipping_rates_table.php | 26 ----- src/Shipping/Carriers/Acs/AcsRateDriver.php | 32 ++----- .../Carriers/BoxNow/BoxNowRateDriver.php | 35 +++---- .../Concerns/ResolvesFixedPricing.php | 42 ++++++++ .../ShippingMethodResourceExtension.php | 73 +++++++++++++- .../Filament/Pages/ManageShippingRates.php | 95 ++++--------------- 6 files changed, 148 insertions(+), 155 deletions(-) delete mode 100644 database/migrations/2026_07_17_000001_add_pricing_mode_to_shipping_rates_table.php create mode 100644 src/Shipping/Concerns/ResolvesFixedPricing.php diff --git a/database/migrations/2026_07_17_000001_add_pricing_mode_to_shipping_rates_table.php b/database/migrations/2026_07_17_000001_add_pricing_mode_to_shipping_rates_table.php deleted file mode 100644 index 5442c1b..0000000 --- a/database/migrations/2026_07_17_000001_add_pricing_mode_to_shipping_rates_table.php +++ /dev/null @@ -1,26 +0,0 @@ -string('pricing_mode')->default('live')->after('enabled'); - }); - } - - public function down(): void - { - $prefix = config('lunar.database.table_prefix'); - - Schema::table("{$prefix}shipping_rates", function (Blueprint $table) { - $table->dropColumn('pricing_mode'); - }); - } -}; diff --git a/src/Shipping/Carriers/Acs/AcsRateDriver.php b/src/Shipping/Carriers/Acs/AcsRateDriver.php index 0d92a48..6c8af72 100644 --- a/src/Shipping/Carriers/Acs/AcsRateDriver.php +++ b/src/Shipping/Carriers/Acs/AcsRateDriver.php @@ -4,15 +4,17 @@ namespace Modules\Core\Shipping\Carriers\Acs; use Lunar\DataTypes\Price; use Lunar\DataTypes\ShippingOption; -use Lunar\Facades\Pricing; use Lunar\Shipping\DataTransferObjects\ShippingOptionRequest; use Lunar\Shipping\Interfaces\ShippingRateInterface; use Lunar\Shipping\Models\ShippingRate; use Modules\Core\Shipping\Carriers\Acs\Exceptions\AcsApiException; +use Modules\Core\Shipping\Concerns\ResolvesFixedPricing; use Modules\Core\Shipping\Contracts\SupportsLivePricing; class AcsRateDriver implements ShippingRateInterface, SupportsLivePricing { + use ResolvesFixedPricing; + public ShippingRate $shippingRate; public function __construct( @@ -36,39 +38,19 @@ class AcsRateDriver implements ShippingRateInterface, SupportsLivePricing $shippingMethod = $shippingRate->shippingMethod; $cart = $shippingOptionRequest->cart; + if (($shippingMethod->data['charge_by'] ?? 'cart_total') !== 'live') { + return $this->resolveFixedPrice($shippingRate, $shippingMethod, $cart); + } + $postcode = $cart->shippingAddress?->postcode; if (! $postcode) { return null; } - if (($shippingRate->pricing_mode ?? 'live') === 'fixed') { - return $this->resolveFixedPrice($shippingRate, $shippingMethod, $cart); - } - return $this->resolveLivePrice($shippingRate, $shippingMethod, $cart, $postcode); } - private function resolveFixedPrice(ShippingRate $shippingRate, $shippingMethod, $cart): ?ShippingOption - { - $subTotal = $cart->lines->sum('subTotal.value'); - - $pricing = Pricing::for($shippingRate)->qty($subTotal)->get(); - - if (! $pricing->matched) { - return null; - } - - return new ShippingOption( - name: $shippingMethod->name ?: $this->name(), - description: $shippingMethod->description ?: $this->description(), - identifier: $shippingRate->getIdentifier(), - price: $pricing->matched->price, - taxClass: $shippingRate->getTaxClass(), - taxReference: $shippingRate->getTaxReference(), - ); - } - private function resolveLivePrice(ShippingRate $shippingRate, $shippingMethod, $cart, string $postcode): ?ShippingOption { try { diff --git a/src/Shipping/Carriers/BoxNow/BoxNowRateDriver.php b/src/Shipping/Carriers/BoxNow/BoxNowRateDriver.php index 4897657..46fe211 100644 --- a/src/Shipping/Carriers/BoxNow/BoxNowRateDriver.php +++ b/src/Shipping/Carriers/BoxNow/BoxNowRateDriver.php @@ -3,19 +3,21 @@ namespace Modules\Core\Shipping\Carriers\BoxNow; use Lunar\DataTypes\ShippingOption; -use Lunar\Facades\Pricing; use Lunar\Shipping\DataTransferObjects\ShippingOptionRequest; use Lunar\Shipping\Interfaces\ShippingRateInterface; use Lunar\Shipping\Models\ShippingRate; +use Modules\Core\Shipping\Concerns\ResolvesFixedPricing; /** - * Box Now has no pricing API, so this always resolves the admin-configured - * price/price-breaks on the ShippingRate — the same mechanism the built-in - * flat-rate driver uses. Unlike AcsRateDriver, this does not implement - * SupportsLivePricing: there is nothing to toggle between. + * Box Now has no pricing API, so this always resolves the method's normal + * charge_by + price-break configuration — the same mechanism the built-in + * flat-rate/ship-by drivers use. Does not implement SupportsLivePricing: + * there is no live option to offer. */ class BoxNowRateDriver implements ShippingRateInterface { + use ResolvesFixedPricing; + public ShippingRate $shippingRate; public function name(): string @@ -30,25 +32,10 @@ class BoxNowRateDriver implements ShippingRateInterface public function resolve(ShippingOptionRequest $shippingOptionRequest): ?ShippingOption { - $shippingRate = $shippingOptionRequest->shippingRate; - $shippingMethod = $shippingRate->shippingMethod; - $cart = $shippingOptionRequest->cart; - - $subTotal = $cart->lines->sum('subTotal.value'); - - $pricing = Pricing::for($shippingRate)->qty($subTotal)->get(); - - if (! $pricing->matched) { - return null; - } - - return new ShippingOption( - name: $shippingMethod->name ?: $this->name(), - description: $shippingMethod->description ?: $this->description(), - identifier: $shippingRate->getIdentifier(), - price: $pricing->matched->price, - taxClass: $shippingRate->getTaxClass(), - taxReference: $shippingRate->getTaxReference(), + return $this->resolveFixedPrice( + $shippingOptionRequest->shippingRate, + $shippingOptionRequest->shippingRate->shippingMethod, + $shippingOptionRequest->cart, ); } diff --git a/src/Shipping/Concerns/ResolvesFixedPricing.php b/src/Shipping/Concerns/ResolvesFixedPricing.php new file mode 100644 index 0000000..d0a31bd --- /dev/null +++ b/src/Shipping/Concerns/ResolvesFixedPricing.php @@ -0,0 +1,42 @@ +data['charge_by'] ?? 'cart_total'; + + $tier = $chargeBy === 'weight' + ? $cart->lines->load('purchasable')->sum(fn ($line) => ($line->purchasable->weight_value ?? 0) * $line->quantity) + : $cart->lines->sum('subTotal.value'); + + $pricing = Pricing::for($shippingRate)->qty($tier)->get(); + + if (! $pricing->matched) { + return null; + } + + return new ShippingOption( + name: $shippingMethod->name ?: $this->name(), + description: $shippingMethod->description ?: $this->description(), + identifier: $shippingRate->getIdentifier(), + price: $pricing->matched->price, + taxClass: $shippingRate->getTaxClass(), + taxReference: $shippingRate->getTaxReference(), + ); + } +} diff --git a/src/Shipping/Extensions/ShippingMethodResourceExtension.php b/src/Shipping/Extensions/ShippingMethodResourceExtension.php index 0aa8cd6..5cfadc7 100644 --- a/src/Shipping/Extensions/ShippingMethodResourceExtension.php +++ b/src/Shipping/Extensions/ShippingMethodResourceExtension.php @@ -6,20 +6,88 @@ 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->replaceDriverField($form->getComponents()) + $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( @@ -78,6 +146,7 @@ class ShippingMethodResourceExtension extends ResourceExtension ->label('Type') ->options(fn () => collect(Shipping::getSupportedDrivers()) ->mapWithKeys(fn ($driver, $key) => [$key => $driver->name()])) - ->default('flat-rate'); + ->default('flat-rate') + ->live(); } } diff --git a/src/Shipping/Filament/Pages/ManageShippingRates.php b/src/Shipping/Filament/Pages/ManageShippingRates.php index d91b287..bf6cdb3 100644 --- a/src/Shipping/Filament/Pages/ManageShippingRates.php +++ b/src/Shipping/Filament/Pages/ManageShippingRates.php @@ -2,16 +2,13 @@ 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\Filament\Resources\ShippingZoneResource\Pages\ManageShippingRates as BaseManageShippingRates; 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 @@ -20,11 +17,13 @@ use Modules\Core\Shipping\Contracts\SupportsLivePricing; * 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. + * Hides the price / price-break fields for a rate whose method has + * charge_by = "live" (see ShippingMethodResourceExtension, which adds that + * option to methods whose driver supports live pricing) — those fields + * would otherwise be dead configuration the driver never reads. Pricing + * strategy (cart_total / weight / live) stays entirely on the Shipping + * Method, matching Lunar's own existing charge_by convention; nothing new + * is stored on the rate itself. */ class ManageShippingRates extends BaseManageShippingRates { @@ -33,56 +32,13 @@ class ManageShippingRates extends BaseManageShippingRates $form = parent::form($form); return $form->schema( - $this->insertPricingModeFieldAfterShippingMethod( - $this->hidePriceFieldsWhenLive($form->getComponents()) - ) + $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'; + $isNotLive = fn (Get $get) => static::methodChargeBy($get('shipping_method_id')) !== 'live'; foreach ($components as $component) { if (! method_exists($component, 'getName')) { @@ -90,14 +46,11 @@ class ManageShippingRates extends BaseManageShippingRates } if ($component->getName() === 'price') { - $component->visible($isFixedOrNotLiveCapable) - ->required($isFixedOrNotLiveCapable) - ->dehydrated(true); + $component->visible($isNotLive)->required($isNotLive)->dehydrated(true); } if ($component->getName() === 'prices') { - $component->visible($isFixedOrNotLiveCapable) - ->dehydrated(true); + $component->visible($isNotLive)->dehydrated(true); } } @@ -114,7 +67,7 @@ class ManageShippingRates extends BaseManageShippingRates 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') { + if (static::methodChargeBy($record->shipping_method_id) === 'live') { return 'Live API pricing'; } @@ -129,37 +82,23 @@ class ManageShippingRates extends BaseManageShippingRates 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) { + if (static::methodChargeBy($data['shipping_method_id'] ?? $shippingRate?->shipping_method_id) === 'live') { return; } parent::saveShippingRate($shippingRate, $data); } - protected static function methodHasLivePricing(ShippingMethod|int|string|null $method): bool + protected static function methodChargeBy(ShippingMethod|int|string|null $method): ?string { if (blank($method)) { - return false; + return null; } if (! $method instanceof ShippingMethod) { $method = ShippingMethod::find($method); } - if (! $method) { - return false; - } - - try { - return Shipping::driver($method->driver) instanceof SupportsLivePricing; - } catch (\InvalidArgumentException) { - return false; - } + return $method?->data['charge_by'] ?? null; } }