personal discounts to businesses done
This commit is contained in:
49
src/models/_entities/account_product_prices.rs
Normal file
49
src/models/_entities/account_product_prices.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
//! `SeaORM` Entity for per-account negotiated product prices. Hand-written to
|
||||
//! match the `account_product_prices` migration (one row per (user, product)).
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "account_product_prices")]
|
||||
pub struct Model {
|
||||
pub created_at: DateTimeWithTimeZone,
|
||||
pub updated_at: DateTimeWithTimeZone,
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub user_id: i32,
|
||||
pub product_id: i32,
|
||||
pub price_cents: i64,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::users::Entity",
|
||||
from = "Column::UserId",
|
||||
to = "super::users::Column::Id",
|
||||
on_update = "Cascade",
|
||||
on_delete = "Cascade"
|
||||
)]
|
||||
Users,
|
||||
#[sea_orm(
|
||||
belongs_to = "super::products::Entity",
|
||||
from = "Column::ProductId",
|
||||
to = "super::products::Column::Id",
|
||||
on_update = "Cascade",
|
||||
on_delete = "Cascade"
|
||||
)]
|
||||
Products,
|
||||
}
|
||||
|
||||
impl Related<super::users::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Users.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::products::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Products.def()
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
pub mod prelude;
|
||||
|
||||
pub mod account_product_prices;
|
||||
pub mod audit_logs;
|
||||
pub mod categories;
|
||||
pub mod customer_profiles;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.20
|
||||
|
||||
pub use super::account_product_prices::Entity as AccountProductPrices;
|
||||
pub use super::audit_logs::Entity as AuditLogs;
|
||||
pub use super::categories::Entity as Categories;
|
||||
pub use super::customer_profiles::Entity as CustomerProfiles;
|
||||
|
||||
77
src/models/account_product_prices.rs
Normal file
77
src/models/account_product_prices.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
//! Per-account negotiated product prices: an admin-set price for one product,
|
||||
//! for one business account ("personal agreement"). One row per (user, product),
|
||||
//! kept unique by the index in the migration.
|
||||
|
||||
pub use crate::models::_entities::account_product_prices::{ActiveModel, Column, Entity, Model};
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::{ActiveValue, IntoActiveModel, QueryFilter, TryIntoModel};
|
||||
|
||||
pub type AccountProductPrices = 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 = ActiveValue::set(chrono::Utc::now().into());
|
||||
Ok(this)
|
||||
} else {
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Model {
|
||||
/// All negotiated prices for one account, as a `(product_id -> cents)` map.
|
||||
pub async fn map_for_user(
|
||||
db: &DatabaseConnection,
|
||||
user_id: i32,
|
||||
) -> Result<std::collections::HashMap<i32, i64>, DbErr> {
|
||||
let rows = Entity::find()
|
||||
.filter(Column::UserId.eq(user_id))
|
||||
.all(db)
|
||||
.await?;
|
||||
Ok(rows.into_iter().map(|r| (r.product_id, r.price_cents)).collect())
|
||||
}
|
||||
|
||||
/// Insert or update the negotiated price for `(user_id, product_id)`.
|
||||
pub async fn upsert(
|
||||
db: &DatabaseConnection,
|
||||
user_id: i32,
|
||||
product_id: i32,
|
||||
price_cents: i64,
|
||||
) -> Result<Self, DbErr> {
|
||||
let existing = Entity::find()
|
||||
.filter(Column::UserId.eq(user_id))
|
||||
.filter(Column::ProductId.eq(product_id))
|
||||
.one(db)
|
||||
.await?;
|
||||
let mut active = match existing {
|
||||
Some(row) => row.into_active_model(),
|
||||
None => ActiveModel {
|
||||
user_id: ActiveValue::set(user_id),
|
||||
product_id: ActiveValue::set(product_id),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
active.price_cents = ActiveValue::set(price_cents);
|
||||
active.save(db).await?.try_into_model()
|
||||
}
|
||||
|
||||
/// Remove the negotiated price for `(user_id, product_id)`, if any.
|
||||
pub async fn clear(
|
||||
db: &DatabaseConnection,
|
||||
user_id: i32,
|
||||
product_id: i32,
|
||||
) -> Result<(), DbErr> {
|
||||
Entity::delete_many()
|
||||
.filter(Column::UserId.eq(user_id))
|
||||
.filter(Column::ProductId.eq(product_id))
|
||||
.exec(db)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
pub mod _entities;
|
||||
|
||||
pub mod account_product_prices;
|
||||
pub mod audit_logs;
|
||||
pub mod categories;
|
||||
pub mod customer_profiles;
|
||||
|
||||
@@ -4,6 +4,8 @@ use sea_orm::{Set, TransactionTrait};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::models::_entities::{order_items, products, shipping_methods};
|
||||
use crate::models::users;
|
||||
use crate::shared::pricing;
|
||||
pub use crate::models::_entities::orders::{ActiveModel, Column, Entity, Model};
|
||||
pub type Orders = Entity;
|
||||
|
||||
@@ -42,7 +44,12 @@ fn generate_order_number() -> String {
|
||||
/// 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> {
|
||||
pub async fn place(
|
||||
ctx: &AppContext,
|
||||
items: &[(i32, i32)],
|
||||
details: Checkout,
|
||||
user: Option<&users::Model>,
|
||||
) -> Result<Model> {
|
||||
let txn = ctx.db.begin().await?;
|
||||
|
||||
let mut subtotal: i64 = 0;
|
||||
@@ -61,8 +68,10 @@ pub async fn place(ctx: &AppContext, items: &[(i32, i32)], details: Checkout) ->
|
||||
)));
|
||||
}
|
||||
currency = product.currency.clone();
|
||||
// Snapshot the effective price (honouring any active discount).
|
||||
let unit_price_cents = product.effective_price_cents();
|
||||
// Snapshot the price the buyer actually pays — public sale or, for a
|
||||
// business account, their negotiated/lowest price (same resolver the
|
||||
// cart and storefront use).
|
||||
let unit_price_cents = pricing::price_for(ctx, &product, user).await?.price_cents;
|
||||
subtotal += unit_price_cents * i64::from(*qty);
|
||||
|
||||
let mut active = product.clone().into_active_model();
|
||||
|
||||
Reference in New Issue
Block a user