Feat: Hiding unused shipping types and removing unused fulfillment types
This commit is contained in:
@@ -60,16 +60,6 @@ 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
|
||||||
@@ -94,10 +84,9 @@ class ShippingServiceProvider extends ServiceProvider
|
|||||||
// resolveCarrier() for the same lookup pattern already used to
|
// resolveCarrier() for the same lookup pattern already used to
|
||||||
// resolve a carrier driver from it).
|
// resolve a carrier driver from it).
|
||||||
//
|
//
|
||||||
// Resolves via Modules\Core\Shipping\Support\FulfillmentType (driver-
|
// Resolves via Modules\Core\Shipping\Support\FulfillmentType
|
||||||
// declared for acs/box-now, merchant-configured data['fulfillment_type']
|
// (hardcoded per driver — see that class's own docblock) rather
|
||||||
// fallback for table-rate-shipping's generic drivers) rather than
|
// than through a ShippingMethod::macro('isStorePickup', ...) —
|
||||||
// through a ShippingMethod::macro('isStorePickup', ...) —
|
|
||||||
// Lunar\Base\Traits\HasModelExtending::__callStatic() (used by
|
// Lunar\Base\Traits\HasModelExtending::__callStatic() (used by
|
||||||
// Lunar\Shipping\Models\ShippingMethod via Lunar\Base\BaseModel)
|
// Lunar\Shipping\Models\ShippingMethod via Lunar\Base\BaseModel)
|
||||||
// intercepts EVERY unmatched static call, including macro()
|
// intercepts EVERY unmatched static call, including macro()
|
||||||
@@ -132,8 +121,19 @@ class ShippingServiceProvider extends ServiceProvider
|
|||||||
|
|
||||||
// Deferred: the Shipping facade resolves a binding registered in
|
// Deferred: the Shipping facade resolves a binding registered in
|
||||||
// lunarphp/table-rate-shipping's own ShippingServiceProvider::boot(),
|
// lunarphp/table-rate-shipping's own ShippingServiceProvider::boot(),
|
||||||
// and provider boot order between packages isn't guaranteed.
|
// and provider boot order between packages isn't guaranteed — in
|
||||||
|
// fact composer's package discovery registers boboko/core BEFORE
|
||||||
|
// lunarphp/table-rate-shipping (alphabetical), so a bind() in this
|
||||||
|
// provider's own register()/boot() runs first and gets silently
|
||||||
|
// overwritten by the vendor's own later bind() of the same
|
||||||
|
// abstract. booted() is the first point every provider's
|
||||||
|
// register()/boot() has definitely already run, so this is also
|
||||||
|
// where the ShippingMethodManagerInterface override belongs (must
|
||||||
|
// run before the Shipping::extend() calls below, which resolve —
|
||||||
|
// and the facade then CACHES — whatever's bound at that moment).
|
||||||
$this->app->booted(function () {
|
$this->app->booted(function () {
|
||||||
|
$this->app->bind(ShippingMethodManagerInterface::class, fn ($app) => $app->make(ShippingManager::class));
|
||||||
|
|
||||||
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));
|
Shipping::extend('store-pickup', fn ($app) => $app->make(StorePickupRateDriver::class));
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ use Modules\Core\Shipping\Contracts\DeclaresFulfillmentType;
|
|||||||
* ShippingMethodName's own docblock), so the storefront showed the raw
|
* ShippingMethodName's own docblock), so the storefront showed the raw
|
||||||
* JSON blob as the option's name instead of the translated string.
|
* JSON blob as the option's name instead of the translated string.
|
||||||
* 2. Unambiguously store pickup, like ACS/Box Now are unambiguously
|
* 2. Unambiguously store pickup, like ACS/Box Now are unambiguously
|
||||||
* carrier — implementing DeclaresFulfillmentType means a merchant never
|
* carrier — implementing DeclaresFulfillmentType means this is a
|
||||||
* has to separately pick "Collect in store" from the generic
|
* hardcoded fact about the driver, never a merchant configuration
|
||||||
* data['fulfillment_type'] field (see that contract's own docblock).
|
* choice (see that contract's own docblock).
|
||||||
*
|
*
|
||||||
* No live pricing — there's no API for in-person pickup, just the method's
|
* 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
|
* own charge_by + price-break configuration (usually free), the same
|
||||||
|
|||||||
@@ -3,18 +3,14 @@
|
|||||||
namespace Modules\Core\Shipping\Contracts;
|
namespace Modules\Core\Shipping\Contracts;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optional contract a shipping rate driver implements to declare whether
|
* Every shipping rate driver this module registers implements this to
|
||||||
* it fulfils via carrier delivery or in-store pickup — e.g.
|
* declare whether it fulfils via carrier delivery or in-store pickup —
|
||||||
* Modules\Core\Shipping\Carriers\Acs\AcsRateDriver and BoxNowRateDriver
|
* Modules\Core\Shipping\Carriers\Acs\AcsRateDriver and BoxNowRateDriver
|
||||||
* are unambiguously carrier-only, so this is a hardcoded fact about the
|
* are unambiguously carrier-only, Modules\Core\Shipping\Carriers\
|
||||||
* driver, not something a merchant should have to configure per row.
|
* StorePickup\StorePickupRateDriver unambiguously store-pickup, so this
|
||||||
*
|
* is a hardcoded fact about each driver, never something a merchant
|
||||||
* table-rate-shipping's own generic drivers (flat-rate, ship-by,
|
* configures per row. See Modules\Core\Shipping\Support\FulfillmentType::
|
||||||
* free-shipping) don't implement this — they're genuinely ambiguous (a
|
* resolve(), the single source of truth this feeds.
|
||||||
* merchant could configure one for either carrier delivery or store
|
|
||||||
* pickup), so Modules\Core\Shipping\Support\FulfillmentType::resolve()
|
|
||||||
* falls back to ShippingMethod.data['fulfillment_type'] (still merchant-
|
|
||||||
* overridable) only for drivers that don't implement this contract.
|
|
||||||
*/
|
*/
|
||||||
interface DeclaresFulfillmentType
|
interface DeclaresFulfillmentType
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -48,6 +48,6 @@ class ShippingMethodListExtension extends BaseExtension
|
|||||||
->label('Type')
|
->label('Type')
|
||||||
->options(fn () => collect(Shipping::getSupportedDrivers())
|
->options(fn () => collect(Shipping::getSupportedDrivers())
|
||||||
->mapWithKeys(fn ($driver, $key) => [$key => $driver->name()]))
|
->mapWithKeys(fn ($driver, $key) => [$key => $driver->name()]))
|
||||||
->default('flat-rate');
|
->default('acs');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ use Filament\Tables\Table;
|
|||||||
use Lunar\Admin\Support\Extending\ResourceExtension;
|
use Lunar\Admin\Support\Extending\ResourceExtension;
|
||||||
use Lunar\Admin\Support\Forms\Components\TranslatedText;
|
use Lunar\Admin\Support\Forms\Components\TranslatedText;
|
||||||
use Lunar\Shipping\Facades\Shipping;
|
use Lunar\Shipping\Facades\Shipping;
|
||||||
use Modules\Core\Shipping\Contracts\DeclaresFulfillmentType;
|
|
||||||
use Modules\Core\Shipping\Contracts\SupportsLivePricing;
|
use Modules\Core\Shipping\Contracts\SupportsLivePricing;
|
||||||
use Modules\Core\Shipping\Support\ShippingMethodName;
|
use Modules\Core\Shipping\Support\ShippingMethodName;
|
||||||
|
|
||||||
@@ -22,11 +21,9 @@ class ShippingMethodResourceExtension extends ResourceExtension
|
|||||||
public function extendForm(Schema $schema): Schema
|
public function extendForm(Schema $schema): Schema
|
||||||
{
|
{
|
||||||
return $schema->components(
|
return $schema->components(
|
||||||
$this->replaceFulfillmentTypeField(
|
$this->replaceChargeByField(
|
||||||
$this->replaceChargeByField(
|
$this->replaceNameField(
|
||||||
$this->replaceNameField(
|
$this->replaceDriverField($schema->getComponents())
|
||||||
$this->replaceDriverField($schema->getComponents())
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -98,64 +95,6 @@ class ShippingMethodResourceExtension extends ResourceExtension
|
|||||||
return $field;
|
return $field;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Inserts the `fulfillment_type` Select right after `charge_by`, in
|
|
||||||
* the SAME Group (vendor's own `Group::make([getChargeByFormComponent()])
|
|
||||||
* ->columns(2)`) — formerly appended at the very end of the whole
|
|
||||||
* form, disconnected from `driver`/`charge_by`, the decisions it
|
|
||||||
* actually relates to. Only rendered at all for a driver that DOESN'T
|
|
||||||
* already declare its own fulfillment type (see Modules\Core\Shipping\
|
|
||||||
* Contracts\DeclaresFulfillmentType, Modules\Core\Shipping\Support\
|
|
||||||
* FulfillmentType) — acs/box-now are unambiguously carrier-only, so
|
|
||||||
* asking a merchant to also pick "Carrier delivery" for every ACS/Box
|
|
||||||
* Now method was redundant, error-prone config with no real decision
|
|
||||||
* behind it. Still offered for table-rate-shipping's generic drivers
|
|
||||||
* (flat-rate, ship-by, free-shipping), which are genuinely ambiguous.
|
|
||||||
*/
|
|
||||||
private function replaceFulfillmentTypeField(array $components): array
|
|
||||||
{
|
|
||||||
$result = [];
|
|
||||||
|
|
||||||
foreach ($components as $component) {
|
|
||||||
$result[] = $component;
|
|
||||||
|
|
||||||
if (method_exists($component, 'getName') && $component->getName() === 'charge_by') {
|
|
||||||
$result[] = $this->fulfillmentTypeSelect();
|
|
||||||
} elseif (in_array(HasChildComponents::class, class_uses_recursive($component), true)) {
|
|
||||||
$component->schema($this->replaceFulfillmentTypeField($component->getChildComponents()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function fulfillmentTypeSelect(): Select
|
|
||||||
{
|
|
||||||
return Select::make('data.fulfillment_type')
|
|
||||||
->label('Fulfillment type')
|
|
||||||
->options([
|
|
||||||
'carrier' => 'Carrier delivery',
|
|
||||||
'store_pickup' => 'Collect in store',
|
|
||||||
])
|
|
||||||
->default('carrier')
|
|
||||||
->required()
|
|
||||||
->visible(fn (Get $get) => $this->driverIsFulfillmentAmbiguous($get('../driver')))
|
|
||||||
->helperText('Whether an order using this method is handed to a carrier, or collected by the customer in person.');
|
|
||||||
}
|
|
||||||
|
|
||||||
private function driverIsFulfillmentAmbiguous(?string $driver): bool
|
|
||||||
{
|
|
||||||
if (! $driver) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return ! Shipping::driver($driver) instanceof DeclaresFulfillmentType;
|
|
||||||
} catch (InvalidArgumentException) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extend the vendor's cart_total/weight charge_by Select with a third
|
* Extend the vendor's cart_total/weight charge_by Select with a third
|
||||||
* "live" option — only offered when the currently selected driver
|
* "live" option — only offered when the currently selected driver
|
||||||
@@ -297,7 +236,7 @@ class ShippingMethodResourceExtension extends ResourceExtension
|
|||||||
->label('Type')
|
->label('Type')
|
||||||
->options(fn () => collect(Shipping::getSupportedDrivers())
|
->options(fn () => collect(Shipping::getSupportedDrivers())
|
||||||
->mapWithKeys(fn ($driver, $key) => [$key => $driver->name()]))
|
->mapWithKeys(fn ($driver, $key) => [$key => $driver->name()]))
|
||||||
->default('flat-rate')
|
->default('acs')
|
||||||
->live();
|
->live();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,20 +8,16 @@ use Modules\Core\Shipping\Contracts\DeclaresFulfillmentType;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The single source of truth for "is this ShippingMethod a carrier
|
* The single source of truth for "is this ShippingMethod a carrier
|
||||||
* delivery or an in-store pickup" — replaces a merchant-facing
|
* delivery or an in-store pickup" — every driver this module registers
|
||||||
* data['fulfillment_type'] Select that used to exist for every method
|
* (ACS, Box Now, Modules\Core\Shipping\Carriers\StorePickup\
|
||||||
* regardless of driver. Modules\Core\Shipping\Carriers\Acs\AcsRateDriver
|
* StorePickupRateDriver) implements DeclaresFulfillmentType, so this is
|
||||||
* and BoxNowRateDriver are unambiguously carrier-only (see
|
* now a hardcoded fact about the driver, never a merchant choice. There
|
||||||
* Modules\Core\Shipping\Contracts\DeclaresFulfillmentType's own
|
* used to be a merchant-facing data['fulfillment_type'] Select as a
|
||||||
* docblock), so asking a merchant to also pick "Carrier delivery" for
|
* fallback for table-rate-shipping's own generic drivers (flat-rate,
|
||||||
* every ACS/Box Now method was redundant, error-prone config with no
|
* ship-by, free-shipping, collection), which were genuinely ambiguous
|
||||||
* real decision behind it.
|
* (a merchant could configure one for either purpose) — those drivers
|
||||||
*
|
* are no longer offered at all (see Modules\Core\Shipping\Support\
|
||||||
* table-rate-shipping's own generic drivers (flat-rate, ship-by,
|
* ShippingManager), so the fallback and the field it read from are gone.
|
||||||
* free-shipping) don't implement DeclaresFulfillmentType — a merchant
|
|
||||||
* could genuinely configure one for either purpose (e.g. "Flat Rate —
|
|
||||||
* Athens Store Pickup") — so those still fall back to the merchant-set
|
|
||||||
* data['fulfillment_type'], defaulting to 'carrier' when unset.
|
|
||||||
*/
|
*/
|
||||||
class FulfillmentType
|
class FulfillmentType
|
||||||
{
|
{
|
||||||
@@ -29,32 +25,11 @@ class FulfillmentType
|
|||||||
{
|
{
|
||||||
$driver = collect(Shipping::getSupportedDrivers())->get($method->driver);
|
$driver = collect(Shipping::getSupportedDrivers())->get($method->driver);
|
||||||
|
|
||||||
if ($driver instanceof DeclaresFulfillmentType) {
|
return $driver instanceof DeclaresFulfillmentType ? $driver->fulfillmentType() : 'carrier';
|
||||||
return $driver->fulfillmentType();
|
|
||||||
}
|
|
||||||
|
|
||||||
return $method->data['fulfillment_type'] ?? 'carrier';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function isStorePickup(ShippingMethod $method): bool
|
public static function isStorePickup(ShippingMethod $method): bool
|
||||||
{
|
{
|
||||||
return static::resolve($method) === 'store_pickup';
|
return static::resolve($method) === 'store_pickup';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the merchant-facing "Fulfillment type" Select should be
|
|
||||||
* shown at all for a given driver — hidden entirely for a driver that
|
|
||||||
* already declares its own fulfillment type, since there is no real
|
|
||||||
* decision left for the merchant to make.
|
|
||||||
*/
|
|
||||||
public static function isConfigurableFor(?string $driverKey): bool
|
|
||||||
{
|
|
||||||
if ($driverKey === null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
$driver = collect(Shipping::getSupportedDrivers())->get($driverKey);
|
|
||||||
|
|
||||||
return ! $driver instanceof DeclaresFulfillmentType;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,8 +20,11 @@ use Lunar\Shipping\Managers\ShippingManager as VendorShippingManager;
|
|||||||
* only needs to override the one method that lists what's offered.
|
* only needs to override the one method that lists what's offered.
|
||||||
*
|
*
|
||||||
* Bound over the vendor's own ShippingMethodManagerInterface binding in
|
* Bound over the vendor's own ShippingMethodManagerInterface binding in
|
||||||
* ShippingServiceProvider — see that class for why (registration order:
|
* ShippingServiceProvider, from inside its own $this->app->booted(...)
|
||||||
* this module's provider binds after the vendor's own).
|
* callback — a plain register()-time bind() here runs BEFORE the
|
||||||
|
* vendor's own (composer discovers boboko/core before lunarphp/
|
||||||
|
* table-rate-shipping alphabetically), so the vendor's later bind()
|
||||||
|
* would silently win instead. See that provider's own comment.
|
||||||
*/
|
*/
|
||||||
class ShippingManager extends VendorShippingManager
|
class ShippingManager extends VendorShippingManager
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user