now products have different options, like different parameters
Some checks failed
CI / Check Style (push) Has been cancelled
CI / Run Clippy (push) Has been cancelled
CI / Run Tests (push) Has been cancelled

This commit is contained in:
Priec
2026-06-22 15:44:02 +02:00
parent 29854a972b
commit 3f798432a0
52 changed files with 1281 additions and 628 deletions

View File

@@ -1,5 +1,4 @@
//! `SeaORM` Entity assigning a discount profile to a business account.
//! Hand-written to match the `account_discount_profiles` migration.
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
@@ -17,14 +16,6 @@ pub struct Model {
#[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::discount_profiles::Entity",
from = "Column::DiscountProfileId",
@@ -33,12 +24,14 @@ pub enum Relation {
on_delete = "Cascade"
)]
DiscountProfiles,
}
impl Related<super::users::Entity> for Entity {
fn to() -> RelationDef {
Relation::Users.def()
}
#[sea_orm(
belongs_to = "super::users::Entity",
from = "Column::UserId",
to = "super::users::Column::Id",
on_update = "Cascade",
on_delete = "Cascade"
)]
Users,
}
impl Related<super::discount_profiles::Entity> for Entity {
@@ -46,3 +39,9 @@ impl Related<super::discount_profiles::Entity> for Entity {
Relation::DiscountProfiles.def()
}
}
impl Related<super::users::Entity> for Entity {
fn to() -> RelationDef {
Relation::Users.def()
}
}

View File

@@ -1,5 +1,4 @@
//! `SeaORM` Entity for per-account negotiated product prices. Hand-written to
//! match the `account_product_prices` migration (one row per (user, product)).
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
@@ -11,13 +10,21 @@ pub struct Model {
pub updated_at: DateTimeWithTimeZone,
#[sea_orm(primary_key)]
pub id: i32,
pub user_id: i32,
pub product_id: i32,
pub price_cents: i64,
pub user_id: i32,
pub variant_id: i32,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::product_variants::Entity",
from = "Column::VariantId",
to = "super::product_variants::Column::Id",
on_update = "NoAction",
on_delete = "Cascade"
)]
ProductVariants,
#[sea_orm(
belongs_to = "super::users::Entity",
from = "Column::UserId",
@@ -26,14 +33,12 @@ pub enum Relation {
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::product_variants::Entity> for Entity {
fn to() -> RelationDef {
Relation::ProductVariants.def()
}
}
impl Related<super::users::Entity> for Entity {
@@ -41,9 +46,3 @@ impl Related<super::users::Entity> for Entity {
Relation::Users.def()
}
}
impl Related<super::products::Entity> for Entity {
fn to() -> RelationDef {
Relation::Products.def()
}
}

View File

@@ -1,6 +1,4 @@
//! `SeaORM` Entity for an account's chosen profile when two assigned profiles
//! cover one product. Hand-written to match the `account_product_resolutions`
//! migration.
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
@@ -13,28 +11,12 @@ pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub user_id: i32,
pub product_id: i32,
pub discount_profile_id: i32,
pub variant_id: i32,
}
#[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,
#[sea_orm(
belongs_to = "super::discount_profiles::Entity",
from = "Column::DiscountProfileId",
@@ -43,18 +25,22 @@ pub enum Relation {
on_delete = "Cascade"
)]
DiscountProfiles,
}
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()
}
#[sea_orm(
belongs_to = "super::product_variants::Entity",
from = "Column::VariantId",
to = "super::product_variants::Column::Id",
on_update = "NoAction",
on_delete = "Cascade"
)]
ProductVariants,
#[sea_orm(
belongs_to = "super::users::Entity",
from = "Column::UserId",
to = "super::users::Column::Id",
on_update = "Cascade",
on_delete = "Cascade"
)]
Users,
}
impl Related<super::discount_profiles::Entity> for Entity {
@@ -62,3 +48,15 @@ impl Related<super::discount_profiles::Entity> for Entity {
Relation::DiscountProfiles.def()
}
}
impl Related<super::product_variants::Entity> for Entity {
fn to() -> RelationDef {
Relation::ProductVariants.def()
}
}
impl Related<super::users::Entity> for Entity {
fn to() -> RelationDef {
Relation::Users.def()
}
}

