Compare commits

...
1 Commits
11 changed files with 126 additions and 11 deletions
+16
View File
@@ -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,
+1 -1
View File
@@ -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/"
+1 -1
View File
@@ -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.",
@@ -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()
@@ -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
>
<p class="bbk-checkout-box-now-locker-label">
{{ __('checkout.page.box_now_locker_label') }}
+2 -1
View File
@@ -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'])) {
@@ -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;
}
@@ -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') {
+53
View File
@@ -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();
}
}
+11
View File
@@ -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)