Fix: Update Order status to Processing when payment has been recieved
This commit is contained in:
+2
-2
@@ -28,7 +28,7 @@ use Modules\Core\Catalog\Filament\Extensions\ValuesRelationManagerExtension;
|
|||||||
use Modules\Core\Localization\Filament\Resources\LanguageLineResource;
|
use Modules\Core\Localization\Filament\Resources\LanguageLineResource;
|
||||||
use Modules\Core\Order\Filament\Extensions\OrderItemsTableExtension;
|
use Modules\Core\Order\Filament\Extensions\OrderItemsTableExtension;
|
||||||
use Modules\Core\Order\Filament\Extensions\OrderPaymentMethodSummaryExtension;
|
use Modules\Core\Order\Filament\Extensions\OrderPaymentMethodSummaryExtension;
|
||||||
use Modules\Core\Order\Filament\Extensions\OrderRefundActionsExtension;
|
use Modules\Core\Order\Filament\Extensions\OrderActionsExtension;
|
||||||
use Modules\Core\Order\Filament\Extensions\OrderTransactionsExtension;
|
use Modules\Core\Order\Filament\Extensions\OrderTransactionsExtension;
|
||||||
use Modules\Core\Payment\Filament\Resources\PaymentMethodResource;
|
use Modules\Core\Payment\Filament\Resources\PaymentMethodResource;
|
||||||
use Modules\Core\Review\Filament\Extensions\ProductResourceExtension;
|
use Modules\Core\Review\Filament\Extensions\ProductResourceExtension;
|
||||||
@@ -70,7 +70,7 @@ class CorePlugin implements Plugin
|
|||||||
ValuesRelationManager::class => ValuesRelationManagerExtension::class,
|
ValuesRelationManager::class => ValuesRelationManagerExtension::class,
|
||||||
ShippingMethodResource::class => ShippingMethodResourceExtension::class,
|
ShippingMethodResource::class => ShippingMethodResourceExtension::class,
|
||||||
ListShippingMethod::class => ShippingMethodListExtension::class,
|
ListShippingMethod::class => ShippingMethodListExtension::class,
|
||||||
ManageOrder::class => [OrderViewExtension::class, OrderRefundActionsExtension::class, OrderTransactionsExtension::class, OrderPaymentMethodSummaryExtension::class, OrderShipmentsExtension::class],
|
ManageOrder::class => [OrderViewExtension::class, OrderActionsExtension::class, OrderTransactionsExtension::class, OrderPaymentMethodSummaryExtension::class, OrderShipmentsExtension::class],
|
||||||
OrderItemsTable::class => OrderItemsTableExtension::class,
|
OrderItemsTable::class => OrderItemsTableExtension::class,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
+50
-39
@@ -32,10 +32,6 @@ use ReflectionProperty;
|
|||||||
* could return a real, honest failure — see Payment\Support\
|
* could return a real, honest failure — see Payment\Support\
|
||||||
* TransactionDriverAdapter's own docblock for that history.
|
* TransactionDriverAdapter's own docblock for that history.
|
||||||
*
|
*
|
||||||
* Fix, for capture: wrap the action's own action() closure so that, on
|
|
||||||
* Halt, we call $action->sendFailureNotification() ourselves before letting
|
|
||||||
* the Halt continue propagating — everything else is untouched.
|
|
||||||
*
|
|
||||||
* Fix, for refund: same notification fix, but the action() closure is
|
* Fix, for refund: same notification fix, but the action() closure is
|
||||||
* replaced outright (not wrapped) rather than reused, because refund also
|
* replaced outright (not wrapped) rather than reused, because refund also
|
||||||
* needs a "Refund via" driver Select added to the modal (see
|
* needs a "Refund via" driver Select added to the modal (see
|
||||||
@@ -43,15 +39,28 @@ use ReflectionProperty;
|
|||||||
* Payment\Support\TransactionDriverAdapter::refundVia() instead of
|
* Payment\Support\TransactionDriverAdapter::refundVia() instead of
|
||||||
* Lunar\Models\Transaction::refund() — see fixRefundAction()'s own
|
* Lunar\Models\Transaction::refund() — see fixRefundAction()'s own
|
||||||
* docblock.
|
* docblock.
|
||||||
|
*
|
||||||
|
* Fix, for capture: same notification fix, but the action() closure is
|
||||||
|
* also replaced outright — the actual call is routed through
|
||||||
|
* Payment\Support\TransactionDriverAdapter::capture() instead of
|
||||||
|
* Lunar\Models\Transaction::capture() (see fixCaptureAction()), so a
|
||||||
|
* manual backoffice capture goes through the app's own payment driver
|
||||||
|
* registry and dispatches Payment\Events\PaymentCaptured exactly like a
|
||||||
|
* checkout-time capture does — the vendor path resolved
|
||||||
|
* Lunar\Facades\Payments (an entirely separate, unused driver registry)
|
||||||
|
* and never dispatched that event, which is why Order::status used to
|
||||||
|
* stay stuck on 'awaiting_payment' after a manual capture even though
|
||||||
|
* Modules\Core\Order\Listeners\ApplyResolvedPaymentStatus now advances it
|
||||||
|
* on PaymentCaptured.
|
||||||
*/
|
*/
|
||||||
class OrderRefundActionsExtension extends ViewPageExtension
|
class OrderActionsExtension extends ViewPageExtension
|
||||||
{
|
{
|
||||||
public function headerActions(array $actions): array
|
public function headerActions(array $actions): array
|
||||||
{
|
{
|
||||||
return array_map(
|
return array_map(
|
||||||
fn (Action $action) => match ($action->getName()) {
|
fn (Action $action) => match ($action->getName()) {
|
||||||
'refund' => $this->fixRefundAction($action),
|
'refund' => $this->fixRefundAction($action),
|
||||||
'capture' => $this->fixFailureNotification($action),
|
'capture' => $this->fixCaptureAction($action),
|
||||||
default => $action,
|
default => $action,
|
||||||
},
|
},
|
||||||
$actions,
|
$actions,
|
||||||
@@ -123,6 +132,41 @@ class OrderRefundActionsExtension extends ViewPageExtension
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mirrors fixRefundAction()'s notification fix, but for the "amount"
|
||||||
|
* field already on the vendor schema — no extra field needed, since
|
||||||
|
* capture always goes back through the transaction's own original
|
||||||
|
* driver (there's no equivalent to refunding via a different driver).
|
||||||
|
*/
|
||||||
|
private function fixCaptureAction(Action $action): Action
|
||||||
|
{
|
||||||
|
return $action->action(function (array $data, Action $action) {
|
||||||
|
$transaction = Transaction::find($data['transaction']);
|
||||||
|
|
||||||
|
if (! $transaction instanceof CoreTransaction) {
|
||||||
|
$action->failureNotification(fn () => Notification::make('capture_failure')->danger()->title('Transaction not found.'))
|
||||||
|
->sendFailureNotification();
|
||||||
|
|
||||||
|
throw new Halt;
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = app(TransactionDriverAdapter::class)->capture(
|
||||||
|
$transaction,
|
||||||
|
(int) bcmul((string) $data['amount'], (string) $transaction->order->currency->factor),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (! $response->success) {
|
||||||
|
$action->failureNotification(
|
||||||
|
fn () => Notification::make('capture_failure')->color('danger')->title($response->message)
|
||||||
|
)->sendFailureNotification();
|
||||||
|
|
||||||
|
throw new Halt;
|
||||||
|
}
|
||||||
|
|
||||||
|
$action->success();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<string, string>
|
* @return array<string, string>
|
||||||
*/
|
*/
|
||||||
@@ -163,37 +207,4 @@ class OrderRefundActionsExtension extends ViewPageExtension
|
|||||||
|
|
||||||
return $reflected->getValue($object);
|
return $reflected->getValue($object);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Wraps the action's own configured action() closure so that, if it
|
|
||||||
* halts (Lunar's closures throw via $action->halt() to signal failure —
|
|
||||||
* see this class's own docblock for why that alone never sends the
|
|
||||||
* notification queued via failureNotification()), we send that
|
|
||||||
* notification ourselves before letting the Halt continue propagating
|
|
||||||
* (still needed — it's what stops callMountedAction() from treating
|
|
||||||
* this as a success and closing the modal/committing the DB transaction).
|
|
||||||
*
|
|
||||||
* $this->evaluate() (not a plain call) matches exactly how Action::call()
|
|
||||||
* itself invokes the closure — Lunar's closures type-hint $data/$record/
|
|
||||||
* $action and rely on Filament's own container-style parameter
|
|
||||||
* resolution, not positional arguments.
|
|
||||||
*/
|
|
||||||
private function fixFailureNotification(Action $action): Action
|
|
||||||
{
|
|
||||||
$originalAction = $action->getActionFunction();
|
|
||||||
|
|
||||||
if ($originalAction === null) {
|
|
||||||
return $action;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $action->action(function (array $arguments) use ($action, $originalAction) {
|
|
||||||
try {
|
|
||||||
return $action->evaluate($originalAction, $arguments);
|
|
||||||
} catch (Halt $exception) {
|
|
||||||
$action->sendFailureNotification();
|
|
||||||
|
|
||||||
throw $exception;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,7 @@ use Filament\Tables\Table;
|
|||||||
use Lunar\Admin\Support\Extending\BaseExtension;
|
use Lunar\Admin\Support\Extending\BaseExtension;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Same fix as OrderRefundActionsExtension, applied to the order lines
|
* Same fix as OrderActionsExtension, applied to the order lines
|
||||||
* table's "bulk_refund" toolbar action (Lunar\Admin\...\OrderItemsTable::
|
* table's "bulk_refund" toolbar action (Lunar\Admin\...\OrderItemsTable::
|
||||||
* getBulkRefundAction()) — see that class's docblock for the underlying
|
* getBulkRefundAction()) — see that class's docblock for the underlying
|
||||||
* Filament bug (failureNotification()+failure()+halt() never actually
|
* Filament bug (failureNotification()+failure()+halt() never actually
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use Illuminate\Support\Facades\Event;
|
|||||||
use Lunar\Models\Order;
|
use Lunar\Models\Order;
|
||||||
use Modules\Core\Checkout\Events\OrderPlaced;
|
use Modules\Core\Checkout\Events\OrderPlaced;
|
||||||
use Modules\Core\Order\Enums\PaymentStatus;
|
use Modules\Core\Order\Enums\PaymentStatus;
|
||||||
|
use Modules\Core\Order\Services\OrderStatusFlow;
|
||||||
use Modules\Core\Order\Services\OrderStatusWriter;
|
use Modules\Core\Order\Services\OrderStatusWriter;
|
||||||
use Modules\Core\Order\Support\OrderStatus;
|
use Modules\Core\Order\Support\OrderStatus;
|
||||||
use Modules\Core\Payment\Events\PaymentAuthorized;
|
use Modules\Core\Payment\Events\PaymentAuthorized;
|
||||||
@@ -16,13 +17,16 @@ use Modules\Core\Payment\Events\PaymentRefunded;
|
|||||||
* Registered against PaymentCaptured, PaymentAuthorized, AND
|
* Registered against PaymentCaptured, PaymentAuthorized, AND
|
||||||
* PaymentRefunded (see OrderServiceProvider).
|
* PaymentRefunded (see OrderServiceProvider).
|
||||||
*
|
*
|
||||||
* A capture/authorization only ever writes Order::paid/paid_at (via
|
* PaymentCaptured writes both Order::paid/paid_at (via
|
||||||
* OrderStatusWriter::markPaid()) — never `status`. Confirmed with the
|
* OrderStatusWriter::markPaid()) AND advances `status` out of
|
||||||
* user: status leaving 'awaiting_payment' is always a staff-driven
|
* 'awaiting_payment' to the next step in the order's flow (see
|
||||||
* "Update Status" click, regardless of payment method — no special-casing
|
* OrderStatusFlow::nextOptions()) — re-confirmed with the user: a
|
||||||
* prepaid vs. cash-on-delivery. A prepaid order briefly sitting at
|
* captured payment, manual or via Stripe's webhook, should never leave an
|
||||||
* 'awaiting_payment' with paid = true (until staff notice and advance it)
|
* order sitting at 'awaiting_payment'. Only fires when status is still
|
||||||
* is expected, not a bug.
|
* exactly 'awaiting_payment', so a duplicate/delayed capture event never
|
||||||
|
* regresses an order staff already advanced further. PaymentAuthorized
|
||||||
|
* only marks paid — an authorization is not yet captured funds, so
|
||||||
|
* status stays put until the actual capture.
|
||||||
*
|
*
|
||||||
* A refund still moves `status` (returned -> refunded/partially_refunded)
|
* A refund still moves `status` (returned -> refunded/partially_refunded)
|
||||||
* — refunds are a normal step in Modules\Core\Order\Services\
|
* — refunds are a normal step in Modules\Core\Order\Services\
|
||||||
@@ -44,6 +48,7 @@ class ApplyResolvedPaymentStatus
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly OrderStatusWriter $writer,
|
private readonly OrderStatusWriter $writer,
|
||||||
|
private readonly OrderStatusFlow $flow,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function handle(PaymentCaptured|PaymentAuthorized|PaymentRefunded $event): void
|
public function handle(PaymentCaptured|PaymentAuthorized|PaymentRefunded $event): void
|
||||||
@@ -66,12 +71,30 @@ class ApplyResolvedPaymentStatus
|
|||||||
|
|
||||||
$this->writer->markPaid($order, $event::class);
|
$this->writer->markPaid($order, $event::class);
|
||||||
|
|
||||||
|
if ($event instanceof PaymentCaptured) {
|
||||||
|
$this->advancePastAwaitingPayment($order, $event);
|
||||||
|
}
|
||||||
|
|
||||||
if (! $wasPlaced) {
|
if (! $wasPlaced) {
|
||||||
$order->update(['placed_at' => $order->placed_at ?? now()]);
|
$order->update(['placed_at' => $order->placed_at ?? now()]);
|
||||||
Event::dispatch(new OrderPlaced($order));
|
Event::dispatch(new OrderPlaced($order));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function advancePastAwaitingPayment(Order $order, PaymentCaptured $event): 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, $event::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Requires the refund Transaction row to already exist (Modules\Core\
|
* Requires the refund Transaction row to already exist (Modules\Core\
|
||||||
* Order\Listeners\RecordPaymentTransaction must run first — see
|
* Order\Listeners\RecordPaymentTransaction must run first — see
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ use Modules\Core\Payment\Events\PaymentRefunded;
|
|||||||
* chooses this driver explicitly in the refund action, independent of
|
* chooses this driver explicitly in the refund action, independent of
|
||||||
* which driver the original payment went through (see
|
* which driver the original payment went through (see
|
||||||
* Payment\Support\TransactionDriverAdapter::refundVia() and
|
* Payment\Support\TransactionDriverAdapter::refundVia() and
|
||||||
* Order\Filament\Extensions\OrderRefundActionsExtension). pay() exists so
|
* Order\Filament\Extensions\OrderActionsExtension). pay() exists so
|
||||||
* the same driver also covers receiving a payment by bank transfer, but
|
* the same driver also covers receiving a payment by bank transfer, but
|
||||||
* the admin UI for that (bank reference, notes, proof-of-transfer upload)
|
* the admin UI for that (bank reference, notes, proof-of-transfer upload)
|
||||||
* is deliberately not built yet — see the follow-up work tracked from this
|
* is deliberately not built yet — see the follow-up work tracked from this
|
||||||
|
|||||||
@@ -70,7 +70,6 @@ class StripePaymentDriver implements
|
|||||||
{
|
{
|
||||||
return filled(config('services.stripe.key'));
|
return filled(config('services.stripe.key'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Atomic charge — capture_method: automatic. Stripe still frequently
|
* Atomic charge — capture_method: automatic. Stripe still frequently
|
||||||
* confirms into requires_action/requires_confirmation rather than
|
* confirms into requires_action/requires_confirmation rather than
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ class TransactionDriverAdapter
|
|||||||
/**
|
/**
|
||||||
* The PaymentDriverRegistry key $transaction was originally taken
|
* The PaymentDriverRegistry key $transaction was originally taken
|
||||||
* through — what refund()/capture() resolve against by default, and
|
* through — what refund()/capture() resolve against by default, and
|
||||||
* what Order\Filament\Extensions\OrderRefundActionsExtension defaults
|
* what Order\Filament\Extensions\OrderActionsExtension defaults
|
||||||
* its "Refund via" driver Select to, before an admin overrides it.
|
* its "Refund via" driver Select to, before an admin overrides it.
|
||||||
*/
|
*/
|
||||||
public function driverKeyFor(Transaction $transaction): ?string
|
public function driverKeyFor(Transaction $transaction): ?string
|
||||||
@@ -72,7 +72,7 @@ class TransactionDriverAdapter
|
|||||||
* when refunding through the transaction's own original driver.
|
* when refunding through the transaction's own original driver.
|
||||||
*
|
*
|
||||||
* Called directly by Order\Filament\Extensions\
|
* Called directly by Order\Filament\Extensions\
|
||||||
* OrderRefundActionsExtension when the admin picks a different driver
|
* OrderActionsExtension when the admin picks a different driver
|
||||||
* in the refund modal, bypassing Lunar\Models\Transaction::refund()
|
* in the refund modal, bypassing Lunar\Models\Transaction::refund()
|
||||||
* (whose fixed refund(int $amount, $notes = null) signature has no
|
* (whose fixed refund(int $amount, $notes = null) signature has no
|
||||||
* room for a driver override) — see that extension's own docblock.
|
* room for a driver override) — see that extension's own docblock.
|
||||||
|
|||||||
Reference in New Issue
Block a user