Feat: Recording Payment Transactions
This commit is contained in:
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Core\Order\Listeners;
|
||||||
|
|
||||||
|
use Lunar\Models\Order;
|
||||||
|
use Modules\Core\Order\Services\TransactionRecorder;
|
||||||
|
use Modules\Core\Payment\Events\PaymentAuthorized;
|
||||||
|
use Modules\Core\Payment\Events\PaymentCaptured;
|
||||||
|
use Modules\Core\Payment\Events\PaymentRefunded;
|
||||||
|
use Modules\Core\Payment\Events\PaymentVoided;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes the Transaction row for a successful payment outcome — the
|
||||||
|
* "record what happened" half of reacting to Payment's events, separate
|
||||||
|
* from Modules\Core\Order\Listeners\ApplyResolvedPaymentStatus's "update
|
||||||
|
* the order's status" half. Both listen to the same events for the same
|
||||||
|
* reason: two independent reactions to one payment outcome, neither
|
||||||
|
* calling the other (see docs/payments.md).
|
||||||
|
*
|
||||||
|
* Only registered against the SUCCESS events (PaymentCaptured,
|
||||||
|
* PaymentAuthorized, PaymentVoided, PaymentRefunded) — a Failed event
|
||||||
|
* never reaches here, since a failed attempt moved no money and settled
|
||||||
|
* nothing worth auditing as a Transaction row (see OrderServiceProvider's
|
||||||
|
* registration and docs/payments.md's "Explicitly out of scope" section
|
||||||
|
* on why no Failed-side Order reaction exists at all).
|
||||||
|
*
|
||||||
|
* Same defensive $context['order_id'] ?? null early-return as
|
||||||
|
* ApplyResolvedPaymentStatus — $context is caller-supplied and optional,
|
||||||
|
* and this listener must not crash for a future non-Checkout caller of
|
||||||
|
* pay()/authorize() with no order_id in its context.
|
||||||
|
*/
|
||||||
|
class RecordPaymentTransaction
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly TransactionRecorder $transactions,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function handle(PaymentCaptured|PaymentAuthorized|PaymentVoided|PaymentRefunded $event): void
|
||||||
|
{
|
||||||
|
$orderId = $event->context['order_id'] ?? null;
|
||||||
|
|
||||||
|
if ($orderId === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$order = Order::findOrFail($orderId);
|
||||||
|
|
||||||
|
$type = match ($event::class) {
|
||||||
|
PaymentAuthorized::class => 'intent',
|
||||||
|
PaymentCaptured::class => 'capture',
|
||||||
|
PaymentRefunded::class => 'refund',
|
||||||
|
PaymentVoided::class => 'void',
|
||||||
|
};
|
||||||
|
|
||||||
|
$this->transactions->record($order, $type, $event->type, $event->result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Core\Order\Services;
|
||||||
|
|
||||||
|
use Lunar\Models\Order;
|
||||||
|
use Lunar\Models\Transaction;
|
||||||
|
use Modules\Core\Payment\DTOs\PaymentResult;
|
||||||
|
use Modules\Core\Payment\Enums\PaymentResultStatus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes the Transaction row a Payment operation's PaymentResult becomes —
|
||||||
|
* the one place that translates Payment's gateway-agnostic result into
|
||||||
|
* Lunar's own transactions table, in the same shape lunarphp/stripe's own
|
||||||
|
* StoreCharges already writes (type, success, amount, reference, driver).
|
||||||
|
* Lives in Order, not Payment — Transaction.order_id is required, and
|
||||||
|
* Payment never writes to another module's models (see docs/payments.md);
|
||||||
|
* this is the "read the event, do the write" half of that boundary, same
|
||||||
|
* shape as Modules\Core\Order\Listeners\ApplyResolvedPaymentStatus.
|
||||||
|
*
|
||||||
|
* Kept as its own class (not inlined into the listener that calls it) so a
|
||||||
|
* future admin action (a manually-triggered capture/refund from Filament)
|
||||||
|
* can write a row the same way, without going through an event at all.
|
||||||
|
*/
|
||||||
|
class TransactionRecorder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* $type is Lunar's own transaction type string — 'intent' (an
|
||||||
|
* authorize()-produced hold), 'capture' (settled funds, whether via
|
||||||
|
* pay() directly or capture() settling a prior intent), 'refund',
|
||||||
|
* 'void' is NOT one of Lunar's three built-in types (Order::
|
||||||
|
* paymentStatus() only ever reads 'intent'/'capture'/'refund' — see
|
||||||
|
* Modules\Core\Order\Support\OrderStatus::payment()) — a void never
|
||||||
|
* moved money, so it's still recorded for audit but $success reflects
|
||||||
|
* whether the RELEASE succeeded, not a captured amount.
|
||||||
|
*
|
||||||
|
* $driver is the payment type key (e.g. 'stripe', 'cash-on-delivery'),
|
||||||
|
* not a class name — matches the $type PaymentCaptured/etc. events
|
||||||
|
* themselves carry, and what Transaction.driver already means
|
||||||
|
* elsewhere in this codebase (see the old, now-removed
|
||||||
|
* TransactionRecorder this replaces).
|
||||||
|
*/
|
||||||
|
public function record(Order $order, string $type, string $driver, PaymentResult $result): Transaction
|
||||||
|
{
|
||||||
|
return $order->transactions()->create([
|
||||||
|
'success' => $result->status === PaymentResultStatus::Succeeded,
|
||||||
|
'type' => $type,
|
||||||
|
'driver' => $driver,
|
||||||
|
'amount' => $result->amount->value,
|
||||||
|
'reference' => $result->reference,
|
||||||
|
'status' => $result->status->name,
|
||||||
|
'notes' => $result->failureReason,
|
||||||
|
'meta' => $result->meta,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Modules\Core\Payment\Services;
|
|
||||||
|
|
||||||
use Lunar\Models\Order;
|
|
||||||
use Lunar\Models\Transaction;
|
|
||||||
use Modules\Core\Payment\DTOs\CaptureResult;
|
|
||||||
use Modules\Core\Payment\DTOs\RefundResult;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Writes the Transaction row a SupportsRefunds/SupportsCaptures driver's
|
|
||||||
* result becomes — the one place that translates a gateway-agnostic
|
|
||||||
* RefundResult/CaptureResult into Lunar's own transactions table, in the
|
|
||||||
* same shape lunarphp/stripe's StoreCharges already writes (type, success,
|
|
||||||
* amount, reference, driver, notes). Kept here rather than inside each
|
|
||||||
* driver so every driver's rows land in a consistent shape that
|
|
||||||
* Order::paymentStatus() and TransactionObserver both already understand,
|
|
||||||
* without any driver needing to know about either.
|
|
||||||
*/
|
|
||||||
class TransactionRecorder
|
|
||||||
{
|
|
||||||
public function recordRefund(Order $order, string $driver, RefundResult $result, ?string $notes = null): Transaction
|
|
||||||
{
|
|
||||||
return $order->transactions()->create([
|
|
||||||
'success' => $result->success,
|
|
||||||
'type' => 'refund',
|
|
||||||
'driver' => $driver,
|
|
||||||
'amount' => $result->amount,
|
|
||||||
'reference' => $result->reference,
|
|
||||||
'status' => $result->success ? 'succeeded' : 'failed',
|
|
||||||
'notes' => $notes ?? $result->message,
|
|
||||||
'meta' => $result->meta,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function recordCapture(Order $order, string $driver, CaptureResult $result, ?string $notes = null): Transaction
|
|
||||||
{
|
|
||||||
return $order->transactions()->create([
|
|
||||||
'success' => $result->success,
|
|
||||||
'type' => 'capture',
|
|
||||||
'driver' => $driver,
|
|
||||||
'amount' => $result->amount,
|
|
||||||
'reference' => $result->reference,
|
|
||||||
'status' => $result->success ? 'succeeded' : 'failed',
|
|
||||||
'notes' => $notes ?? $result->message,
|
|
||||||
'meta' => $result->meta,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,6 +9,7 @@ use Lunar\Models\Transaction;
|
|||||||
use Modules\Core\Notification\NotificationRegistry;
|
use Modules\Core\Notification\NotificationRegistry;
|
||||||
use Modules\Core\Order\Listeners\ApplyResolvedPaymentStatus;
|
use Modules\Core\Order\Listeners\ApplyResolvedPaymentStatus;
|
||||||
use Modules\Core\Order\Listeners\DeriveOrderDeliveredFromShipment;
|
use Modules\Core\Order\Listeners\DeriveOrderDeliveredFromShipment;
|
||||||
|
use Modules\Core\Order\Listeners\RecordPaymentTransaction;
|
||||||
use Modules\Core\Order\Notifications\OrderCapturedNotification;
|
use Modules\Core\Order\Notifications\OrderCapturedNotification;
|
||||||
use Modules\Core\Order\Notifications\OrderDeliveredNotification;
|
use Modules\Core\Order\Notifications\OrderDeliveredNotification;
|
||||||
use Modules\Core\Order\Notifications\OrderRefundedNotification;
|
use Modules\Core\Order\Notifications\OrderRefundedNotification;
|
||||||
@@ -18,6 +19,8 @@ use Modules\Core\Order\Observers\TransactionObserver;
|
|||||||
use Modules\Core\Order\Support\OrderStatus;
|
use Modules\Core\Order\Support\OrderStatus;
|
||||||
use Modules\Core\Payment\Events\PaymentAuthorized;
|
use Modules\Core\Payment\Events\PaymentAuthorized;
|
||||||
use Modules\Core\Payment\Events\PaymentCaptured;
|
use Modules\Core\Payment\Events\PaymentCaptured;
|
||||||
|
use Modules\Core\Payment\Events\PaymentRefunded;
|
||||||
|
use Modules\Core\Payment\Events\PaymentVoided;
|
||||||
use Modules\Core\Shipping\Events\ShipmentStatusUpdatedByCarrier;
|
use Modules\Core\Shipping\Events\ShipmentStatusUpdatedByCarrier;
|
||||||
|
|
||||||
class OrderServiceProvider extends ServiceProvider
|
class OrderServiceProvider extends ServiceProvider
|
||||||
@@ -33,6 +36,10 @@ class OrderServiceProvider extends ServiceProvider
|
|||||||
Event::listen(ShipmentStatusUpdatedByCarrier::class, DeriveOrderDeliveredFromShipment::class);
|
Event::listen(ShipmentStatusUpdatedByCarrier::class, DeriveOrderDeliveredFromShipment::class);
|
||||||
Event::listen(PaymentCaptured::class, ApplyResolvedPaymentStatus::class);
|
Event::listen(PaymentCaptured::class, ApplyResolvedPaymentStatus::class);
|
||||||
Event::listen(PaymentAuthorized::class, ApplyResolvedPaymentStatus::class);
|
Event::listen(PaymentAuthorized::class, ApplyResolvedPaymentStatus::class);
|
||||||
|
Event::listen(PaymentCaptured::class, RecordPaymentTransaction::class);
|
||||||
|
Event::listen(PaymentAuthorized::class, RecordPaymentTransaction::class);
|
||||||
|
Event::listen(PaymentVoided::class, RecordPaymentTransaction::class);
|
||||||
|
Event::listen(PaymentRefunded::class, RecordPaymentTransaction::class);
|
||||||
|
|
||||||
NotificationRegistry::get()->register([
|
NotificationRegistry::get()->register([
|
||||||
OrderDeliveredNotification::class,
|
OrderDeliveredNotification::class,
|
||||||
|
|||||||
Reference in New Issue
Block a user