144 lines
5.0 KiB
Rust
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 {}
|