From dbea4a8d69f1369228d83ad779cb63bee47bdae3 Mon Sep 17 00:00:00 2001 From: Konstantinos Arvanitakis Date: Wed, 30 Sep 2026 17:48:59 +0300 Subject: [PATCH] Fix: Resolving box now by instance, not by type. Also, fixing bug where missing shipping driver would break checkout --- CHANGELOG.md | 16 ++++++ composer.json | 2 +- package.json | 2 +- .../checkout/bbk-checkout-form-controller.js | 12 +++-- .../partials/shipping-options.blade.php | 5 +- src/Checkout/Services/CheckoutService.php | 3 +- .../Extensions/OrderShipmentsExtension.php | 3 +- .../Extensions/OrderViewExtension.php | 3 +- src/Shipping/Support/BoxNowShipping.php | 53 +++++++++++++++++++ .../Support/RegisteredDriverRateResolver.php | 27 ++++++++++ src/Shipping/Support/ShippingManager.php | 11 ++++ 11 files changed, 126 insertions(+), 11 deletions(-) create mode 100644 src/Shipping/Support/BoxNowShipping.php create mode 100644 src/Shipping/Support/RegisteredDriverRateResolver.php diff --git a/CHANGELOG.md b/CHANGELOG.md index e3df1cc..b4978e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,22 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +## [0.29.1] - 2026-09-30 +### Fixed +- Checkout crashed ("Driver [acs] not supported", a 500) when a shipping + method pointed at a driver that is no longer registered — e.g. a carrier + commented out while its shipping method still exists in the admin. Such + methods are now left out of the options offered at checkout + (`Modules\Core\Shipping\Support\RegisteredDriverRateResolver`); the admin + is unchanged. +- The Box Now locker map at checkout only appeared when the shipping + method's **code** was exactly `box-now`; a method with any other code + (e.g. `boxnow`) never showed it, and switching options cleared the chosen + locker. Box Now is now recognized by its driver being a `BoxNowRateDriver` + (`Modules\Core\Shipping\Support\BoxNowShipping`), not by any name. The + admin order page's Create Shipment (locker + boxes form) and the + shipment's locker line use the same check. + ## [0.29.0] - 2026-09-30 ### Added - **ELTA Courier** carrier (`elta` shipping driver): rates, shipment creation, diff --git a/composer.json b/composer.json index d8c6d79..3f94515 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "boboko/core", "description": "Core module — authentication and shared panel behaviour", "type": "library", - "version": "0.29.0", + "version": "0.29.1", "autoload": { "psr-4": { "Modules\\Core\\": "src/" diff --git a/package.json b/package.json index 75a4e58..2405a86 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@boboko/core", - "version": "0.29.0", + "version": "0.29.1", "private": true, "type": "module", "description": "Portable Stimulus controllers and styles for boboko-core's cart + checkout module. Installed as a real npm dependency (file:../boboko-core in dev, a tagged git install in prod) so a consuming app's `npm install` resolves this package's own dependencies (leaflet, @hotwired/stimulus) transitively, the same way `composer update boboko/*` does for PHP. See CONTRIBUTE.md's \"JS/CSS: a real npm package\" section.", diff --git a/resources/js/checkout/bbk-checkout-form-controller.js b/resources/js/checkout/bbk-checkout-form-controller.js index 161b49a..a1bf650 100644 --- a/resources/js/checkout/bbk-checkout-form-controller.js +++ b/resources/js/checkout/bbk-checkout-form-controller.js @@ -123,7 +123,7 @@ export default class extends Controller { } async selectShipping(event) { - this.toggleBoxNowLocker(event.target.value) + this.toggleBoxNowLocker(event.target.dataset.boxNow === 'true') // Tracked so flush() can await it — nothing else stops "place order" // (a separate, unrelated click) from racing ahead of this request. @@ -136,15 +136,17 @@ export default class extends Controller { // re-renders wholesale on every shipping-option change — so its own // Stimulus controller reconnects fresh each time and has no memory of // which option was previously selected. This is the one place that - // knows the newly-chosen option's identifier, so it also owns - // showing/hiding the picker. - toggleBoxNowLocker(identifier) { + // knows the newly-chosen option, so it also owns showing/hiding the + // picker. Whether an option is Box Now comes from the server + // (data-box-now, by the method's driver) — the option's value is the + // merchant-typed method code, which needn't be "box-now". + toggleBoxNowLocker(isBoxNow) { const picker = this.shippingOptionsTarget.querySelector('#bbk-box-now-locker') if (!picker) return const controller = this.application.getControllerForElementAndIdentifier(picker, 'bbk-box-now-locker') - if (identifier === 'box-now') { + if (isBoxNow) { controller?.show() } else { controller?.hide() diff --git a/resources/views/checkout/partials/shipping-options.blade.php b/resources/views/checkout/partials/shipping-options.blade.php index 9030c5e..5f1db6a 100644 --- a/resources/views/checkout/partials/shipping-options.blade.php +++ b/resources/views/checkout/partials/shipping-options.blade.php @@ -8,6 +8,8 @@ $shippingAddress, $shippingOptions come from the controller / page scope. --}} @php($selected = $shippingAddress?->shipping_option) +{{-- Box Now by the method's driver, not its (merchant-typed) code --}} +@php($boxNowCodes = \Modules\Core\Shipping\Support\BoxNowShipping::codes()) {{-- Rate resolution needs country (always Greece here) + postcode; until a postcode is saved there's nothing to quote against yet. --}} @@ -34,6 +36,7 @@ type="radio" name="shipping_option" value="{{ $option->identifier }}" + data-box-now="{{ in_array($option->identifier, $boxNowCodes, true) ? 'true' : 'false' }}" @checked($selected === $option->identifier) data-action="change->bbk-checkout-form#selectShipping" > @@ -73,7 +76,7 @@ data-bbk-box-now-locker-select-label-value="{{ __('checkout.page.box_now_locker_select') }}" data-bbk-box-now-locker-selected-label-value="{{ __('checkout.page.box_now_locker_selected') }}" data-bbk-box-now-locker-no-results-value="{{ __('checkout.page.box_now_locker_no_results') }}" - @if ($selected !== 'box-now') hidden @endif + @if (! in_array($selected, $boxNowCodes, true)) hidden @endif >

