79 lines
3.2 KiB
PHP
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;
|
|
}
|
|
}
|