View File

@@ -1,6 +1,4 @@
//! `SeaORM` Entity assigning a discount profile to a whole audience (all
//! personal viewers or all company accounts). Hand-written to match the
//! `audience_discount_profiles` migration.
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
@@ -12,7 +10,6 @@ pub struct Model {
pub updated_at: DateTimeWithTimeZone,
#[sea_orm(primary_key)]
pub id: i32,
/// "personal" or "business".
pub audience: String,
pub discount_profile_id: i32,
}

View File

@@ -1,4 +1,4 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.20
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};

View File

@@ -1,4 +1,4 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.20
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
@@ -23,8 +23,6 @@ pub struct Model {
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::products::Entity")]
Products,
#[sea_orm(
belongs_to = "Entity",
from = "Column::ParentId",
@@ -32,7 +30,9 @@ pub enum Relation {
on_update = "Cascade",
on_delete = "SetNull"
)]
Parent,
SelfRef,
#[sea_orm(has_many = "super::products::Entity")]
Products,
}
impl Related<super::products::Entity> for Entity {

View File

@@ -1,5 +1,4 @@
//! `SeaORM` Entity for customer shipping/contact profiles. Hand-written to match
//! the `customer_profiles` migration (1:1 with `users` via a unique `user_id`).
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
@@ -11,18 +10,18 @@ pub struct Model {
pub updated_at: DateTimeWithTimeZone,
#[sea_orm(primary_key)]
pub id: i32,
#[sea_orm(unique)]
pub user_id: i32,
pub company_name: Option<String>,
pub company_id: Option<String>,
pub tax_id: Option<String>,
pub vat_id: Option<String>,
pub phone_prefix: Option<String>,
pub phone: Option<String>,
pub address: Option<String>,
pub city: Option<String>,
pub zip: Option<String>,
pub country: Option<String>,
#[sea_orm(unique)]
pub user_id: i32,
pub company_name: Option<String>,
pub company_id: Option<String>,
pub tax_id: Option<String>,
pub vat_id: Option<String>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View File

@@ -1,5 +1,4 @@
//! `SeaORM` Entity for a discount profile's product membership. Hand-written to
//! match the `discount_profile_products` migration.
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};

View File

@@ -1,5 +1,4 @@
//! `SeaORM` Entity for reusable discount profiles. Hand-written to match the
//! `discount_profiles` migration.
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
@@ -12,24 +11,20 @@ pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub name: String,
/// Discount in basis points (5% = 500).
pub percent_bp: i32,
/// "include" (covers listed products) or "all_except" (covers all but them).
pub scope_type: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::discount_profile_products::Entity")]
DiscountProfileProducts,
#[sea_orm(has_many = "super::account_discount_profiles::Entity")]
AccountDiscountProfiles,
}
impl Related<super::discount_profile_products::Entity> for Entity {
fn to() -> RelationDef {
Relation::DiscountProfileProducts.def()
}
#[sea_orm(has_many = "super::account_product_resolutions::Entity")]
AccountProductResolutions,
#[sea_orm(has_many = "super::audience_discount_profiles::Entity")]
AudienceDiscountProfiles,
#[sea_orm(has_many = "super::discount_profile_products::Entity")]
DiscountProfileProducts,
}
impl Related<super::account_discount_profiles::Entity> for Entity {
@@ -37,3 +32,21 @@ impl Related<super::account_discount_profiles::Entity> for Entity {
Relation::AccountDiscountProfiles.def()
}
}
impl Related<super::account_product_resolutions::Entity> for Entity {
fn to() -> RelationDef {
Relation::AccountProductResolutions.def()
}
}
impl Related<super::audience_discount_profiles::Entity> for Entity {
fn to() -> RelationDef {
Relation::AudienceDiscountProfiles.def()
}
}
impl Related<super::discount_profile_products::Entity> for Entity {
fn to() -> RelationDef {
Relation::DiscountProfileProducts.def()
}
}

