Fix: Correct shipment resolving

This commit is contained in:
2026-09-29 00:35:29 +03:00
parent 57d7a716bc
commit d6c6bf6a1c
12 changed files with 278 additions and 22 deletions
@@ -646,6 +646,18 @@ class CheckoutController extends Controller
])->render(),
// Composer (Providers\CheckoutModuleServiceProvider) fills $cart / $lines.
'summaryHtml' => $options === null ? null : view('checkout::partials.cart-body')->render(),
// getPaymentMethods() filters by the cart's CURRENT fulfillment
// type (Modules\Core\Checkout\Services\CheckoutService's own
// docblock) — a shipping-option change can change that filter's
// result (e.g. switching to store pickup should drop
// cash-on-delivery and offer "pay in store" instead), so this
// needs to be re-rendered every time shipping options are,
// otherwise the payment section silently goes stale until a
// full page reload.
'paymentMethodsHtml' => $options === null ? null : view('checkout::partials.payment-methods', [
'paymentMethods' => $this->checkout->getPaymentMethods(),
'cart' => $cart,
])->render(),
]);
}
+36 -19
View File
@@ -22,11 +22,13 @@ use Modules\Core\Checkout\Exceptions\InvalidShippingOptionException;
use Modules\Core\Checkout\Exceptions\NoShippingAddressException;
use Modules\Core\Checkout\Exceptions\TermsNotAcceptedException;
use Modules\Core\Checkout\Exceptions\UnknownPaymentTypeException;
use Lunar\Shipping\Facades\Shipping;
use Modules\Core\Payment\Contracts\RequiresFulfillmentType;
use Modules\Core\Payment\DTOs\PaymentResult;
use Modules\Core\Payment\Models\PaymentMethod;
use Modules\Core\Payment\Services\PaymentDriverRegistry;
use Modules\Core\Payment\Services\PaymentMethodCache;
use Modules\Core\Shipping\Contracts\SupportsCashCollection;
use Modules\Core\Shipping\Support\FulfillmentType;
/**
@@ -254,42 +256,59 @@ class CheckoutService
* a courier delivery makes no sense (no staff member present at
* handoff to take cash), and cash-on-delivery alongside store
* pickup is equally meaningless (OfflinePaymentDriver already
* covers that in-person moment). A cart with no shipping option
* selected yet imposes no constraint here — every method is
* offered until a fulfillment type is actually known, the same
* leniency setShippingAddress()'s own docblock describes for
* required-field enforcement happening at the payment gate, not
* mid-checkout.
* covers that in-person moment).
* 5. for cash-on-delivery specifically, the cart's currently selected
* shipping method's own driver implements Modules\Core\Shipping\
* Contracts\SupportsCashCollection — "carrier" alone
* (RequiresFulfillmentType) isn't precise enough, since an
* unattended parcel locker network (Modules\Core\Shipping\
* Carriers\BoxNow\BoxNowRateDriver) is a carrier delivery with
* nobody there to collect cash, unlike an actual courier
* (Modules\Core\Shipping\Carriers\Acs\AcsRateDriver).
* Checks 4 and 5 both impose no constraint when the cart has no
* shipping option selected yet — every method is offered until a
* shipping method is actually known, the same leniency
* setShippingAddress()'s own docblock describes for required-field
* enforcement happening at the payment gate, not mid-checkout.
*
* @return Collection<int, PaymentMethod>
*/
public function getPaymentMethods(): Collection
{
$fulfillmentType = $this->currentFulfillmentType();
$shippingMethod = $this->currentShippingMethod();
$fulfillmentType = $shippingMethod ? FulfillmentType::resolve($shippingMethod) : null;
return $this->paymentMethods->all()
->filter(fn (PaymentMethod $method) => $method->enabled && $method->driver_missing_at === null)
->filter(function (PaymentMethod $method) use ($fulfillmentType) {
->filter(function (PaymentMethod $method) use ($fulfillmentType, $shippingMethod) {
$driver = $this->paymentDrivers->resolve($method->driver);
if (! $driver?->isConfigured()) {
return false;
}
if ($fulfillmentType === null || ! $driver instanceof RequiresFulfillmentType) {
return true;
if ($fulfillmentType !== null && $driver instanceof RequiresFulfillmentType
&& $driver->requiredFulfillmentType() !== $fulfillmentType) {
return false;
}
return $driver->requiredFulfillmentType() === $fulfillmentType;
// collect(...)->get() rather than Shipping::driver(), which
// throws for an unregistered key — a stale ShippingMethod
// row (e.g. still pointing at a removed generic driver)
// must fail this check quietly, the same as any other
// driver this module doesn't recognize.
if ($shippingMethod !== null && $method->driver === 'cash-on-delivery') {
$shippingDriver = collect(Shipping::getSupportedDrivers())->get($shippingMethod->driver);
return $shippingDriver instanceof SupportsCashCollection && $shippingDriver->collectsCash();
}
return true;
})
->values();
}
/**
* @return 'carrier'|'store_pickup'|null null when the cart has no
* shipping option selected yet
*/
private function currentFulfillmentType(): ?string
private function currentShippingMethod(): ?ShippingMethod
{
$identifier = $this->cart->currentOrCreate()->shippingAddress?->shipping_option;
@@ -297,9 +316,7 @@ class CheckoutService
return null;
}
$method = ShippingMethod::where('code', $identifier)->first();
return $method ? FulfillmentType::resolve($method) : null;
return ShippingMethod::where('code', $identifier)->first();
}
/**