basket logic working

This commit is contained in:
Priec
2026-06-27 22:11:13 +02:00
parent 9bdf91e717
commit c549e2bc03
13 changed files with 301 additions and 37 deletions

View File

@@ -52,6 +52,7 @@ mod m20260623_000004_currencies;
mod m20260625_000001_add_avatar_to_users; mod m20260625_000001_add_avatar_to_users;
mod m20260627_000001_order_residence_address; mod m20260627_000001_order_residence_address;
mod m20260627_000002_payment_settings; mod m20260627_000002_payment_settings;
mod m20260627_000003_account_cart_items;
pub struct Migrator; pub struct Migrator;
#[async_trait::async_trait] #[async_trait::async_trait]
@@ -108,6 +109,7 @@ impl MigratorTrait for Migrator {
Box::new(m20260625_000001_add_avatar_to_users::Migration), Box::new(m20260625_000001_add_avatar_to_users::Migration),
Box::new(m20260627_000001_order_residence_address::Migration), Box::new(m20260627_000001_order_residence_address::Migration),
Box::new(m20260627_000002_payment_settings::Migration), Box::new(m20260627_000002_payment_settings::Migration),
Box::new(m20260627_000003_account_cart_items::Migration),
// inject-above (do not remove this comment) // inject-above (do not remove this comment)
] ]
} }

View File

@@ -0,0 +1,48 @@
use loco_rs::schema::*;
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {
create_table(
m,
"account_cart_items",
&[
("id", ColType::PkAuto),
("variant_id", ColType::Integer),
("quantity", ColType::Integer),
],
&[("user", "")],
)
.await?;
m.create_foreign_key(
ForeignKey::create()
.name("fk-account_cart_items-variant_id-to-product_variants")
.from(Alias::new("account_cart_items"), Alias::new("variant_id"))
.to(Alias::new("product_variants"), Alias::new("id"))
.on_delete(ForeignKeyAction::Cascade)
.on_update(ForeignKeyAction::NoAction)
.to_owned(),
)
.await?;
m.create_index(
Index::create()
.name("idx_account_cart_items_user_variant_unique")
.table(Alias::new("account_cart_items"))
.col(Alias::new("user_id"))
.col(Alias::new("variant_id"))
.unique()
.to_owned(),
)
.await
}
async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> {
drop_table(m, "account_cart_items").await
}
}

View File

