Feature: Payment resolver, Payment Provider, Completing Stripe Webhooks, Wiring Payments to checkout service
This commit is contained in:
@@ -5,13 +5,17 @@ namespace Modules\Core\Checkout\Events;
|
||||
use Lunar\Models\Order;
|
||||
|
||||
/**
|
||||
* Dispatched by CheckoutService::placeOrder() the moment an Order exists —
|
||||
* the handoff point between Checkout and Order (see docs/checkout.md's
|
||||
* "Three-stage lifecycle"). Checkout has no opinion about what happens
|
||||
* after this fires; Order's own listeners (not built yet — Order is a
|
||||
* named-but-unscoped concern, same status Recovery had before it existed)
|
||||
* would be what reacts to it — e.g. a confirmation email, initializing
|
||||
* order status tracking.
|
||||
* Dispatched once an Order's placed_at is set — the handoff point between
|
||||
* Checkout/Payment and Order (see docs/checkout.md's "Three-stage
|
||||
* lifecycle"). Fired by Modules\Core\Order\Listeners\
|
||||
* ApplyResolvedPaymentStatus once it resolves a PaymentCaptured/
|
||||
* PaymentAuthorized event into an actual order status change, not by
|
||||
* CheckoutService directly — a draft Order can exist (via
|
||||
* CheckoutService::initiatePayment()) well before this fires, if payment
|
||||
* resolves asynchronously (e.g. a redirect-based gateway). Checkout has no
|
||||
* opinion about what happens after this fires; Order's own listeners are
|
||||
* what react to it — e.g. a confirmation email, initializing order status
|
||||
* tracking.
|
||||
*/
|
||||
class OrderPlaced
|
||||
{
|
||||
|
||||
@@ -10,16 +10,14 @@ use Lunar\Base\Addressable;
|
||||
use Lunar\DataTypes\ShippingOption;
|
||||
use Lunar\Facades\ShippingManifest;
|
||||
use Lunar\Models\Cart;
|
||||
use Lunar\Models\Order;
|
||||
use Modules\Core\Cart\Services\CartService;
|
||||
use Modules\Core\Checkout\Events\BillingAddressSet;
|
||||
use Modules\Core\Checkout\Events\OrderPlaced;
|
||||
use Modules\Core\Checkout\Events\PaymentConfirmed;
|
||||
use Modules\Core\Checkout\Events\PaymentMethodSelected;
|
||||
use Modules\Core\Checkout\Events\ShippingAddressSet;
|
||||
use Modules\Core\Checkout\Events\ShippingOptionSelected;
|
||||
use Modules\Core\Checkout\Exceptions\InvalidShippingOptionException;
|
||||
use Modules\Core\Checkout\Exceptions\UnknownPaymentTypeException;
|
||||
use Modules\Core\Payment\DTOs\PaymentResult;
|
||||
use Modules\Core\Payment\Models\PaymentMethod;
|
||||
use Modules\Core\Payment\Services\PaymentDriverResolver;
|
||||
|
||||
@@ -30,9 +28,11 @@ use Modules\Core\Payment\Services\PaymentDriverResolver;
|
||||
* implementation detail. See docs/checkout.md for the full design —
|
||||
* Checkout is the middle of a three-stage lifecycle (Cart → Checkout →
|
||||
* Order): it owns the placement moment itself (address, shipping selection,
|
||||
* placeOrder()) and ends the instant an Order exists. What happens to that
|
||||
* Order afterward (status transitions, fulfillment) is deliberately out of
|
||||
* scope here — see OrderPlaced's docblock.
|
||||
* ensuring a draft Order exists) and hands off to Payment the instant that
|
||||
* draft exists — see initiatePayment(). What happens to that Order
|
||||
* afterward (status transitions, fulfillment) is deliberately out of
|
||||
* scope here — see docs/payments.md and Checkout\Events\OrderPlaced's
|
||||
* docblock for where that now lives.
|
||||
*
|
||||
* Depends on CartService for cart access rather than reaching into
|
||||
* Lunar\Facades\CartSession directly a second time, so Checkout stays
|
||||
@@ -99,47 +99,12 @@ class CheckoutService
|
||||
return $cart;
|
||||
}
|
||||
|
||||
/**
|
||||
* $fingerprint is mandatory, not optional — the caller must prove the
|
||||
* cart total the shopper last saw (Cart::fingerprint()) still matches
|
||||
* before an order is placed. Cart::checkFingerprint() throws Lunar's own
|
||||
* FingerprintMismatchException on a mismatch (a line's price changed,
|
||||
* stock adjusted the total, another tab modified the cart) rather than
|
||||
* silently placing an order at a different total than what was shown.
|
||||
*
|
||||
* Not called directly by a storefront — see confirmPayment(), which is
|
||||
* the only caller and supplies the fingerprint captured in
|
||||
* selectPaymentMethod(), not one the storefront has to obtain itself.
|
||||
*
|
||||
* No exception wrapping: Lunar\Validation\Cart\ValidateCartForOrderCreation
|
||||
* (run inside Cart::createOrder()) already throws
|
||||
* Lunar\Exceptions\Carts\CartException with a field-keyed MessageBag
|
||||
* ($exception->errors()) for address/shipping-option validation and the
|
||||
* duplicate-order guard — already the right shape for a storefront to
|
||||
* render as form errors directly. FingerprintMismatchException
|
||||
* propagates the same way, for the same reason.
|
||||
*
|
||||
* @throws FingerprintMismatchException
|
||||
* @throws CartException
|
||||
*/
|
||||
public function placeOrder(string $fingerprint): Order
|
||||
{
|
||||
$cart = $this->cart->currentOrCreate();
|
||||
$cart->checkFingerprint($fingerprint);
|
||||
|
||||
$order = $cart->createOrder();
|
||||
|
||||
Event::dispatch(new OrderPlaced($order));
|
||||
|
||||
return $order;
|
||||
}
|
||||
|
||||
/**
|
||||
* Every payment type currently offered to the storefront — every key
|
||||
* in config('lunar.payments.types') that is BOTH administratively
|
||||
* enabled (Modules\Core\Payment\Models\PaymentMethod::enabled) AND
|
||||
* whose registered PaymentDriver reports itself usable right now
|
||||
* (PaymentDriver::isConfigured() — e.g. Stripe with no API key set is
|
||||
* whose registered driver reports itself usable right now
|
||||
* (Configurable::isConfigured() — e.g. Stripe with no API key set is
|
||||
* never offered, regardless of the enabled toggle). A type with no
|
||||
* PaymentMethod row at all (never seeded) is treated as not offered,
|
||||
* same as disabled — nothing here creates one; see
|
||||
@@ -167,13 +132,13 @@ class CheckoutService
|
||||
* including any payment-type-specific adjustment (e.g. a COD
|
||||
* surcharge), which only exists once payment_method is set and the
|
||||
* cart recalculates. Captured here, server-side, rather than asked of
|
||||
* the storefront: this is the last moment before confirmPayment() that
|
||||
* the shopper's reviewed total is known, and confirmPayment() reads it
|
||||
* the storefront: this is the last moment before initiatePayment() that
|
||||
* the shopper's reviewed total is known, and initiatePayment() reads it
|
||||
* back internally instead of taking a fingerprint parameter — a
|
||||
* storefront should never need to know Cart::fingerprint() exists.
|
||||
*
|
||||
* Does not itself call a PaymentDriver — selecting a method and
|
||||
* confirming payment against it are deliberately separate steps, same
|
||||
* Does not itself call a payment driver — selecting a method and
|
||||
* initiating payment against it are deliberately separate steps, same
|
||||
* as selecting a shipping option happens before placing the order.
|
||||
*
|
||||
* @throws UnknownPaymentTypeException if $type isn't currently offered
|
||||
@@ -200,44 +165,76 @@ class CheckoutService
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves $type's registered PaymentDriver and calls confirm() — the
|
||||
* driver independently decides whether payment succeeded and, if so,
|
||||
* dispatches PaymentConfirmed (see PaymentDriver's docblock) rather
|
||||
* than placing the order itself or returning it here. This method is
|
||||
* fire-and-forget as far as the Order is concerned: a caller that
|
||||
* needs it back listens for OrderPlaced, the same way a driver's own
|
||||
* post-placement step does — see PaymentConfirmed's docblock for why a
|
||||
* direct return value doesn't fit every gateway (async/webhook-driven
|
||||
* confirmations have no synchronous caller waiting for one at all).
|
||||
* The one storefront-facing "place this order and pay for it" call —
|
||||
* the point where Checkout hands off to Payment. Ensures a draft
|
||||
* Order exists (Cart::createOrder() — confirmed idempotent against a
|
||||
* cart's own pre-existing, not-yet-placed-at draft; see
|
||||
* vendor/lunarphp/core/src/Actions/Carts/CreateOrder.php), then
|
||||
* resolves the payment type selected by selectPaymentMethod() and
|
||||
* calls pay() or authorize() on its driver, per that type's
|
||||
* config('lunar.payments.types.{type}.capture_mode') — boboko-core's
|
||||
* own types (config/payment.php) are merged into that same Lunar
|
||||
* config key by PaymentServiceProvider::boot().
|
||||
*
|
||||
* $data carries whatever that driver needs (Stripe's payment_intent
|
||||
* id, a future redirect-based provider's callback payload).
|
||||
* Returns the driver's own PaymentResult UNCHANGED — this method does
|
||||
* not wait for or resolve anything past what pay()/authorize() itself
|
||||
* returns synchronously. A Pending result (an async gateway like
|
||||
* Stripe requiring 3-D Secure/a redirect) is a normal, expected
|
||||
* outcome, not an error — the caller (a storefront controller) is
|
||||
* responsible for whatever the gateway needs next.
|
||||
*
|
||||
* The fingerprint passed to the driver is the one captured by
|
||||
* selectPaymentMethod(), not supplied by the caller — see that
|
||||
* method's docblock. Throws the same FingerprintMismatchException a
|
||||
* caller-supplied one would if the cart's total has since changed;
|
||||
* missing entirely (selectPaymentMethod() was never called for this
|
||||
* cart) is treated the same as a mismatch, not a different error.
|
||||
* KNOWN GAP, explicitly out of scope for now: PaymentResult alone does
|
||||
* not carry gateway-specific continuation data (e.g. Stripe's
|
||||
* PaymentIntent client_secret for a Pending result needing frontend
|
||||
* confirmation) — that concept existed on the deleted PaymentInitiation
|
||||
* DTO and was intentionally removed from Payment's abstraction layer.
|
||||
* Nothing here re-introduces it; only OfflinePaymentDriver's
|
||||
* always-Immediate-Succeeded path is fully wired end-to-end today.
|
||||
*
|
||||
* @param array<string, mixed> $data
|
||||
* The draft order's own $order->total (not the Cart's) is what gets
|
||||
* passed as $amount — Order::$total is Lunar's own Price-cast
|
||||
* attribute, already resolving the correct Currency via the order's
|
||||
* own currency_code, and is the authoritative total once the draft
|
||||
* row exists.
|
||||
*
|
||||
* @throws UnknownPaymentTypeException if $type isn't currently offered
|
||||
* (see getPaymentMethods()) — re-checked here, not just in
|
||||
* selectPaymentMethod(), since a type could be disabled between
|
||||
* selection and confirmation
|
||||
* @throws \Lunar\Exceptions\FingerprintMismatchException
|
||||
* @throws \Lunar\Exceptions\Carts\CartException
|
||||
* $context passed to the driver is {cart_id, order_id} — the exact
|
||||
* keys Modules\Core\Payment\Drivers\StripePaymentDriver::
|
||||
* rememberIntent() already reads.
|
||||
*
|
||||
* Same fingerprint precondition the old placeOrder() had: mandatory,
|
||||
* not optional, checked before the draft is created.
|
||||
*
|
||||
* @param array<string, mixed> $data passed through untouched to
|
||||
* the driver's pay()/authorize() — e.g. Stripe's payment_method
|
||||
* token.
|
||||
*
|
||||
* @throws UnknownPaymentTypeException if the cart's selected
|
||||
* payment_method (from selectPaymentMethod()) is no longer offered
|
||||
* — re-checked here, not just at selection time, since a type could
|
||||
* be disabled in between
|
||||
* @throws FingerprintMismatchException
|
||||
* @throws CartException
|
||||
*/
|
||||
public function confirmPayment(string $type, array $data = []): void
|
||||
public function initiatePayment(string $fingerprint, array $data = []): PaymentResult
|
||||
{
|
||||
if (! in_array($type, $this->getPaymentMethods(), true)) {
|
||||
throw new UnknownPaymentTypeException($type);
|
||||
$cart = $this->cart->currentOrCreate();
|
||||
$cart->checkFingerprint($fingerprint);
|
||||
|
||||
$type = $cart->meta['payment_method'] ?? null;
|
||||
|
||||
if ($type === null || ! in_array($type, $this->getPaymentMethods(), true)) {
|
||||
throw new UnknownPaymentTypeException((string) $type);
|
||||
}
|
||||
|
||||
$cart = $this->cart->currentOrCreate();
|
||||
$fingerprint = $cart->meta['checkout_fingerprint'] ?? '';
|
||||
$order = $cart->createOrder();
|
||||
|
||||
$this->paymentDrivers->resolve($type)->confirm($cart, $type, $fingerprint, $data);
|
||||
$driver = $this->paymentDrivers->resolve($type);
|
||||
$captureMode = config("lunar.payments.types.{$type}.capture_mode", 'pay');
|
||||
|
||||
$context = ['cart_id' => $cart->id, 'order_id' => $order->id];
|
||||
|
||||
return $captureMode === 'authorize'
|
||||
? $driver->authorize($type, $order->total, $data, $context)
|
||||
: $driver->pay($type, $order->total, $data, $context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,25 +2,62 @@
|
||||
|
||||
namespace Modules\Core\Order\Listeners;
|
||||
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Lunar\Models\Order;
|
||||
use Modules\Core\Payment\Events\OrderPaymentStatusResolved;
|
||||
use Modules\Core\Checkout\Events\OrderPlaced;
|
||||
use Modules\Core\Payment\Events\PaymentAuthorized;
|
||||
use Modules\Core\Payment\Events\PaymentCaptured;
|
||||
|
||||
/**
|
||||
* The only place an Order's status column is written in reaction to a
|
||||
* payment outcome — Payment dispatches OrderPaymentStatusResolved with
|
||||
* what the status should become, never touching the Order model itself;
|
||||
* this listener, living in Order's own module, is what applies it.
|
||||
* payment outcome. Registered against BOTH PaymentCaptured and
|
||||
* PaymentAuthorized (see OrderServiceProvider) — same handler either way,
|
||||
* since both carry the same {type, result, context} shape and only differ
|
||||
* in which config key decides the resulting status.
|
||||
*
|
||||
* Reads $event->context['order_id'] to find which Order this outcome
|
||||
* belongs to — Payment has no concept of an Order, so this is the one
|
||||
* place that context key gets consumed on the Order side (Payment's own
|
||||
* StripePaymentDriver reads $context['order_id'] independently, for its
|
||||
* own unrelated correlation need — see that class's rememberIntent()).
|
||||
*
|
||||
* Loads and saves the model (not a bulk ::whereKey()->update()) so
|
||||
* Order::observe()'s updated() hook fires and OrderStatusUpdated goes out
|
||||
* the same as any other status write — see that event's own docblock for
|
||||
* why it's meant to fire "regardless of what wrote it."
|
||||
*
|
||||
* Dispatches Checkout\Events\OrderPlaced itself, once placed_at is set —
|
||||
* see that event's own docblock for why this, not CheckoutService, is now
|
||||
* the dispatch point.
|
||||
*/
|
||||
class ApplyResolvedPaymentStatus
|
||||
{
|
||||
public function handle(OrderPaymentStatusResolved $event): void
|
||||
public function handle(PaymentCaptured|PaymentAuthorized $event): void
|
||||
{
|
||||
$order = Order::findOrFail($event->orderId);
|
||||
$order->update(['status' => $event->status]);
|
||||
$orderId = $event->context['order_id'] ?? null;
|
||||
|
||||
if ($orderId === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$order = Order::findOrFail($orderId);
|
||||
|
||||
$configKey = $event instanceof PaymentCaptured ? 'captured_status' : 'authorized_status';
|
||||
$status = config("lunar.payments.types.{$event->type}.{$configKey}");
|
||||
|
||||
if ($status === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$wasPlaced = ! blank($order->placed_at);
|
||||
|
||||
$order->update([
|
||||
'status' => $status,
|
||||
'placed_at' => $order->placed_at ?? now(),
|
||||
]);
|
||||
|
||||
if (! $wasPlaced) {
|
||||
Event::dispatch(new OrderPlaced($order));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Core\Payment\DTOs;
|
||||
|
||||
use Modules\Core\Payment\Enums\PaymentContinuationType;
|
||||
|
||||
/**
|
||||
* What a caller does next with a Pending PaymentResult, gateway-agnostic —
|
||||
* see PaymentContinuationType for the two shapes. Deliberately minimal:
|
||||
* this is NOT a return to the deleted PaymentInitiation DTO (which also
|
||||
* carried mode/reference/meta) — reference already lives on PaymentResult
|
||||
* itself, and mode is now this DTO's own $type.
|
||||
*/
|
||||
final class PaymentContinuation
|
||||
{
|
||||
public function __construct(
|
||||
public readonly PaymentContinuationType $type,
|
||||
public readonly string $value,
|
||||
) {}
|
||||
}
|
||||
@@ -47,6 +47,11 @@ final class PaymentResult
|
||||
* normalize into anything above.
|
||||
* @param $meta driver-specific extras that don't fit the normalized
|
||||
* fields above (e.g. a card's last four digits).
|
||||
* @param $continuation only meaningful when $status is Pending —
|
||||
* what the caller does next (a redirect URL, a client secret for
|
||||
* frontend JS), gateway-agnostic. Null for every other status, and
|
||||
* for any driver whose pay()/authorize() never returns Pending
|
||||
* (e.g. OfflinePaymentDriver).
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly PaymentResultStatus $status,
|
||||
@@ -56,5 +61,6 @@ final class PaymentResult
|
||||
public readonly bool $retriable = false,
|
||||
public readonly array $raw = [],
|
||||
public readonly array $meta = [],
|
||||
public readonly ?PaymentContinuation $continuation = null,
|
||||
) {}
|
||||
}
|
||||
@@ -14,7 +14,9 @@ use Modules\Core\Payment\Contracts\SupportsCaptures;
|
||||
use Modules\Core\Payment\Contracts\SupportsPay;
|
||||
use Modules\Core\Payment\Contracts\SupportsRefunds;
|
||||
use Modules\Core\Payment\Contracts\SupportsVoids;
|
||||
use Modules\Core\Payment\DTOs\PaymentContinuation;
|
||||
use Modules\Core\Payment\DTOs\PaymentResult;
|
||||
use Modules\Core\Payment\Enums\PaymentContinuationType;
|
||||
use Modules\Core\Payment\Enums\PaymentResultStatus;
|
||||
use Modules\Core\Payment\Events\PaymentAuthorizationFailed;
|
||||
use Modules\Core\Payment\Events\PaymentAuthorized;
|
||||
@@ -319,12 +321,17 @@ class StripePaymentDriver implements
|
||||
default => PaymentResultStatus::Pending,
|
||||
};
|
||||
|
||||
$continuation = $status === PaymentResultStatus::Pending
|
||||
? new PaymentContinuation(PaymentContinuationType::ClientSecret, $paymentIntent->client_secret)
|
||||
: null;
|
||||
|
||||
$result = new PaymentResult(
|
||||
status: $status,
|
||||
reference: $paymentIntent->id,
|
||||
amount: $amount,
|
||||
failureReason: $paymentIntent->last_payment_error->message ?? null,
|
||||
raw: $paymentIntent->toArray(),
|
||||
continuation: $continuation,
|
||||
);
|
||||
|
||||
if ($status === PaymentResultStatus::Pending) {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Core\Payment\Enums;
|
||||
|
||||
/**
|
||||
* What a caller of a Pending PaymentResult needs to do next, gateway-
|
||||
* agnostically. Only meaningful when PaymentResult::$continuation is not
|
||||
* null (status === Pending).
|
||||
*/
|
||||
enum PaymentContinuationType
|
||||
{
|
||||
/** Send the shopper to $continuation->value (a URL) — a redirect-based gateway. */
|
||||
case Redirect;
|
||||
|
||||
/** Hand $continuation->value (a client secret) to frontend JS — Stripe Elements-style. */
|
||||
case ClientSecret;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Core\Payment\Exceptions;
|
||||
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Thrown by a Modules\Core\Payment\Contracts\PaymentDriver when the
|
||||
* gateway has not confirmed payment — wrong/expired intent, already
|
||||
* processed, or the gateway itself rejects the confirmation. A driver
|
||||
* throws this instead of silently placing the order: CheckoutService::
|
||||
* placeOrder() must only ever be called once a driver has positively
|
||||
* confirmed payment, never as a fallback.
|
||||
*/
|
||||
class PaymentNotConfirmedException extends RuntimeException
|
||||
{
|
||||
public function __construct(string $message, ?Throwable $previous = null)
|
||||
{
|
||||
parent::__construct($message, previous: $previous);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Core\Payment\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
use Modules\Core\Payment\Drivers\StripePaymentDriver;
|
||||
use Stripe\Webhook;
|
||||
|
||||
/**
|
||||
* A boboko-owned webhook endpoint for Stripe — deliberately NOT
|
||||
* lunarphp/stripe's own route (vendor/lunarphp/stripe/routes/webhooks.php),
|
||||
* which dispatches into Lunar's own Payments::driver('stripe') flow (the
|
||||
* flow StripePaymentDriver was built to replace, see that class's own
|
||||
* docblock). Signature verification is handled by
|
||||
* Lunar\Stripe\Http\Middleware\StripeWebhookMiddleware, registered on this
|
||||
* route (see src/Payment/routes/webhooks.php) — pure Stripe SDK
|
||||
* verification + event-type filtering, safe to reuse even though this
|
||||
* controller never touches the rest of that vendor package's flow. This
|
||||
* controller verifies the signature again itself (Webhook::constructEvent())
|
||||
* to get the constructed Event object — the middleware doesn't stash one
|
||||
* anywhere reusable, it only gates the request through.
|
||||
*
|
||||
* Resolves the driver directly by class, not via
|
||||
* Modules\Core\Payment\Services\PaymentDriverResolver — this endpoint is
|
||||
* inherently Stripe-specific (Stripe's own webhook payload carries no
|
||||
* boboko payment-type key, only its own payment_intent id), and
|
||||
* StripePaymentDriver::handleCallback() already recovers $type itself
|
||||
* from the StripePaymentIntent row pay()/authorize() wrote.
|
||||
*/
|
||||
class StripeWebhookController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request, StripePaymentDriver $driver): JsonResponse
|
||||
{
|
||||
$event = Webhook::constructEvent(
|
||||
$request->getContent(),
|
||||
$request->header('Stripe-Signature'),
|
||||
config('services.stripe.webhooks.lunar'),
|
||||
);
|
||||
|
||||
$driver->handleCallback($event->data->object->id, $event->data->object->toArray());
|
||||
|
||||
return response()->json(['webhook_successful' => true]);
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,19 @@
|
||||
|
||||
namespace Modules\Core\Payment\Services;
|
||||
|
||||
use Modules\Core\Payment\Contracts\PaymentDriver;
|
||||
|
||||
/**
|
||||
* Resolves a payment type key (e.g. 'stripe', 'cash-on-delivery') to its
|
||||
* registered PaymentDriver — extracted out of CheckoutService so both it
|
||||
* and anything else needing the same lookup (e.g. a listener reacting to
|
||||
* OrderPlaced, which has no reason to depend on Checkout's own service)
|
||||
* share one implementation instead of duplicating this config read.
|
||||
* registered driver instance — extracted out of CheckoutService so both it
|
||||
* and anything else needing the same lookup share one implementation
|
||||
* instead of duplicating this config read.
|
||||
*
|
||||
* Returns a plain object, not a shared interface — Payment's own drivers
|
||||
* implement several independent, orthogonal capability interfaces at once
|
||||
* (Configurable, SupportsPay, SupportsAuthorization, ...; see
|
||||
* StripePaymentDriver implementing all six). There is no single common
|
||||
* "PaymentDriver" contract to type this against; a caller checks
|
||||
* `instanceof SupportsPay` / `instanceof SupportsAuthorization` itself,
|
||||
* the same way Payment's own contracts are designed to be consumed.
|
||||
*/
|
||||
class PaymentDriverResolver
|
||||
{
|
||||
@@ -20,7 +25,7 @@ class PaymentDriverResolver
|
||||
* can filter unresolvable types silently rather than treating "not
|
||||
* registered" as an error condition when just checking availability.
|
||||
*/
|
||||
public function resolve(string $type): ?PaymentDriver
|
||||
public function resolve(string $type): ?object
|
||||
{
|
||||
$driverClass = config("lunar.payments.types.{$type}.payment_driver");
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Lunar\Stripe\Http\Middleware\StripeWebhookMiddleware;
|
||||
use Modules\Core\Payment\Http\Controllers\StripeWebhookController;
|
||||
|
||||
Route::post(
|
||||
config('payment.stripe.webhook_path', 'payments/stripe/webhook'),
|
||||
StripeWebhookController::class
|
||||
)
|
||||
->middleware([StripeWebhookMiddleware::class, 'api'])
|
||||
->withoutMiddleware([VerifyCsrfToken::class])
|
||||
->name('payment.stripe.webhook');
|
||||
@@ -7,6 +7,7 @@ use Illuminate\Support\ServiceProvider;
|
||||
use Lunar\Models\Order;
|
||||
use Lunar\Models\Transaction;
|
||||
use Modules\Core\Notification\NotificationRegistry;
|
||||
use Modules\Core\Order\Listeners\ApplyResolvedPaymentStatus;
|
||||
use Modules\Core\Order\Listeners\DeriveOrderDeliveredFromShipment;
|
||||
use Modules\Core\Order\Notifications\OrderCapturedNotification;
|
||||
use Modules\Core\Order\Notifications\OrderDeliveredNotification;
|
||||
@@ -15,6 +16,8 @@ use Modules\Core\Order\Notifications\OrderStatusUpdatedNotification;
|
||||
use Modules\Core\Order\Observers\OrderObserver;
|
||||
use Modules\Core\Order\Observers\TransactionObserver;
|
||||
use Modules\Core\Order\Support\OrderStatus;
|
||||
use Modules\Core\Payment\Events\PaymentAuthorized;
|
||||
use Modules\Core\Payment\Events\PaymentCaptured;
|
||||
use Modules\Core\Shipping\Events\ShipmentStatusUpdatedByCarrier;
|
||||
|
||||
class OrderServiceProvider extends ServiceProvider
|
||||
@@ -28,6 +31,8 @@ class OrderServiceProvider extends ServiceProvider
|
||||
Order::macro('fulfillmentStatus', fn () => OrderStatus::fulfillment($this));
|
||||
|
||||
Event::listen(ShipmentStatusUpdatedByCarrier::class, DeriveOrderDeliveredFromShipment::class);
|
||||
Event::listen(PaymentCaptured::class, ApplyResolvedPaymentStatus::class);
|
||||
Event::listen(PaymentAuthorized::class, ApplyResolvedPaymentStatus::class);
|
||||
|
||||
NotificationRegistry::get()->register([
|
||||
OrderDeliveredNotification::class,
|
||||
|
||||
@@ -38,5 +38,7 @@ class PaymentServiceProvider extends ServiceProvider
|
||||
}
|
||||
|
||||
config(['lunar.cart.pipelines.cart' => $cartPipeline]);
|
||||
|
||||
$this->loadRoutesFrom(__DIR__ . '/../Payment/routes/webhooks.php');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user