Compare commits
5
Commits
409e8204f6
...
v0.17.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4489475840 | ||
|
|
e4e008167a | ||
|
|
a5f3008ce2 | ||
|
|
d9fb3bbde6 | ||
|
|
26b4c5bfd7 |
@@ -4,6 +4,52 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||||
|
|
||||||
|
## [0.17.2] - 2026-09-15
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- `Modules\Core\Order\Listeners\ApplyResolvedPaymentStatus` never advanced `Order::status` past
|
||||||
|
`awaiting_payment` on a capture — only `paid`/`paid_at` were written, so a fully captured order
|
||||||
|
could sit indefinitely at "awaiting payment" until a staff member manually clicked "Update
|
||||||
|
Status". Now, on `PaymentCaptured` (not `PaymentAuthorized` — an authorization isn't yet
|
||||||
|
captured funds), `status` advances to the next step in the order's flow
|
||||||
|
(`Modules\Core\Order\Services\OrderStatusFlow::nextOptions()`) — but only when it's still
|
||||||
|
exactly `awaiting_payment`, so a duplicate/delayed capture event never regresses an order staff
|
||||||
|
already moved further.
|
||||||
|
- The backoffice "Capture" action on the order page (Filament) called vendor Lunar's
|
||||||
|
`Lunar\Models\Transaction::capture()` directly, which resolves `Lunar\Facades\Payments` — an
|
||||||
|
entirely separate, unused driver registry — and never dispatched `Modules\Core\Payment\Events\
|
||||||
|
PaymentCaptured`. This meant a manual capture from the admin panel never ran this app's own
|
||||||
|
payment pipeline at all (including the status-advance fix above). `Modules\Core\Order\Filament\
|
||||||
|
Extensions\OrderActionsExtension` (renamed from `OrderRefundActionsExtension`, since it now
|
||||||
|
fixes both the refund and capture header actions — see below) now routes capture through
|
||||||
|
`Modules\Core\Payment\Support\TransactionDriverAdapter::capture()`, the same app-level path
|
||||||
|
checkout-time captures use.
|
||||||
|
- `Modules\Core\Payment\Drivers\StripePaymentDriver` never extracted a card's brand/last four
|
||||||
|
digits from Stripe's response, so `Lunar\Models\Transaction::card_type`/`last_four` were always
|
||||||
|
empty and the admin's "Payment of :amount on card ending :last_four" activity-log line rendered
|
||||||
|
with no digits — reproduced on both checkout-time and manual captures. Added
|
||||||
|
`cardMetaFromIntent()`, reading `payment_method_details` off the PaymentIntent's `latest_charge`
|
||||||
|
(same source `lunarphp/stripe`'s own `StoreCharges` uses), populated into `PaymentResult::$meta`
|
||||||
|
from `resultFromIntent()` and `capture()`. `Modules\Core\Order\Services\TransactionRecorder`
|
||||||
|
now maps `meta['card_type']`/`meta['last_four']` onto the `Transaction` row. Only applies to
|
||||||
|
transactions recorded after this change — existing rows are not backfilled.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- `Modules\Core\Order\Filament\Extensions\OrderRefundActionsExtension` renamed to
|
||||||
|
`OrderActionsExtension` — the class now fixes both the refund and capture header actions on the
|
||||||
|
order page, not just refund, so the old name undersold its scope.
|
||||||
|
|
||||||
|
## [0.17.1] - 2026-09-15
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- `Modules\Core\Payment\Drivers\StripePaymentDriver::createAndConfirm()` only set
|
||||||
|
`automatic_payment_methods` when no `payment_method` was given — the actual checkout flow always
|
||||||
|
sends one, so it was omitted, and Stripe fell back to whatever payment methods are enabled in the
|
||||||
|
Dashboard and demanded a `return_url` on confirm. Fixed by setting `automatic_payment_methods`
|
||||||
|
unconditionally with `allow_redirects: never` — the storefront's Payment Element already restricts
|
||||||
|
itself to `paymentMethodTypes: ['card']`, so this just tells Stripe the same thing server-side,
|
||||||
|
which drops the `return_url` requirement.
|
||||||
|
|
||||||
## [0.17.0] - 2026-09-14
|
## [0.17.0] - 2026-09-14
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
"name": "boboko/core",
|
"name": "boboko/core",
|
||||||
"description": "Core module — authentication and shared panel behaviour",
|
"description": "Core module — authentication and shared panel behaviour",
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"version": "0.17.0",
|
"version": "0.17.2",
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"Modules\\Core\\": "src/"
|
"Modules\\Core\\": "src/"
|
||||||
|
|||||||
+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
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ class TransactionRecorder
|
|||||||
'reference' => $result->reference,
|
'reference' => $result->reference,
|
||||||
'status' => $result->status->name,
|
'status' => $result->status->name,
|
||||||
'notes' => $result->failureReason,
|
'notes' => $result->failureReason,
|
||||||
|
'card_type' => $result->meta['card_type'] ?? null,
|
||||||
|
'last_four' => $result->meta['last_four'] ?? null,
|
||||||
'meta' => $result->meta,
|
'meta' => $result->meta,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -100,12 +99,18 @@ class StripePaymentDriver implements
|
|||||||
'currency' => $amount->currency->code,
|
'currency' => $amount->currency->code,
|
||||||
'capture_method' => $captureMethod,
|
'capture_method' => $captureMethod,
|
||||||
'confirm' => true,
|
'confirm' => true,
|
||||||
|
// 'never' rather than the client-side paymentMethodTypes: ['card']
|
||||||
|
// restriction alone — the storefront's Payment Element already
|
||||||
|
// excludes every redirect-based method, but without this Stripe
|
||||||
|
// still falls back to whatever's enabled in the Dashboard and
|
||||||
|
// demands a return_url on confirm. Setting this unconditionally
|
||||||
|
// (not only when no payment_method is given) matches the actual
|
||||||
|
// flow: a payment_method is always supplied here.
|
||||||
|
'automatic_payment_methods' => ['enabled' => true, 'allow_redirects' => 'never'],
|
||||||
];
|
];
|
||||||
|
|
||||||
if (isset($data['payment_method'])) {
|
if (isset($data['payment_method'])) {
|
||||||
$params['payment_method'] = $data['payment_method'];
|
$params['payment_method'] = $data['payment_method'];
|
||||||
} else {
|
|
||||||
$params['automatic_payment_methods'] = ['enabled' => true];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -165,6 +170,7 @@ class StripePaymentDriver implements
|
|||||||
reference: $paymentIntent->id,
|
reference: $paymentIntent->id,
|
||||||
amount: $amount,
|
amount: $amount,
|
||||||
raw: $paymentIntent->toArray(),
|
raw: $paymentIntent->toArray(),
|
||||||
|
meta: $this->cardMetaFromIntent($paymentIntent),
|
||||||
);
|
);
|
||||||
|
|
||||||
$paymentIntent->status === PaymentIntent::STATUS_SUCCEEDED
|
$paymentIntent->status === PaymentIntent::STATUS_SUCCEEDED
|
||||||
@@ -335,6 +341,7 @@ class StripePaymentDriver implements
|
|||||||
amount: $amount,
|
amount: $amount,
|
||||||
failureReason: $paymentIntent->last_payment_error->message ?? null,
|
failureReason: $paymentIntent->last_payment_error->message ?? null,
|
||||||
raw: $paymentIntent->toArray(),
|
raw: $paymentIntent->toArray(),
|
||||||
|
meta: $status === PaymentResultStatus::Pending ? [] : $this->cardMetaFromIntent($paymentIntent),
|
||||||
continuation: $continuation,
|
continuation: $continuation,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -357,6 +364,40 @@ class StripePaymentDriver implements
|
|||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* card_type/last_four for Modules\Core\Order\Services\
|
||||||
|
* TransactionRecorder to map onto Transaction (see PaymentResult::
|
||||||
|
* $meta's own docblock) — same fields, same source
|
||||||
|
* (payment_method_details on the underlying Charge) as lunarphp/
|
||||||
|
* stripe's own StoreCharges, just reached via latest_charge instead of
|
||||||
|
* an order-level charge list, since this driver has no Order/Cart to
|
||||||
|
* enumerate charges from.
|
||||||
|
*
|
||||||
|
* @return array{card_type?: string, last_four?: string}
|
||||||
|
*/
|
||||||
|
private function cardMetaFromIntent(PaymentIntent $paymentIntent): array
|
||||||
|
{
|
||||||
|
$chargeId = $paymentIntent->latest_charge;
|
||||||
|
|
||||||
|
if (blank($chargeId)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$charge = Stripe::getCharge(is_string($chargeId) ? $chargeId : $chargeId->id);
|
||||||
|
|
||||||
|
$paymentType = collect($charge->payment_method_details)->keys()->first();
|
||||||
|
$details = collect($charge->payment_method_details)->first();
|
||||||
|
|
||||||
|
if (blank($details)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_filter([
|
||||||
|
'card_type' => $details['brand'] ?? $paymentType,
|
||||||
|
'last_four' => $details['last4'] ?? null,
|
||||||
|
], fn ($value) => filled($value));
|
||||||
|
}
|
||||||
|
|
||||||
private function declined(string $type, Price $amount, ApiErrorException $e, array $context, bool $authorizing): PaymentResult
|
private function declined(string $type, Price $amount, ApiErrorException $e, array $context, bool $authorizing): PaymentResult
|
||||||
{
|
{
|
||||||
$result = $this->failure($amount, $e);
|
$result = $this->failure($amount, $e);
|
||||||
|
|||||||
@@ -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