account type is permanent and password registration is now working at checkout
Some checks failed
CI / Check Style (push) Has been cancelled
CI / Run Clippy (push) Has been cancelled
CI / Run Tests (push) Has been cancelled

This commit is contained in:
Priec
2026-06-18 22:10:17 +02:00
parent 46cc2459bd
commit f3daa27ce7
24 changed files with 483 additions and 103 deletions

View File

@@ -2,6 +2,10 @@
//! fields prefill the checkout form. Gated to authenticated non-admin users:
//! anonymous visitors are bounced to `/login`. Admins have their own area and
//! are sent to the dashboard.
//!
//! The account *type* (personal vs company) is fixed at registration and lives
//! on the user — it is shown here read-only and can never be changed. The
//! profile only edits the type-specific details (company identity + address).
use axum_extra::extract::cookie::CookieJar;
use loco_rs::prelude::*;
@@ -10,13 +14,15 @@ use serde_json::json;
use crate::{
controllers::i18n::current_lang,
models::customer_profiles::{self, ProfileFields},
models::{
customer_profiles::{self, ProfileFields},
users,
},
shared::guard,
};
#[derive(Debug, Deserialize)]
struct ProfileForm {
account_type: Option<String>,
company_name: Option<String>,
company_id: Option<String>,
tax_id: Option<String>,
@@ -33,34 +39,21 @@ fn trimmed(value: Option<&str>) -> Option<String> {
value.map(str::trim).filter(|v| !v.is_empty()).map(String::from)
}
/// Normalize an account type to one of the two known values, defaulting to
/// "personal" for anything unexpected.
pub fn normalize_account_type(value: Option<&str>) -> String {
match value.map(str::trim) {
Some("company") => "company".to_string(),
_ => "personal".to_string(),
}
}
impl From<ProfileForm> for ProfileFields {
fn from(form: ProfileForm) -> Self {
let is_company = normalize_account_type(form.account_type.as_deref()) == "company";
// Company identifiers are only stored for company accounts, so switching
// back to personal clears stale data.
let company = |v: Option<&str>| if is_company { trimmed(v) } else { None };
Self {
account_type: normalize_account_type(form.account_type.as_deref()),
company_name: company(form.company_name.as_deref()),
company_id: company(form.company_id.as_deref()),
tax_id: company(form.tax_id.as_deref()),
vat_id: company(form.vat_id.as_deref()),
phone_prefix: trimmed(form.phone_prefix.as_deref()),
phone: trimmed(form.phone.as_deref()),
address: trimmed(form.address.as_deref()),
city: trimmed(form.city.as_deref()),
zip: trimmed(form.zip.as_deref()),
country: trimmed(form.country.as_deref()),
}
/// Build the persisted fields from the submitted form. Company identifiers are
/// only kept for company accounts (a personal account can never carry them).
fn fields_from_form(form: &ProfileForm, is_company: bool) -> ProfileFields {
let company = |v: Option<&str>| if is_company { trimmed(v) } else { None };
ProfileFields {
company_name: company(form.company_name.as_deref()),
company_id: company(form.company_id.as_deref()),
tax_id: company(form.tax_id.as_deref()),
vat_id: company(form.vat_id.as_deref()),
phone_prefix: trimmed(form.phone_prefix.as_deref()),
phone: trimmed(form.phone.as_deref()),
address: trimmed(form.address.as_deref()),
city: trimmed(form.city.as_deref()),
zip: trimmed(form.zip.as_deref()),
country: trimmed(form.country.as_deref()),
}
}
@@ -68,7 +61,6 @@ impl From<ProfileForm> for ProfileFields {
fn fields_of(profile: Option<&customer_profiles::Model>) -> ProfileFields {
match profile {
Some(p) => ProfileFields {
account_type: p.account_type.clone(),
company_name: p.company_name.clone(),
company_id: p.company_id.clone(),
tax_id: p.tax_id.clone(),
@@ -80,30 +72,22 @@ fn fields_of(profile: Option<&customer_profiles::Model>) -> ProfileFields {
zip: p.zip.clone(),
country: p.country.clone(),
},
None => ProfileFields {
account_type: "personal".to_string(),
..Default::default()
},
None => ProfileFields::default(),
}
}
/// A company profile must carry its invoicing identity (company name + IČO +
/// DIČ; IČ DPH stays optional). Personal profiles have no such requirement.
/// A company account must carry its invoicing identity (company name + IČO +
/// DIČ; IČ DPH stays optional). Personal accounts have no such requirement.
fn company_fields_missing(fields: &ProfileFields) -> bool {
fields.account_type == "company"
&& (fields.company_name.is_none()
|| fields.company_id.is_none()
|| fields.tax_id.is_none())
fields.company_name.is_none() || fields.company_id.is_none() || fields.tax_id.is_none()
}
/// Render the profile form prefilled from `fields`. `saved` shows the success
/// banner; `error` shows a validation message and is set when a company profile
/// is missing required identifiers.
/// Render the profile form for `user`, prefilled from `fields`. `saved` shows
/// the success banner; `error` shows the company-required validation message.
fn profile_view(
v: &TeraView,
jar: &CookieJar,
name: &str,
email: &str,
user: &users::Model,
fields: &ProfileFields,
saved: bool,
error: bool,
@@ -116,9 +100,9 @@ fn profile_view(
"logged_in_customer": true,
"saved": saved,
"error": error,
"name": name,
"email": email,
"account_type": fields.account_type,
"name": user.name,
"email": user.email,
"account_type": user.account_type,
"company_name": fields.company_name,
"company_id": fields.company_id,
"tax_id": fields.tax_id,
@@ -147,7 +131,7 @@ async fn profile_page(
return format::redirect("/admin/dashboard");
}
let profile = customer_profiles::Model::find_for_user(&ctx.db, user.id).await?;
profile_view(&v, &jar, &user.name, &user.email, &fields_of(profile.as_ref()), false, false)
profile_view(&v, &jar, &user, &fields_of(profile.as_ref()), false, false)
}
#[debug_handler]
@@ -163,14 +147,14 @@ async fn save_profile(
if guard::is_admin(&ctx, &user) {
return format::redirect("/admin/dashboard");
}
let fields: ProfileFields = form.into();
// A company profile is rejected (and the form re-shown with the entered
let fields = fields_from_form(&form, user.is_company());
// A company account's profile is rejected (and re-shown with the entered
// values) until it carries its required identifiers.
if company_fields_missing(&fields) {
return profile_view(&v, &jar, &user.name, &user.email, &fields, false, true);
if user.is_company() && company_fields_missing(&fields) {
return profile_view(&v, &jar, &user, &fields, false, true);
}
customer_profiles::Model::upsert(&ctx.db, user.id, fields.clone()).await?;
profile_view(&v, &jar, &user.name, &user.email, &fields, true, false)
profile_view(&v, &jar, &user, &fields, true, false)
}
pub fn routes() -> Routes {