payment fix

This commit is contained in:
elvira
2026-09-15 19:20:59 +03:00
parent 1598f053ee
commit ba6d68c5b9
5 changed files with 155 additions and 96 deletions
@@ -61,7 +61,9 @@ public function show(string $locale): View
$shippingOptions = collect();
if ($cart?->shippingAddress) {
$shippingOptions = $this->syncShipping($cart);
// Nothing recreates the address row in this path — its own current
// value is the correct "previous" to carry forward if still valid.
$shippingOptions = $this->syncShipping($cart, $cart->shippingAddress->shipping_option);
// Cart's CachesProperties::refresh() explicitly nulls total/
// subTotal/shippingTotal/etc. back to their defaults — every
@@ -97,7 +99,13 @@ public function saveAddress(string $locale, Request $request): JsonResponse
// Only the fields shipping rates resolve against — if none of these
// changed (shopper edited their name, phone, email, …) there's no point
// re-quoting shipping or re-rendering the summary.
$rateKeyBefore = $this->cart->current()?->shippingAddress?->only(['postcode', 'state', 'country_id']);
$addressBefore = $this->cart->current()?->shippingAddress;
$rateKeyBefore = $addressBefore?->only(['postcode', 'state', 'country_id']);
// setShippingAddress() below always deletes + recreates this row (see
// syncShipping()'s docblock) — capture what was selected NOW, before
// it's gone, so it can be carried forward onto the fresh row.
$previousOption = $addressBefore?->shipping_option;
$stateRule = $storeCountry
? ['nullable', 'string', Rule::exists((new State)->getTable(), 'name')->where('country_id', $storeCountry->id)]
@@ -195,7 +203,7 @@ public function saveAddress(string $locale, Request $request): JsonResponse
// to restore/re-validate it, even on a save that only touched e.g. the
// phone number. Only the fragment RE-RENDER is skippable when nothing
// rate-relevant moved — the re-select itself is not optional.
$options = $this->syncShipping($cart);
$options = $this->syncShipping($cart, $previousOption);
if (! $rateChanged) {
return $this->fragments($cart, null, $errors);
@@ -265,7 +273,21 @@ public function placeOrder(string $locale, Request $request): JsonResponse
return response()->json(['error' => __('checkout.page.choose_payment_method')], 422);
}
$fingerprint = (string) ($this->cart->current()?->meta['checkout_fingerprint'] ?? '');
$cart = $this->cart->current();
// The one incomplete-cart case worth a specific message + pointing the
// shopper at the right section: a region resolving 2+ methods needs an
// explicit pick (no auto-select), easy to miss since nothing else on
// the page demands it. Everything else CartException catches below.
if ($cart?->shippingAddress && ! $cart->shippingAddress->shipping_option) {
return response()->json([
'status' => 'invalid',
'message' => __('checkout.page.shipping_method_required'),
'field' => 'shipping_option',
], 422);
}
$fingerprint = (string) ($cart?->meta['checkout_fingerprint'] ?? '');
$data = $request->filled('payment_method')
? ['payment_method' => (string) $request->input('payment_method')]
@@ -360,35 +382,46 @@ private function placedOrder(): ?Order
/**
* Re-resolve shipping options for the cart's current address and keep the
* selection sane: auto-select when exactly one resolves, and drop a
* previously-picked option that no longer applies (e.g. region changed).
* selection sane: auto-select when exactly one resolves, or carry a
* previous pick forward when it's still among the resolved options.
*
* $previousOption must be captured by the CALLER before setShippingAddress()
* runs — Lunar's AddAddress action always deletes and recreates the
* CartAddress row on every save (see saveAddress()), so by the time this
* runs, $address->shipping_option is unconditionally null regardless of
* what was selected a moment ago. There is nothing meaningful left to read
* off $address itself; $previousOption is the only source of truth for
* "what was chosen before this save wiped the row." show() passes the
* address's own (not-just-wiped) current value, since nothing recreated
* anything in that path.
*
* Always (re-)applies the resolved target via selectShippingOption() rather
* than comparing against the (always-blank, post-recreation) current value
* — the fresh row needs the write regardless of whether the decision
* "which option" actually changed.
*
* @return Collection<int, \Lunar\DataTypes\ShippingOption>
*/
private function syncShipping(Cart $cart): Collection
private function syncShipping(Cart $cart, ?string $previousOption): Collection
{
$address = $cart->shippingAddress;
if (! $address) {
if (! $cart->shippingAddress) {
return collect();
}
$options = $this->checkout->getShippingOptions();
$current = $address->shipping_option;
if ($options->count() === 1) {
$only = $options->first();
$target = match (true) {
$options->count() === 1 => $options->first()->identifier,
$previousOption !== null && $options->contains(fn ($option) => $option->identifier === $previousOption) => $previousOption,
default => null,
};
if ($current !== $only->identifier) {
try {
$this->checkout->selectShippingOption($only->identifier);
} catch (InvalidShippingOptionException) {
// nothing to select against — leave as is
}
if ($target !== null) {
try {
$this->checkout->selectShippingOption($target);
} catch (InvalidShippingOptionException) {
// $target came from $options itself — shouldn't happen, stay defensive
}
} elseif ($current !== null && ! $options->contains(fn ($option) => $option->identifier === $current)) {
$address->update(['shipping_option' => null]);
$cart->calculate();
}
return $options;