From 755363c23f3de40c1d4b302c6b0be3dee30887e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Wo=C5=BAniak?= Date: Wed, 9 Nov 2022 16:59:12 +0100 Subject: [PATCH] Work on stocks database --- Cargo.lock | 15 + Cargo.toml | 1 + crates/account_manager/Cargo.toml | 4 + crates/account_manager/src/db/accounts.rs | 14 +- crates/account_manager/src/db/addresses.rs | 2 +- crates/db-utils/Cargo.toml | 9 + crates/db-utils/src/lib.rs | 97 +++++ crates/stock_manager/Cargo.toml | 6 + crates/stock_manager/src/db/mod.rs | 1 + .../stock_manager/src/db/product_variants.rs | 55 +++ crates/stock_manager/src/db/products.rs | 376 ++++++++++++++++++ crates/stock_manager/src/db/stocks.rs | 337 ++++++++++++++++ 12 files changed, 909 insertions(+), 8 deletions(-) create mode 100644 crates/db-utils/Cargo.toml create mode 100644 crates/db-utils/src/lib.rs create mode 100644 crates/stock_manager/src/db/product_variants.rs diff --git a/Cargo.lock b/Cargo.lock index 6961253..6e541ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,7 @@ dependencies = [ "channels", "config", "dotenv", + "fake", "futures 0.3.25", "gumdrop", "json", @@ -23,6 +24,7 @@ dependencies = [ "sqlx", "sqlx-core", "tarpc", + "testx", "thiserror", "tokio", "tracing", @@ -1253,6 +1255,15 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "db-utils" +version = "0.1.0" +dependencies = [ + "model", + "sqlx", + "sqlx-core", +] + [[package]] name = "dbg" version = "1.0.4" @@ -4092,8 +4103,10 @@ dependencies = [ "channels", "chrono", "config", + "db-utils", "derive_more", "dotenv", + "fake", "futures 0.3.25", "model", "opentelemetry 0.17.0", @@ -4104,11 +4117,13 @@ dependencies = [ "sqlx", "sqlx-core", "tarpc", + "testx", "thiserror", "tokio", "tracing", "tracing-opentelemetry", "tracing-subscriber", + "uuid 1.2.1", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index fb835f4..9f30506 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "crates/channels", "crates/config", "crates/testx", + "crates/db-utils", # actors "crates/account_manager", "crates/cart_manager", diff --git a/crates/account_manager/Cargo.toml b/crates/account_manager/Cargo.toml index e4c83a8..87fe392 100644 --- a/crates/account_manager/Cargo.toml +++ b/crates/account_manager/Cargo.toml @@ -30,3 +30,7 @@ tokio = { version = "1.21.2", features = ['full'] } tracing = { version = "0.1.6" } tracing-opentelemetry = { version = "0.17.4" } tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } + +[dev-dependencies] +fake = { version = "2.5.0" } +testx = { path = "../testx" } diff --git a/crates/account_manager/src/db/accounts.rs b/crates/account_manager/src/db/accounts.rs index cf004f8..ae3bece 100644 --- a/crates/account_manager/src/db/accounts.rs +++ b/crates/account_manager/src/db/accounts.rs @@ -234,7 +234,7 @@ mod tests { .unwrap() } - #[actix::test] + #[tokio::test] async fn create_account() { testx::db_t_ref!(t); @@ -266,7 +266,7 @@ mod tests { assert_eq!(account, expected); } - #[actix::test] + #[tokio::test] async fn all_accounts() { testx::db_t_ref!(t); @@ -280,7 +280,7 @@ mod tests { assert!(v.len() >= 3); } - #[actix::test] + #[tokio::test] async fn update_account_without_pass() { testx::db_t_ref!(t); @@ -326,7 +326,7 @@ mod tests { assert_eq!(updated_account, expected); } - #[actix::test] + #[tokio::test] async fn update_account_with_pass() { testx::db_t_ref!(t); @@ -373,7 +373,7 @@ mod tests { assert_eq!(updated_account, expected); } - #[actix::test] + #[tokio::test] async fn find() { testx::db_t_ref!(t); @@ -390,7 +390,7 @@ mod tests { assert_eq!(account, res); } - #[actix::test] + #[tokio::test] async fn find_identity_email() { testx::db_t_ref!(t); @@ -408,7 +408,7 @@ mod tests { assert_eq!(account, res); } - #[actix::test] + #[tokio::test] async fn find_identity_login() { testx::db_t_ref!(t); diff --git a/crates/account_manager/src/db/addresses.rs b/crates/account_manager/src/db/addresses.rs index 69d4acf..8a7ca37 100644 --- a/crates/account_manager/src/db/addresses.rs +++ b/crates/account_manager/src/db/addresses.rs @@ -215,7 +215,7 @@ mod test { .unwrap() } - #[actix::test] + #[tokio::test] async fn full_check() { testx::db_t_ref!(t); diff --git a/crates/db-utils/Cargo.toml b/crates/db-utils/Cargo.toml new file mode 100644 index 0000000..f57b9c4 --- /dev/null +++ b/crates/db-utils/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "db-utils" +version = "0.1.0" +edition = "2021" + +[dependencies] +model = { path = "../model" } +sqlx = { version = "0.6.2", features = ["migrate", "runtime-actix-rustls", "all-types", "postgres"] } +sqlx-core = { version = "0.6.2", features = [] } diff --git a/crates/db-utils/src/lib.rs b/crates/db-utils/src/lib.rs new file mode 100644 index 0000000..c3b873f --- /dev/null +++ b/crates/db-utils/src/lib.rs @@ -0,0 +1,97 @@ +use sqlx::Arguments; + +pub struct MultiLoad<'transaction, 'transaction2, 'header, 'condition, T> { + pool: &'transaction mut sqlx::Transaction<'transaction2, sqlx::Postgres>, + header: &'header str, + condition: &'condition str, + sort: Option, + size: Option, + __phantom: std::marker::PhantomData, +} + +impl<'transaction, 'transaction2, 'header, 'condition, T> + MultiLoad<'transaction, 'transaction2, 'header, 'condition, T> +where + T: for<'r> sqlx::FromRow<'r, sqlx::postgres::PgRow> + Send + Unpin, +{ + pub fn new( + pool: &'transaction mut sqlx::Transaction<'transaction2, sqlx::Postgres>, + header: &'header str, + condition: &'condition str, + ) -> Self { + Self { + pool, + header, + condition, + sort: None, + size: None, + __phantom: Default::default(), + } + } + + pub fn with_sorting>(mut self, order: S) -> Self { + self.sort = Some(order.into()); + self + } + + pub fn with_size(mut self, size: usize) -> Self { + self.size = Some(size); + self + } + + pub async fn load<'query, Error, ErrorFn, Ids>( + &mut self, + len: usize, + items: Ids, + on_error: ErrorFn, + ) -> Result, Error> + where + Ids: Iterator, + ErrorFn: Fn(sqlx::Error) -> Error, + { + let mut res = Vec::new(); + let size = self.size.unwrap_or(20).min(200); + + for ids in items.fold( + Vec::>::with_capacity(len), + |mut v, id| { + let last_len = v.last().map(|v| v.len()); + if last_len == Some(size) || last_len.is_none() { + v.push(Vec::with_capacity(size)); + } + v.last_mut().unwrap().push(id); + v + }, + ) { + let query: String = self.header.into(); + let mut query = ids.iter().enumerate().fold(query, |mut q, (idx, _id)| { + if idx != 0 { + q.push_str(" OR"); + } + q.push_str(&format!(" {} ${}", self.condition, idx + 1)); + q + }); + if let Some(s) = self.sort.as_deref() { + query.push_str("\nORDER BY "); + query.push_str(s); + query.push(' '); + } + let q = sqlx::query_as_with( + query.as_str(), + ids.into_iter() + .fold(sqlx::postgres::PgArguments::default(), |mut args, id| { + args.add(id); + args + }), + ); + + let records: Vec = match q.fetch_all(&mut *self.pool).await { + Ok(rec) => rec, + Err(e) => return Err(on_error(e)), + }; + res.extend(records); + } + + Ok(res) + } +} diff --git a/crates/stock_manager/Cargo.toml b/crates/stock_manager/Cargo.toml index ab6b169..bd34108 100644 --- a/crates/stock_manager/Cargo.toml +++ b/crates/stock_manager/Cargo.toml @@ -11,6 +11,7 @@ path = "./src/main.rs" channels = { path = "../channels" } chrono = { version = "0.4", features = ["serde"] } config = { path = "../config" } +db-utils = { path = "../db-utils" } derive_more = { version = "0.99", features = [] } dotenv = { version = "0.15.0" } futures = { version = "0.3.25" } @@ -28,3 +29,8 @@ tokio = { version = "1.21.2", features = ['full'] } tracing = { version = "0.1.6" } tracing-opentelemetry = { version = "0.17.4" } tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } +uuid = { version = "1.2.1" } + +[dev-dependencies] +fake = { version = "2.5.0" } +testx = { path = "../testx" } diff --git a/crates/stock_manager/src/db/mod.rs b/crates/stock_manager/src/db/mod.rs index 0c2983c..c16862e 100644 --- a/crates/stock_manager/src/db/mod.rs +++ b/crates/stock_manager/src/db/mod.rs @@ -4,6 +4,7 @@ use sqlx_core::postgres::Postgres; mod photos; mod product_photos; +mod product_variants; mod products; mod stocks; diff --git a/crates/stock_manager/src/db/product_variants.rs b/crates/stock_manager/src/db/product_variants.rs new file mode 100644 index 0000000..e9d296b --- /dev/null +++ b/crates/stock_manager/src/db/product_variants.rs @@ -0,0 +1,55 @@ +use model::v2::*; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("")] + CreateProductVariant, +} + +pub type Result = std::result::Result; + +#[derive(Debug)] +pub struct CreateProductVariant { + pub product_id: ProductId, + pub name: ProductName, + pub short_description: ProductShortDesc, + pub long_description: ProductLongDesc, + pub price: Price, +} + +impl CreateProductVariant { + pub async fn run( + self, + pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result { + sqlx::query_as( + r#" +INSERT INTO product_variants ( + product_id, + name, + short_description, + long_description, + price +) VALUES ($1, $2, $3, $4, $5) +RETURNINGS id, + product_id, + name, + short_description, + long_description, + price + "#, + ) + .bind(self.product_id) + .bind(self.name) + .bind(self.short_description) + .bind(self.long_description) + .bind(self.price) + .fetch_one(pool) + .await + .map_err(|e| { + tracing::error!("{}", e); + dbg!(e); + Error::CreateProductVariant + }) + } +} diff --git a/crates/stock_manager/src/db/products.rs b/crates/stock_manager/src/db/products.rs index e633e09..04deed6 100644 --- a/crates/stock_manager/src/db/products.rs +++ b/crates/stock_manager/src/db/products.rs @@ -1 +1,377 @@ use model::v2::*; +use model::ShoppingCartId; + +#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, thiserror::Error)] +pub enum Error { + #[error("Unable to load all products")] + All, + #[error("Unable to create product")] + Create, + #[error("Unable to update product")] + Update(ProductId), + #[error("Unable to delete product")] + Delete(ProductId), + #[error("Unable to find products for shopping cart")] + ShoppingCartProducts(ShoppingCartId), + #[error("Product with id {0} can't be found")] + Single(ProductId), + #[error("Failed to load products for given ids")] + FindProducts, +} + +pub type Result = std::result::Result; + +#[derive(Debug)] +pub struct AllProducts { + limit: i32, + offset: i32, +} + +impl AllProducts { + pub async fn run<'e, E>(self, pool: E) -> Result> + where + E: sqlx::Executor<'e, Database = sqlx::Postgres>, + { + sqlx::query_as( + r#" +SELECT id, + name, + category, + deliver_days_flag +FROM products +ORDER BY id +LIMIT $1 OFFSET $2 + "#, + ) + .bind(self.limit.max(1).min(200)) + .bind(self.offset.max(0)) + .fetch_all(pool) + .await + .map_err(|e| { + tracing::error!("{e:?}"); + Error::All + }) + } +} + +#[derive(Debug)] +pub struct FindProduct { + pub product_id: ProductId, +} + +impl FindProduct { + pub async fn run<'e, E>(self, pool: E) -> Result + where + E: sqlx::Executor<'e, Database = sqlx::Postgres>, + { + sqlx::query_as( + r#" +SELECT id, + name, + category, + deliver_days_flag +FROM products +WHERE id = $1 + "#, + ) + .bind(self.product_id) + .fetch_one(pool) + .await + .map_err(|e| { + tracing::error!("{e:?}"); + Error::Single(self.product_id) + }) + } +} + +#[derive(Debug)] +pub struct CreateProduct { + pub name: ProductName, + pub category: Option, + pub deliver_days_flag: Days, +} + +impl CreateProduct { + pub async fn run<'e, E>(self, pool: E) -> Result + where + E: sqlx::Executor<'e, Database = sqlx::Postgres>, + { + sqlx::query_as( + r#" +INSERT INTO products (name, category, deliver_days_flag) +VALUES ($1, $2, $3) +RETURNING id, + name, + category, + deliver_days_flag + "#, + ) + .bind(self.name) + .bind(self.category) + .bind(self.deliver_days_flag) + .fetch_one(pool) + .await + .map_err(|e| { + tracing::error!("{e:?}"); + dbg!(e); + Error::Create + }) + } +} + +#[derive(Debug)] +pub struct UpdateProduct { + pub id: ProductId, + pub name: ProductName, + pub category: Option, + pub deliver_days_flag: Days, +} + +impl UpdateProduct { + pub async fn run<'e, E>(self, pool: E) -> Result + where + E: sqlx::Executor<'e, Database = sqlx::Postgres>, + { + sqlx::query_as( + r#" +UPDATE products +SET name = $2, + category = $3, + deliver_days_flag = $4 +WHERE id = $1 +RETURNING id, + name, + category, + deliver_days_flag + "#, + ) + .bind(self.id) + .bind(self.name) + .bind(self.category) + .bind(self.deliver_days_flag) + .fetch_one(pool) + .await + .map_err(|e| { + tracing::error!("{e:?}"); + dbg!(e); + Error::Update(self.id) + }) + } +} + +#[derive(Debug)] +pub struct DeleteProduct { + pub product_id: ProductId, +} + +impl DeleteProduct { + pub async fn run<'e, E>(self, pool: E) -> Result> + where + E: sqlx::Executor<'e, Database = sqlx::Postgres>, + { + sqlx::query_as( + r#" +DELETE FROM products +WHERE id = $1 +RETURNING id, + name, + category, + deliver_days_flag + "#, + ) + .bind(self.product_id) + .fetch_optional(pool) + .await + .map_err(|e| { + tracing::error!("{e:?}"); + Error::Delete(self.product_id) + }) + } +} + +#[derive(Debug)] +pub struct ShoppingCartProducts { + pub shopping_cart_id: ShoppingCartId, + pub limit: i32, + pub offset: i32, +} + +impl ShoppingCartProducts { + pub async fn shopping_cart_products<'e, E>(self, pool: E) -> Result> + where + E: sqlx::Executor<'e, Database = sqlx::Postgres>, + { + sqlx::query_as( + r#" +SELECT products.id, + products.name, + products.category, + products.deliver_days_flag +FROM products +INNER JOIN shopping_cart_items ON shopping_cart_items.product_id = products.id +WHERE shopping_cart_id = $1 +ORDER BY products.id +LIMIT $2 OFFSET $3 + "#, + ) + .bind(self.shopping_cart_id) + .bind(self.limit.min(1).max(200)) + .bind(self.offset.min(0)) + .fetch_all(pool) + .await + .map_err(|e| { + tracing::error!("{e:?}"); + Error::ShoppingCartProducts(self.shopping_cart_id) + }) + } +} + +#[derive(Debug)] +pub struct FindProducts { + pub product_ids: Vec, +} + +impl FindProducts { + pub async fn run( + self, + pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result> { + db_utils::MultiLoad::new( + pool, + r#" +SELECT id, + name, + category, + deliver_days_flag +FROM products +WHERE + "#, + "products.id =", + ) + .with_size(200) + .load( + self.product_ids.len(), + self.product_ids.into_iter().map(|id| *id), + |e| { + tracing::error!("{e:?}"); + Error::FindProducts + }, + ) + .await + } +} + +#[cfg(test)] +mod tests { + use config::UpdateConfig; + use model::*; + use uuid::Uuid; + + pub struct NoOpts; + + impl UpdateConfig for NoOpts {} + + use super::*; + + async fn test_product( + t: &mut sqlx::Transaction<'_, sqlx::Postgres>, + name: Option, + short_description: Option, + long_description: Option, + category: Option, + price: Option, + deliver_days_flag: Option, + ) -> Product { + CreateProduct { + name: ProductName::new(name.unwrap_or_else(|| format!("{}", Uuid::new_v4()))), + category, + deliver_days_flag: deliver_days_flag + .unwrap_or_else(|| Days(vec![Day::Friday, Day::Sunday])), + } + .run(t) + .await + .unwrap() + } + + #[tokio::test] + async fn create() { + testx::db_t_ref!(t); + + test_product(&mut t, None, None, None, None, None, None).await; + + testx::db_rollback!(t); + } + + #[tokio::test] + async fn all() { + testx::db_t_ref!(t); + + let p1 = test_product(&mut t, None, None, None, None, None, None).await; + let p2 = test_product(&mut t, None, None, None, None, None, None).await; + let p3 = test_product(&mut t, None, None, None, None, None, None).await; + + let products = super::all(AllProducts, &mut t).await.unwrap(); + + testx::db_rollback!(t); + assert_eq!(products, vec![p1, p2, p3]); + } + + #[tokio::test] + async fn find() { + testx::db_t_ref!(t); + + let p1 = test_product(&mut t, None, None, None, None, None, None).await; + let p2 = test_product(&mut t, None, None, None, None, None, None).await; + let p3 = test_product(&mut t, None, None, None, None, None, None).await; + + let product = find_product(FindProduct { product_id: p2.id }, &mut t) + .await + .unwrap(); + + testx::db_rollback!(t); + assert_ne!(product, p1); + assert_eq!(product, p2); + assert_ne!(product, p3); + } + + #[tokio::test] + async fn update() { + testx::db_t_ref!(t); + + let original = test_product(&mut t, None, None, None, None, None, None).await; + let updated = UpdateProduct { + id: original.id, + name: ProductName::new("a9s0dja0sjd0jas09dj"), + short_description: ProductShortDesc::new("ajs9d8ua9sdu9ahsd98has"), + long_description: ProductLongDesc::new("hja89sdy9yha9sdy98ayusd9hya9sy8dh"), + category: None, + price: Price::from_u32(823794), + deliver_days_flag: Day::Tuesday | Day::Saturday, + } + .run(&mut t) + .await + .unwrap(); + let reloaded = FindProduct { + product_id: original.id, + } + .run(&mut t) + .await + .unwrap(); + + testx::db_rollback!(t); + assert_ne!(updated, original); + assert_eq!(updated, reloaded); + assert_eq!( + updated, + Product { + id: original.id, + name: ProductName::new("a9s0dja0sjd0jas09dj"), + short_description: ProductShortDesc::new("ajs9d8ua9sdu9ahsd98has"), + long_description: ProductLongDesc::new("hja89sdy9yha9sdy98ayusd9hya9sy8dh"), + category: None, + price: Price::from_u32(823794), + deliver_days_flag: Day::Tuesday | Day::Saturday, + } + ); + } +} diff --git a/crates/stock_manager/src/db/stocks.rs b/crates/stock_manager/src/db/stocks.rs index e633e09..f55b09d 100644 --- a/crates/stock_manager/src/db/stocks.rs +++ b/crates/stock_manager/src/db/stocks.rs @@ -1 +1,338 @@ use model::v2::*; + +use crate::db::products::AllProducts; + +#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, thiserror::Error)] +pub enum Error { + #[error("Unable to load all stocks")] + All, + #[error("Unable to create stock")] + Create, + #[error("Unable to update stock {0:?}")] + Update(StockId), + #[error("Unable to delete stock {0:?}")] + Delete(StockId), + #[error("Unable find stock for product")] + ProductVariantStock, + #[error("Stock {0:?} does not exists")] + NotFound(StockId), +} + +pub type Result = std::result::Result; + +#[derive(Debug)] +pub struct AllStocks { + pub limit: i32, + pub offset: i32, +} + +impl AllStocks { + pub async fn run(self, pool: &mut sqlx::Transaction<'_, sqlx::Postgres>) -> Result> { + sqlx::query_as( + r#" +SELECT id, product_variant_id, quantity, quantity_unit +FROM stocks +ORDER BY id ASC +LIMIT $1 OFFSET $2 + "#, + ) + .bind(self.limit) + .bind(self.offset) + .fetch_all(pool) + .await + .map_err(|e| { + tracing::error!("{e:?}"); + Error::All + }) + } +} + +#[derive(Debug)] +pub struct FindStock { + pub id: StockId, +} + +impl FindStock { + pub async fn run(self, pool: &mut sqlx::Transaction<'_, sqlx::Postgres>) -> Result { + sqlx::query_as( + r#" +SELECT id, product_variant_id, quantity, quantity_unit +FROM stocks +WHERE id = $1 + "#, + ) + .bind(self.id) + .fetch_one(pool) + .await + .map_err(|e| { + tracing::error!("{e:?}"); + dbg!(e); + Error::NotFound(self.id) + }) + } +} + +#[derive(Debug)] +pub struct CreateStock { + pub product_variant_id: ProductVariantId, + pub quantity: Quantity, + pub quantity_unit: QuantityUnit, +} + +impl CreateStock { + pub async fn run(self, pool: &mut sqlx::Transaction<'_, sqlx::Postgres>) -> Result { + sqlx::query_as( + r#" +INSERT INTO stocks (product_variant_id, quantity, quantity_unit) +VALUES ($1, $2, $3) +RETURNING id, product_variant_id, quantity, quantity_unit + "#, + ) + .bind(self.product_variant_id) + .bind(self.quantity) + .bind(self.quantity_unit) + .fetch_one(pool) + .await + .map_err(|e| { + tracing::error!("{e:?}"); + dbg!(e); + Error::Create + }) + } +} + +#[derive(Debug)] +pub struct UpdateStock { + pub id: StockId, + pub product_id: ProductId, + pub quantity: Quantity, + pub quantity_unit: QuantityUnit, +} + +impl UpdateStock { + pub async fn run(self, pool: &mut sqlx::Transaction<'_, sqlx::Postgres>) -> Result { + sqlx::query_as( + r#" +UPDATE stocks +SET product_variant_id = $1, + quantity = $2, + quantity_unit = $3 +WHERE id = $4 +RETURNING id, product_variant_id, quantity, quantity_unit + "#, + ) + .bind(self.product_id) + .bind(self.quantity) + .bind(self.quantity_unit) + .bind(self.id) + .fetch_one(pool) + .await + .map_err(|e| { + tracing::error!("{e:?}"); + Error::Update(self.id) + }) + } +} + +#[derive(Debug)] +pub struct DeleteStock { + pub stock_id: StockId, +} + +impl DeleteStock { + async fn run(self, pool: &mut sqlx::Transaction<'_, sqlx::Postgres>) -> Result> { + sqlx::query_as( + r#" +DELETE FROM stocks +WHERE id = $1 +RETURNING id, product_variant_id, quantity, quantity_unit + "#, + ) + .bind(self.stock_id) + .fetch_optional(pool) + .await + .map_err(|e| { + tracing::error!("{e:?}"); + Error::Delete(self.stock_id) + }) + } +} + +#[derive(Debug)] +pub struct ProductVariantsStock { + pub product_variant_ids: Vec, +} + +impl ProductVariantsStock { + async fn run(self, pool: &mut sqlx::Transaction<'_, sqlx::Postgres>) -> Result> { + db_utils::MultiLoad::new( + pool, + r#" +SELECT id, product_variant_id, quantity, quantity_unit +FROM stocks +WHERE + "#, + " product_variant_id =", + ) + .with_size(200) + .load( + self.product_variant_ids.len(), + self.product_variant_ids.into_iter().map(|id| *id), + |_e| Error::ProductVariantStock, + ) + .await + } +} + +#[cfg(test)] +mod tests { + use config::UpdateConfig; + use fake::faker::lorem::en as lorem; + use fake::Fake; + use model::*; + use uuid::Uuid; + + pub struct NoOpts; + + impl UpdateConfig for NoOpts {} + + use super::*; + use crate::db::products::*; + + async fn test_product(pool: &mut sqlx::Transaction<'_, sqlx::Postgres>) -> Product { + CreateProduct { + name: ProductName::new(format!("db stocks test product {}", Uuid::new_v4())), + category: None, + deliver_days_flag: Days(vec![Day::Friday, Day::Sunday]), + } + .run(pool) + .await + .unwrap() + } + + async fn test_stock( + pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, + product_variant_id: Option, + quantity: Option, + quantity_unit: Option, + ) -> Stock { + let product_variant_id = match product_variant_id { + Some(id) => id, + _ => test_product(&mut *pool).await.id, + }; + let quantity = quantity.unwrap_or_else(|| Quantity::from_u32(345)); + let quantity_unit = quantity_unit.unwrap_or(QuantityUnit::Piece); + + CreateStock { + product_variant_id, + quantity_unit, + quantity, + } + .run(&mut *pool) + .await + .unwrap() + } + + #[tokio::test] + async fn create_stock() { + testx::db_t_ref!(t); + + test_stock(&mut t, None, None, None).await; + + testx::db_rollback!(t); + } + + #[tokio::test] + async fn products_stock() { + testx::db_t_ref!(t); + + let first = test_stock(&mut t, None, None, None).await; + let second = test_stock(&mut t, None, None, None).await; + + let stocks: Vec = ProductVariantsStock { + product_variant_ids: vec![first.product_id, second.product_id], + } + .run(&mut t) + .await + .unwrap(); + + testx::db_rollback!(t); + assert_eq!(stocks, vec![first, second]); + } + + #[tokio::test] + async fn all_stocks() { + testx::db_t_ref!(t); + + let first = test_stock(&mut t, None, None, None).await; + let second = test_stock(&mut t, None, None, None).await; + + let stocks: Vec = AllStocks { + limit: 200, + offset: 0, + } + .run(&mut t) + .await + .unwrap(); + + testx::db_rollback!(t); + assert_eq!(stocks, vec![first, second]); + } + + #[tokio::test] + async fn delete_stock() { + testx::db_t_ref!(t); + + let first = test_stock(&mut t, None, None, None).await; + let second = test_stock(&mut t, None, None, None).await; + + let deleted: Option = DeleteStock { + stock_id: second.id, + } + .run(&mut t) + .await + .unwrap(); + let reloaded = super::find_stock(FindStock { id: second.id }, &mut t).await; + + testx::db_rollback!(t); + assert_eq!(deleted, Some(second)); + assert_ne!(deleted, Some(first)); + assert_eq!(reloaded, Err(crate::Error::Stock(super::Error::NotFound))); + } + + #[tokio::test] + async fn update_stock() { + testx::db_t_ref!(t); + + let first = test_stock(&mut t, None, None, None).await; + let second = test_stock(&mut t, None, None, None).await; + let another_product = test_product(&mut t).await; + + let updated: Stock = UpdateStock { + id: second.id, + product_id: another_product.id, + quantity: Quantity::from_u32(19191), + quantity_unit: QuantityUnit::Gram, + } + .run(&mut t) + .await + .unwrap(); + let reloaded = super::find_stock(FindStock { id: second.id }, &mut t) + .await + .unwrap(); + + testx::db_rollback!(t); + assert_eq!( + updated, + Stock { + id: second.id, + product_id: another_product.id, + quantity: Quantity::from_u32(19191), + quantity_unit: QuantityUnit::Gram, + } + ); + assert_ne!(updated, second); + assert_ne!(updated, first); + assert_eq!(reloaded, updated); + } +}