Fix: Resolving box now by instance, not by type. Also, fixing bug where missing shipping driver would break checkout
This commit is contained in:
@@ -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/).
|
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
|
## [0.29.0] - 2026-09-30
|
||||||
### Added
|
### Added
|
||||||
- **ELTA Courier** carrier (`elta` shipping driver): rates, shipment creation,
|
- **ELTA Courier** carrier (`elta` shipping driver): rates, shipment creation,
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
"name": "boboko/core",
|
"name": "boboko/core",
|
||||||
"description": "Core module — authentication and shared panel behaviour",
|
"description": "Core module — authentication and shared panel behaviour",
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"version": "0.29.0",
|
"version": "0.29.1",
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"Modules\\Core\\": "src/"
|
"Modules\\Core\\": "src/"
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@boboko/core",
|
"name": "@boboko/core",
|
||||||
"version": "0.29.0",
|
"version": "0.29.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"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.",
|
"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.",
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ export default class extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async selectShipping(event) {
|
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"
|
// Tracked so flush() can await it — nothing else stops "place order"
|
||||||
// (a separate, unrelated click) from racing ahead of this request.
|
// (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
|
// re-renders wholesale on every shipping-option change — so its own
|
||||||
// Stimulus controller reconnects fresh each time and has no memory of
|
// Stimulus controller reconnects fresh each time and has no memory of
|
||||||
// which option was previously selected. This is the one place that
|
// which option was previously selected. This is the one place that
|
||||||
// knows the newly-chosen option's identifier, so it also owns
|
// knows the newly-chosen option, so it also owns showing/hiding the
|
||||||
// showing/hiding the picker.
|
// picker. Whether an option is Box Now comes from the server
|
||||||
toggleBoxNowLocker(identifier) {
|
// (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')
|
const picker = this.shippingOptionsTarget.querySelector('#bbk-box-now-locker')
|
||||||
if (!picker) return
|
if (!picker) return
|
||||||
|
|
||||||
const controller = this.application.getControllerForElementAndIdentifier(picker, 'bbk-box-now-locker')
|
const controller = this.application.getControllerForElementAndIdentifier(picker, 'bbk-box-now-locker')
|
||||||
|
|
||||||
if (identifier === 'box-now') {
|
if (isBoxNow) {
|
||||||
controller?.show()
|
controller?.show()
|
||||||
} else {
|
} else {
|
||||||
controller?.hide()
|
controller?.hide()
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
$shippingAddress, $shippingOptions come from the controller / page scope.
|
$shippingAddress, $shippingOptions come from the controller / page scope.
|
||||||
--}}
|
--}}
|
||||||
@php($selected = $shippingAddress?->shipping_option)
|
@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
|
{{-- Rate resolution needs country (always Greece here) + postcode; until a
|
||||||
postcode is saved there's nothing to quote against yet. --}}
|
postcode is saved there's nothing to quote against yet. --}}
|
||||||
@@ -34,6 +36,7 @@
|
|||||||
type="radio"
|
type="radio"
|
||||||
name="shipping_option"
|
name="shipping_option"
|
||||||
value="{{ $option->identifier }}"
|
value="{{ $option->identifier }}"
|
||||||
|
data-box-now="{{ in_array($option->identifier, $boxNowCodes, true) ? 'true' : 'false' }}"
|
||||||
@checked($selected === $option->identifier)
|
@checked($selected === $option->identifier)
|
||||||
data-action="change->bbk-checkout-form#selectShipping"
|
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-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-selected-label-value="{{ __('checkout.page.box_now_locker_selected') }}"
|
||||||
data-bbk-box-now-locker-no-results-value="{{ __('checkout.page.box_now_locker_no_results') }}"
|
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
|
||||||
>
|
>
|
||||||
<p class="bbk-checkout-box-now-locker-label">
|
<p class="bbk-checkout-box-now-locker-label">
|
||||||
{{ __('checkout.page.box_now_locker_label') }}
|
{{ __('checkout.page.box_now_locker_label') }}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ use Modules\Core\Payment\Models\PaymentMethod;
|
|||||||
use Modules\Core\Payment\Services\PaymentDriverRegistry;
|
use Modules\Core\Payment\Services\PaymentDriverRegistry;
|
||||||
use Modules\Core\Payment\Services\PaymentMethodCache;
|
use Modules\Core\Payment\Services\PaymentMethodCache;
|
||||||
use Modules\Core\Shipping\Contracts\SupportsCashCollection;
|
use Modules\Core\Shipping\Contracts\SupportsCashCollection;
|
||||||
|
use Modules\Core\Shipping\Support\BoxNowShipping;
|
||||||
use Modules\Core\Shipping\Support\FulfillmentType;
|
use Modules\Core\Shipping\Support\FulfillmentType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -181,7 +182,7 @@ class CheckoutService
|
|||||||
// switches BACK to Box Now and it resurfaces as if still chosen,
|
// switches BACK to Box Now and it resurfaces as if still chosen,
|
||||||
// possibly for a locker that no longer exists/fits. Cleared here,
|
// possibly for a locker that no longer exists/fits. Cleared here,
|
||||||
// the one place that knows the method just changed.
|
// the one place that knows the method just changed.
|
||||||
if ($identifier !== 'box-now') {
|
if (! BoxNowShipping::isOption($identifier)) {
|
||||||
$address = $cart->shippingAddress;
|
$address = $cart->shippingAddress;
|
||||||
|
|
||||||
if ($address && isset($address->meta['box_now_locker'])) {
|
if ($address && isset($address->meta['box_now_locker'])) {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ use Modules\Core\Shipping\Enums\TrackingStatus;
|
|||||||
use Modules\Core\Shipping\Events\ShipmentStatusUpdatedByCarrier;
|
use Modules\Core\Shipping\Events\ShipmentStatusUpdatedByCarrier;
|
||||||
use Modules\Core\Shipping\Filament\Resources\ShipmentResource;
|
use Modules\Core\Shipping\Filament\Resources\ShipmentResource;
|
||||||
use Modules\Core\Shipping\Models\Shipment;
|
use Modules\Core\Shipping\Models\Shipment;
|
||||||
|
use Modules\Core\Shipping\Support\BoxNowShipping;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a "Shipments" section to the order page's main column — previously
|
* 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')];
|
$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;
|
$parts[] = 'Locker '.$locker;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ use Modules\Core\Shipping\Contracts\SupportsVoucherLookup;
|
|||||||
use Modules\Core\Shipping\DTOs\ShipmentRequest;
|
use Modules\Core\Shipping\DTOs\ShipmentRequest;
|
||||||
use Modules\Core\Shipping\Enums\ExtraService;
|
use Modules\Core\Shipping\Enums\ExtraService;
|
||||||
use Modules\Core\Shipping\Models\Shipment;
|
use Modules\Core\Shipping\Models\Shipment;
|
||||||
|
use Modules\Core\Shipping\Support\BoxNowShipping;
|
||||||
use Modules\Core\Shipping\Support\WeightCalculator;
|
use Modules\Core\Shipping\Support\WeightCalculator;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
@@ -122,7 +123,7 @@ class OrderViewExtension extends ViewPageExtension
|
|||||||
->modalSubmitActionLabel('Create Shipment')
|
->modalSubmitActionLabel('Create Shipment')
|
||||||
->schema(function (Order $record) {
|
->schema(function (Order $record) {
|
||||||
$carrier = $this->service()->carrierFor($record);
|
$carrier = $this->service()->carrierFor($record);
|
||||||
$isBoxNow = $carrier === 'box-now';
|
$isBoxNow = BoxNowShipping::isDriver($carrier);
|
||||||
$lockerId = $record->shippingAddress?->meta['box_now_locker']['locationId'] ?? null;
|
$lockerId = $record->shippingAddress?->meta['box_now_locker']['locationId'] ?? null;
|
||||||
|
|
||||||
if ($carrier === 'manual') {
|
if ($carrier === 'manual') {
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Core\Shipping\Support;
|
||||||
|
|
||||||
|
use Lunar\Shipping\Facades\Shipping;
|
||||||
|
use Lunar\Shipping\Models\ShippingMethod;
|
||||||
|
use Modules\Core\Shipping\Carriers\BoxNow\BoxNowRateDriver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether something is Box Now — a checkout option, a shipping method, a
|
||||||
|
* driver key — by checking that the driver is a BoxNowRateDriver, never by
|
||||||
|
* comparing names: an option's identifier is the merchant-typed method
|
||||||
|
* code (may well not be "box-now"), and the driver may be registered under
|
||||||
|
* any key.
|
||||||
|
*/
|
||||||
|
class BoxNowShipping
|
||||||
|
{
|
||||||
|
public static function isOption(?string $identifier): bool
|
||||||
|
{
|
||||||
|
return $identifier !== null
|
||||||
|
&& self::isMethod(ShippingMethod::where('code', $identifier)->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<int, string>
|
||||||
|
*/
|
||||||
|
public static function codes(): array
|
||||||
|
{
|
||||||
|
return ShippingMethod::all()
|
||||||
|
->filter(fn (ShippingMethod $method) => self::isMethod($method))
|
||||||
|
->pluck('code')
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Core\Shipping\Support;
|
||||||
|
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Lunar\Shipping\Facades\Shipping;
|
||||||
|
use Lunar\Shipping\Resolvers\ShippingRateResolver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lunar's rate resolver, minus rates whose shipping method points at a
|
||||||
|
* driver that isn't registered anymore (e.g. a carrier commented out of
|
||||||
|
* ShippingServiceProvider while its shipping method still exists). Lunar
|
||||||
|
* would go on to call that driver to price the option and throw "Driver
|
||||||
|
* [x] not supported", taking the whole checkout down; here the method is
|
||||||
|
* simply not offered. It stays in the admin as it was.
|
||||||
|
*/
|
||||||
|
class RegisteredDriverRateResolver extends ShippingRateResolver
|
||||||
|
{
|
||||||
|
public function get(): Collection
|
||||||
|
{
|
||||||
|
$registered = Shipping::getSupportedDrivers();
|
||||||
|
|
||||||
|
return parent::get()
|
||||||
|
->filter(fn ($rate) => $registered->has($rate->shippingMethod?->driver))
|
||||||
|
->values();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
namespace Modules\Core\Shipping\Support;
|
namespace Modules\Core\Shipping\Support;
|
||||||
|
|
||||||
|
use Lunar\Models\Contracts\Cart as CartContract;
|
||||||
use Lunar\Shipping\Managers\ShippingManager as VendorShippingManager;
|
use Lunar\Shipping\Managers\ShippingManager as VendorShippingManager;
|
||||||
|
use Lunar\Shipping\Resolvers\ShippingRateResolver;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Drops lunarphp/table-rate-shipping's own generic drivers (free-shipping,
|
* 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
|
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
|
public function getSupportedDrivers(): \Illuminate\Support\Collection
|
||||||
{
|
{
|
||||||
return collect($this->customCreators)
|
return collect($this->customCreators)
|
||||||
|
|||||||
Reference in New Issue
Block a user