@@ -1,10 +1,11 @@
use crate::{ use crate::{
controllers::cart,
models::users::{self, LoginParams, RegisterParams}, models::users::{self, LoginParams, RegisterParams},
views::auth::{CurrentResponse, LoginResponse}, views::auth::{CurrentResponse, LoginResponse},
mailers::auth::AuthMailer, mailers::auth::AuthMailer,
shared::guard::is_admin, shared::guard::is_admin,
}; };
use axum_extra::extract::cookie::{Cookie, SameSite}; use axum_extra::extract::cookie::{Cookie, CookieJar, SameSite};
use loco_rs::prelude::*; use loco_rs::prelude::*;
use regex::Regex; use regex::Regex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -88,6 +89,7 @@ pub struct ResendVerificationParams {
/// welcome email to the user /// welcome email to the user
#[debug_handler] #[debug_handler]
async fn register( async fn register(
jar: CookieJar,
State(ctx): State<AppContext>, State(ctx): State<AppContext>,
Json(params): Json<RegisterParams>, Json(params): Json<RegisterParams>,
) -> Result<Response> { ) -> Result<Response> {
@@ -109,6 +111,7 @@ async fn register(
.into_active_model() .into_active_model()
.set_email_verification_sent(&ctx.db) .set_email_verification_sent(&ctx.db)
.await?; .await?;
cart::claim_guest_cart(&ctx, &jar, user.id).await?;
AuthMailer::send_welcome(&ctx, &user).await?; AuthMailer::send_welcome(&ctx, &user).await?;
@@ -199,8 +202,9 @@ async fn login(State(ctx): State<AppContext>, Json(params): Json<LoginParams>) -
.generate_jwt(&jwt_secret.secret, jwt_secret.expiration) .generate_jwt(&jwt_secret.secret, jwt_secret.expiration)
.or_else(|_| unauthorized("unauthorized!"))?; .or_else(|_| unauthorized("unauthorized!"))?;
let cart_cookie = cart::cart_cookie_for_user(&ctx, user.id).await?;
format::render() format::render()
.cookies(&[auth_cookie(&token, jwt_secret.expiration)])? .cookies(&[auth_cookie(&token, jwt_secret.expiration), cart_cookie])?
.json(LoginResponse::new(&user, &token, is_admin(&ctx, &user))) .json(LoginResponse::new(&user, &token, is_admin(&ctx, &user)))
} }
@@ -212,7 +216,9 @@ async fn current(auth: auth::JWT, State(ctx): State<AppContext>) -> Result<Respo
#[debug_handler] #[debug_handler]
async fn logout() -> Result<Response> { async fn logout() -> Result<Response> {
format::render().cookies(&[clear_auth_cookie()])?.json(()) format::render()
.cookies(&[clear_auth_cookie(), cart::cleared_cart_cookie()])?
.json(())
} }
/// Magic link authentication provides a secure and passwordless way to log in to the application. /// Magic link authentication provides a secure and passwordless way to log in to the application.
@@ -274,8 +280,9 @@ async fn magic_link_verify(
.generate_jwt(&jwt_secret.secret, jwt_secret.expiration) .generate_jwt(&jwt_secret.secret, jwt_secret.expiration)
.or_else(|_| unauthorized("unauthorized!"))?; .or_else(|_| unauthorized("unauthorized!"))?;
let cart_cookie = cart::cart_cookie_for_user(&ctx, user.id).await?;
format::render() format::render()
.cookies(&[auth_cookie(&token, jwt_secret.expiration)])? .cookies(&[auth_cookie(&token, jwt_secret.expiration), cart_cookie])?
.json(LoginResponse::new(&user, &token, is_admin(&ctx, &user))) .json(LoginResponse::new(&user, &token, is_admin(&ctx, &user)))
} }

View File

@@ -13,6 +13,7 @@ use serde_json::json;
use crate::{ use crate::{
controllers::auth as auth_controller, controllers::auth as auth_controller,
controllers::cart,
controllers::i18n::current_lang, controllers::i18n::current_lang,
mailers::auth::AuthMailer, mailers::auth::AuthMailer,
models::users::{self, LoginParams, RegisterParams}, models::users::{self, LoginParams, RegisterParams},
@@ -105,9 +106,13 @@ async fn login(
let token = user let token = user
.generate_jwt(&jwt_secret.secret, jwt_secret.expiration) .generate_jwt(&jwt_secret.secret, jwt_secret.expiration)
.or_else(|_| unauthorized("unauthorized!"))?; .or_else(|_| unauthorized("unauthorized!"))?;
let cart_cookie = cart::cart_cookie_for_user(&ctx, user.id).await?;
format::render() format::render()
.cookies(&[auth_controller::auth_cookie(&token, jwt_secret.expiration)])? .cookies(&[
auth_controller::auth_cookie(&token, jwt_secret.expiration),
cart_cookie,
])?
.redirect(home_for(&ctx, &user)) .redirect(home_for(&ctx, &user))
} }
@@ -185,11 +190,13 @@ async fn login_totp(
let token = user let token = user
.generate_jwt(&jwt_secret.secret, jwt_secret.expiration) .generate_jwt(&jwt_secret.secret, jwt_secret.expiration)
.or_else(|_| unauthorized("unauthorized!"))?; .or_else(|_| unauthorized("unauthorized!"))?;
let cart_cookie = cart::cart_cookie_for_user(&ctx, user.id).await?;
format::render() format::render()
.cookies(&[ .cookies(&[
auth_controller::auth_cookie(&token, jwt_secret.expiration), auth_controller::auth_cookie(&token, jwt_secret.expiration),
auth_controller::clear_totp_pending_cookie(), auth_controller::clear_totp_pending_cookie(),
cart_cookie,
])? ])?
.redirect(home_for(&ctx, &user)) .redirect(home_for(&ctx, &user))
} }
@@ -270,6 +277,7 @@ async fn register(
.into_active_model() .into_active_model()
.set_email_verification_sent(&ctx.db) .set_email_verification_sent(&ctx.db)
.await?; .await?;
cart::claim_guest_cart(&ctx, &jar, user.id).await?;
// The account already exists; a failed email send shouldn't 500 the page — // The account already exists; a failed email send shouldn't 500 the page —
// log it and let the user fall back to resend-verification. // log it and let the user fall back to resend-verification.
@@ -304,7 +312,9 @@ async fn verify(
}; };
if user.email_verified_at.is_none() { if user.email_verified_at.is_none() {
let user_id = user.id;
user.into_active_model().verified(&ctx.db).await?; user.into_active_model().verified(&ctx.db).await?;
cart::claim_guest_cart(&ctx, &jar, user_id).await?;
} }
verified_view(&v, &jar, true) verified_view(&v, &jar, true)
@@ -446,7 +456,10 @@ async fn set_password(
#[debug_handler] #[debug_handler]
async fn logout() -> Result<Response> { async fn logout() -> Result<Response> {
format::render() format::render()
.cookies(&[auth_controller::clear_auth_cookie()])? .cookies(&[
auth_controller::clear_auth_cookie(),
cart::cleared_cart_cookie(),
])?
.redirect("/login") .redirect("/login")
} }

View File

@@ -1,4 +1,11 @@
use crate::{controllers::i18n::current_lang, shared::{currency::{self, Currency}, guard, pricing}, models::{product_variants, products}}; use crate::{
controllers::i18n::current_lang,
models::{account_cart_items, product_variants, products, users},
shared::{
currency::{self, Currency},
guard, pricing,
},
};
use axum::{ use axum::{
http::{HeaderMap, StatusCode}, http::{HeaderMap, StatusCode},
response::Redirect, response::Redirect,
@@ -64,6 +71,75 @@ fn cart_cookie(value: String) -> Cookie<'static> {
.build() .build()
} }
pub(crate) fn cleared_cart_cookie() -> Cookie<'static> {
Cookie::build((CART_COOKIE, ""))
.path("/")
.same_site(SameSite::Lax)
.max_age(TimeDuration::seconds(0))
.build()
}
fn normalize_items(items: Vec<(i32, i32)>) -> Vec<(i32, i32)> {
let mut normalized: Vec<(i32, i32)> = Vec::new();
for (id, qty) in items.into_iter().filter(|(_, qty)| *qty > 0) {
if let Some(existing) = normalized.iter_mut().find(|(existing_id, _)| *existing_id == id) {
existing.1 += qty;
} else {
normalized.push((id, qty));
}
}
normalized
}
async fn stored_cart(
ctx: &AppContext,
user: Option<&users::Model>,
jar: &CookieJar,
) -> Result<Vec<(i32, i32)>> {
match user {
Some(user) => Ok(account_cart_items::Model::find_for_user(&ctx.db, user.id).await?),
None => Ok(normalize_items(parse_cart(jar))),
}
}
async fn persist_cart(
ctx: &AppContext,
jar: CookieJar,
user: Option<&users::Model>,
items: &[(i32, i32)],
) -> Result<CookieJar> {
let items = normalize_items(items.to_vec());
if let Some(user) = user {
account_cart_items::Model::replace_for_user(&ctx.db, user.id, &items).await?;
}
Ok(jar.add(cart_cookie(serialize_cart(&items))))
}
pub(crate) async fn claim_guest_cart(
ctx: &AppContext,
jar: &CookieJar,
user_id: i32,
) -> Result<()> {
let items = normalize_items(parse_cart(jar));
if !items.is_empty() {
account_cart_items::Model::replace_for_user(&ctx.db, user_id, &items).await?;
}
Ok(())
}
pub(crate) async fn cart_cookie_for_user(
ctx: &AppContext,
user_id: i32,
) -> Result<Cookie<'static>> {
let items = account_cart_items::Model::find_for_user(&ctx.db, user_id).await?;
Ok(cart_cookie(serialize_cart(&items)))
}
pub(crate) async fn clear_account_cart(ctx: &AppContext, user_id: i32) -> Result<()> {
account_cart_items::Model::replace_for_user(&ctx.db, user_id, &[]).await?;
Ok(())
}
/// Look up a variant whose product is published, returning the variant together /// Look up a variant whose product is published, returning the variant together
/// with its parent product (for name/slug). /// with its parent product (for name/slug).
async fn published_variant( async fn published_variant(
@@ -94,7 +170,8 @@ async fn add(
return Err(Error::NotFound); return Err(Error::NotFound);
}; };
let mut items = parse_cart(&jar); let user = guard::current_user(&ctx, &jar).await;
let mut items = stored_cart(&ctx, user.as_ref(), &jar).await?;
let add_qty = form.quantity.unwrap_or(1).max(1); let add_qty = form.quantity.unwrap_or(1).max(1);
if let Some(entry) = items.iter_mut().find(|(id, _)| *id == variant.id) { if let Some(entry) = items.iter_mut().find(|(id, _)| *id == variant.id) {
entry.1 = variant.cap(entry.1 + add_qty); entry.1 = variant.cap(entry.1 + add_qty);
@@ -103,7 +180,7 @@ async fn add(
} }
items.retain(|(_, qty)| *qty > 0); items.retain(|(_, qty)| *qty > 0);
let jar = jar.add(cart_cookie(serialize_cart(&items))); let jar = persist_cart(&ctx, jar, user.as_ref(), &items).await?;
// Adding to the cart should never navigate away: htmx requests get an empty // Adding to the cart should never navigate away: htmx requests get an empty
// 204 (the header cart badge updates client-side), and a no-JS submit goes // 204 (the header cart badge updates client-side), and a no-JS submit goes
@@ -135,13 +212,14 @@ async fn update(
None => 0, None => 0,
}; };
let mut items = parse_cart(&jar); let user = guard::current_user(&ctx, &jar).await;
let mut items = stored_cart(&ctx, user.as_ref(), &jar).await?;
if let Some(entry) = items.iter_mut().find(|(id, _)| *id == form.variant_id) { if let Some(entry) = items.iter_mut().find(|(id, _)| *id == form.variant_id) {
entry.1 = clamped; entry.1 = clamped;
} }
items.retain(|(_, qty)| *qty > 0); items.retain(|(_, qty)| *qty > 0);
let jar = jar.add(cart_cookie(serialize_cart(&items))); let jar = persist_cart(&ctx, jar, user.as_ref(), &items).await?;
cart_response(&ctx, &v, jar, &headers).await cart_response(&ctx, &v, jar, &headers).await
} }
@@ -153,10 +231,11 @@ async fn remove(
headers: HeaderMap, headers: HeaderMap,
Form(form): Form<RemoveForm>, Form(form): Form<RemoveForm>,
) -> Result<Response> { ) -> Result<Response> {
let mut items = parse_cart(&jar); let user = guard::current_user(&ctx, &jar).await;
let mut items = stored_cart(&ctx, user.as_ref(), &jar).await?;
items.retain(|(id, _)| *id != form.variant_id); items.retain(|(id, _)| *id != form.variant_id);
let jar = jar.add(cart_cookie(serialize_cart(&items))); let jar = persist_cart(&ctx, jar, user.as_ref(), &items).await?;
cart_response(&ctx, &v, jar, &headers).await cart_response(&ctx, &v, jar, &headers).await
} }
@@ -176,7 +255,8 @@ async fn cart_response(
let cur = currency::resolve(ctx, &jar).await; let cur = currency::resolve(ctx, &jar).await;
let (lines, valid, total) = resolve_cart(ctx, &jar, &cur).await?; let (lines, valid, total) = resolve_cart(ctx, &jar, &cur).await?;
// Persist the re-validated cookie (drops now-invalid lines). // Persist the re-validated cookie (drops now-invalid lines).
let jar = jar.add(cart_cookie(serialize_cart(&valid))); let user = guard::current_user(ctx, &jar).await;
let jar = persist_cart(ctx, jar, user.as_ref(), &valid).await?;
let response = format::view( let response = format::view(
v, v,
"shop/_cart_body.html", "shop/_cart_body.html",
@@ -190,9 +270,9 @@ async fn cart_response(
Ok((jar, response).into_response()) Ok((jar, response).into_response())
} }
/// Resolve the cart cookie into priced line items, dropping anything that is no /// Resolve the active cart into priced line items, dropping anything that is no
/// longer purchasable and clamping quantities to current stock. Returns the /// longer purchasable and clamping quantities to current stock. Guests resolve
/// (re-validated) lines, the rebuilt cookie value, and the total in cents. /// from the cookie; authenticated users resolve from their account cart.
pub(crate) async fn resolve_cart( pub(crate) async fn resolve_cart(
ctx: &AppContext, ctx: &AppContext,
jar: &CookieJar, jar: &CookieJar,
@@ -202,7 +282,7 @@ pub(crate) async fn resolve_cart(
// for the current viewer in one batch (the price depends on who's logged in). // for the current viewer in one batch (the price depends on who's logged in).
let user = guard::current_user(ctx, jar).await; let user = guard::current_user(ctx, jar).await;
let mut items: Vec<(product_variants::Model, products::Model, i32)> = Vec::new(); let mut items: Vec<(product_variants::Model, products::Model, i32)> = Vec::new();
for (id, qty) in parse_cart(jar) { for (id, qty) in stored_cart(ctx, user.as_ref(), jar).await? {
let Some((variant, product)) = published_variant(ctx, id).await? else { let Some((variant, product)) = published_variant(ctx, id).await? else {
continue; continue;
}; };
@@ -238,6 +318,10 @@ pub(crate) async fn resolve_cart(
})); }));
} }
if let Some(user) = user.as_ref() {
account_cart_items::Model::replace_for_user(&ctx.db, user.id, &valid).await?;
}
Ok((lines, valid, total)) Ok((lines, valid, total))
} }
@@ -250,8 +334,6 @@ async fn show(
let cur = currency::resolve(&ctx, &jar).await; let cur = currency::resolve(&ctx, &jar).await;
let (lines, valid, total) = resolve_cart(&ctx, &jar, &cur).await?; let (lines, valid, total) = resolve_cart(&ctx, &jar, &cur).await?;
// Drop any now-invalid lines from the cookie so the badge stays accurate.
let rebuilt = serialize_cart(&valid);
let c = guard::chrome(&ctx, &jar).await; let c = guard::chrome(&ctx, &jar).await;
let response = format::view( let response = format::view(
&v, &v,
@@ -269,7 +351,9 @@ async fn show(
}), }),
)?; )?;
Ok((jar.add(cart_cookie(rebuilt)), response).into_response()) let user = guard::current_user(&ctx, &jar).await;
let jar = persist_cart(&ctx, jar, user.as_ref(), &valid).await?;
Ok((jar, response).into_response())
} }
/// Mini-cart preview for the navbar hover dropdown. Lazy-loaded via htmx from /// Mini-cart preview for the navbar hover dropdown. Lazy-loaded via htmx from
@@ -282,7 +366,6 @@ async fn preview(
) -> Result<Response> { ) -> Result<Response> {
let cur = currency::resolve(&ctx, &jar).await; let cur = currency::resolve(&ctx, &jar).await;
let (lines, valid, total) = resolve_cart(&ctx, &jar, &cur).await?; let (lines, valid, total) = resolve_cart(&ctx, &jar, &cur).await?;
let rebuilt = serialize_cart(&valid);
let response = format::view( let response = format::view(
&v, &v,
"shop/_cart_preview.html", "shop/_cart_preview.html",
@@ -293,7 +376,9 @@ async fn preview(
"lang": current_lang(&jar), "lang": current_lang(&jar),
}), }),
)?; )?;
Ok((jar.add(cart_cookie(rebuilt)), response).into_response()) let user = guard::current_user(&ctx, &jar).await;
let jar = persist_cart(&ctx, jar, user.as_ref(), &valid).await?;
Ok((jar, response).into_response())
} }
pub fn routes() -> Routes { pub fn routes() -> Routes {

View File

@@ -2,15 +2,13 @@
//! confirmation page. //! confirmation page.
use axum::extract::Query; use axum::extract::Query;
use axum_extra::extract::cookie::{Cookie, CookieJar, SameSite}; use axum_extra::extract::cookie::CookieJar;
use loco_rs::prelude::*; use loco_rs::prelude::*;
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QueryOrder}; use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QueryOrder};
use serde::Deserialize; use serde::Deserialize;
use serde_json::json; use serde_json::json;
use time::Duration as TimeDuration;
use crate::{ use crate::{
controllers::cart::{resolve_cart, CART_COOKIE}, controllers::cart::{self, resolve_cart},
mailers::auth::AuthMailer, mailers::auth::AuthMailer,
models::{ models::{
customer_profiles::{self, ProfileFields}, customer_profiles::{self, ProfileFields},
@@ -58,14 +56,6 @@ fn trimmed(value: &str) -> Option<String> {
(!value.is_empty()).then(|| value.to_string()) (!value.is_empty()).then(|| value.to_string())
} }
fn cleared_cart_cookie() -> Cookie<'static> {
Cookie::build((CART_COOKIE, ""))
.path("/")
.same_site(SameSite::Lax)
.max_age(TimeDuration::seconds(0))
.build()
}
async fn enabled_shipping_methods(ctx: &AppContext) -> Result<Vec<shipping_methods::Model>> { async fn enabled_shipping_methods(ctx: &AppContext) -> Result<Vec<shipping_methods::Model>> {
shipping_rules::disable_packeta_if_unconfigured(ctx).await?; shipping_rules::disable_packeta_if_unconfigured(ctx).await?;
let packeta_ready = shipping_rules::packeta_ready(ctx); let packeta_ready = shipping_rules::packeta_ready(ctx);
@@ -388,8 +378,11 @@ async fn place_order(
} else { } else {
format!("/orders/{}", order.order_number) format!("/orders/{}", order.order_number)
}; };
if let Some(user) = logged_in_customer {
cart::clear_account_cart(&ctx, user.id).await?;
}
format::render() format::render()
.cookies(&[cleared_cart_cookie()])? .cookies(&[cart::cleared_cart_cookie()])?
.redirect(&target) .redirect(&target)
} }

View File

@@ -17,6 +17,7 @@ use loco_rs::prelude::*;
use crate::{ use crate::{
controllers::auth as auth_controller, controllers::auth as auth_controller,
controllers::cart,
models::{o_auth2_sessions, users, users::OAuth2UserProfile}, models::{o_auth2_sessions, users, users::OAuth2UserProfile},
shared::guard, shared::guard,
}; };
@@ -36,8 +37,9 @@ async fn complete(State(ctx): State<AppContext>, user: GoogleCookieUser) -> Resu
} else { } else {
"/" "/"
}; };
let cart_cookie = cart::cart_cookie_for_user(&ctx, user.id).await?;
format::render() format::render()
.cookies(&[auth_controller::auth_cookie(&token, jwt_secret.expiration)])? .cookies(&[auth_controller::auth_cookie(&token, jwt_secret.expiration), cart_cookie])?
.redirect(dest) .redirect(dest)
} }

