components([ ...$this->replaceChargeByField( $this->replaceDriverField($schema->getComponents()) ), $this->fulfillmentTypeSelect(), ]); } /** * ShippingMethod.data['fulfillment_type'] — 'carrier' (default) or * 'store_pickup'. Same free-form-`data`-column pattern as charge_by * above, not a migrated column: ShippingMethod is a vendor * (lunarphp/table-rate-shipping) table, and this codebase avoids * forking vendor migrations for a merchant-configurable extra (see * PaymentMethod.data.fee for the same convention on a different * vendor-adjacent model). * * What this actually gates: Modules\Core\Shipping\Extensions\ * OrderViewExtension's "Create Shipment" action only makes sense for * a 'carrier' method (it books a real carrier voucher) — a * 'store_pickup' order instead moves through Order.status * 'ready-for-pickup' -> a staff "Mark Picked Up" action, no shipment * ever created. See docs/checkout.md for the full status-flow design. */ 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() ->helperText('Whether an order using this method is handed to a carrier, or collected by the customer in person.'); } /** * Extend the vendor's cart_total/weight charge_by Select with a third * "live" option — only offered when the currently selected driver * supports live pricing (see SupportsLivePricing). Picking it is what * tells the driver to call its carrier API instead of resolving a * price break. */ private function replaceChargeByField(array $components): array { return array_map(function (Component $component) { if (method_exists($component, 'getName') && $component->getName() === 'charge_by') { return $this->chargeBySelect(); } if (in_array(HasChildComponents::class, class_uses_recursive($component), true)) { $component->schema( $this->replaceChargeByField($component->getChildComponents()) ); } return $component; }, $components); } private function chargeBySelect(): Select { return Select::make('charge_by') ->label('Charge by') ->options(function (Get $get) { $options = [ 'cart_total' => 'Cart Total', 'weight' => 'Weight', ]; // "charge_by" is nested inside a Group with // ->statePath('data'), while "driver" sits one level up, at // the form root. Note: an *absolute* path here would need to // additionally account for the page's own form wrapper // (EditRecord::getFormStatePath() === 'data'), which relative // paths never cross — so "../driver" (relative) is the // correct, page-independent way to reach it, not an // absolute 'driver' string. if ($this->driverSupportsLivePricing($get('../driver'))) { $options['live'] = 'Live API pricing'; } return $options; }) ->live(); } private function driverSupportsLivePricing(?string $driver): bool { if (! $driver) { return false; } try { return Shipping::driver($driver) instanceof SupportsLivePricing; } catch (InvalidArgumentException) { return false; } } public function extendTable(Table $table): Table { return $table->columns( array_map(function ($column) { if (method_exists($column, 'getName') && $column->getName() === 'driver') { return $this->driverColumn(); } return $column; }, $table->getColumns()) ); } private function driverColumn(): TextColumn { return TextColumn::make('driver') ->label('Type') ->formatStateUsing(fn ($state) => $this->driverLabel($state)); } private function driverLabel(string $key): string { $driver = collect(Shipping::getSupportedDrivers())->get($key); return $driver?->name() ?? $key; } /** * Recursively walk the form tree and replace the hardcoded driver * Select (nested inside Section > Group) with one listing every * registered driver, built-in or custom. * * @param array $components * @return array */ private function replaceDriverField(array $components): array { return array_map(function (Component $component) { if (method_exists($component, 'getName') && $component->getName() === 'driver') { return $this->driverSelect(); } if (in_array(HasChildComponents::class, class_uses_recursive($component), true)) { $component->schema( $this->replaceDriverField($component->getChildComponents()) ); } return $component; }, $components); } private function driverSelect(): Select { return Select::make('driver') ->label('Type') ->options(fn () => collect(Shipping::getSupportedDrivers()) ->mapWithKeys(fn ($driver, $key) => [$key => $driver->name()])) ->default('flat-rate') ->live(); } }