Files
kompress_eshop/src/models/orders.rs
Priec f3daa27ce7
Some checks failed
CI / Check Style (push) Has been cancelled
CI / Run Clippy (push) Has been cancelled
CI / Run Tests (push) Has been cancelled
account type is permanent and password registration is now working at checkout
2026-06-18 22:10:17 +02:00

144 lines
5.0 KiB
Rust

use loco_rs::prelude::*;
use sea_orm::entity::prelude::*;
use sea_orm::{Set, TransactionTrait};
use uuid::Uuid;
use crate::models::_entities::{order_items, products, shipping_methods};
pub use crate::models::_entities::orders::{ActiveModel, Column, Entity, Model};
pub type Orders = Entity;
/// The customer-supplied and carrier details needed to place an order. Prices
/// and product names are never taken from here — they are snapshotted from the
/// database inside [`place`] so the customer cannot influence what they pay.
pub struct Checkout {
pub email: String,
pub phone: String,
pub customer_name: Option<String>,
/// The account that owns this order, if any (a logged-in buyer or a guest
/// who created an account during checkout). `None` for pure guest orders.
pub user_id: Option<i32>,
pub account_type: String,
pub company_name: Option<String>,
pub company_id: Option<String>,
pub tax_id: Option<String>,
pub vat_id: Option<String>,
pub address: Option<String>,
pub city: Option<String>,
pub zip: Option<String>,
pub country: Option<String>,
pub note: Option<String>,
pub payment_method: String,
pub method: shipping_methods::Model,
pub pickup_point_id: Option<String>,
pub pickup_point_name: Option<String>,
}
fn generate_order_number() -> String {
let suffix = Uuid::new_v4().simple().to_string()[..8].to_uppercase();
format!("ORD-{suffix}")
}
/// Atomically place an order for the given `(product_id, quantity)` lines:
/// snapshot each product's price/name, decrement stock (re-checking inside the
/// transaction so an item can't oversell between cart and pay), then write the
/// order and its line items. Returns the persisted order.
pub async fn place(ctx: &AppContext, items: &[(i32, i32)], details: Checkout) -> Result<Model> {
let txn = ctx.db.begin().await?;
let mut subtotal: i64 = 0;
let mut currency = "EUR".to_string();
let mut snapshots = Vec::new();
for (product_id, qty) in items {
let product = products::Entity::find_by_id(*product_id)
.filter(products::Column::Published.eq(true))
.one(&txn)
.await?
.ok_or_else(|| Error::BadRequest("a product is no longer available".to_string()))?;
if product.stock < *qty {
return Err(Error::BadRequest(format!(
"not enough stock for {}",
product.name
)));
}
currency = product.currency.clone();
subtotal += product.price_cents * i64::from(*qty);
let mut active = product.clone().into_active_model();
active.stock = Set(product.stock - *qty);
active.update(&txn).await?;
snapshots.push((product.id, product.name, product.price_cents, *qty));
}
let order = ActiveModel {
order_number: Set(generate_order_number()),
email: Set(details.email),
phone: Set(Some(details.phone)),
customer_name: Set(details.customer_name),
status: Set("pending".to_string()),
total_cents: Set(subtotal + details.method.price_cents),
currency: Set(currency),
user_id: Set(details.user_id),
account_type: Set(details.account_type),
company_name: Set(details.company_name),
company_id: Set(details.company_id),
tax_id: Set(details.tax_id),
vat_id: Set(details.vat_id),
address: Set(details.address),
city: Set(details.city),
zip: Set(details.zip),
country: Set(details.country),
note: Set(details.note),
payment_method: Set(Some(details.payment_method)),
carrier_code: Set(Some(details.method.code)),
carrier_name: Set(Some(details.method.name)),
shipping_cents: Set(details.method.price_cents),
pickup_point_id: Set(details.pickup_point_id),
pickup_point_name: Set(details.pickup_point_name),
..Default::default()
}
.insert(&txn)
.await?;
for (product_id, name, unit_price_cents, qty) in snapshots {
order_items::ActiveModel {
order_id: Set(order.id),
product_id: Set(Some(product_id)),
product_name: Set(name),
unit_price_cents: Set(unit_price_cents),
quantity: Set(qty),
..Default::default()
}
.insert(&txn)
.await?;
}
txn.commit().await?;
Ok(order)
}
#[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,
{
if !insert && self.updated_at.is_unchanged() {
let mut this = self;
this.updated_at = sea_orm::ActiveValue::Set(chrono::Utc::now().into());
Ok(this)
} else {
Ok(self)
}
}
}
// implement your read-oriented logic here
impl Model {}
// implement your write-oriented logic here
impl ActiveModel {}
// implement your custom finders, selectors oriented logic here
impl Entity {}