Feature: Order Updates, Events, Order Flows, Shipment And COD support
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
<?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\SupportsPay;
|
||||
use Modules\Core\Payment\DTOs\PaymentResult;
|
||||
use Modules\Core\Payment\Enums\PaymentResultStatus;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
class CashOnDeliveryPaymentDriver implements Configurable, SupportsPay
|
||||
{
|
||||
public function isConfigured(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function pay(string $type, Price $amount, array $data = [], array $context = []): PaymentResult
|
||||
{
|
||||
return new PaymentResult(
|
||||
status: PaymentResultStatus::Pending,
|
||||
reference: 'cod-'.Str::uuid(),
|
||||
amount: $amount,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -11,9 +11,12 @@ use Modules\Core\Payment\Enums\PaymentResultStatus;
|
||||
use Modules\Core\Payment\Events\PaymentCaptured;
|
||||
|
||||
/**
|
||||
* Shared by every payment type with no real gateway to confirm against —
|
||||
* cash-in-hand, cash-on-delivery — where the shopper pays at pickup/on
|
||||
* delivery, not at checkout. There is no separate hold-then-settle model
|
||||
* Cash-in-hand — a shopper paying in person at the moment of pickup, with
|
||||
* nothing left to reconcile afterward, so capture is immediate. NOT used
|
||||
* for cash-on-delivery, which has its own Modules\Core\Payment\Drivers\
|
||||
* CashOnDeliveryPaymentDriver — COD payment happens at an unpredictable
|
||||
* later time (same-day to months), so it must not capture immediately the
|
||||
* way this driver does. There is no separate hold-then-settle model
|
||||
* (SupportsAuthorization/SupportsCaptures/SupportsVoids) and no async
|
||||
* resolution (HandlesPaymentCallback) — pay() decides success immediately
|
||||
* and dispatches PaymentCaptured before returning.
|
||||
|
||||
@@ -7,7 +7,6 @@ use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Components\Component;
|
||||
use Filament\Schemas\Components\Utilities\Get;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Columns\ToggleColumn;
|
||||
@@ -55,11 +54,6 @@ use Modules\Core\Payment\Services\PaymentMethodService;
|
||||
* admin needs the same at-a-glance warning for it, not just a silently
|
||||
* absent checkout option.
|
||||
*
|
||||
* `authorized_status` only appears in the form when `capture_mode` is
|
||||
* "Hold now, charge later" — it's simply unreachable for a "Charge
|
||||
* immediately" method (that mode only ever produces PaymentCaptured,
|
||||
* never PaymentAuthorized), so showing it unconditionally would just be
|
||||
* a confusing, always-irrelevant field for most methods.
|
||||
*/
|
||||
class PaymentMethodResource extends Resource
|
||||
{
|
||||
@@ -151,13 +145,6 @@ class PaymentMethodResource extends Resource
|
||||
->default('pay')
|
||||
->live()
|
||||
->required(),
|
||||
static::getOrderStatusSelect('captured_status', 'Order status once paid')
|
||||
->helperText('Applied the moment a payment is fully charged.'),
|
||||
static::getOrderStatusSelect('authorized_status', 'Order status once held')
|
||||
->helperText('Applied the moment a hold is placed, before it\'s charged.')
|
||||
->visible(fn (Get $get) => $get('capture_mode') === 'authorize'),
|
||||
static::getOrderStatusSelect('refunded_status', 'Order status once refunded')
|
||||
->helperText('Applied when a payment taken through this method is refunded — even if the refund itself is processed through a different method.'),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -169,24 +156,6 @@ class PaymentMethodResource extends Resource
|
||||
->required();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lunar's own Order::status is a plain, admin-extensible string
|
||||
* (config('lunar.orders.statuses')) rather than a fixed enum —
|
||||
* deliberately so a store can add its own custom status without a
|
||||
* code change (see docs/payments.md). This Select still reads from
|
||||
* that same open-ended list, just so an admin picks a real status
|
||||
* instead of typing a slug from memory.
|
||||
*/
|
||||
private static function getOrderStatusSelect(string $name, string $label): Select
|
||||
{
|
||||
return Select::make($name)
|
||||
->label($label)
|
||||
->options(collect(config('lunar.orders.statuses', []))
|
||||
->map(fn (array $status) => $status['label'] ?? $status)
|
||||
->all())
|
||||
->native(false);
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
@@ -211,7 +180,7 @@ class PaymentMethodResource extends Resource
|
||||
->icon('heroicon-o-pencil-square')
|
||||
->schema(static::getFormComponents())
|
||||
->fillForm(fn (PaymentMethod $record) => $record->only([
|
||||
'name', 'type', 'driver', 'capture_mode', 'captured_status', 'authorized_status', 'refunded_status',
|
||||
'name', 'type', 'driver', 'capture_mode',
|
||||
]))
|
||||
->action(fn (PaymentMethod $record, array $data) => app(PaymentMethodService::class)->update($record, $data));
|
||||
}
|
||||
|
||||
@@ -18,16 +18,6 @@ use Illuminate\Database\Eloquent\Model;
|
||||
* - capture_mode: 'pay' or 'authorize' — which SupportsPay/
|
||||
* SupportsAuthorization method CheckoutService::initiatePayment()
|
||||
* calls for this row.
|
||||
* - captured_status / authorized_status / refunded_status: the
|
||||
* Order::status value Modules\Core\Order\Listeners\
|
||||
* ApplyResolvedPaymentStatus applies on a PaymentCaptured/
|
||||
* PaymentAuthorized/PaymentRefunded event. For a refund, this is
|
||||
* always the ORIGINAL payment method's row (the one the customer
|
||||
* actually paid with), never the driver the refund itself was routed
|
||||
* through (Payment\Support\TransactionDriverAdapter::refundVia() may
|
||||
* use a different one entirely — e.g. a cash-on-delivery order
|
||||
* refunded via a Bank Transfer driver with no PaymentMethod row of
|
||||
* its own) — see that listener's own docblock.
|
||||
* - position: admin-controlled display/checkout order.
|
||||
* - driver_missing_at: set by `payment:sync-drivers` when `driver` no
|
||||
* longer resolves via the registry — separate from `enabled`, so a
|
||||
|
||||
@@ -70,8 +70,7 @@ class PaymentMethodService
|
||||
public function delete(PaymentMethod $method): void
|
||||
{
|
||||
$snapshot = $method->only([
|
||||
'id', 'type', 'name', 'driver', 'capture_mode',
|
||||
'captured_status', 'authorized_status', 'position', 'enabled',
|
||||
'id', 'type', 'name', 'driver', 'capture_mode', 'position', 'enabled',
|
||||
]);
|
||||
|
||||
$method->delete();
|
||||
|
||||
Reference in New Issue
Block a user