price change while ordering2

This commit is contained in:
Priec
2026-06-28 23:45:48 +02:00
parent 4c12972422
commit 6c341e9f28
5 changed files with 75 additions and 9 deletions

View File

@@ -386,7 +386,13 @@ field-optional = optional
checkout-place-order = Place order checkout-place-order = Place order
checkout-continue-payment = Continue checkout-continue-payment = Continue
checkout-back-info = Back to details checkout-back-info = Back to details
checkout-price-changed = The price of an item in your basket has changed since you opened this page. Please review the updated total below before placing your order. checkout-price-changed-title = Price changed
checkout-price-changed = The price changed while you were checking out. Please review the new total before continuing.
checkout-price-old = Previous total
checkout-price-new = New total
checkout-price-diff = Difference
checkout-return-basket = Return to basket
checkout-accept-price = Continue with new price
checkout-summary = Order summary checkout-summary = Order summary
profile-title = My profile profile-title = My profile
profile-intro = We'll use these details to prefill checkout. profile-intro = We'll use these details to prefill checkout.

View File

@@ -386,7 +386,13 @@ field-optional = nepovinné
checkout-place-order = Odoslať objednávku checkout-place-order = Odoslať objednávku
checkout-continue-payment = Pokračovať checkout-continue-payment = Pokračovať
checkout-back-info = Späť na údaje checkout-back-info = Späť na údaje
checkout-price-changed = Cena niektorej položky v košíku sa od otvorenia tejto stránky zmenila. Pred odoslaním objednávky si prosím skontrolujte aktualizovanú sumu nižšie. checkout-price-changed-title = Cena sa zmenila
checkout-price-changed = Počas objednávania sa zmenila cena. Pred pokračovaním si prosím skontrolujte novú sumu.
checkout-price-old = Pôvodná suma
checkout-price-new = Nová suma
checkout-price-diff = Rozdiel
checkout-return-basket = Späť do košíka
checkout-accept-price = Pokračovať s novou cenou
checkout-summary = Súhrn objednávky checkout-summary = Súhrn objednávky
profile-title = Môj profil profile-title = Môj profil
profile-intro = Tieto údaje použijeme na predvyplnenie pokladne. profile-intro = Tieto údaje použijeme na predvyplnenie pokladne.

File diff suppressed because one or more lines are too long

View File

@@ -13,9 +13,43 @@
<h1 class="text-3xl font-bold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-title", lang=lang | default(value='sk')) }}</h1> <h1 class="text-3xl font-bold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-title", lang=lang | default(value='sk')) }}</h1>
{% if price_changed %} {% if price_changed %}
<div role="alert" class="mt-4 flex items-start gap-3 rounded-radius border border-warning/40 bg-warning/10 p-4 text-sm text-on-surface dark:text-on-surface-dark"> <div x-data="{ open: true }" x-show="open" x-cloak x-trap.noscroll="open"
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="size-5 shrink-0 text-warning" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z" /></svg> @keydown.escape.window="open = false"
<span>{{ t(key="checkout-price-changed", lang=lang | default(value='sk')) }}</span> class="fixed inset-0 z-50 flex items-center justify-center p-4"
role="dialog" aria-modal="true" aria-labelledby="price-changed-title">
<div class="fixed inset-0 bg-black/50" @click="open = false" aria-hidden="true"></div>
<div x-show="open" x-transition
class="relative w-full max-w-md rounded-radius border border-outline bg-surface p-6 shadow-lg dark:border-outline-dark dark:bg-surface-dark-alt">
<div class="flex items-start gap-3">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="size-6 shrink-0 text-warning" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z" /></svg>
<div class="space-y-1">
<h2 id="price-changed-title" class="text-lg font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-price-changed-title", lang=lang | default(value='sk')) }}</h2>
<p class="text-sm text-on-surface/80 dark:text-on-surface-dark/80">{{ t(key="checkout-price-changed", lang=lang | default(value='sk')) }}</p>
</div>
</div>
{% if price_old and price_new %}
<dl class="mt-4 space-y-1.5 rounded-radius border border-outline p-3 text-sm dark:border-outline-dark">
<div class="flex justify-between">
<dt class="text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="checkout-price-old", lang=lang | default(value='sk')) }}</dt>
<dd class="tabular-nums text-on-surface/70 line-through dark:text-on-surface-dark/70">{{ price_old }} €</dd>
</div>
<div class="flex justify-between">
<dt class="text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-price-new", lang=lang | default(value='sk')) }}</dt>
<dd class="tabular-nums font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ price_new }} €</dd>
</div>
<div class="flex justify-between border-t border-outline pt-1.5 dark:border-outline-dark">
<dt class="text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="checkout-price-diff", lang=lang | default(value='sk')) }}</dt>
<dd class="tabular-nums font-semibold {% if price_increased %}text-danger{% else %}text-success{% endif %}">{% if price_increased %}+{% else %}{% endif %}{{ price_diff_abs }} €</dd>
</div>
</dl>
{% endif %}
<div class="mt-6 flex flex-col gap-2 sm:flex-row sm:justify-end">
<a href="/cart" class="inline-flex items-center justify-center rounded-radius border border-outline px-6 py-2.5 text-sm font-medium text-on-surface transition hover:bg-surface-alt dark:border-outline-dark dark:text-on-surface-dark dark:hover:bg-surface-dark-alt">{{ t(key="checkout-return-basket", lang=lang | default(value='sk')) }}</a>
<button type="button" @click="open = false" class="inline-flex items-center justify-center rounded-radius bg-primary px-6 py-2.5 text-sm font-medium text-on-primary transition hover:opacity-90 dark:bg-primary-dark dark:text-on-primary-dark">{{ t(key="checkout-accept-price", lang=lang | default(value='sk')) }}</button>
</div>
</div>
</div> </div>
{% endif %} {% endif %}