View File

@@ -1,22 +1,23 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.20
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
pub mod prelude;
pub mod account_discount_profiles;
pub mod account_product_prices;
pub mod audience_discount_profiles;
pub mod account_product_resolutions;
pub mod audience_discount_profiles;
pub mod audit_logs;
pub mod categories;
pub mod customer_profiles;
pub mod discount_profile_products;
pub mod discount_profiles;
pub mod customer_profiles;
pub mod o_auth2_sessions;
pub mod order_items;
pub mod orders;
pub mod product_images;
pub mod product_product_tags;
pub mod product_tags;
pub mod product_variants;
pub mod products;
pub mod shipping_methods;
pub mod users;

View File

@@ -1,6 +1,4 @@
//! `SeaORM` Entity for loco-oauth2 sessions. Hand-written to match the
//! `o_auth2_sessions` migration (the rest of `_entities/` is codegen; this table
//! is owned by the loco-oauth2 integration).
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
@@ -8,12 +6,13 @@ use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "o_auth2_sessions")]
pub struct Model {
pub created_at: DateTimeUtc,
pub updated_at: DateTimeUtc,
pub created_at: DateTimeWithTimeZone,
pub updated_at: DateTimeWithTimeZone,
#[sea_orm(primary_key)]
pub id: i32,
#[sea_orm(unique)]
pub session_id: String,
pub expires_at: DateTimeUtc,
pub expires_at: DateTimeWithTimeZone,
pub user_id: i32,
}

View File

@@ -1,4 +1,4 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.20
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
@@ -15,6 +15,8 @@ pub struct Model {
pub quantity: i32,
pub order_id: i32,
pub product_id: Option<i32>,
pub variant_label: String,
pub variant_id: Option<i32>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
@@ -27,6 +29,14 @@ pub enum Relation {
on_delete = "Cascade"
)]
Orders,
#[sea_orm(
belongs_to = "super::product_variants::Entity",
from = "Column::VariantId",
to = "super::product_variants::Column::Id",
on_update = "NoAction",
on_delete = "SetNull"
)]
ProductVariants,
#[sea_orm(
belongs_to = "super::products::Entity",
from = "Column::ProductId",
@@ -43,6 +53,12 @@ impl Related<super::orders::Entity> for Entity {
}
}
impl Related<super::product_variants::Entity> for Entity {
fn to() -> RelationDef {
Relation::ProductVariants.def()
}
}
impl Related<super::products::Entity> for Entity {
fn to() -> RelationDef {
Relation::Products.def()

View File

@@ -1,4 +1,4 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.20
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
@@ -13,17 +13,10 @@ pub struct Model {
#[sea_orm(unique)]
pub order_number: String,
pub email: String,
pub phone: Option<String>,
pub customer_name: Option<String>,
pub status: String,
pub total_cents: i64,
pub currency: String,
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>,
@@ -39,6 +32,13 @@ pub struct Model {
pub tracking_number: Option<String>,
pub shipment_id: Option<String>,
pub label_url: Option<String>,
pub phone: Option<String>,
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 user_id: Option<i32>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View File

@@ -1,4 +1,4 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.20
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
pub use super::account_discount_profiles::Entity as AccountDiscountProfiles;
pub use super::account_product_prices::Entity as AccountProductPrices;
@@ -15,6 +15,7 @@ pub use super::orders::Entity as Orders;
pub use super::product_images::Entity as ProductImages;
pub use super::product_product_tags::Entity as ProductProductTags;
pub use super::product_tags::Entity as ProductTags;
pub use super::product_variants::Entity as ProductVariants;
pub use super::products::Entity as Products;
pub use super::shipping_methods::Entity as ShippingMethods;
pub use super::users::Entity as Users;

View File

@@ -1,4 +1,4 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.20
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};

View File

@@ -1,4 +1,4 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.20
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};

View File

@@ -1,4 +1,4 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.20
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};

View File

@@ -0,0 +1,63 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "product_variants")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub created_at: DateTimeWithTimeZone,
pub updated_at: DateTimeWithTimeZone,
pub product_id: i32,
pub label: String,
pub position: i32,
pub sku: Option<String>,
pub stock: i32,
pub price_cents: i64,
pub sale_price_cents: Option<i64>,
pub business_sale_price_cents: Option<i64>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::account_product_prices::Entity")]
AccountProductPrices,
#[sea_orm(has_many = "super::account_product_resolutions::Entity")]
AccountProductResolutions,
#[sea_orm(has_many = "super::order_items::Entity")]
OrderItems,
#[sea_orm(
belongs_to = "super::products::Entity",
from = "Column::ProductId",
to = "super::products::Column::Id",
on_update = "NoAction",
on_delete = "Cascade"
)]
Products,
}
impl Related<super::account_product_prices::Entity> for Entity {
fn to() -> RelationDef {
Relation::AccountProductPrices.def()
}
}
impl Related<super::account_product_resolutions::Entity> for Entity {
fn to() -> RelationDef {
Relation::AccountProductResolutions.def()
}
}
impl Related<super::order_items::Entity> for Entity {
fn to() -> RelationDef {
Relation::OrderItems.def()
}
}
impl Related<super::products::Entity> for Entity {
fn to() -> RelationDef {
Relation::Products.def()
}
}

