checkout
This commit is contained in:
@@ -1,12 +1,20 @@
|
||||
//! Public checkout flow: the checkout form, placing an order, and the order
|
||||
//! confirmation page.
|
||||
//! Public checkout flow, split across pages: the basket is `/cart`, then a
|
||||
//! two-step wizard — `/checkout/info` (contact + addresses + company details)
|
||||
//! followed by `/checkout/payment` (carrier + payment, which places the order)
|
||||
//! — and finally the order confirmation page.
|
||||
//!
|
||||
//! The info step is persisted between pages in a short-lived, base64-encoded
|
||||
//! JSON cookie (`checkout_info`); the payment step reads it back, places the
|
||||
//! order, and clears both it and the cart cookie.
|
||||
|
||||
use axum::extract::Query;
|
||||
use axum_extra::extract::cookie::CookieJar;
|
||||
use axum::{extract::Query, response::Redirect};
|
||||
use axum_extra::extract::cookie::{Cookie, CookieJar, SameSite};
|
||||
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
|
||||
use loco_rs::prelude::*;
|
||||
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QueryOrder};
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use time::Duration as TimeDuration;
|
||||
use crate::{
|
||||
controllers::cart::{self, resolve_cart},
|
||||
mailers::auth::AuthMailer,
|
||||
@@ -20,8 +28,37 @@ use crate::{
|
||||
views::checkout as view,
|
||||
};
|
||||
|
||||
const INFO_COOKIE: &str = "checkout_info";
|
||||
const INFO_MAX_AGE_HOURS: i64 = 2;
|
||||
|
||||
/// The contact + address details captured on `/checkout/info`, carried to the
|
||||
/// `/checkout/payment` step via the `checkout_info` cookie. `phone` is the local
|
||||
/// number only; it is combined with `phone_prefix` when the order is placed.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct CheckoutInfo {
|
||||
email: String,
|
||||
phone_prefix: String,
|
||||
phone: String,
|
||||
customer_name: String,
|
||||
account_type: String,
|
||||
company_name: Option<String>,
|
||||
company_id: Option<String>,
|
||||
tax_id: Option<String>,
|
||||
vat_id: Option<String>,
|
||||
residence_address: String,
|
||||
residence_city: String,
|
||||
residence_zip: String,
|
||||
residence_country: String,
|
||||
delivery_same: bool,
|
||||
address: String,
|
||||
city: String,
|
||||
zip: String,
|
||||
country: String,
|
||||
}
|
||||
|
||||
/// Step 1 form (`POST /checkout/info`).
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct CheckoutForm {
|
||||
struct InfoForm {
|
||||
email: String,
|
||||
phone_prefix: String,
|
||||
phone: String,
|
||||
@@ -40,6 +77,11 @@ struct CheckoutForm {
|
||||
city: Option<String>,
|
||||
zip: Option<String>,
|
||||
country: Option<String>,
|
||||
}
|
||||
|
||||
/// Step 2 form (`POST /checkout/payment`).
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct PaymentForm {
|
||||
note: Option<String>,
|
||||
payment_method: String,
|
||||
carrier_code: String,
|
||||
@@ -56,6 +98,33 @@ fn trimmed(value: &str) -> Option<String> {
|
||||
(!value.is_empty()).then(|| value.to_string())
|
||||
}
|
||||
|
||||
fn info_cookie(value: String) -> Cookie<'static> {
|
||||
Cookie::build((INFO_COOKIE, value))
|
||||
.path("/")
|
||||
.same_site(SameSite::Lax)
|
||||
.http_only(true)
|
||||
.max_age(TimeDuration::hours(INFO_MAX_AGE_HOURS))
|
||||
.build()
|
||||
}
|
||||
|
||||
fn cleared_info_cookie() -> Cookie<'static> {
|
||||
Cookie::build((INFO_COOKIE, ""))
|
||||
.path("/")
|
||||
.same_site(SameSite::Lax)
|
||||
.max_age(TimeDuration::seconds(0))
|
||||
.build()
|
||||
}
|
||||
|
||||
fn encode_info(info: &CheckoutInfo) -> String {
|
||||
URL_SAFE_NO_PAD.encode(serde_json::to_vec(info).unwrap_or_default())
|
||||
}
|
||||
|
||||
fn decode_info(jar: &CookieJar) -> Option<CheckoutInfo> {
|
||||
let raw = jar.get(INFO_COOKIE)?;
|
||||
let bytes = URL_SAFE_NO_PAD.decode(raw.value()).ok()?;
|
||||
serde_json::from_slice(&bytes).ok()
|
||||
}
|
||||
|
||||
async fn enabled_shipping_methods(ctx: &AppContext) -> Result<Vec<shipping_methods::Model>> {
|
||||
shipping_rules::disable_packeta_if_unconfigured(ctx).await?;
|
||||
let packeta_ready = shipping_rules::packeta_ready(ctx);
|
||||
@@ -73,8 +142,17 @@ async fn enabled_payment_methods(ctx: &AppContext) -> Result<Vec<payment_methods
|
||||
Ok(payment_methods::Entity::enabled(&ctx.db).await?)
|
||||
}
|
||||
|
||||
/// `/checkout` lands on the first wizard step.
|
||||
#[debug_handler]
|
||||
async fn checkout_page(
|
||||
async fn checkout_redirect() -> Result<Response> {
|
||||
format::redirect("/checkout/info")
|
||||
}
|
||||
|
||||
/// Step 1 page: contact, residence/delivery addresses and (for companies)
|
||||
/// invoicing details. Prefilled from a returning customer's profile, or from
|
||||
/// the `checkout_info` cookie when the buyer steps back from the payment page.
|
||||
#[debug_handler]
|
||||
async fn info_page(
|
||||
jar: CookieJar,
|
||||
ViewEngine(v): ViewEngine<TeraView>,
|
||||
State(ctx): State<AppContext>,
|
||||
@@ -86,6 +164,180 @@ async fn checkout_page(
|
||||
return format::redirect("/cart");
|
||||
}
|
||||
|
||||
// Prefill the form for a logged-in customer: contact name/email come from
|
||||
// the user account, the address/phone from their saved profile (if any).
|
||||
let user = guard::current_user(&ctx, &jar).await;
|
||||
let is_admin = user.as_ref().is_some_and(|u| guard::is_admin(&ctx, u));
|
||||
let is_customer = user.is_some() && !is_admin;
|
||||
let profile = match (&user, is_customer) {
|
||||
(Some(u), true) => customer_profiles::Model::find_for_user(&ctx.db, u.id).await?,
|
||||
_ => None,
|
||||
};
|
||||
let p = |get: fn(&customer_profiles::Model) -> Option<String>| {
|
||||
profile.as_ref().and_then(get)
|
||||
};
|
||||
|
||||
// A previously entered info step (back navigation from the payment page)
|
||||
// takes precedence over the profile defaults.
|
||||
let saved = decode_info(&jar);
|
||||
let s = |get: fn(&CheckoutInfo) -> String| saved.as_ref().map(get);
|
||||
let s_opt = |get: fn(&CheckoutInfo) -> Option<String>| saved.as_ref().and_then(get);
|
||||
|
||||
let prefill_account_type = if is_customer {
|
||||
user.as_ref().map_or("personal", |u| u.account_type.as_str()).to_string()
|
||||
} else {
|
||||
saved.as_ref().map_or_else(|| "personal".to_string(), |s| s.account_type.clone())
|
||||
};
|
||||
|
||||
format::view(
|
||||
&v,
|
||||
"shop/checkout_info.html",
|
||||
json!({
|
||||
"items": lines,
|
||||
"subtotal": format_price(subtotal),
|
||||
"subtotal_cents": subtotal,
|
||||
"logged_in_admin": is_admin,
|
||||
"logged_in_customer": is_customer,
|
||||
// Required by the navbar profile menu (base.html includes it whenever
|
||||
// logged_in_customer is true); None for admins/guests.
|
||||
"customer_name": user.as_ref().filter(|_| is_customer).map(|u| u.name.clone()),
|
||||
"customer_account_type": user.as_ref().filter(|_| is_customer).map(|u| u.account_type.clone()),
|
||||
"customer_avatar": user.as_ref().filter(|_| is_customer).and_then(|u| u.avatar_id.clone()),
|
||||
// A logged-in customer's account type is fixed; only guests pick it.
|
||||
"account_fixed": is_customer,
|
||||
"prefill_email": s(|x| x.email.clone()).or_else(|| user.as_ref().filter(|_| is_customer).map(|u| u.email.clone())),
|
||||
"prefill_name": s(|x| x.customer_name.clone()).or_else(|| user.as_ref().filter(|_| is_customer).map(|u| u.name.clone())),
|
||||
"prefill_account_type": prefill_account_type,
|
||||
"prefill_company_name": s_opt(|x| x.company_name.clone()).or_else(|| p(|x| x.company_name.clone())),
|
||||
"prefill_company_id": s_opt(|x| x.company_id.clone()).or_else(|| p(|x| x.company_id.clone())),
|
||||
"prefill_tax_id": s_opt(|x| x.tax_id.clone()).or_else(|| p(|x| x.tax_id.clone())),
|
||||
"prefill_vat_id": s_opt(|x| x.vat_id.clone()).or_else(|| p(|x| x.vat_id.clone())),
|
||||
"prefill_phone_prefix": s(|x| x.phone_prefix.clone()).or_else(|| p(|x| x.phone_prefix.clone())),
|
||||
"prefill_phone": s(|x| x.phone.clone()).or_else(|| p(|x| x.phone.clone())),
|
||||
"prefill_residence_address": s(|x| x.residence_address.clone()).or_else(|| p(|x| x.address.clone())),
|
||||
"prefill_residence_city": s(|x| x.residence_city.clone()).or_else(|| p(|x| x.city.clone())),
|
||||
"prefill_residence_zip": s(|x| x.residence_zip.clone()).or_else(|| p(|x| x.zip.clone())),
|
||||
"prefill_residence_country": s(|x| x.residence_country.clone()).or_else(|| p(|x| x.country.clone())),
|
||||
"prefill_delivery_same": saved.as_ref().is_some_and(|x| x.delivery_same),
|
||||
"prefill_delivery_address": s(|x| x.address.clone()),
|
||||
"prefill_delivery_city": s(|x| x.city.clone()),
|
||||
"prefill_delivery_zip": s(|x| x.zip.clone()),
|
||||
"prefill_delivery_country": s(|x| x.country.clone()),
|
||||
"lang": current_lang(&jar),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/// Validate step 1, stash it in the `checkout_info` cookie and advance to the
|
||||
/// payment step.
|
||||
#[debug_handler]
|
||||
async fn submit_info(
|
||||
jar: CookieJar,
|
||||
State(ctx): State<AppContext>,
|
||||
Form(form): Form<InfoForm>,
|
||||
) -> Result<Response> {
|
||||
let (_lines, valid, _total) = resolve_cart(&ctx, &jar, &Currency::eur()).await?;
|
||||
if valid.is_empty() {
|
||||
return format::redirect("/cart");
|
||||
}
|
||||
|
||||
let require = |value: &str, field: &str| -> Result<String> {
|
||||
trimmed(value).ok_or_else(|| Error::BadRequest(format!("{field} is required")))
|
||||
};
|
||||
let require_opt = |value: Option<&str>, field: &str| -> Result<String> {
|
||||
value
|
||||
.and_then(trimmed)
|
||||
.ok_or_else(|| Error::BadRequest(format!("{field} is required")))
|
||||
};
|
||||
|
||||
let email = require(&form.email, "email")?;
|
||||
let number = require(&form.phone, "phone")?;
|
||||
let customer_name = require(&form.customer_name, "name")?;
|
||||
let residence_address = require(&form.residence_address, "residence address")?;
|
||||
let residence_city = require(&form.residence_city, "residence city")?;
|
||||
let residence_zip = require(&form.residence_zip, "residence zip")?;
|
||||
let residence_country = require(&form.residence_country, "residence country")?;
|
||||
|
||||
let delivery_same = form.delivery_same_as_residence.is_some();
|
||||
let (address, city, zip, country) = if delivery_same {
|
||||
(
|
||||
residence_address.clone(),
|
||||
residence_city.clone(),
|
||||
residence_zip.clone(),
|
||||
residence_country.clone(),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
require_opt(form.address.as_deref(), "delivery address")?,
|
||||
require_opt(form.city.as_deref(), "delivery city")?,
|
||||
require_opt(form.zip.as_deref(), "delivery zip")?,
|
||||
require_opt(form.country.as_deref(), "delivery country")?,
|
||||
)
|
||||
};
|
||||
|
||||
// The account type is fixed for a logged-in customer (taken from their
|
||||
// account, never the form); a guest picks it on the form. Admins are guests.
|
||||
let current_user = guard::current_user(&ctx, &jar).await;
|
||||
let logged_in_customer = current_user.as_ref().filter(|u| !guard::is_admin(&ctx, u));
|
||||
let account_type = match logged_in_customer {
|
||||
Some(u) => u.account_type.clone(),
|
||||
None => normalize_account_type(form.account_type.as_deref()),
|
||||
};
|
||||
|
||||
// Company purchases must carry the invoicing identifiers (IČO + DIČ
|
||||
// required, IČ DPH optional). Personal orders carry none.
|
||||
let (company_name, company_id, tax_id, vat_id) = if account_type == "company" {
|
||||
(
|
||||
Some(require(form.company_name.as_deref().unwrap_or(""), "company name")?),
|
||||
Some(require(form.company_id.as_deref().unwrap_or(""), "IČO")?),
|
||||
Some(require(form.tax_id.as_deref().unwrap_or(""), "DIČ")?),
|
||||
form.vat_id.as_deref().and_then(trimmed),
|
||||
)
|
||||
} else {
|
||||
(None, None, None, None)
|
||||
};
|
||||
|
||||
let info = CheckoutInfo {
|
||||
email,
|
||||
phone_prefix: trimmed(&form.phone_prefix).unwrap_or_default(),
|
||||
phone: number,
|
||||
customer_name,
|
||||
account_type,
|
||||
company_name,
|
||||
company_id,
|
||||
tax_id,
|
||||
vat_id,
|
||||
residence_address,
|
||||
residence_city,
|
||||
residence_zip,
|
||||
residence_country,
|
||||
delivery_same,
|
||||
address,
|
||||
city,
|
||||
zip,
|
||||
country,
|
||||
};
|
||||
|
||||
let jar = jar.add(info_cookie(encode_info(&info)));
|
||||
Ok((jar, Redirect::to("/checkout/payment")).into_response())
|
||||
}
|
||||
|
||||
/// Step 2 page: carrier (with optional pickup point) and payment method, plus
|
||||
/// the order summary. Requires the info step to have been completed.
|
||||
#[debug_handler]
|
||||
async fn payment_page(
|
||||
jar: CookieJar,
|
||||
ViewEngine(v): ViewEngine<TeraView>,
|
||||
State(ctx): State<AppContext>,
|
||||
) -> Result<Response> {
|
||||
let (lines, _valid, subtotal) = resolve_cart(&ctx, &jar, &Currency::eur()).await?;
|
||||
if lines.is_empty() {
|
||||
return format::redirect("/cart");
|
||||
}
|
||||
if decode_info(&jar).is_none() {
|
||||
return format::redirect("/checkout/info");
|
||||
}
|
||||
|
||||
let methods: Vec<serde_json::Value> = enabled_shipping_methods(&ctx)
|
||||
.await?
|
||||
.iter()
|
||||
@@ -110,8 +362,6 @@ async fn checkout_page(
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Prefill the form for a logged-in customer: contact name/email come from
|
||||
// the user account, the address/phone from their saved profile (if any).
|
||||
let user = guard::current_user(&ctx, &jar).await;
|
||||
let is_admin = user.as_ref().is_some_and(|u| guard::is_admin(&ctx, u));
|
||||
let is_customer = user.is_some() && !is_admin;
|
||||
@@ -119,19 +369,15 @@ async fn checkout_page(
|
||||
(Some(u), true) => customer_profiles::Model::find_for_user(&ctx.db, u.id).await?,
|
||||
_ => None,
|
||||
};
|
||||
let p = |get: fn(&customer_profiles::Model) -> Option<String>| {
|
||||
profile.as_ref().and_then(get)
|
||||
};
|
||||
// Whether the customer already has a residence address on file. When they do,
|
||||
// the "save this address to my profile" opt-in is pointless (the profile was
|
||||
// filled in advance), so it's hidden and the existing profile is left alone.
|
||||
// the "save this address to my profile" opt-in is pointless, so it's hidden.
|
||||
let profile_filled = profile
|
||||
.as_ref()
|
||||
.is_some_and(|pr| pr.address.is_some() && pr.city.is_some() && pr.zip.is_some());
|
||||
|
||||
format::view(
|
||||
&v,
|
||||
"shop/checkout.html",
|
||||
"shop/checkout_payment.html",
|
||||
json!({
|
||||
"items": lines,
|
||||
"subtotal": format_price(subtotal),
|
||||
@@ -141,29 +387,11 @@ async fn checkout_page(
|
||||
"packeta_api_key": settings::get(&ctx, "packeta_api_key").unwrap_or(""),
|
||||
"logged_in_admin": is_admin,
|
||||
"logged_in_customer": is_customer,
|
||||
// Required by the navbar profile menu (base.html includes it whenever
|
||||
// logged_in_customer is true); None for admins/guests.
|
||||
"customer_name": user.as_ref().filter(|_| is_customer).map(|u| u.name.clone()),
|
||||
"customer_account_type": user.as_ref().filter(|_| is_customer).map(|u| u.account_type.clone()),
|
||||
"customer_avatar": user.as_ref().filter(|_| is_customer).and_then(|u| u.avatar_id.clone()),
|
||||
"profile_filled": profile_filled,
|
||||
// A logged-in customer's account type is fixed; only guests pick it
|
||||
// and may opt to create an account from the order.
|
||||
"account_fixed": is_customer,
|
||||
"can_create_account": user.is_none(),
|
||||
"prefill_email": user.as_ref().filter(|_| is_customer).map(|u| u.email.clone()),
|
||||
"prefill_name": user.as_ref().filter(|_| is_customer).map(|u| u.name.clone()),
|
||||
"prefill_account_type": user.as_ref().filter(|_| is_customer).map_or("personal", |u| u.account_type.as_str()),
|
||||
"prefill_company_name": p(|x| x.company_name.clone()),
|
||||
"prefill_company_id": p(|x| x.company_id.clone()),
|
||||
"prefill_tax_id": p(|x| x.tax_id.clone()),
|
||||
"prefill_vat_id": p(|x| x.vat_id.clone()),
|
||||
"prefill_phone_prefix": p(|x| x.phone_prefix.clone()),
|
||||
"prefill_phone": p(|x| x.phone.clone()),
|
||||
"prefill_residence_address": p(|x| x.address.clone()),
|
||||
"prefill_residence_city": p(|x| x.city.clone()),
|
||||
"prefill_residence_zip": p(|x| x.zip.clone()),
|
||||
"prefill_residence_country": p(|x| x.country.clone()),
|
||||
"lang": current_lang(&jar),
|
||||
}),
|
||||
)
|
||||
@@ -173,75 +401,41 @@ async fn checkout_page(
|
||||
async fn place_order(
|
||||
jar: CookieJar,
|
||||
State(ctx): State<AppContext>,
|
||||
Form(form): Form<CheckoutForm>,
|
||||
Form(form): Form<PaymentForm>,
|
||||
) -> Result<Response> {
|
||||
let (_lines, valid, _total) = resolve_cart(&ctx, &jar, &Currency::eur()).await?;
|
||||
if valid.is_empty() {
|
||||
return format::redirect("/cart");
|
||||
}
|
||||
let email =
|
||||
trimmed(&form.email).ok_or_else(|| Error::BadRequest("email is required".to_string()))?;
|
||||
// Combine the dialling-code prefix with the local number into one E.164-ish
|
||||
// value (e.g. "+421 900123456").
|
||||
let number =
|
||||
trimmed(&form.phone).ok_or_else(|| Error::BadRequest("phone is required".to_string()))?;
|
||||
let phone = match trimmed(&form.phone_prefix) {
|
||||
Some(prefix) => format!("{prefix} {number}"),
|
||||
None => number.clone(),
|
||||
let Some(info) = decode_info(&jar) else {
|
||||
return format::redirect("/checkout/info");
|
||||
};
|
||||
|
||||
// Contact and residence-address fields are mandatory (also enforced in the
|
||||
// browser via `required`).
|
||||
let require = |value: &str, field: &str| -> Result<String> {
|
||||
trimmed(value).ok_or_else(|| Error::BadRequest(format!("{field} is required")))
|
||||
};
|
||||
let require_opt = |value: Option<&str>, field: &str| -> Result<String> {
|
||||
value
|
||||
.and_then(trimmed)
|
||||
.ok_or_else(|| Error::BadRequest(format!("{field} is required")))
|
||||
};
|
||||
let customer_name = require(&form.customer_name, "name")?;
|
||||
let residence_address = require(&form.residence_address, "residence address")?;
|
||||
let residence_city = require(&form.residence_city, "residence city")?;
|
||||
let residence_zip = require(&form.residence_zip, "residence zip")?;
|
||||
let residence_country = require(&form.residence_country, "residence country")?;
|
||||
let same_address = form.delivery_same_as_residence.is_some();
|
||||
let (address, city, zip, country) = if same_address {
|
||||
(
|
||||
residence_address.clone(),
|
||||
residence_city.clone(),
|
||||
residence_zip.clone(),
|
||||
residence_country.clone(),
|
||||
)
|
||||
let email = info.email.clone();
|
||||
let customer_name = info.customer_name.clone();
|
||||
let number = info.phone.clone();
|
||||
// Combine the dialling-code prefix with the local number into one E.164-ish
|
||||
// value (e.g. "+421 900123456").
|
||||
let phone = if info.phone_prefix.is_empty() {
|
||||
number.clone()
|
||||
} else {
|
||||
(
|
||||
require_opt(form.address.as_deref(), "delivery address")?,
|
||||
require_opt(form.city.as_deref(), "delivery city")?,
|
||||
require_opt(form.zip.as_deref(), "delivery zip")?,
|
||||
require_opt(form.country.as_deref(), "delivery country")?,
|
||||
)
|
||||
format!("{} {}", info.phone_prefix, number)
|
||||
};
|
||||
|
||||
// The account type is fixed for a logged-in customer (taken from their
|
||||
// account, never the form); a guest picks it on the form. Admins are treated
|
||||
// as guests here.
|
||||
// account, never the cookie); a guest's choice rides in the info cookie.
|
||||
let current_user = guard::current_user(&ctx, &jar).await;
|
||||
let logged_in_customer = current_user
|
||||
.as_ref()
|
||||
.filter(|u| !guard::is_admin(&ctx, u));
|
||||
let logged_in_customer = current_user.as_ref().filter(|u| !guard::is_admin(&ctx, u));
|
||||
let account_type = match logged_in_customer {
|
||||
Some(u) => u.account_type.clone(),
|
||||
None => normalize_account_type(form.account_type.as_deref()),
|
||||
None => info.account_type.clone(),
|
||||
};
|
||||
|
||||
// Company purchases must carry the invoicing identifiers (IČO + DIČ
|
||||
// required, IČ DPH optional). Personal orders carry none.
|
||||
let (company_name, company_id, tax_id, vat_id) = if account_type == "company" {
|
||||
(
|
||||
Some(require(form.company_name.as_deref().unwrap_or(""), "company name")?),
|
||||
Some(require(form.company_id.as_deref().unwrap_or(""), "IČO")?),
|
||||
Some(require(form.tax_id.as_deref().unwrap_or(""), "DIČ")?),
|
||||
form.vat_id.as_deref().and_then(trimmed),
|
||||
info.company_name.clone(),
|
||||
info.company_id.clone(),
|
||||
info.tax_id.clone(),
|
||||
info.vat_id.clone(),
|
||||
)
|
||||
} else {
|
||||
(None, None, None, None)
|
||||
@@ -274,19 +468,19 @@ async fn place_order(
|
||||
(None, None)
|
||||
};
|
||||
|
||||
// The address/contact captured here, ready to seed a profile (for the
|
||||
// logged-in "save my address" opt-in or a freshly created guest account).
|
||||
// The address/contact captured in the info step, ready to seed a profile (for
|
||||
// the logged-in "save my address" opt-in or a freshly created guest account).
|
||||
let entered_profile = || ProfileFields {
|
||||
company_name: company_name.clone(),
|
||||
company_id: company_id.clone(),
|
||||
tax_id: tax_id.clone(),
|
||||
vat_id: vat_id.clone(),
|
||||
phone_prefix: trimmed(&form.phone_prefix),
|
||||
phone_prefix: trimmed(&info.phone_prefix),
|
||||
phone: Some(number.clone()),
|
||||
address: Some(residence_address.clone()),
|
||||
city: Some(residence_city.clone()),
|
||||
zip: Some(residence_zip.clone()),
|
||||
country: Some(residence_country.clone()),
|
||||
address: Some(info.residence_address.clone()),
|
||||
city: Some(info.residence_city.clone()),
|
||||
zip: Some(info.residence_zip.clone()),
|
||||
country: Some(info.residence_country.clone()),
|
||||
};
|
||||
|
||||
// Resolve the account that will own this order. A logged-in customer always
|
||||
@@ -355,14 +549,14 @@ async fn place_order(
|
||||
company_id,
|
||||
tax_id,
|
||||
vat_id,
|
||||
residence_address: Some(residence_address),
|
||||
residence_city: Some(residence_city),
|
||||
residence_zip: Some(residence_zip),
|
||||
residence_country: Some(residence_country),
|
||||
address: Some(address),
|
||||
city: Some(city),
|
||||
zip: Some(zip),
|
||||
country: Some(country),
|
||||
residence_address: Some(info.residence_address.clone()),
|
||||
residence_city: Some(info.residence_city.clone()),
|
||||
residence_zip: Some(info.residence_zip.clone()),
|
||||
residence_country: Some(info.residence_country.clone()),
|
||||
address: Some(info.address.clone()),
|
||||
city: Some(info.city.clone()),
|
||||
zip: Some(info.zip.clone()),
|
||||
country: Some(info.country.clone()),
|
||||
note: form.note.as_deref().and_then(trimmed),
|
||||
payment_method: form.payment_method,
|
||||
method,
|
||||
@@ -382,7 +576,7 @@ async fn place_order(
|
||||
cart::clear_account_cart(&ctx, user.id).await?;
|
||||
}
|
||||
format::render()
|
||||
.cookies(&[cart::cleared_cart_cookie()])?
|
||||
.cookies(&[cart::cleared_cart_cookie(), cleared_info_cookie()])?
|
||||
.redirect(&target)
|
||||
}
|
||||
|
||||
@@ -430,7 +624,10 @@ async fn order_confirmation(
|
||||
|
||||
pub fn routes() -> Routes {
|
||||
Routes::new()
|
||||
.add("/checkout", get(checkout_page))
|
||||
.add("/checkout", post(place_order))
|
||||
.add("/checkout", get(checkout_redirect))
|
||||
.add("/checkout/info", get(info_page))
|
||||
.add("/checkout/info", post(submit_info))
|
||||
.add("/checkout/payment", get(payment_page))
|
||||
.add("/checkout/payment", post(place_order))
|
||||
.add("/orders/{order_number}", get(order_confirmation))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user