{{ __('checkout.page.box_now_locker_label') }} diff --git a/src/Checkout/Services/CheckoutService.php b/src/Checkout/Services/CheckoutService.php index cb5bbc3..a379e59 100644 --- a/src/Checkout/Services/CheckoutService.php +++ b/src/Checkout/Services/CheckoutService.php @@ -29,6 +29,7 @@ use Modules\Core\Payment\Models\PaymentMethod; use Modules\Core\Payment\Services\PaymentDriverRegistry; use Modules\Core\Payment\Services\PaymentMethodCache; use Modules\Core\Shipping\Contracts\SupportsCashCollection; +use Modules\Core\Shipping\Support\BoxNowShipping; use Modules\Core\Shipping\Support\FulfillmentType; /** @@ -181,7 +182,7 @@ class CheckoutService // switches BACK to Box Now and it resurfaces as if still chosen, // possibly for a locker that no longer exists/fits. Cleared here, // the one place that knows the method just changed. - if ($identifier !== 'box-now') { + if (! BoxNowShipping::isOption($identifier)) { $address = $cart->shippingAddress; if ($address && isset($address->meta['box_now_locker'])) { diff --git a/src/Shipping/Extensions/OrderShipmentsExtension.php b/src/Shipping/Extensions/OrderShipmentsExtension.php index 1c74d5d..328edde 100644 --- a/src/Shipping/Extensions/OrderShipmentsExtension.php +++ b/src/Shipping/Extensions/OrderShipmentsExtension.php @@ -19,6 +19,7 @@ use Modules\Core\Shipping\Enums\TrackingStatus; use Modules\Core\Shipping\Events\ShipmentStatusUpdatedByCarrier; use Modules\Core\Shipping\Filament\Resources\ShipmentResource; use Modules\Core\Shipping\Models\Shipment; +use Modules\Core\Shipping\Support\BoxNowShipping; /** * Adds a "Shipments" section to the order page's main column — previously @@ -224,7 +225,7 @@ class OrderShipmentsExtension extends ViewPageExtension { $parts = ['Created '.$record->created_at->format('Y-m-d H:i')]; - if ($record->carrier === 'box-now' && $locker = $record->meta['locker_id'] ?? null) { + if (BoxNowShipping::isDriver($record->carrier) && $locker = $record->meta['locker_id'] ?? null) { $parts[] = 'Locker '.$locker; } diff --git a/src/Shipping/Extensions/OrderViewExtension.php b/src/Shipping/Extensions/OrderViewExtension.php index 7b46f1d..1ce4451 100644 --- a/src/Shipping/Extensions/OrderViewExtension.php +++ b/src/Shipping/Extensions/OrderViewExtension.php @@ -25,6 +25,7 @@ use Modules\Core\Shipping\Contracts\SupportsVoucherLookup; use Modules\Core\Shipping\DTOs\ShipmentRequest; use Modules\Core\Shipping\Enums\ExtraService; use Modules\Core\Shipping\Models\Shipment; +use Modules\Core\Shipping\Support\BoxNowShipping; use Modules\Core\Shipping\Support\WeightCalculator; use Throwable; @@ -122,7 +123,7 @@ class OrderViewExtension extends ViewPageExtension ->modalSubmitActionLabel('Create Shipment') ->schema(function (Order $record) { $carrier = $this->service()->carrierFor($record); - $isBoxNow = $carrier === 'box-now'; + $isBoxNow = BoxNowShipping::isDriver($carrier); $lockerId = $record->shippingAddress?->meta['box_now_locker']['locationId'] ?? null; if ($carrier === 'manual') { diff --git a/src/Shipping/Support/BoxNowShipping.php b/src/Shipping/Support/BoxNowShipping.php new file mode 100644 index 0000000..2b52e12 --- /dev/null +++ b/src/Shipping/Support/BoxNowShipping.php @@ -0,0 +1,53 @@ +first()); + } + + public static function isMethod(?ShippingMethod $method): bool + { + return self::isDriver($method?->driver); + } + + /** + * A shipping driver key (a method's `driver`, a shipment's `carrier`) + * that resolves to Box Now. getSupportedDrivers() rather than + * Shipping::driver(), which throws for a key no longer registered. + */ + public static function isDriver(?string $driver): bool + { + return $driver !== null + && Shipping::getSupportedDrivers()->get($driver) instanceof BoxNowRateDriver; + } + + /** + * Codes of every Box Now shipping method — for marking options in bulk. + * + * @return array + */ + public static function codes(): array + { + return ShippingMethod::all() + ->filter(fn (ShippingMethod $method) => self::isMethod($method)) + ->pluck('code') + ->values() + ->all(); + } +} diff --git a/src/Shipping/Support/RegisteredDriverRateResolver.php b/src/Shipping/Support/RegisteredDriverRateResolver.php new file mode 100644 index 0000000..4b3748b --- /dev/null +++ b/src/Shipping/Support/RegisteredDriverRateResolver.php @@ -0,0 +1,27 @@ +filter(fn ($rate) => $registered->has($rate->shippingMethod?->driver)) + ->values(); + } +} diff --git a/src/Shipping/Support/ShippingManager.php b/src/Shipping/Support/ShippingManager.php index f63f4d7..f0d0137 100644 --- a/src/Shipping/Support/ShippingManager.php +++ b/src/Shipping/Support/ShippingManager.php @@ -2,7 +2,9 @@ namespace Modules\Core\Shipping\Support; +use Lunar\Models\Contracts\Cart as CartContract; use Lunar\Shipping\Managers\ShippingManager as VendorShippingManager; +use Lunar\Shipping\Resolvers\ShippingRateResolver; /** * Drops lunarphp/table-rate-shipping's own generic drivers (free-shipping, @@ -28,6 +30,15 @@ use Lunar\Shipping\Managers\ShippingManager as VendorShippingManager; */ class ShippingManager extends VendorShippingManager { + /** + * Rates for a cart, leaving out shipping methods whose driver is no + * longer registered (see RegisteredDriverRateResolver). + */ + public function shippingRates(?CartContract $cart = null): ShippingRateResolver + { + return new RegisteredDriverRateResolver($cart); + } + public function getSupportedDrivers(): \Illuminate\Support\Collection { return collect($this->customCreators)