View File

@@ -1,4 +1,4 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.20
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
@@ -15,12 +15,7 @@ pub struct Model {
pub slug: String,
#[sea_orm(column_type = "Text", nullable)]
pub description: Option<String>,
pub price_cents: i64,
pub sale_price_cents: Option<i64>,
pub business_sale_price_cents: Option<i64>,
pub currency: String,
pub sku: Option<String>,
pub stock: i32,
pub view_count: i32,
pub published: bool,
pub published_at: Option<DateTimeWithTimeZone>,
@@ -37,12 +32,16 @@ pub enum Relation {
on_delete = "SetNull"
)]
Categories,
#[sea_orm(has_many = "super::discount_profile_products::Entity")]
DiscountProfileProducts,
#[sea_orm(has_many = "super::order_items::Entity")]
OrderItems,
#[sea_orm(has_many = "super::product_images::Entity")]
ProductImages,
#[sea_orm(has_many = "super::product_product_tags::Entity")]
ProductProductTags,
#[sea_orm(has_many = "super::product_variants::Entity")]
ProductVariants,
}
impl Related<super::categories::Entity> for Entity {
@@ -51,6 +50,12 @@ impl Related<super::categories::Entity> for Entity {
}
}
impl Related<super::discount_profile_products::Entity> for Entity {
fn to() -> RelationDef {
Relation::DiscountProfileProducts.def()
}
}
impl Related<super::order_items::Entity> for Entity {
fn to() -> RelationDef {
Relation::OrderItems.def()
@@ -69,6 +74,12 @@ impl Related<super::product_product_tags::Entity> for Entity {
}
}
impl Related<super::product_variants::Entity> for Entity {
fn to() -> RelationDef {
Relation::ProductVariants.def()
}
}
impl Related<super::product_tags::Entity> for Entity {
fn to() -> RelationDef {
super::product_product_tags::Relation::ProductTags.def()

View File

@@ -1,4 +1,4 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.20
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};

View File

@@ -1,4 +1,4 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.20
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
@@ -26,15 +26,45 @@ pub struct Model {
pub magic_link_expiration: Option<DateTimeWithTimeZone>,
pub theme: String,
pub account_type: String,
#[sea_orm(column_type = "Text", nullable)]
pub totp_secret: Option<String>,
pub totp_enabled_at: Option<DateTimeWithTimeZone>,
#[sea_orm(column_type = "Text", nullable)]
pub totp_backup_codes: Option<String>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::account_discount_profiles::Entity")]
AccountDiscountProfiles,
#[sea_orm(has_many = "super::account_product_prices::Entity")]
AccountProductPrices,
#[sea_orm(has_many = "super::account_product_resolutions::Entity")]
AccountProductResolutions,
#[sea_orm(has_many = "super::audit_logs::Entity")]
AuditLogs,
#[sea_orm(has_one = "super::customer_profiles::Entity")]
CustomerProfiles,
#[sea_orm(has_many = "super::o_auth2_sessions::Entity")]
OAuth2Sessions,
}
impl Related<super::account_discount_profiles::Entity> for Entity {
fn to() -> RelationDef {
Relation::AccountDiscountProfiles.def()
}
}
impl Related<super::account_product_prices::Entity> for Entity {
fn to() -> RelationDef {
Relation::AccountProductPrices.def()
}
}
impl Related<super::account_product_resolutions::Entity> for Entity {
fn to() -> RelationDef {
Relation::AccountProductResolutions.def()
}
}
impl Related<super::audit_logs::Entity> for Entity {
@@ -42,3 +72,15 @@ impl Related<super::audit_logs::Entity> for Entity {
Relation::AuditLogs.def()
}
}
impl Related<super::customer_profiles::Entity> for Entity {
fn to() -> RelationDef {
Relation::CustomerProfiles.def()
}
}
impl Related<super::o_auth2_sessions::Entity> for Entity {
fn to() -> RelationDef {
Relation::OAuth2Sessions.def()
}
}

View File

@@ -25,7 +25,7 @@ impl ActiveModelBehavior for ActiveModel {
}
impl Model {
/// All negotiated prices for one account, as a `(product_id -> cents)` map.
/// All negotiated prices for one account, as a `(variant_id -> cents)` map.
pub async fn map_for_user(
db: &DatabaseConnection,
user_id: i32,
@@ -34,26 +34,26 @@ impl Model {
.filter(Column::UserId.eq(user_id))
.all(db)
.await?;
Ok(rows.into_iter().map(|r| (r.product_id, r.price_cents)).collect())
Ok(rows.into_iter().map(|r| (r.variant_id, r.price_cents)).collect())
}
/// Insert or update the negotiated price for `(user_id, product_id)`.
/// Insert or update the negotiated price for `(user_id, variant_id)`.
pub async fn upsert(
db: &DatabaseConnection,
user_id: i32,
product_id: i32,
variant_id: i32,
price_cents: i64,
) -> Result<Self, DbErr> {
let existing = Entity::find()
.filter(Column::UserId.eq(user_id))
.filter(Column::ProductId.eq(product_id))
.filter(Column::VariantId.eq(variant_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),
variant_id: ActiveValue::set(variant_id),
..Default::default()
},
};
@@ -61,15 +61,15 @@ impl Model {
active.save(db).await?.try_into_model()
}
/// Remove the negotiated price for `(user_id, product_id)`, if any.
/// Remove the negotiated price for `(user_id, variant_id)`, if any.
pub async fn clear(
db: &DatabaseConnection,
user_id: i32,
product_id: i32,
variant_id: i32,
) -> Result<(), DbErr> {
Entity::delete_many()
.filter(Column::UserId.eq(user_id))
.filter(Column::ProductId.eq(product_id))
.filter(Column::VariantId.eq(variant_id))
.exec(db)
.await?;
Ok(())

View File

@@ -24,3 +24,4 @@ pub mod product_tags;
pub mod products;
pub mod shipping_methods;
pub mod users;
pub mod product_variants;

View File

@@ -17,7 +17,7 @@ impl ActiveModelBehavior for ActiveModel {
{
if !insert && self.updated_at.is_unchanged() {
let mut this = self;
this.updated_at = sea_orm::ActiveValue::Set(Utc::now());
this.updated_at = sea_orm::ActiveValue::Set(Utc::now().into());
Ok(this)
} else {
Ok(self)
@@ -34,7 +34,7 @@ impl OAuth2SessionsTrait<users::Model> for Model {
.one(db)
.await?
.ok_or_else(|| ModelError::EntityNotFound)?;
Ok(session.expires_at < Utc::now())
Ok(session.expires_at < Utc::now().fixed_offset())
}
/// Create or refresh the session row for `user` from the provider token.
@@ -58,14 +58,14 @@ impl OAuth2SessionsTrait<users::Model> for Model {
Some(session) => {
let mut session: o_auth2_sessions::ActiveModel = session.into();
session.session_id = ActiveValue::set(session_id);
session.expires_at = ActiveValue::set(expires_at);
session.updated_at = ActiveValue::set(Utc::now());
session.expires_at = ActiveValue::set(expires_at.into());
session.updated_at = ActiveValue::set(Utc::now().into());
session.update(&txn).await?
}
None => {
o_auth2_sessions::ActiveModel {
session_id: ActiveValue::set(session_id),
expires_at: ActiveValue::set(expires_at),
expires_at: ActiveValue::set(expires_at.into()),
user_id: ActiveValue::set(user.id),
..Default::default()
}

View File

@@ -3,7 +3,7 @@ use sea_orm::entity::prelude::*;
use sea_orm::{Set, TransactionTrait};
use uuid::Uuid;
use crate::models::_entities::{order_items, products, shipping_methods};
use crate::models::_entities::{order_items, product_variants, products, shipping_methods};
use crate::models::users;
use crate::shared::pricing;
pub use crate::models::_entities::orders::{ActiveModel, Column, Entity, Model};
@@ -40,10 +40,10 @@ fn generate_order_number() -> String {
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.
/// Atomically place an order for the given `(variant_id, quantity)` lines:
/// snapshot each variant's price/name/label, decrement its 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)],
@@ -55,13 +55,17 @@ pub async fn place(
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)
for (variant_id, qty) in items {
let variant = product_variants::Entity::find_by_id(*variant_id)
.one(&txn)
.await?
.ok_or_else(|| Error::BadRequest("an item is no longer available".to_string()))?;
let product = products::Entity::find_by_id(variant.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 {
if variant.stock < *qty {
return Err(Error::BadRequest(format!(
"not enough stock for {}",
product.name
@@ -71,14 +75,14 @@ pub async fn place(
// 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;
let unit_price_cents = pricing::price_variant(ctx, &variant, user).await?.price_cents;
subtotal += unit_price_cents * i64::from(*qty);
let mut active = product.clone().into_active_model();
active.stock = Set(product.stock - *qty);
let mut active = variant.clone().into_active_model();
active.stock = Set(variant.stock - *qty);
active.update(&txn).await?;
snapshots.push((product.id, product.name, unit_price_cents, *qty));
snapshots.push((product.id, variant.id, product.name, variant.label, unit_price_cents, *qty));
}
let order = ActiveModel {
@@ -111,11 +115,13 @@ pub async fn place(
.insert(&txn)
.await?;
for (product_id, name, unit_price_cents, qty) in snapshots {
for (product_id, variant_id, name, variant_label, unit_price_cents, qty) in snapshots {
order_items::ActiveModel {
order_id: Set(order.id),
product_id: Set(Some(product_id)),
variant_id: Set(Some(variant_id)),
product_name: Set(name),
variant_label: Set(variant_label),
unit_price_cents: Set(unit_price_cents),
quantity: Set(qty),
..Default::default()

View File

@@ -0,0 +1,89 @@
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)
}
}
// 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)
}
}

View File

@@ -19,32 +19,7 @@ impl ActiveModelBehavior for ActiveModel {
}
// 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)
}
}
impl Model {}
// implement your write-oriented logic here
impl ActiveModel {}