diff --git a/src/Checkout/Contracts/PaymentDriver.php b/src/Checkout/Contracts/PaymentDriver.php deleted file mode 100644 index f03c302..0000000 --- a/src/Checkout/Contracts/PaymentDriver.php +++ /dev/null @@ -1,62 +0,0 @@ - $id], a redirect-based - * provider: its callback payload) — passed explicitly by the caller - * (a controller, a webhook job) rather than a driver reaching into the - * global request(), so confirm() works the same whether it's called - * from a synchronous HTTP request or an async webhook/job with no - * active request at all. - * - * @param array $data - * - * @throws FingerprintMismatchException - * @throws CartException - */ - public function confirm(Cart $cart, string $type, string $fingerprint, array $data): Order; -} diff --git a/src/Checkout/Events/PaymentConfirmed.php b/src/Checkout/Events/PaymentConfirmed.php deleted file mode 100644 index ceded07..0000000 --- a/src/Checkout/Events/PaymentConfirmed.php +++ /dev/null @@ -1,36 +0,0 @@ -meta['payment_method'] against its own - * type(s) to recognize which OrderPlaced is its own — carrying $fingerprint - * here too lets a driver correlate its own OrderPlaced listener call back - * to the specific confirmation that triggered it, if it needs to. - */ -class PaymentConfirmed -{ - use Dispatchable; - - public function __construct( - public readonly Cart $cart, - public readonly string $type, - public readonly string $fingerprint, - public readonly array $data = [], - ) {} -} diff --git a/src/Payment/Contracts/HandlesPaymentCallback.php b/src/Payment/Contracts/HandlesPaymentCallback.php new file mode 100644 index 0000000..bce6500 --- /dev/null +++ b/src/Payment/Contracts/HandlesPaymentCallback.php @@ -0,0 +1,50 @@ + $id], a redirect-based provider: its + * query params or POST body) — passed explicitly by the caller rather + * than the driver reaching into the global request(), so this works + * the same whether it's called from a synchronous HTTP request or an + * async webhook job with no active request at all. + * + * $context is opaque to the driver, carried through untouched into + * whichever Payment event this callback produces — see SupportsPay:: + * pay()'s own $context param for the full reasoning. A driver that + * needs the ORIGINAL context from the pay()/authorize() call (a + * webhook's own payload carries none of its own) must have persisted + * it itself when that call returned Pending — Payment provides no + * storage for this. + * + * @param array $data + * @param array $context + */ + public function handleCallback(string $reference, array $data, array $context = []): PaymentResult; +} diff --git a/src/Payment/Contracts/InitiatesPayment.php b/src/Payment/Contracts/InitiatesPayment.php deleted file mode 100644 index d042080..0000000 --- a/src/Payment/Contracts/InitiatesPayment.php +++ /dev/null @@ -1,54 +0,0 @@ - $data - * @param array $context - */ - public function initiate(string $type, array $data, array $context = []): PaymentInitiation; -} diff --git a/src/Payment/Contracts/PaymentDriver.php b/src/Payment/Contracts/PaymentDriver.php deleted file mode 100644 index 1f128bf..0000000 --- a/src/Payment/Contracts/PaymentDriver.php +++ /dev/null @@ -1,65 +0,0 @@ -meta['payment_method'] — see PaymentConfirmed's docblock for - * why. This split is what makes an async/webhook-driven gateway (payment - * confirmed in a request that has no synchronous caller waiting for an - * Order at all) and a synchronous one (Stripe) work through the exact same - * contract. See docs/checkout.md / docs/payments.md. - */ -interface PaymentDriver -{ - /** - * Whether this driver can actually be used right now — e.g. Stripe - * checking its own API key is present, an offline-style driver always - * returning true since it has no external dependency. Independent of - * Modules\Core\Payment\Models\PaymentMethod::enabled (the admin - * on/off toggle) — CheckoutService::getPaymentMethods() combines both: - * a type is only offered to the storefront if it's administratively - * enabled AND its driver reports itself configured. - */ - public function isConfigured(): bool; - - /** - * $type is the payment type key being confirmed (e.g. 'cash-in-hand', - * 'cash-on-delivery', 'stripe') — passed through even though most - * drivers only ever serve one type, because a driver shared across - * several types (e.g. one "no real confirmation" offline driver behind - * both cash-in-hand and cash-on-delivery) needs it to look up that - * type's own config (e.g. its 'authorized' status) rather than another - * type's. - * - * $data carries whatever the gateway needs to confirm this specific - * payment (Stripe: ['payment_intent' => $id], a redirect-based - * provider: its callback payload) — passed explicitly by the caller - * (a controller, a webhook job) rather than a driver reaching into the - * global request(), so confirm() works the same whether it's called - * from a synchronous HTTP request or an async webhook/job with no - * active request at all. - * - * @param array $data - * - * @throws FingerprintMismatchException - * @throws CartException - */ - public function confirm(Cart $cart, string $type, string $fingerprint, array $data): void; -} diff --git a/src/Payment/Contracts/SupportsAuthorization.php b/src/Payment/Contracts/SupportsAuthorization.php new file mode 100644 index 0000000..65a58bb --- /dev/null +++ b/src/Payment/Contracts/SupportsAuthorization.php @@ -0,0 +1,36 @@ +status is Pending — see SupportsPay's docblock for the + * same async-resolution note. + */ +interface SupportsAuthorization +{ + /** + * Same $type/$data/$context reasoning as SupportsPay::pay(). + * + * @param array $data + * @param array $context + */ + public function authorize(string $type, array $data, array $context = []): PaymentResult; +} \ No newline at end of file diff --git a/src/Payment/Contracts/SupportsCaptures.php b/src/Payment/Contracts/SupportsCaptures.php index a704d0f..6b6fa59 100644 --- a/src/Payment/Contracts/SupportsCaptures.php +++ b/src/Payment/Contracts/SupportsCaptures.php @@ -2,22 +2,33 @@ namespace Modules\Core\Payment\Contracts; -use Lunar\Models\Order; -use Modules\Core\Payment\DataTransferObjects\CaptureResult; +use Modules\Core\Payment\DTOs\PaymentResult; /** - * Optional capability for payment drivers whose gateway supports a - * separate authorize-then-capture step. Many redirect/wallet-style - * gateways (Viva Wallet included, for most flows) charge in full at - * checkout and never need this — SupportsRefunds is the one they're more - * likely to implement instead. + * Settles a PRIOR SupportsAuthorization::authorize() hold — only ever + * valid against a reference that call (or a HandlesPaymentCallback + * resolving it) produced, never called standalone. A driver with no + * authorize-then-settle model at all (most redirect/wallet gateways, any + * offline driver) never implements this — it settles everything through + * SupportsPay::pay() in one step instead. + * + * Dispatches Modules\Core\Payment\Events\PaymentCaptured or + * PaymentCaptureFailed — the same terminal events SupportsPay::pay() + * produces, since "money has been captured" is the same business fact + * regardless of which path reached it. */ interface SupportsCaptures { /** - * $reference is the gateway's own identifier for the authorized charge - * — see SupportsRefunds::refund() for why this isn't a Lunar - * Transaction. $amount is in the currency's minor unit. + * $reference is the identifier SupportsAuthorization::authorize() + * returned (PaymentResult::$reference) for the hold being settled. + * + * $amount lets a driver capture less than the full authorized amount + * (e.g. shipping less than ordered) — up to the driver/gateway + * whether a partial capture also releases the remainder or leaves it + * capturable again later (multicapture-style gateways). + * + * @param array $context */ - public function capture(Order $order, string $reference, int $amount, ?string $notes = null): CaptureResult; -} + public function capture(string $reference, int $amount, array $context = []): PaymentResult; +} \ No newline at end of file diff --git a/src/Payment/Contracts/SupportsPay.php b/src/Payment/Contracts/SupportsPay.php new file mode 100644 index 0000000..b8272ac --- /dev/null +++ b/src/Payment/Contracts/SupportsPay.php @@ -0,0 +1,45 @@ +status is Pending (an async gateway that hasn't + * resolved yet — see HandlesPaymentCallback for how that gets resolved + * later, from a separate call this method's return value does not wait + * on). + */ +interface SupportsPay +{ + /** + * $type is the payment type key being charged (e.g. 'cash-on-delivery', + * 'stripe') — passed through even though most drivers only ever serve + * one type, because a driver shared across several types needs it to + * look up that type's own config. + * + * $data carries whatever the gateway needs (amount, currency, customer + * details, a payment method token) — the caller's responsibility to + * assemble, since a driver has no notion of a cart or order to pull + * them from itself. + * + * $context is opaque to the driver — carried through untouched into + * whichever Payment event this call (or a later handleCallback() + * resolving it) produces, so the caller can correlate the result back + * to whatever it needs, without Payment ever needing to know what + * that is. + * + * @param array $data + * @param array $context + */ + public function pay(string $type, array $data, array $context = []): PaymentResult; +} \ No newline at end of file diff --git a/src/Payment/Contracts/SupportsRefunds.php b/src/Payment/Contracts/SupportsRefunds.php index d719a05..1ace080 100644 --- a/src/Payment/Contracts/SupportsRefunds.php +++ b/src/Payment/Contracts/SupportsRefunds.php @@ -2,23 +2,30 @@ namespace Modules\Core\Payment\Contracts; -use Lunar\Models\Order; -use Modules\Core\Payment\DataTransferObjects\RefundResult; +use Modules\Core\Payment\DTOs\PaymentResult; /** - * Optional capability for payment drivers whose gateway supports refunding - * a prior charge. Drivers without a refund API (or that never got that far - * — e.g. an offline/manual driver) simply don't implement it. Mirrors - * Shipping\Contracts\SupportsTracking's opt-in shape. + * Reverses settled funds — independent of SupportsCaptures/SupportsVoids: + * a driver that only ever settles via SupportsPay::pay() (no separate + * authorize step) can still implement this, since a refund targets money + * already taken regardless of how it got taken. A driver implements this + * whenever its gateway exposes any refund capability at all, whether or + * not it also supports authorize-then-capture. + * + * Dispatches Modules\Core\Payment\Events\PaymentRefunded or + * PaymentRefundFailed. */ interface SupportsRefunds { /** - * $reference is the gateway's own identifier for the charge being - * refunded (e.g. a Viva Wallet transaction id) — not a Lunar - * Transaction model, since not every gateway's refund flow maps - * cleanly onto one. $amount is in the currency's minor unit, same - * convention as Lunar\Base\Casts\Price. + * $reference is the identifier the original SupportsPay::pay() or + * SupportsCaptures::capture() call returned for the settled funds + * being refunded. + * + * $amount allows a partial refund; a gateway may allow multiple + * partial refunds against one settlement, up to its own total. + * + * @param array $context */ - public function refund(Order $order, string $reference, int $amount, ?string $notes = null): RefundResult; + public function refund(string $reference, int $amount, array $context = []): PaymentResult; } diff --git a/src/Payment/Contracts/SupportsVoids.php b/src/Payment/Contracts/SupportsVoids.php new file mode 100644 index 0000000..df23292 --- /dev/null +++ b/src/Payment/Contracts/SupportsVoids.php @@ -0,0 +1,26 @@ + $context + */ + public function void(string $reference, array $context = []): PaymentResult; +} \ No newline at end of file diff --git a/src/Payment/DTOs/PaymentResult.php b/src/Payment/DTOs/PaymentResult.php new file mode 100644 index 0000000..a0e67e0 --- /dev/null +++ b/src/Payment/DTOs/PaymentResult.php @@ -0,0 +1,52 @@ +retriable is how + * a listener knows whether "try again with the same method" is reasonable + * — see PaymentResult's own docblock. + */ +class PaymentAuthorizationFailed +{ + use Dispatchable; + + /** + * @param array $context + */ + public function __construct( + public readonly string $type, + public readonly PaymentResult $result, + public readonly array $context = [], + ) {} +} diff --git a/src/Payment/Events/PaymentAuthorized.php b/src/Payment/Events/PaymentAuthorized.php new file mode 100644 index 0000000..53f16b9 --- /dev/null +++ b/src/Payment/Events/PaymentAuthorized.php @@ -0,0 +1,32 @@ + $context + */ + public function __construct( + public readonly string $type, + public readonly PaymentResult $result, + public readonly array $context = [], + ) {} +} \ No newline at end of file diff --git a/src/Payment/Events/PaymentCaptureFailed.php b/src/Payment/Events/PaymentCaptureFailed.php new file mode 100644 index 0000000..e5f8b7d --- /dev/null +++ b/src/Payment/Events/PaymentCaptureFailed.php @@ -0,0 +1,29 @@ + $context + */ + public function __construct( + public readonly string $type, + public readonly PaymentResult $result, + public readonly array $context = [], + ) {} +} \ No newline at end of file diff --git a/src/Payment/Events/PaymentCaptured.php b/src/Payment/Events/PaymentCaptured.php new file mode 100644 index 0000000..fb2502d --- /dev/null +++ b/src/Payment/Events/PaymentCaptured.php @@ -0,0 +1,31 @@ + $context + */ + public function __construct( + public readonly string $type, + public readonly PaymentResult $result, + public readonly array $context = [], + ) {} +} \ No newline at end of file diff --git a/src/Payment/Events/PaymentRefundFailed.php b/src/Payment/Events/PaymentRefundFailed.php new file mode 100644 index 0000000..faaab7f --- /dev/null +++ b/src/Payment/Events/PaymentRefundFailed.php @@ -0,0 +1,24 @@ + $context + */ + public function __construct( + public readonly string $type, + public readonly PaymentResult $result, + public readonly array $context = [], + ) {} +} \ No newline at end of file diff --git a/src/Payment/Events/PaymentRefunded.php b/src/Payment/Events/PaymentRefunded.php new file mode 100644 index 0000000..cc6c74c --- /dev/null +++ b/src/Payment/Events/PaymentRefunded.php @@ -0,0 +1,27 @@ + $context + */ + public function __construct( + public readonly string $type, + public readonly PaymentResult $result, + public readonly array $context = [], + ) {} +} \ No newline at end of file diff --git a/src/Payment/Events/PaymentSucceeded.php b/src/Payment/Events/PaymentSucceeded.php deleted file mode 100644 index f841c0a..0000000 --- a/src/Payment/Events/PaymentSucceeded.php +++ /dev/null @@ -1,39 +0,0 @@ - $context - */ - public function __construct( - public readonly string $type, - public readonly string $reference, - public readonly int $amount, - public readonly array $context = [], - ) {} -} diff --git a/src/Payment/Events/PaymentVoidFailed.php b/src/Payment/Events/PaymentVoidFailed.php new file mode 100644 index 0000000..4bde3e2 --- /dev/null +++ b/src/Payment/Events/PaymentVoidFailed.php @@ -0,0 +1,25 @@ + $context + */ + public function __construct( + public readonly string $type, + public readonly PaymentResult $result, + public readonly array $context = [], + ) {} +} \ No newline at end of file diff --git a/src/Payment/Events/PaymentVoided.php b/src/Payment/Events/PaymentVoided.php new file mode 100644 index 0000000..d2d28b6 --- /dev/null +++ b/src/Payment/Events/PaymentVoided.php @@ -0,0 +1,27 @@ + $context + */ + public function __construct( + public readonly string $type, + public readonly PaymentResult $result, + public readonly array $context = [], + ) {} +}