109 lines
4.1 KiB
PHP
109 lines
4.1 KiB
PHP
<?php
|
|
|
|
namespace Modules\Core\Order\Services;
|
|
|
|
use Illuminate\Support\Facades\Event;
|
|
use Lunar\Models\Order;
|
|
use Modules\Core\Checkout\Events\OrderPlaced;
|
|
use Modules\Core\Order\Enums\PaymentStatus;
|
|
use Modules\Core\Order\Support\OrderStatus;
|
|
|
|
/**
|
|
* The actual business decisions behind reacting to a payment outcome —
|
|
* previously these lived entirely inside Modules\Core\Order\Listeners\
|
|
* ApplyResolvedPaymentStatus, a listener with no Service behind it, even
|
|
* though "should this order be marked paid," "should its status advance,
|
|
* and to what," and "what does a refund do to status" are all genuine
|
|
* decisions about Order state, not side effects of Payment's own events.
|
|
* That listener is now a thin reactor: extract the order id from
|
|
* $event->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);
|
|
}
|
|
}
|
|
}
|