Files
core/src/Payment/Drivers/CashOnDeliveryPaymentDriver.php
T

79 lines
3.2 KiB
PHP

<?php
namespace Modules\Core\Payment\Drivers;
use Illuminate\Support\Str;
use Lunar\DataTypes\Price;
use Modules\Core\Payment\Contracts\Configurable;
use Modules\Core\Payment\Contracts\RequiresFulfillmentType;
use Modules\Core\Payment\Contracts\SupportsPay;
use Modules\Core\Payment\DTOs\PaymentResult;
use Modules\Core\Payment\Enums\PaymentResultStatus;
use Modules\Core\Payment\Events\PaymentDeferred;
/**
* Cash-on-delivery/cash-on-pickup — the shopper pays staff in person, at
* delivery or pickup, not at checkout, and reconciliation can happen
* anywhere from same-day to months later, entirely independent of the
* order's fulfillment progress (this is WHY Order::paid is its own field,
* not a status-sequence step — see Modules\Core\Order\Services\
* OrderStatusFlow's own docblock).
*
* Unlike OfflinePaymentDriver (cash-in-hand, immediate capture), pay()
* here must NOT dispatch PaymentCaptured — doing so would immediately
* flip Order::paid via Modules\Core\Order\Listeners\
* ApplyResolvedPaymentStatus, which is exactly wrong: no money has
* changed hands yet. Returns PaymentResultStatus::Pending instead — the
* documented convention for "unresolved" (see SupportsPay's own
* docblock). ApplyResolvedPaymentStatus and RecordPaymentTransaction both
* only listen to Captured/Authorized/Voided/Refunded, so a Pending result
* triggers neither.
*
* Order::paid only ever becomes true for a COD order via staff explicitly
* marking it received (Modules\Core\Order\Services\
* OrderFulfillmentService::markPaid()), offered by the single "Update
* Status" action at any time, independent of status.
*
* Despite returning Pending, this order IS fully placed the moment pay()
* returns — unlike a Stripe 3-D Secure Pending, nothing will ever resolve
* this into a later PaymentCaptured/PaymentAuthorized (COD has no gateway
* callback at all). Without PaymentDeferred, no listener ever set
* Order::placed_at for a COD order: invisible in customer order history,
* no stock decrement (Modules\Core\Order\Listeners\
* DecrementStockOnOrderPlaced only reacts to Checkout\Events\OrderPlaced),
* and the storefront's own post-checkout confirmation could never find it
* — a real bug, not a hypothetical, caught and fixed after the fact. See
* PaymentDeferred's own docblock for the full reasoning.
*/
class CashOnDeliveryPaymentDriver implements Configurable, SupportsPay, RequiresFulfillmentType
{
public function isConfigured(): bool
{
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
{
$result = new PaymentResult(
status: PaymentResultStatus::Pending,
reference: 'cod-'.Str::uuid(),
amount: $amount,
);
PaymentDeferred::dispatch($type, $result, $context);
return $result;
}
}