From 609a63c2f4d0c7e57725ee6f104873d3f07e295a Mon Sep 17 00:00:00 2001 From: Konstantinos Arvanitakis Date: Fri, 18 Sep 2026 01:23:50 +0300 Subject: [PATCH] Feat: Tying Specific Methods with Carrier Drivers --- src/Checkout/Services/CheckoutService.php | 51 ++++++++++++++++++- .../Contracts/RequiresFulfillmentType.php | 38 ++++++++++++++ .../Drivers/CashOnDeliveryPaymentDriver.php | 14 ++++- src/Payment/Drivers/OfflinePaymentDriver.php | 13 ++++- 4 files changed, 112 insertions(+), 4 deletions(-) create mode 100644 src/Payment/Contracts/RequiresFulfillmentType.php diff --git a/src/Checkout/Services/CheckoutService.php b/src/Checkout/Services/CheckoutService.php index 66eaf64..1d33b24 100644 --- a/src/Checkout/Services/CheckoutService.php +++ b/src/Checkout/Services/CheckoutService.php @@ -10,6 +10,7 @@ use Lunar\Base\Addressable; use Lunar\DataTypes\ShippingOption; use Lunar\Facades\ShippingManifest; use Lunar\Models\Cart; +use Lunar\Shipping\Models\ShippingMethod; use Modules\Core\Cart\Services\CartService; use Modules\Core\Checkout\Events\BillingAddressSet; use Modules\Core\Checkout\Events\PaymentMethodSelected; @@ -20,10 +21,12 @@ use Modules\Core\Checkout\Exceptions\InvalidShippingOptionException; use Modules\Core\Checkout\Exceptions\NoShippingAddressException; use Modules\Core\Checkout\Exceptions\TermsNotAcceptedException; use Modules\Core\Checkout\Exceptions\UnknownPaymentTypeException; +use Modules\Core\Payment\Contracts\RequiresFulfillmentType; use Modules\Core\Payment\DTOs\PaymentResult; use Modules\Core\Payment\Models\PaymentMethod; use Modules\Core\Payment\Services\PaymentDriverRegistry; use Modules\Core\Payment\Services\PaymentMethodCache; +use Modules\Core\Shipping\Support\FulfillmentType; /** * Storefront-facing checkout operations, mirroring @@ -232,7 +235,7 @@ class CheckoutService /** * Every payment method currently offered to the storefront, ordered by * Modules\Core\Payment\Models\PaymentMethod::position — a row is - * offered only when ALL three checks pass, each meaning something + * offered only when ALL four checks pass, each meaning something * different to an admin diagnosing why a method isn't showing up (see * docs/payments.md): * 1. `enabled` — an admin turned it on. @@ -243,17 +246,61 @@ class CheckoutService * vanished driver can never silently look "available"). * 3. the resolved driver reports Configurable::isConfigured() — its * own runtime requirements (e.g. an API key) are met. + * 4. its driver's RequiresFulfillmentType (if it declares one) + * agrees with the cart's currently selected shipping method's own + * fulfillment type (Modules\Core\Shipping\Support\ + * FulfillmentType::resolve()) — "Pay in store" offered alongside + * a courier delivery makes no sense (no staff member present at + * handoff to take cash), and cash-on-delivery alongside store + * pickup is equally meaningless (OfflinePaymentDriver already + * covers that in-person moment). A cart with no shipping option + * selected yet imposes no constraint here — every method is + * offered until a fulfillment type is actually known, the same + * leniency setShippingAddress()'s own docblock describes for + * required-field enforcement happening at the payment gate, not + * mid-checkout. * * @return Collection */ public function getPaymentMethods(): Collection { + $fulfillmentType = $this->currentFulfillmentType(); + return $this->paymentMethods->all() ->filter(fn (PaymentMethod $method) => $method->enabled && $method->driver_missing_at === null) - ->filter(fn (PaymentMethod $method) => $this->paymentDrivers->resolve($method->driver)?->isConfigured() ?? false) + ->filter(function (PaymentMethod $method) use ($fulfillmentType) { + $driver = $this->paymentDrivers->resolve($method->driver); + + if (! $driver?->isConfigured()) { + return false; + } + + if ($fulfillmentType === null || ! $driver instanceof RequiresFulfillmentType) { + return true; + } + + return $driver->requiredFulfillmentType() === $fulfillmentType; + }) ->values(); } + /** + * @return 'carrier'|'store_pickup'|null null when the cart has no + * shipping option selected yet + */ + private function currentFulfillmentType(): ?string + { + $identifier = $this->cart->currentOrCreate()->shippingAddress?->shipping_option; + + if ($identifier === null) { + return null; + } + + $method = ShippingMethod::where('code', $identifier)->first(); + + return $method ? FulfillmentType::resolve($method) : null; + } + /** * Records which payment type the shopper picked (Cart::meta * ['payment_method']) — read by Modules\Core\Payment\Pipelines\ diff --git a/src/Payment/Contracts/RequiresFulfillmentType.php b/src/Payment/Contracts/RequiresFulfillmentType.php new file mode 100644 index 0000000..2bd143d --- /dev/null +++ b/src/Payment/Contracts/RequiresFulfillmentType.php @@ -0,0 +1,38 @@ +