status !== 'processing') { return OrderFulfillmentResult::failure('This order must be in Processing before it can be marked ready.'); } $target = $order->isStorePickupOrder() ? 'ready_for_pickup' : 'ready_for_dispatch'; $this->writer->write($order, $target, self::class.'::markReady'); if ($order->isStorePickupOrder()) { OrderReadyForPickup::dispatch($order); } else { OrderReadyForDispatch::dispatch($order); } return OrderFulfillmentResult::success('Order marked ready.'); } public function createShipmentAndDispatch(Order $order, ShipmentRequest $request): OrderFulfillmentResult { if ($order->status !== 'ready_for_dispatch') { return OrderFulfillmentResult::failure('This order is not ready to be dispatched.'); } $service = $this->resolveFulfillmentService($order); if (! $service) { return OrderFulfillmentResult::failure('No carrier fulfillment integration is configured for this order.'); } try { $service->createShipment($order, $request); } catch (Throwable $e) { report($e); return OrderFulfillmentResult::failure('Failed to create shipment: '.$e->getMessage()); } $this->writer->write($order, 'dispatched', self::class.'::createShipmentAndDispatch'); return OrderFulfillmentResult::success('Shipment created and order dispatched.'); } public function markPickedUp(Order $order): OrderFulfillmentResult { if ($order->status !== 'ready_for_pickup') { return OrderFulfillmentResult::failure('This order is not ready for pickup.'); } $this->writer->write($order, 'picked_up', self::class.'::markPickedUp'); OrderPickedUp::dispatch($order); return OrderFulfillmentResult::success('Order marked as picked up.'); } /** * The general-purpose entry point for any transition with no special * side effect — a manual override, not restricted to the guided next * step(s), so staff can revert to an earlier status in the order's * own branch. Validates $to is actually a member of * OrderStatusFlow::allOptions() before writing (server-side * re-validation of whatever the Select offered) — still refuses a * status from the WRONG branch or an unknown value. */ public function transitionTo(Order $order, string $to): OrderFulfillmentResult { if (! array_key_exists($to, $this->flow->allOptions($order))) { return OrderFulfillmentResult::failure('That status is not valid for this order.'); } $this->writer->write($order, $to, self::class.'::transitionTo'); return OrderFulfillmentResult::success('Order status updated.'); } /** * Independent of `status` entirely — offered by the single "Update * Status" action regardless of current status (see * OrderStatusFlow::canMarkPaid()). */ public function markPaid(Order $order): OrderFulfillmentResult { if (! $this->flow->canMarkPaid($order)) { return OrderFulfillmentResult::failure('This order cannot be marked paid right now.'); } $this->writer->markPaid($order, self::class.'::markPaid'); return OrderFulfillmentResult::success('Order marked as paid.'); } public function canCreateShipment(Order $order): bool { return $order->status === 'ready_for_dispatch' && ! $order->isStorePickupOrder() && $order->shipments()->exists() === false && $this->resolveFulfillmentService($order) !== null; } /** * Public wrapper around resolveCarrier() — Modules\Core\Shipping\ * Extensions\OrderViewExtension needs to know which carrier an order * uses to branch the "Create Shipment" form (Box Now's box-size * repeater vs. every other carrier's plain weight field). */ public function carrierFor(Order $order): ?string { return $this->resolveCarrier($order); } private function resolveCarrier(Order $order): ?string { $code = $order->shippingAddress?->shipping_option; if (! $code) { return null; } return ShippingMethod::where('code', $code)->value('driver'); } private function resolveFulfillmentService(Order $order): ?CarrierFulfillmentInterface { $carrier = $this->resolveCarrier($order); if (! $carrier) { return null; } return app(CarrierFulfillmentInterface::class, ['carrier' => $carrier]); } }