View File

@@ -398,6 +398,15 @@ async fn payment_page(
.as_ref() .as_ref()
.is_some_and(|pr| pr.address.is_some() && pr.city.is_some() && pr.zip.is_some()); .is_some_and(|pr| pr.address.is_some() && pr.city.is_some() && pr.zip.is_some());
// The old/new order totals carried by the price-change bounce (display only;
// the actual price is always re-verified server-side on submit).
let price_old = params.get("old").and_then(|s| s.parse::<i64>().ok());
let price_new = params.get("new").and_then(|s| s.parse::<i64>().ok());
let price_delta = match (price_old, price_new) {
(Some(o), Some(n)) => Some(n - o),
_ => None,
};
let response = format::view( let response = format::view(
&v, &v,
"shop/checkout_payment.html", "shop/checkout_payment.html",
@@ -416,8 +425,13 @@ async fn payment_page(
"profile_filled": profile_filled, "profile_filled": profile_filled,
"can_create_account": user.is_none(), "can_create_account": user.is_none(),
// Set when the buyer was bounced back because a price changed between // Set when the buyer was bounced back because a price changed between
// viewing this page and submitting; shows a re-confirm notice. // viewing this page and submitting; opens a dialog with the old/new
// totals and the difference so they can accept or return to the basket.
"price_changed": params.contains_key("price_changed"), "price_changed": params.contains_key("price_changed"),
"price_old": price_old.map(format_price),
"price_new": price_new.map(format_price),
"price_diff_abs": price_delta.map(|d| format_price(d.abs())),
"price_increased": price_delta.map(|d| d > 0),
"lang": current_lang(&jar), "lang": current_lang(&jar),
}), }),
)?; )?;
@@ -514,8 +528,14 @@ async fn place_order(
Some(shown) => quote.subtotal_cents + shown, Some(shown) => quote.subtotal_cents + shown,
None => return format::redirect("/checkout/payment?price_changed=1"), None => return format::redirect("/checkout/payment?price_changed=1"),
}; };
if subtotal + method.price_cents != expected_total { let charge_total = subtotal + method.price_cents;
return format::redirect("/checkout/payment?price_changed=1"); if charge_total != expected_total {
// Bounce back with the old/new totals so the payment page can show the
// buyer a dialog with the difference and let them accept or return to the
// basket.
return format::redirect(&format!(
"/checkout/payment?price_changed=1&old={expected_total}&new={charge_total}"
));
} }
// The address/contact captured in the info step, ready to seed a profile (for // The address/contact captured in the info step, ready to seed a profile (for