Fix: Updates to OrderFullfilmentServices and box now clients, order views and checkout services

This commit is contained in:
2026-09-18 00:54:43 +03:00
parent dcdc998eee
commit 12aaa43f10
15 changed files with 450 additions and 18 deletions
@@ -8,6 +8,8 @@ use Modules\Core\Order\DTOs\OrderFulfillmentResult;
use Modules\Core\Order\Events\OrderPickedUp;
use Modules\Core\Order\Events\OrderReadyForDispatch;
use Modules\Core\Order\Events\OrderReadyForPickup;
use Modules\Core\Payment\DTOs\PaymentResult;
use Modules\Core\Payment\Enums\PaymentResultStatus;
use Modules\Core\Shipping\Contracts\CarrierFulfillmentInterface;
use Modules\Core\Shipping\DTOs\ShipmentRequest;
use Throwable;
@@ -31,6 +33,7 @@ class OrderFulfillmentService
public function __construct(
private readonly OrderStatusWriter $writer,
private readonly OrderStatusFlow $flow,
private readonly TransactionRecorder $transactions,
) {}
public function markReady(Order $order): OrderFulfillmentResult
@@ -121,6 +124,39 @@ class OrderFulfillmentService
return OrderFulfillmentResult::failure('This order cannot be marked paid right now.');
}
// canMarkPaid() only ever returns true for an order whose payment
// method resolves to the cash-on-delivery DRIVER (see
// OrderStatusFlow::isCod(), which checks PaymentMethod::driver,
// never the merchant-chosen `type` slug directly — a store could
// name that method "cod", "pay-on-delivery", anything). Such an
// order never runs through Payment's pay()/authorize() flow at
// checkout, so nothing else records a Transaction for it. Money
// changes hands right here, at this click, so this is the one
// place that write can happen; there is no earlier Payment event
// to hang it off of the way Modules\Core\Order\Listeners\
// RecordPaymentTransaction does for a gateway driver. See
// TransactionRecorder's own docblock — it already anticipated
// exactly this "manually-triggered ... from Filament" call site.
//
// $driver below is the payment method's own `type` slug (whatever
// the merchant named it, e.g. 'cash-on-delivery' or 'cod') —
// Transaction.driver's established meaning everywhere else in this
// codebase (see RecordPaymentTransaction/TransactionRecorder's own
// docblocks) is that type key, never the underlying driver CLASS.
// No fallback guess here: CheckoutService::initiatePayment() always
// writes Order.meta['payment_method'] before charging, and
// canMarkPaid() already guarantees this order got that far.
$this->transactions->record(
$order,
type: 'capture',
driver: (string) $order->meta['payment_method'],
result: new PaymentResult(
status: PaymentResultStatus::Succeeded,
reference: 'cod-manual-'.$order->id,
amount: $order->total,
),
);
$this->writer->markPaid($order, self::class.'::markPaid');
return OrderFulfillmentResult::success('Order marked as paid.');
@@ -52,6 +52,24 @@ class OrderPaymentResolutionService
}
}
/**
* A deferred-capture payment (currently only cash-on-delivery — see
* Payment\Events\PaymentDeferred's own docblock): no money has moved,
* so unlike resolveCaptureOrAuthorization() this never calls
* $writer->markPaid() — Order::paid stays false until staff explicitly
* mark it received. But per OrderStatusFlow's own docblock, payment
* method never affects the status SEQUENCE at all — a COD order has
* nothing to "await" at checkout (no payment attempt happens), so
* 'awaiting_payment' is simply the wrong first status for it. Reuses
* the exact same advancePastAwaitingPayment() a capture uses, since
* the status-sequence logic itself doesn't differ by payment method,
* only whether `paid` also flips alongside it.
*/
public function resolveDeferredPayment(Order $order, string $causeClass): void
{
$this->advancePastAwaitingPayment($order, $causeClass);
}
/**
* Requires the refund Transaction row to already exist (Modules\Core\
* Order\Listeners\RecordPaymentTransaction must run first — see