Fix: Correct shipment resolving

This commit is contained in:
2026-09-29 00:35:29 +03:00
parent 57d7a716bc
commit d6c6bf6a1c
12 changed files with 278 additions and 22 deletions
@@ -192,6 +192,16 @@ export default class extends Controller {
detail: { html: data.summaryHtml }, detail: { html: data.summaryHtml },
})) }))
} }
// bbk-payment is a sibling controller (both sit on
// .bbk-checkout-main), not a target of this one — dispatched as an
// event rather than reached into directly, same shape as
// bbk-cart:changed above.
if (data.paymentMethodsHtml != null) {
window.dispatchEvent(new CustomEvent('bbk-checkout:payment-methods-changed', {
detail: { html: data.paymentMethodsHtml },
}))
}
} }
applyErrors(errors) { applyErrors(errors) {
@@ -17,7 +17,7 @@ const POLL_TIMEOUT = 30000
// poll /order-status until the webhook places it // poll /order-status until the webhook places it
// { status:'failed'|'invalid'|'stale', message } -> show inline, re-enable // { status:'failed'|'invalid'|'stale', message } -> show inline, re-enable
export default class extends Controller { export default class extends Controller {
static targets = ['element', 'terms', 'error', 'submit', 'processing', 'processingText'] static targets = ['element', 'terms', 'error', 'submit', 'processing', 'processingText', 'methods']
static values = { static values = {
selectUrl: String, selectUrl: String,
@@ -56,11 +56,26 @@ export default class extends Controller {
} }
window.addEventListener('bbk-cart:updated', this.onSummaryUpdate) window.addEventListener('bbk-cart:updated', this.onSummaryUpdate)
// Fired by bbk-checkout-form after a shipping-option change —
// getPaymentMethods() filters by fulfillment type, so the offered
// methods (and which one, if any, is still validly selected) can
// change without this controller's own element ever reconnecting.
this.onPaymentMethodsChanged = (event) => {
const html = event.detail?.html
if (html == null || !this.hasMethodsTarget) return
this.methodsTarget.innerHTML = html
if (!this.selectedIsStripe()) this.unmountStripe()
}
window.addEventListener('bbk-checkout:payment-methods-changed', this.onPaymentMethodsChanged)
if (this.selectedIsStripe()) this.mountStripe() if (this.selectedIsStripe()) this.mountStripe()
} }
disconnect() { disconnect() {
window.removeEventListener('bbk-cart:updated', this.onSummaryUpdate) window.removeEventListener('bbk-cart:updated', this.onSummaryUpdate)
window.removeEventListener('bbk-checkout:payment-methods-changed', this.onPaymentMethodsChanged)
this.unmountStripe() this.unmountStripe()
} }
+1 -1
View File
@@ -219,7 +219,7 @@
<section class="bbk-checkout-section"> <section class="bbk-checkout-section">
<h2 class="bbk-checkout-section-heading">{{ __('checkout.page.payment_heading') }}</h2> <h2 class="bbk-checkout-section-heading">{{ __('checkout.page.payment_heading') }}</h2>
<div id="bbk-payment-methods"> <div id="bbk-payment-methods" data-bbk-payment-target="methods">
@include('checkout::partials.payment-methods', [ @include('checkout::partials.payment-methods', [
'paymentMethods' => $paymentMethods, 'paymentMethods' => $paymentMethods,
'cart' => $cart, 'cart' => $cart,
@@ -646,6 +646,18 @@ class CheckoutController extends Controller
])->render(), ])->render(),
// Composer (Providers\CheckoutModuleServiceProvider) fills $cart / $lines. // Composer (Providers\CheckoutModuleServiceProvider) fills $cart / $lines.
'summaryHtml' => $options === null ? null : view('checkout::partials.cart-body')->render(), 'summaryHtml' => $options === null ? null : view('checkout::partials.cart-body')->render(),
// getPaymentMethods() filters by the cart's CURRENT fulfillment
// type (Modules\Core\Checkout\Services\CheckoutService's own
// docblock) — a shipping-option change can change that filter's
// result (e.g. switching to store pickup should drop
// cash-on-delivery and offer "pay in store" instead), so this
// needs to be re-rendered every time shipping options are,
// otherwise the payment section silently goes stale until a
// full page reload.
'paymentMethodsHtml' => $options === null ? null : view('checkout::partials.payment-methods', [
'paymentMethods' => $this->checkout->getPaymentMethods(),
'cart' => $cart,
])->render(),
]); ]);
} }
+36 -19
View File
@@ -22,11 +22,13 @@ use Modules\Core\Checkout\Exceptions\InvalidShippingOptionException;
use Modules\Core\Checkout\Exceptions\NoShippingAddressException; use Modules\Core\Checkout\Exceptions\NoShippingAddressException;
use Modules\Core\Checkout\Exceptions\TermsNotAcceptedException; use Modules\Core\Checkout\Exceptions\TermsNotAcceptedException;
use Modules\Core\Checkout\Exceptions\UnknownPaymentTypeException; use Modules\Core\Checkout\Exceptions\UnknownPaymentTypeException;
use Lunar\Shipping\Facades\Shipping;
use Modules\Core\Payment\Contracts\RequiresFulfillmentType; use Modules\Core\Payment\Contracts\RequiresFulfillmentType;
use Modules\Core\Payment\DTOs\PaymentResult; use Modules\Core\Payment\DTOs\PaymentResult;
use Modules\Core\Payment\Models\PaymentMethod; 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\Support\FulfillmentType; use Modules\Core\Shipping\Support\FulfillmentType;
/** /**
@@ -254,42 +256,59 @@ class CheckoutService
* a courier delivery makes no sense (no staff member present at * a courier delivery makes no sense (no staff member present at
* handoff to take cash), and cash-on-delivery alongside store * handoff to take cash), and cash-on-delivery alongside store
* pickup is equally meaningless (OfflinePaymentDriver already * pickup is equally meaningless (OfflinePaymentDriver already
* covers that in-person moment). A cart with no shipping option * covers that in-person moment).
* selected yet imposes no constraint here — every method is * 5. for cash-on-delivery specifically, the cart's currently selected
* offered until a fulfillment type is actually known, the same * shipping method's own driver implements Modules\Core\Shipping\
* leniency setShippingAddress()'s own docblock describes for * Contracts\SupportsCashCollection — "carrier" alone
* required-field enforcement happening at the payment gate, not * (RequiresFulfillmentType) isn't precise enough, since an
* mid-checkout. * unattended parcel locker network (Modules\Core\Shipping\
* Carriers\BoxNow\BoxNowRateDriver) is a carrier delivery with
* nobody there to collect cash, unlike an actual courier
* (Modules\Core\Shipping\Carriers\Acs\AcsRateDriver).
* Checks 4 and 5 both impose no constraint when the cart has no
* shipping option selected yet — every method is offered until a
* shipping method is actually known, the same leniency
* setShippingAddress()'s own docblock describes for required-field
* enforcement happening at the payment gate, not mid-checkout.
* *
* @return Collection<int, PaymentMethod> * @return Collection<int, PaymentMethod>
*/ */
public function getPaymentMethods(): Collection public function getPaymentMethods(): Collection
{ {
$fulfillmentType = $this->currentFulfillmentType(); $shippingMethod = $this->currentShippingMethod();
$fulfillmentType = $shippingMethod ? FulfillmentType::resolve($shippingMethod) : null;
return $this->paymentMethods->all() return $this->paymentMethods->all()
->filter(fn (PaymentMethod $method) => $method->enabled && $method->driver_missing_at === null) ->filter(fn (PaymentMethod $method) => $method->enabled && $method->driver_missing_at === null)
->filter(function (PaymentMethod $method) use ($fulfillmentType) { ->filter(function (PaymentMethod $method) use ($fulfillmentType, $shippingMethod) {
$driver = $this->paymentDrivers->resolve($method->driver); $driver = $this->paymentDrivers->resolve($method->driver);
if (! $driver?->isConfigured()) { if (! $driver?->isConfigured()) {
return false; return false;
} }
if ($fulfillmentType === null || ! $driver instanceof RequiresFulfillmentType) { if ($fulfillmentType !== null && $driver instanceof RequiresFulfillmentType
return true; && $driver->requiredFulfillmentType() !== $fulfillmentType) {
return false;
} }
return $driver->requiredFulfillmentType() === $fulfillmentType; // collect(...)->get() rather than Shipping::driver(), which
// throws for an unregistered key — a stale ShippingMethod
// row (e.g. still pointing at a removed generic driver)
// must fail this check quietly, the same as any other
// driver this module doesn't recognize.
if ($shippingMethod !== null && $method->driver === 'cash-on-delivery') {
$shippingDriver = collect(Shipping::getSupportedDrivers())->get($shippingMethod->driver);
return $shippingDriver instanceof SupportsCashCollection && $shippingDriver->collectsCash();
}
return true;
}) })
->values(); ->values();
} }
/** private function currentShippingMethod(): ?ShippingMethod
* @return 'carrier'|'store_pickup'|null null when the cart has no
* shipping option selected yet
*/
private function currentFulfillmentType(): ?string
{ {
$identifier = $this->cart->currentOrCreate()->shippingAddress?->shipping_option; $identifier = $this->cart->currentOrCreate()->shippingAddress?->shipping_option;
@@ -297,9 +316,7 @@ class CheckoutService
return null; return null;
} }
$method = ShippingMethod::where('code', $identifier)->first(); return ShippingMethod::where('code', $identifier)->first();
return $method ? FulfillmentType::resolve($method) : null;
} }
/** /**
+14
View File
@@ -10,6 +10,7 @@ use Livewire\Mechanisms\ComponentRegistry;
use Lunar\Models\Order; use Lunar\Models\Order;
use Lunar\Shipping\Facades\Shipping; use Lunar\Shipping\Facades\Shipping;
use Lunar\Shipping\Filament\Resources\ShippingZoneResource\Pages\ManageShippingRates as VendorManageShippingRates; use Lunar\Shipping\Filament\Resources\ShippingZoneResource\Pages\ManageShippingRates as VendorManageShippingRates;
use Lunar\Shipping\Interfaces\ShippingMethodManagerInterface;
use Lunar\Shipping\Models\ShippingMethod; use Lunar\Shipping\Models\ShippingMethod;
use Modules\Core\Cart\Events\CartCleared; use Modules\Core\Cart\Events\CartCleared;
use Modules\Core\Cart\Events\CartLineAdded; use Modules\Core\Cart\Events\CartLineAdded;
@@ -25,9 +26,11 @@ use Modules\Core\Shipping\Carriers\BoxNow\BoxNowFulfillmentService;
use Modules\Core\Shipping\Carriers\BoxNow\BoxNowRateDriver; use Modules\Core\Shipping\Carriers\BoxNow\BoxNowRateDriver;
use Modules\Core\Shipping\Contracts\CarrierFulfillmentInterface; use Modules\Core\Shipping\Contracts\CarrierFulfillmentInterface;
use Modules\Core\Shipping\Filament\Pages\ManageShippingRates; use Modules\Core\Shipping\Filament\Pages\ManageShippingRates;
use Modules\Core\Shipping\Carriers\StorePickup\StorePickupRateDriver;
use Modules\Core\Shipping\Jobs\PollShipmentTrackingJob; use Modules\Core\Shipping\Jobs\PollShipmentTrackingJob;
use Modules\Core\Shipping\Listeners\InvalidateShippingOptions; use Modules\Core\Shipping\Listeners\InvalidateShippingOptions;
use Modules\Core\Shipping\Support\FulfillmentType; use Modules\Core\Shipping\Support\FulfillmentType;
use Modules\Core\Shipping\Support\ShippingManager;
use Modules\Core\Shipping\Models\Shipment; use Modules\Core\Shipping\Models\Shipment;
class ShippingServiceProvider extends ServiceProvider class ShippingServiceProvider extends ServiceProvider
@@ -57,6 +60,16 @@ class ShippingServiceProvider extends ServiceProvider
// first load, the Livewire registration covers every AJAX // first load, the Livewire registration covers every AJAX
// round-trip (form submits, table interactions) afterwards. // round-trip (form submits, table interactions) afterwards.
$this->app->bind(VendorManageShippingRates::class, ManageShippingRates::class); $this->app->bind(VendorManageShippingRates::class, ManageShippingRates::class);
// Binds AFTER the vendor's own ShippingServiceProvider — the
// last-registered bind() for a given abstract wins in Laravel's
// container, and vendor providers register before this one lists
// theirs as a dependency implicitly via composer.json's package
// discovery order — see Modules\Core\Shipping\Support\
// ShippingManager's own docblock for why this override exists at
// all (dropping the vendor's generic drivers from
// getSupportedDrivers()).
$this->app->bind(ShippingMethodManagerInterface::class, fn ($app) => $app->make(ShippingManager::class));
} }
public function boot(): void public function boot(): void
@@ -123,6 +136,7 @@ class ShippingServiceProvider extends ServiceProvider
$this->app->booted(function () { $this->app->booted(function () {
Shipping::extend('acs', fn ($app) => $app->make(AcsRateDriver::class)); Shipping::extend('acs', fn ($app) => $app->make(AcsRateDriver::class));
Shipping::extend('box-now', fn ($app) => $app->make(BoxNowRateDriver::class)); Shipping::extend('box-now', fn ($app) => $app->make(BoxNowRateDriver::class));
Shipping::extend('store-pickup', fn ($app) => $app->make(StorePickupRateDriver::class));
$this->app->make(ConsoleSchedule::class) $this->app->make(ConsoleSchedule::class)
->job(new WarmAcsAreaCacheJob) ->job(new WarmAcsAreaCacheJob)
+13 -1
View File
@@ -9,16 +9,19 @@ use Lunar\Shipping\Interfaces\ShippingRateInterface;
use Lunar\Shipping\Models\ShippingRate; use Lunar\Shipping\Models\ShippingRate;
use Modules\Core\Shipping\Carriers\Acs\Exceptions\AcsApiException; use Modules\Core\Shipping\Carriers\Acs\Exceptions\AcsApiException;
use Modules\Core\Shipping\Concerns\CachesLivePricing; use Modules\Core\Shipping\Concerns\CachesLivePricing;
use Modules\Core\Shipping\Concerns\ExcludesRestrictedProducts;
use Modules\Core\Shipping\Concerns\ResolvesFixedPricing; use Modules\Core\Shipping\Concerns\ResolvesFixedPricing;
use Modules\Core\Shipping\Contracts\DeclaresFulfillmentType; use Modules\Core\Shipping\Contracts\DeclaresFulfillmentType;
use Modules\Core\Shipping\Contracts\SupportsCashCollection;
use Modules\Core\Shipping\Contracts\SupportsLivePricing; use Modules\Core\Shipping\Contracts\SupportsLivePricing;
use Modules\Core\Shipping\Support\ShippingMethodName; use Modules\Core\Shipping\Support\ShippingMethodName;
use Modules\Core\Shipping\Support\WeightCalculator; use Modules\Core\Shipping\Support\WeightCalculator;
class AcsRateDriver implements ShippingRateInterface, SupportsLivePricing, DeclaresFulfillmentType class AcsRateDriver implements ShippingRateInterface, SupportsLivePricing, DeclaresFulfillmentType, SupportsCashCollection
{ {
use ResolvesFixedPricing; use ResolvesFixedPricing;
use CachesLivePricing; use CachesLivePricing;
use ExcludesRestrictedProducts;
public ShippingRate $shippingRate; public ShippingRate $shippingRate;
@@ -37,6 +40,11 @@ class AcsRateDriver implements ShippingRateInterface, SupportsLivePricing, Decla
return 'carrier'; return 'carrier';
} }
public function collectsCash(): bool
{
return true;
}
public function description(): string public function description(): string
{ {
return 'Live rate quote from ACS Courier.'; return 'Live rate quote from ACS Courier.';
@@ -48,6 +56,10 @@ class AcsRateDriver implements ShippingRateInterface, SupportsLivePricing, Decla
$shippingMethod = $shippingRate->shippingMethod; $shippingMethod = $shippingRate->shippingMethod;
$cart = $shippingOptionRequest->cart; $cart = $shippingOptionRequest->cart;
if ($this->cartHasExcludedProducts($shippingRate, $cart)) {
return null;
}
if (($shippingMethod->data['charge_by'] ?? 'cart_total') !== 'live') { if (($shippingMethod->data['charge_by'] ?? 'cart_total') !== 'live') {
return $this->resolveFixedPrice($shippingRate, $shippingMethod, $cart); return $this->resolveFixedPrice($shippingRate, $shippingMethod, $cart);
} }
@@ -6,6 +6,7 @@ use Lunar\DataTypes\ShippingOption;
use Lunar\Shipping\DataTransferObjects\ShippingOptionRequest; use Lunar\Shipping\DataTransferObjects\ShippingOptionRequest;
use Lunar\Shipping\Interfaces\ShippingRateInterface; use Lunar\Shipping\Interfaces\ShippingRateInterface;
use Lunar\Shipping\Models\ShippingRate; use Lunar\Shipping\Models\ShippingRate;
use Modules\Core\Shipping\Concerns\ExcludesRestrictedProducts;
use Modules\Core\Shipping\Concerns\ResolvesFixedPricing; use Modules\Core\Shipping\Concerns\ResolvesFixedPricing;
use Modules\Core\Shipping\Contracts\DeclaresFulfillmentType; use Modules\Core\Shipping\Contracts\DeclaresFulfillmentType;
@@ -18,6 +19,7 @@ use Modules\Core\Shipping\Contracts\DeclaresFulfillmentType;
class BoxNowRateDriver implements ShippingRateInterface, DeclaresFulfillmentType class BoxNowRateDriver implements ShippingRateInterface, DeclaresFulfillmentType
{ {
use ResolvesFixedPricing; use ResolvesFixedPricing;
use ExcludesRestrictedProducts;
public ShippingRate $shippingRate; public ShippingRate $shippingRate;
@@ -38,6 +40,10 @@ class BoxNowRateDriver implements ShippingRateInterface, DeclaresFulfillmentType
public function resolve(ShippingOptionRequest $shippingOptionRequest): ?ShippingOption public function resolve(ShippingOptionRequest $shippingOptionRequest): ?ShippingOption
{ {
if ($this->cartHasExcludedProducts($shippingOptionRequest->shippingRate, $shippingOptionRequest->cart)) {
return null;
}
return $this->resolveFixedPrice( return $this->resolveFixedPrice(
$shippingOptionRequest->shippingRate, $shippingOptionRequest->shippingRate,
$shippingOptionRequest->shippingRate->shippingMethod, $shippingOptionRequest->shippingRate->shippingMethod,
@@ -0,0 +1,75 @@
<?php
namespace Modules\Core\Shipping\Carriers\StorePickup;
use Lunar\DataTypes\ShippingOption;
use Lunar\Shipping\DataTransferObjects\ShippingOptionRequest;
use Lunar\Shipping\Interfaces\ShippingRateInterface;
use Lunar\Shipping\Models\ShippingRate;
use Modules\Core\Shipping\Concerns\ExcludesRestrictedProducts;
use Modules\Core\Shipping\Concerns\ResolvesFixedPricing;
use Modules\Core\Shipping\Contracts\DeclaresFulfillmentType;
/**
* First-party replacement for lunarphp/table-rate-shipping's own Collection
* driver — same shape as AcsRateDriver/BoxNowRateDriver, unlike the vendor
* generic drivers (flat-rate/ship-by/free-shipping/collection), which this
* module no longer registers (see ShippingServiceProvider). Two reasons
* this exists rather than keeping the vendor driver:
*
* 1. Collection::resolve() reads $shippingMethod->name directly instead of
* through Modules\Core\Shipping\Support\ShippingMethodName::resolve() —
* ShippingMethod has no cast for that locale-keyed JSON column (see
* ShippingMethodName's own docblock), so the storefront showed the raw
* JSON blob as the option's name instead of the translated string.
* 2. Unambiguously store pickup, like ACS/Box Now are unambiguously
* carrier — implementing DeclaresFulfillmentType means a merchant never
* has to separately pick "Collect in store" from the generic
* data['fulfillment_type'] field (see that contract's own docblock).
*
* No live pricing — there's no API for in-person pickup, just the method's
* own charge_by + price-break configuration (usually free), the same
* mechanism Box Now uses.
*/
class StorePickupRateDriver implements ShippingRateInterface, DeclaresFulfillmentType
{
use ResolvesFixedPricing;
use ExcludesRestrictedProducts;
public ShippingRate $shippingRate;
public function name(): string
{
return 'Store Pickup';
}
public function fulfillmentType(): string
{
return 'store_pickup';
}
public function description(): string
{
return 'Collect your order in store.';
}
public function resolve(ShippingOptionRequest $shippingOptionRequest): ?ShippingOption
{
if ($this->cartHasExcludedProducts($shippingOptionRequest->shippingRate, $shippingOptionRequest->cart)) {
return null;
}
return $this->resolveFixedPrice(
$shippingOptionRequest->shippingRate,
$shippingOptionRequest->shippingRate->shippingMethod,
$shippingOptionRequest->cart,
);
}
public function on(ShippingRate $shippingRate): self
{
$this->shippingRate = $shippingRate;
return $this;
}
}
@@ -0,0 +1,35 @@
<?php
namespace Modules\Core\Shipping\Concerns;
use Lunar\Models\Cart;
use Lunar\Models\Product;
use Lunar\Shipping\Models\ShippingRate;
/**
* A shipping zone can list specific products that can't go through it (see
* the Filament "Shipping Exclusions" relation manager on ShippingZoneResource)
* — a merchant's way of saying "this item physically can't be handled by
* this carrier/method," independent of price. lunarphp/table-rate-shipping's
* own generic drivers (FlatRate, ShipBy, FreeShipping, Collection) each
* check this before returning an option; every first-party driver in this
* module (ACS, Box Now, store pickup) shares it from here instead, so a
* merchant-configured exclusion is honored no matter which driver a
* shipping method uses — not just the vendor's generic ones. Call this
* FIRST in resolve(), before any pricing lookup (live or fixed): an
* excluded product should make the option disappear entirely, the same
* "return null" a driver already uses for "no rate matched."
*/
trait ExcludesRestrictedProducts
{
private function cartHasExcludedProducts(ShippingRate $shippingRate, Cart $cart): bool
{
$productIds = $cart->lines->load('purchasable')->pluck('purchasable.product_id');
return $shippingRate->shippingZone->shippingExclusions()
->whereHas('exclusions', function ($query) use ($productIds) {
$query->wherePurchasableType(Product::morphName())
->whereIn('purchasable_id', $productIds);
})->exists();
}
}
@@ -0,0 +1,25 @@
<?php
namespace Modules\Core\Shipping\Contracts;
/**
* A shipping rate driver implements this to say a person is physically
* present at handoff to collect cash — true for a courier like
* Modules\Core\Shipping\Carriers\Acs\AcsRateDriver, false for an
* unattended parcel locker network like Modules\Core\Shipping\Carriers\
* BoxNow\BoxNowRateDriver, where no one is there to take payment. Checked
* by Modules\Core\Checkout\Services\CheckoutService::getPaymentMethods()
* for cash-on-delivery specifically (Modules\Core\Payment\Drivers\
* CashOnDeliveryPaymentDriver) — separate from Modules\Core\Shipping\
* Contracts\DeclaresFulfillmentType, which only distinguishes carrier vs.
* store_pickup and can't tell ACS and Box Now apart (both 'carrier').
*
* Not implemented at all means "no" — a driver with no opinion here
* (any of table-rate-shipping's own generic drivers, if one were ever
* re-added) is treated as not supporting cash collection, the safer
* default for a driver this module knows nothing about.
*/
interface SupportsCashCollection
{
public function collectsCash(): bool;
}
+35
View File
@@ -0,0 +1,35 @@
<?php
namespace Modules\Core\Shipping\Support;
use Lunar\Shipping\Managers\ShippingManager as VendorShippingManager;
/**
* Drops lunarphp/table-rate-shipping's own generic drivers (free-shipping,
* flat-rate, ship-by, collection) from getSupportedDrivers() — every
* shipping method in this app now uses a first-party driver (ACS, Box
* Now, or Modules\Core\Shipping\Carriers\StorePickup\StorePickupRateDriver,
* registered via Shipping::extend() in ShippingServiceProvider), each of
* which declares its own fulfillment type and correctly decodes
* ShippingMethod's translatable `name` column (see StorePickupRateDriver's
* own docblock for why the vendor `collection` driver specifically had to
* go — it read $shippingMethod->name raw, showing the storefront the raw
* JSON blob instead of a translated string). The generic drivers'
* create{X}Driver() methods still exist on the parent (harmless — nothing
* calls them once their key is gone from getSupportedDrivers()), so this
* only needs to override the one method that lists what's offered.
*
* Bound over the vendor's own ShippingMethodManagerInterface binding in
* ShippingServiceProvider — see that class for why (registration order:
* this module's provider binds after the vendor's own).
*/
class ShippingManager extends VendorShippingManager
{
public function getSupportedDrivers(): \Illuminate\Support\Collection
{
return collect($this->customCreators)
->mapWithKeys(function ($creator, $key) {
return [$key => $this->callCustomCreator($key)];
});
}
}