Feat: Tying Specific Methods with Carrier Drivers
This commit is contained in:
@@ -10,6 +10,7 @@ use Lunar\Base\Addressable;
|
|||||||
use Lunar\DataTypes\ShippingOption;
|
use Lunar\DataTypes\ShippingOption;
|
||||||
use Lunar\Facades\ShippingManifest;
|
use Lunar\Facades\ShippingManifest;
|
||||||
use Lunar\Models\Cart;
|
use Lunar\Models\Cart;
|
||||||
|
use Lunar\Shipping\Models\ShippingMethod;
|
||||||
use Modules\Core\Cart\Services\CartService;
|
use Modules\Core\Cart\Services\CartService;
|
||||||
use Modules\Core\Checkout\Events\BillingAddressSet;
|
use Modules\Core\Checkout\Events\BillingAddressSet;
|
||||||
use Modules\Core\Checkout\Events\PaymentMethodSelected;
|
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\NoShippingAddressException;
|
||||||
use Modules\Core\Checkout\Exceptions\TermsNotAcceptedException;
|
use Modules\Core\Checkout\Exceptions\TermsNotAcceptedException;
|
||||||
use Modules\Core\Checkout\Exceptions\UnknownPaymentTypeException;
|
use Modules\Core\Checkout\Exceptions\UnknownPaymentTypeException;
|
||||||
|
use Modules\Core\Payment\Contracts\RequiresFulfillmentType;
|
||||||
use Modules\Core\Payment\DTOs\PaymentResult;
|
use Modules\Core\Payment\DTOs\PaymentResult;
|
||||||
use Modules\Core\Payment\Models\PaymentMethod;
|
use Modules\Core\Payment\Models\PaymentMethod;
|
||||||
use Modules\Core\Payment\Services\PaymentDriverRegistry;
|
use Modules\Core\Payment\Services\PaymentDriverRegistry;
|
||||||
use Modules\Core\Payment\Services\PaymentMethodCache;
|
use Modules\Core\Payment\Services\PaymentMethodCache;
|
||||||
|
use Modules\Core\Shipping\Support\FulfillmentType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Storefront-facing checkout operations, mirroring
|
* Storefront-facing checkout operations, mirroring
|
||||||
@@ -232,7 +235,7 @@ class CheckoutService
|
|||||||
/**
|
/**
|
||||||
* Every payment method currently offered to the storefront, ordered by
|
* Every payment method currently offered to the storefront, ordered by
|
||||||
* Modules\Core\Payment\Models\PaymentMethod::position — a row is
|
* 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
|
* different to an admin diagnosing why a method isn't showing up (see
|
||||||
* docs/payments.md):
|
* docs/payments.md):
|
||||||
* 1. `enabled` — an admin turned it on.
|
* 1. `enabled` — an admin turned it on.
|
||||||
@@ -243,17 +246,61 @@ class CheckoutService
|
|||||||
* vanished driver can never silently look "available").
|
* vanished driver can never silently look "available").
|
||||||
* 3. the resolved driver reports Configurable::isConfigured() — its
|
* 3. the resolved driver reports Configurable::isConfigured() — its
|
||||||
* own runtime requirements (e.g. an API key) are met.
|
* 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<int, PaymentMethod>
|
* @return Collection<int, PaymentMethod>
|
||||||
*/
|
*/
|
||||||
public function getPaymentMethods(): Collection
|
public function getPaymentMethods(): Collection
|
||||||
{
|
{
|
||||||
|
$fulfillmentType = $this->currentFulfillmentType();
|
||||||
|
|
||||||
return $this->paymentMethods->all()
|
return $this->paymentMethods->all()
|
||||||
->filter(fn (PaymentMethod $method) => $method->enabled && $method->driver_missing_at === null)
|
->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();
|
->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
|
* Records which payment type the shopper picked (Cart::meta
|
||||||
* ['payment_method']) — read by Modules\Core\Payment\Pipelines\
|
* ['payment_method']) — read by Modules\Core\Payment\Pipelines\
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Core\Payment\Contracts;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional contract a payment driver implements to declare it only makes
|
||||||
|
* sense for one fulfillment type — the payment-side mirror of
|
||||||
|
* Modules\Core\Shipping\Contracts\DeclaresFulfillmentType. Two concrete
|
||||||
|
* cases exist today, both hardcoded facts about the driver rather than a
|
||||||
|
* merchant configuration choice:
|
||||||
|
* - OfflinePaymentDriver ("pay in store," cash-in-hand) only makes
|
||||||
|
* sense when the shopper collects in person — meaningless for a
|
||||||
|
* carrier delivery, where no staff member is present to take the
|
||||||
|
* cash.
|
||||||
|
* - CashOnDeliveryPaymentDriver only makes sense when a carrier
|
||||||
|
* physically hands over the parcel and collects payment at that
|
||||||
|
* moment — meaningless for store pickup, which already has
|
||||||
|
* OfflinePaymentDriver for exactly that in-person moment.
|
||||||
|
*
|
||||||
|
* A driver that doesn't implement this (Stripe, bank transfer) has no
|
||||||
|
* fulfillment-type constraint — offered regardless of the cart's
|
||||||
|
* currently selected shipping method's fulfillment type.
|
||||||
|
*
|
||||||
|
* Read by Modules\Core\Checkout\Services\CheckoutService::
|
||||||
|
* getPaymentMethods(), which excludes a method whose driver implements
|
||||||
|
* this and disagrees with the cart's current fulfillment type (via
|
||||||
|
* Modules\Core\Shipping\Support\FulfillmentType::resolve() on the
|
||||||
|
* currently selected ShippingMethod). A cart with no shipping option
|
||||||
|
* selected yet imposes no constraint — every method is offered until a
|
||||||
|
* fulfillment type is actually known.
|
||||||
|
*/
|
||||||
|
interface RequiresFulfillmentType
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return 'carrier'|'store_pickup'
|
||||||
|
*/
|
||||||
|
public function requiredFulfillmentType(): string;
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ namespace Modules\Core\Payment\Drivers;
|
|||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Lunar\DataTypes\Price;
|
use Lunar\DataTypes\Price;
|
||||||
use Modules\Core\Payment\Contracts\Configurable;
|
use Modules\Core\Payment\Contracts\Configurable;
|
||||||
|
use Modules\Core\Payment\Contracts\RequiresFulfillmentType;
|
||||||
use Modules\Core\Payment\Contracts\SupportsPay;
|
use Modules\Core\Payment\Contracts\SupportsPay;
|
||||||
use Modules\Core\Payment\DTOs\PaymentResult;
|
use Modules\Core\Payment\DTOs\PaymentResult;
|
||||||
use Modules\Core\Payment\Enums\PaymentResultStatus;
|
use Modules\Core\Payment\Enums\PaymentResultStatus;
|
||||||
@@ -44,13 +45,24 @@ use Modules\Core\Payment\Events\PaymentDeferred;
|
|||||||
* — a real bug, not a hypothetical, caught and fixed after the fact. See
|
* — a real bug, not a hypothetical, caught and fixed after the fact. See
|
||||||
* PaymentDeferred's own docblock for the full reasoning.
|
* PaymentDeferred's own docblock for the full reasoning.
|
||||||
*/
|
*/
|
||||||
class CashOnDeliveryPaymentDriver implements Configurable, SupportsPay
|
class CashOnDeliveryPaymentDriver implements Configurable, SupportsPay, RequiresFulfillmentType
|
||||||
{
|
{
|
||||||
public function isConfigured(): bool
|
public function isConfigured(): bool
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "On delivery" is the operative word — a carrier physically hands
|
||||||
|
* over the parcel and collects payment at that moment. Meaningless
|
||||||
|
* for store pickup, which already has OfflinePaymentDriver for the
|
||||||
|
* equivalent in-person moment.
|
||||||
|
*/
|
||||||
|
public function requiredFulfillmentType(): string
|
||||||
|
{
|
||||||
|
return 'carrier';
|
||||||
|
}
|
||||||
|
|
||||||
public function pay(string $type, Price $amount, array $data = [], array $context = []): PaymentResult
|
public function pay(string $type, Price $amount, array $data = [], array $context = []): PaymentResult
|
||||||
{
|
{
|
||||||
$result = new PaymentResult(
|
$result = new PaymentResult(
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace Modules\Core\Payment\Drivers;
|
|||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Lunar\DataTypes\Price;
|
use Lunar\DataTypes\Price;
|
||||||
use Modules\Core\Payment\Contracts\Configurable;
|
use Modules\Core\Payment\Contracts\Configurable;
|
||||||
|
use Modules\Core\Payment\Contracts\RequiresFulfillmentType;
|
||||||
use Modules\Core\Payment\Contracts\SupportsPay;
|
use Modules\Core\Payment\Contracts\SupportsPay;
|
||||||
use Modules\Core\Payment\DTOs\PaymentResult;
|
use Modules\Core\Payment\DTOs\PaymentResult;
|
||||||
use Modules\Core\Payment\Enums\PaymentResultStatus;
|
use Modules\Core\Payment\Enums\PaymentResultStatus;
|
||||||
@@ -25,7 +26,7 @@ use Modules\Core\Payment\Events\PaymentCaptured;
|
|||||||
* none) purely so PaymentCaptured, and anything downstream keying on it,
|
* none) purely so PaymentCaptured, and anything downstream keying on it,
|
||||||
* have something to identify this attempt by.
|
* have something to identify this attempt by.
|
||||||
*/
|
*/
|
||||||
class OfflinePaymentDriver implements Configurable, SupportsPay
|
class OfflinePaymentDriver implements Configurable, SupportsPay, RequiresFulfillmentType
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Always true — no external dependency to be missing.
|
* Always true — no external dependency to be missing.
|
||||||
@@ -35,6 +36,16 @@ class OfflinePaymentDriver implements Configurable, SupportsPay
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cash-in-hand requires a staff member physically present to take the
|
||||||
|
* payment — meaningless for a carrier delivery, where no such person
|
||||||
|
* exists at handoff.
|
||||||
|
*/
|
||||||
|
public function requiredFulfillmentType(): string
|
||||||
|
{
|
||||||
|
return 'store_pickup';
|
||||||
|
}
|
||||||
|
|
||||||
public function pay(string $type, Price $amount, array $data = [], array $context = []): PaymentResult
|
public function pay(string $type, Price $amount, array $data = [], array $context = []): PaymentResult
|
||||||
{
|
{
|
||||||
$reference = 'offline-'.Str::uuid();
|
$reference = 'offline-'.Str::uuid();
|
||||||
|
|||||||
Reference in New Issue
Block a user