Feature: Order Updates, Events, Order Flows, Shipment And COD support

This commit is contained in:
2026-09-14 00:03:06 +03:00
parent 78bbd8390a
commit 44c6b7defd
73 changed files with 2854 additions and 464 deletions
@@ -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,
);
}
}
+6 -3
View File
@@ -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));
}
-10
View File
@@ -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();