context, load the Order, call this service, done. * * See ApplyResolvedPaymentStatus's own docblock for the full business * reasoning (re-confirmed with the user) behind each rule enforced here — * this class only re-documents what's specific to the decision logic * itself, not the "why" already recorded there. */ class OrderPaymentResolutionService { public function __construct( private readonly OrderStatusWriter $writer, private readonly OrderStatusFlow $flow, ) {} /** * A captured or authorized payment: marks the order paid (capture * only — an authorization is not yet captured funds), advances status * out of 'awaiting_payment' (capture only), and marks the order * placed if this is the first payment outcome it's seen. */ public function resolveCaptureOrAuthorization(Order $order, string $causeClass, bool $isCapture): void { $wasPlaced = ! blank($order->placed_at); $this->writer->markPaid($order, $causeClass); if ($isCapture) { $this->advancePastAwaitingPayment($order, $causeClass); } if (! $wasPlaced) { $order->update(['placed_at' => $order->placed_at ?? now()]); Event::dispatch(new OrderPlaced($order)); } } /** * A deferred-capture payment (currently only cash-on-delivery — see * Payment\Events\PaymentDeferred's own docblock): no money has moved, * so unlike resolveCaptureOrAuthorization() this never calls * $writer->markPaid() — Order::paid stays false until staff explicitly * mark it received. But per OrderStatusFlow's own docblock, payment * method never affects the status SEQUENCE at all — a COD order has * nothing to "await" at checkout (no payment attempt happens), so * 'awaiting_payment' is simply the wrong first status for it. Reuses * the exact same advancePastAwaitingPayment() a capture uses, since * the status-sequence logic itself doesn't differ by payment method, * only whether `paid` also flips alongside it. */ public function resolveDeferredPayment(Order $order, string $causeClass): void { $this->advancePastAwaitingPayment($order, $causeClass); } /** * Requires the refund Transaction row to already exist (Modules\Core\ * Order\Listeners\RecordPaymentTransaction must run first — see * OrderServiceProvider's listener registration order for * PaymentRefunded), so the relation is refreshed here rather than * trusted from a possibly-stale $order instance. */ public function resolveRefund(Order $order, string $causeClass): void { $order->load('transactions'); $target = match (OrderStatus::payment($order)) { PaymentStatus::Refunded => 'refunded', PaymentStatus::PartialRefund => 'partially_refunded', default => null, }; if ($target !== null && $order->status !== $target) { $this->writer->write($order, $target, $causeClass); } } private function advancePastAwaitingPayment(Order $order, string $causeClass): void { if ($order->status !== 'awaiting_payment') { return; } $next = $this->flow->nextOptions($order); $target = array_key_first($next); if ($target !== null) { $this->writer->write($order, $target, $causeClass); } } }