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, /// 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, pub account_type: String, pub company_name: Option, pub company_id: Option, pub tax_id: Option, pub vat_id: Option, pub address: Option, pub city: Option, pub zip: Option, pub country: Option, pub note: Option, pub payment_method: String, pub method: shipping_methods::Model, pub pickup_point_id: Option, pub pickup_point_name: Option, } 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 { 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(self, _db: &C, insert: bool) -> std::result::Result 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 {}