View File

@@ -0,0 +1,48 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "account_cart_items")]
pub struct Model {
pub created_at: DateTimeWithTimeZone,
pub updated_at: DateTimeWithTimeZone,
#[sea_orm(primary_key)]
pub id: i32,
pub variant_id: i32,
pub quantity: i32,
pub user_id: i32,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::product_variants::Entity",
from = "Column::VariantId",
to = "super::product_variants::Column::Id",
on_update = "NoAction",
on_delete = "Cascade"
)]
ProductVariants,
#[sea_orm(
belongs_to = "super::users::Entity",
from = "Column::UserId",
to = "super::users::Column::Id",
on_update = "Cascade",
on_delete = "Cascade"
)]
Users,
}
impl Related<super::product_variants::Entity> for Entity {
fn to() -> RelationDef {
Relation::ProductVariants.def()
}
}
impl Related<super::users::Entity> for Entity {
fn to() -> RelationDef {
Relation::Users.def()
}
}

View File

@@ -2,6 +2,7 @@
pub mod prelude; pub mod prelude;
pub mod account_cart_items;
pub mod account_discount_profiles; pub mod account_discount_profiles;
pub mod account_product_prices; pub mod account_product_prices;
pub mod account_product_resolutions; pub mod account_product_resolutions;

View File

