Files
core/src/Providers/ShippingServiceProvider.php
T

180 lines
9.1 KiB
PHP
Raw Normal View History

<?php
namespace Modules\Core\Providers;
use Illuminate\Console\Scheduling\Schedule as ConsoleSchedule;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\ServiceProvider;
use Livewire\Livewire;
use Livewire\Mechanisms\ComponentRegistry;
use Lunar\Models\Order;
use Lunar\Shipping\Facades\Shipping;
use Lunar\Shipping\Filament\Resources\ShippingZoneResource\Pages\ManageShippingRates as VendorManageShippingRates;
2026-09-29 00:35:29 +03:00
use Lunar\Shipping\Interfaces\ShippingMethodManagerInterface;
use Lunar\Shipping\Models\ShippingMethod;
use Modules\Core\Cart\Events\CartCleared;
use Modules\Core\Cart\Events\CartLineAdded;
use Modules\Core\Cart\Events\CartLineRemoved;
use Modules\Core\Cart\Events\CartLineUpdated;
use Modules\Core\Checkout\Events\ShippingAddressSet;
use Modules\Core\Shipping\Carriers\Acs\AcsClient;
use Modules\Core\Shipping\Carriers\Acs\AcsFulfillmentService;
use Modules\Core\Shipping\Carriers\Acs\AcsRateDriver;
use Modules\Core\Shipping\Carriers\Acs\Jobs\WarmAcsAreaCacheJob;
use Modules\Core\Shipping\Carriers\BoxNow\BoxNowClient;
use Modules\Core\Shipping\Carriers\BoxNow\BoxNowFulfillmentService;
use Modules\Core\Shipping\Carriers\BoxNow\BoxNowRateDriver;
2026-09-29 14:31:30 +03:00
use Modules\Core\Shipping\Carriers\Elta\EltaClient;
use Modules\Core\Shipping\Carriers\Elta\EltaFulfillmentService;
use Modules\Core\Shipping\Carriers\Elta\EltaRateDriver;
use Modules\Core\Shipping\Contracts\CarrierFulfillmentInterface;
use Modules\Core\Shipping\Filament\Pages\ManageShippingRates;
2026-09-29 00:35:29 +03:00
use Modules\Core\Shipping\Carriers\StorePickup\StorePickupRateDriver;
2026-07-19 17:41:09 +03:00
use Modules\Core\Shipping\Jobs\PollShipmentTrackingJob;
use Modules\Core\Shipping\Listeners\InvalidateShippingOptions;
use Modules\Core\Shipping\Support\FulfillmentType;
2026-09-29 00:35:29 +03:00
use Modules\Core\Shipping\Support\ShippingManager;
use Modules\Core\Shipping\Models\Shipment;
class ShippingServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->mergeConfigFrom(__DIR__ . '/../../config/shippingCarriers/acs.php', 'acs');
$this->mergeConfigFrom(__DIR__ . '/../../config/shippingCarriers/boxnow.php', 'boxnow');
2026-09-29 14:31:30 +03:00
$this->mergeConfigFrom(__DIR__ . '/../../config/shippingCarriers/elta.php', 'elta');
$this->app->singleton(AcsClient::class, fn () => new AcsClient(config('acs')));
$this->app->singleton(BoxNowClient::class, fn () => new BoxNowClient(config('boxnow')));
2026-09-29 14:31:30 +03:00
$this->app->singleton(EltaClient::class, fn () => new EltaClient(config('elta')));
$this->app->bind(CarrierFulfillmentInterface::class, function ($app, array $params) {
return match ($params['carrier'] ?? null) {
'acs' => $app->make(AcsFulfillmentService::class),
'box-now' => $app->make(BoxNowFulfillmentService::class),
2026-09-29 14:31:30 +03:00
'elta' => $app->make(EltaFulfillmentService::class),
default => null,
};
});
// The vendor Rates page has no extension hook, so we swap it for
// our subclass everywhere. Route::get($path, VendorClass::class)
// instantiates the vendor class directly via the container for the
// initial full-page load (bypassing Livewire's component registry
// entirely), so this container bind is required in addition to the
// Livewire::component() re-registration below — the bind covers
// first load, the Livewire registration covers every AJAX
// round-trip (form submits, table interactions) afterwards.
$this->app->bind(VendorManageShippingRates::class, ManageShippingRates::class);
}
public function boot(): void
{
$this->publishes([
__DIR__ . '/../../config/shippingCarriers/acs.php' => config_path('shippingCarriers/acs.php'),
__DIR__ . '/../../config/shippingCarriers/boxnow.php' => config_path('shippingCarriers/boxnow.php'),
2026-09-29 14:31:30 +03:00
__DIR__ . '/../../config/shippingCarriers/elta.php' => config_path('shippingCarriers/elta.php'),
], 'core-config');
// Signed-URL auth only, same model as Lunar's own vendor
// lunar.pdf.download route — see Modules\Core\Shipping\Http\
// Controllers\DownloadShipmentLabelController's own docblock.
$this->loadRoutesFrom(__DIR__ . '/../Shipping/routes/web.php');
Order::resolveRelationUsing('shipments', function ($order) {
return $order->hasMany(Shipment::class);
});
// Order has no direct ShippingMethod relation — shippingAddress.
// shipping_option is only ever a code string (see
// Modules\Core\Shipping\Extensions\OrderViewExtension::
// resolveCarrier() for the same lookup pattern already used to
// resolve a carrier driver from it).
//
// Resolves via Modules\Core\Shipping\Support\FulfillmentType
// (hardcoded per driver — see that class's own docblock) rather
// than through a ShippingMethod::macro('isStorePickup', ...) —
// Lunar\Base\Traits\HasModelExtending::__callStatic() (used by
// Lunar\Shipping\Models\ShippingMethod via Lunar\Base\BaseModel)
// intercepts EVERY unmatched static call, including macro()
// itself, and dispatches it as (new static)->macro(...) instead
// of forwarding to Macroable — so a macro registered this way
// silently never gets stored, and hasMacro() always returns
// false. (Lunar\Models\Order is unaffected because it declares
// its own macro() method directly, bypassing __callStatic
// entirely — that's why Order::macro('isStorePickupOrder', ...)
// below still works.)
Order::macro('isStorePickupOrder', function () {
/** @var Order $this */
$code = $this->shippingAddress?->shipping_option;
if (! $code) {
return false;
}
// ->value('data') is deliberately avoided here — on Postgres,
// ->value()/->pluck() on a JSON column silently return the
// wrong result (works fine in ->where(), not in a column
// projection); loading the model and reading its cast
// attribute avoids that entirely.
$method = ShippingMethod::where('code', $code)->first();
return $method && FulfillmentType::isStorePickup($method);
});
foreach ([CartLineAdded::class, CartLineUpdated::class, CartLineRemoved::class, CartCleared::class, ShippingAddressSet::class] as $event) {
Event::listen($event, [InvalidateShippingOptions::class, 'handle']);
}
// Deferred: the Shipping facade resolves a binding registered in
// lunarphp/table-rate-shipping's own ShippingServiceProvider::boot(),
// 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->bind(ShippingMethodManagerInterface::class, fn ($app) => $app->make(ShippingManager::class));
Shipping::extend('acs', fn ($app) => $app->make(AcsRateDriver::class));
Shipping::extend('box-now', fn ($app) => $app->make(BoxNowRateDriver::class));
2026-09-29 00:35:29 +03:00
Shipping::extend('store-pickup', fn ($app) => $app->make(StorePickupRateDriver::class));
2026-09-29 14:31:30 +03:00
Shipping::extend('elta', fn ($app) => $app->make(EltaRateDriver::class));
$this->app->make(ConsoleSchedule::class)
->job(new WarmAcsAreaCacheJob)
->dailyAt('06:00')
->when(fn () => ShippingMethod::where('driver', 'acs')->exists());
2026-07-19 17:41:09 +03:00
$this->app->make(ConsoleSchedule::class)
->job(new PollShipmentTrackingJob)
->everyThirtyMinutes();
$this->overrideRatesPageLivewireComponent();
});
}
/**
* The vendor Rates page has no extension hook, so we swap it for our
* subclass (see Shipping/Filament/Pages/ManageShippingRates). Filament
* already registered the vendor class as a Livewire component under a
* name derived from its class string (see
* Panel::registerLivewireComponents()); Livewire's own registry is a
* simple last-write-wins name => class map, so re-registering the same
* derived name against our subclass here overrides it — keeping the
* route, sub-navigation, and every Livewire round-trip (including form
* submissions) pointed at one consistent component identity.
*/
private function overrideRatesPageLivewireComponent(): void
{
$name = $this->app->make(ComponentRegistry::class)->getName(VendorManageShippingRates::class);
Livewire::component($name, ManageShippingRates::class);
}
}