114 lines
3.6 KiB
Rust
114 lines
3.6 KiB
Rust
use sea_orm::entity::prelude::*;
|
|
use sea_orm::QueryOrder;
|
|
pub use super::_entities::product_variants::{ActiveModel, Column, Entity, Model};
|
|
pub type ProductVariants = 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,
|
|
{
|
|
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 {
|
|
/// Whether a discount is currently active: a sale price is set and is
|
|
/// strictly below the regular price.
|
|
#[must_use]
|
|
pub fn on_sale(&self) -> bool {
|
|
matches!(self.sale_price_cents, Some(sale) if sale < self.price_cents)
|
|
}
|
|
|
|
/// The price actually charged: the sale price when [`Model::on_sale`],
|
|
/// otherwise the regular price.
|
|
#[must_use]
|
|
pub fn effective_price_cents(&self) -> i64 {
|
|
if self.on_sale() {
|
|
self.sale_price_cents.unwrap_or(self.price_cents)
|
|
} else {
|
|
self.price_cents
|
|
}
|
|
}
|
|
|
|
/// Whether a baseline business discount (for all company accounts) is set and
|
|
/// actually below the regular price.
|
|
#[must_use]
|
|
pub fn business_on_sale(&self) -> bool {
|
|
matches!(self.business_sale_price_cents, Some(sale) if sale < self.price_cents)
|
|
}
|
|
|
|
/// Whether the variant's inventory is tracked. A `None` stock means
|
|
/// "available, not tracked" (always purchasable, unlimited).
|
|
#[must_use]
|
|
pub fn tracked(&self) -> bool {
|
|
self.stock.is_some()
|
|
}
|
|
|
|
/// Whether the variant can currently be bought: untracked variants are always
|
|
/// available; tracked ones need a positive quantity on hand.
|
|
#[must_use]
|
|
pub fn in_stock(&self) -> bool {
|
|
self.stock.map_or(true, |s| s > 0)
|
|
}
|
|
|
|
/// Clamp a desired quantity to what's available: capped at the tracked stock,
|
|
/// or left as-is (only floored at 0) when untracked.
|
|
#[must_use]
|
|
pub fn cap(&self, qty: i32) -> i32 {
|
|
match self.stock {
|
|
Some(s) => qty.clamp(0, s),
|
|
None => qty.max(0),
|
|
}
|
|
}
|
|
}
|
|
|
|
// implement your write-oriented logic here
|
|
impl ActiveModel {}
|
|
|
|
// implement your custom finders, selectors oriented logic here
|
|
impl Entity {
|
|
/// All variants for one product, in display order.
|
|
pub async fn for_product<C: ConnectionTrait>(
|
|
db: &C,
|
|
product_id: i32,
|
|
) -> Result<Vec<Model>, DbErr> {
|
|
Entity::find()
|
|
.filter(Column::ProductId.eq(product_id))
|
|
.order_by_asc(Column::Position)
|
|
.order_by_asc(Column::Id)
|
|
.all(db)
|
|
.await
|
|
}
|
|
|
|
/// All variants for many products in one query, grouped by `product_id` and
|
|
/// ordered within each group. Products with no variants are absent.
|
|
pub async fn grouped_for_products<C: ConnectionTrait>(
|
|
db: &C,
|
|
product_ids: &[i32],
|
|
) -> Result<std::collections::HashMap<i32, Vec<Model>>, DbErr> {
|
|
let mut map: std::collections::HashMap<i32, Vec<Model>> = std::collections::HashMap::new();
|
|
if product_ids.is_empty() {
|
|
return Ok(map);
|
|
}
|
|
let rows = Entity::find()
|
|
.filter(Column::ProductId.is_in(product_ids.to_vec()))
|
|
.order_by_asc(Column::Position)
|
|
.order_by_asc(Column::Id)
|
|
.all(db)
|
|
.await?;
|
|
for row in rows {
|
|
map.entry(row.product_id).or_default().push(row);
|
|
}
|
|
Ok(map)
|
|
}
|
|
}
|