mergeConfigFrom(__DIR__ . '/../../config/shippingCarriers/acs.php', 'acs'); $this->mergeConfigFrom(__DIR__ . '/../../config/shippingCarriers/boxnow.php', 'boxnow'); $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'))); $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), 'elta' => $app->make(EltaFulfillmentService::class), 'manual' => $app->make(ManualFulfillmentService::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 { // Carrier checkpoints on the order page's Timeline. Event::listen(ShipmentStatusUpdatedByCarrier::class, LogShipmentCheckpointOnOrderTimeline::class); // Added when Lunar's manifest is built, not now: resolving it during // boot builds it before the morph map exists, which keys every // renderer (Lunar's own too) under a name the Timeline never looks up. $this->app->afterResolving('lunar-activity-log', fn (ActivityLogManifest $manifest) => $manifest->addRender(Order::class, ShipmentCheckpointRender::class)); $this->publishes([ __DIR__ . '/../../config/shippingCarriers/acs.php' => config_path('shippingCarriers/acs.php'), __DIR__ . '/../../config/shippingCarriers/boxnow.php' => config_path('shippingCarriers/boxnow.php'), __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 () { // A singleton, not bind(): the queue worker clears every // facade's cached instance before each job // (Facade::clearResolvedInstances()), so with bind() the Shipping // facade then built a fresh, empty manager — without the drivers // extended below — and every queued job (tracking, voucher sync) // saw no carriers at all. $this->app->singleton(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)); Shipping::extend('store-pickup', fn ($app) => $app->make(StorePickupRateDriver::class)); Shipping::extend('elta', fn ($app) => $app->make(EltaRateDriver::class)); Shipping::extend('manual', fn ($app) => $app->make(ManualRateDriver::class)); // $this->app->make(ConsoleSchedule::class) // ->job(new WarmAcsAreaCacheJob) // ->dailyAt('06:00') // ->when(fn () => ShippingMethod::where('driver', 'acs')->exists()); $this->app->make(ConsoleSchedule::class) ->job(new PollShipmentTrackingJob) ->everyFiveMinutes(); $this->app->make(ConsoleSchedule::class) ->job(new SyncCarrierVouchersJob) ->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); } }