@@ -1,5 +1,6 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19 //! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
pub use super::account_cart_items::Entity as AccountCartItems;
pub use super::account_discount_profiles::Entity as AccountDiscountProfiles; pub use super::account_discount_profiles::Entity as AccountDiscountProfiles;
pub use super::account_product_prices::Entity as AccountProductPrices; pub use super::account_product_prices::Entity as AccountProductPrices;
pub use super::account_product_resolutions::Entity as AccountProductResolutions; pub use super::account_product_resolutions::Entity as AccountProductResolutions;

View File

@@ -22,6 +22,8 @@ pub struct Model {
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation { pub enum Relation {
#[sea_orm(has_many = "super::account_cart_items::Entity")]
AccountCartItems,
#[sea_orm(has_many = "super::account_product_prices::Entity")] #[sea_orm(has_many = "super::account_product_prices::Entity")]
AccountProductPrices, AccountProductPrices,
#[sea_orm(has_many = "super::account_product_resolutions::Entity")] #[sea_orm(has_many = "super::account_product_resolutions::Entity")]
@@ -38,6 +40,12 @@ pub enum Relation {
Products, Products,
} }
impl Related<super::account_cart_items::Entity> for Entity {
fn to() -> RelationDef {
Relation::AccountCartItems.def()
}
}
impl Related<super::account_product_prices::Entity> for Entity { impl Related<super::account_product_prices::Entity> for Entity {
fn to() -> RelationDef { fn to() -> RelationDef {
Relation::AccountProductPrices.def() Relation::AccountProductPrices.def()

View File

@@ -0,0 +1,55 @@
pub use crate::models::_entities::account_cart_items::{ActiveModel, Column, Entity, Model};
use sea_orm::entity::prelude::*;
use sea_orm::{ActiveValue, QueryFilter, QueryOrder};
pub type AccountCartItems = Entity;
#[async_trait::async_trait]
impl ActiveModelBehavior for ActiveModel {
async fn before_save<C>(self, _db: &C, _insert: bool) -> std::result::Result<Self, DbErr>
where
C: ConnectionTrait,
{
Ok(self)
}
}
impl Model {
pub async fn find_for_user(
db: &DatabaseConnection,
user_id: i32,
) -> Result<Vec<(i32, i32)>, DbErr> {
Ok(Entity::find()
.filter(Column::UserId.eq(user_id))
.order_by_asc(Column::Id)
.all(db)
.await?
.into_iter()
.filter_map(|item| (item.quantity > 0).then_some((item.variant_id, item.quantity)))
.collect())
}
pub async fn replace_for_user(
db: &DatabaseConnection,
user_id: i32,
items: &[(i32, i32)],
) -> Result<(), DbErr> {
Entity::delete_many()
.filter(Column::UserId.eq(user_id))
.exec(db)
.await?;
for (variant_id, quantity) in items.iter().copied().filter(|(_, qty)| *qty > 0) {
ActiveModel {
user_id: ActiveValue::set(user_id),
variant_id: ActiveValue::set(variant_id),
quantity: ActiveValue::set(quantity),
..Default::default()
}
.insert(db)
.await?;
}
Ok(())
}
}

View File

@@ -6,6 +6,7 @@
pub mod _entities; pub mod _entities;
pub mod account_cart_items;
pub mod account_discount_profiles; pub mod account_discount_profiles;
pub mod account_product_prices; pub mod account_product_prices;
pub mod account_product_resolutions; pub mod account_product_resolutions;