91 lines
3.2 KiB
PHP
91 lines
3.2 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));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 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);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|