From 7c272d310f3ebc9f641e5c308760e11e8c42e730 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Wo=C5=BAniak?= Date: Mon, 6 Jun 2022 15:09:13 +0200 Subject: [PATCH] Additional tests --- .../database_manager/src/account_addresses.rs | 4 +- actors/database_manager/src/lib.rs | 14 +- actors/database_manager/src/order_items.rs | 6 +- actors/database_manager/src/photos.rs | 163 +++++++- actors/database_manager/src/product_photos.rs | 152 ++++++- actors/database_manager/src/products.rs | 87 +++- .../src/shopping_cart_items.rs | 375 ++++++++++++++++-- actors/database_manager/src/shopping_carts.rs | 211 +++++++++- actors/database_manager/src/stocks.rs | 84 +++- ...20220606123105_drop_unique_active_cart.sql | 2 + shared/model/src/api.rs | 2 +- shared/model/src/lib.rs | 56 +-- 12 files changed, 1050 insertions(+), 106 deletions(-) create mode 100644 migrations/20220606123105_drop_unique_active_cart.sql diff --git a/actors/database_manager/src/account_addresses.rs b/actors/database_manager/src/account_addresses.rs index 2a06621..69f2305 100644 --- a/actors/database_manager/src/account_addresses.rs +++ b/actors/database_manager/src/account_addresses.rs @@ -138,7 +138,7 @@ WHERE account_id = $1 .await { log::error!("{e}"); - eprintln!("{e}") + dbg!(e); } } @@ -162,7 +162,7 @@ RETURNING id, name, email, phone, street, city, country, zip, account_id, is_def .await .map_err(|e| { log::error!("{e}"); - eprintln!("{e}"); + dbg!(e); Error::CreateAccountAddress.into() }) } diff --git a/actors/database_manager/src/lib.rs b/actors/database_manager/src/lib.rs index aa4804d..129b60b 100644 --- a/actors/database_manager/src/lib.rs +++ b/actors/database_manager/src/lib.rs @@ -213,6 +213,7 @@ pub struct MultiLoad<'transaction, 'transaction2, 'header, 'condition, T> { pool: &'transaction mut sqlx::Transaction<'transaction2, sqlx::Postgres>, header: &'header str, condition: &'condition str, + sort: Option, __phantom: std::marker::PhantomData, } @@ -230,10 +231,16 @@ where pool, header, condition, + sort: None, __phantom: Default::default(), } } + pub fn with_sorting>(mut self, order: S) -> Self { + self.sort = Some(order.into()); + self + } + pub async fn load<'query, Error, Ids>( &mut self, len: usize, @@ -257,13 +264,18 @@ where }, ) { let query: String = self.header.into(); - let query = ids.iter().enumerate().fold(query, |mut q, (idx, _id)| { + 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_str(" "); + } let q = sqlx::query_as_with( query.as_str(), ids.into_iter() diff --git a/actors/database_manager/src/order_items.rs b/actors/database_manager/src/order_items.rs index 990ec16..578ea8f 100644 --- a/actors/database_manager/src/order_items.rs +++ b/actors/database_manager/src/order_items.rs @@ -38,7 +38,7 @@ ORDER BY id DESC .await .map_err(|e| { log::error!("{e:?}"); - super::Error::from(Error::All) + Error::All.into() }) } @@ -102,7 +102,7 @@ WHERE id = $1 .await .map_err(|e| { log::error!("{e:?}"); - super::Error::OrderItem(Error::NotExists) + Error::NotExists.into() }) } @@ -128,6 +128,6 @@ ORDER BY id DESC .await .map_err(|e| { log::error!("{e:?}"); - super::Error::OrderItem(Error::OrderItems) + Error::OrderItems.into() }) } diff --git a/actors/database_manager/src/photos.rs b/actors/database_manager/src/photos.rs index 75ed5de..cb14250 100644 --- a/actors/database_manager/src/photos.rs +++ b/actors/database_manager/src/photos.rs @@ -6,6 +6,8 @@ pub enum Error { Create, #[error("Failed to fetch all photo")] All, + #[error("Failed to fetch photos for products")] + PhotosForProducts, } #[derive(actix::Message)] @@ -22,6 +24,7 @@ where r#" SELECT id, local_path, file_name, unique_name FROM photos +ORDER BY id ASC "#, ) .fetch_all(pool) @@ -91,20 +94,168 @@ pub(crate) async fn photos_for_products( let res: Vec = MultiLoad::new( pool, r#" - SELECT photos.id, photos.local_path, photos.file_name, - product_photos.product_id, photos.unique_name FROM photos - INNER JOIN product_photos - ON photos.id = product_photos.photo_id - WHERE +SELECT photos.id AS photo_id, + photos.local_path AS local_path, + photos.file_name AS file_name, + product_photos.product_id AS product_id, + photos.unique_name AS unique_name +FROM photos +INNER JOIN product_photos + ON photos.id = product_photos.photo_id +WHERE "#, " product_photos.product_id =", ) + .with_sorting("photos.id ASC") .load( msg.product_ids.len(), msg.product_ids.into_iter().map(|id| *id), - |_e| crate::Error::Photo(Error::All), + |e| { + log::error!("{}", e); + dbg!(e); + crate::Error::Photo(Error::PhotosForProducts) + }, ) .await?; log::debug!("product linked photos {:?}", res); Ok(res) } + +#[cfg(test)] +mod tests { + use config::UpdateConfig; + use model::*; + use uuid::Uuid; + + pub struct NoOpts; + + impl UpdateConfig for NoOpts {} + + use crate::*; + + async fn test_product(t: &mut sqlx::Transaction<'_, sqlx::Postgres>) -> Product { + create_product( + CreateProduct { + name: ProductName::new(format!("{}", Uuid::new_v4())), + short_description: ProductShortDesc::new(format!("{}", Uuid::new_v4())), + long_description: ProductLongDesc::new(format!("{}", Uuid::new_v4())), + category: None, + price: Price::from_u32(4687), + deliver_days_flag: Days(vec![Day::Friday, Day::Sunday]), + }, + t, + ) + .await + .unwrap() + } + + async fn test_product_photo( + t: &mut sqlx::Transaction<'_, sqlx::Postgres>, + product_id: ProductId, + ) -> ProductPhoto { + let photo = test_photo(&mut *t, None, None, None).await; + + create_product_photo( + CreateProductPhoto { + product_id, + photo_id: photo.id, + }, + t, + ) + .await + .unwrap() + } + + async fn test_photo( + t: &mut sqlx::Transaction<'_, sqlx::Postgres>, + local_path: Option, + file_name: Option, + unique_name: Option, + ) -> Photo { + super::create_photo( + CreatePhoto { + local_path: LocalPath::new( + local_path.unwrap_or_else(|| format!("{}", Uuid::new_v4())), + ), + file_name: FileName::new( + file_name.unwrap_or_else(|| format!("{}", Uuid::new_v4())), + ), + unique_name: UniqueName::new( + unique_name.unwrap_or_else(|| format!("{}", Uuid::new_v4())), + ), + }, + t, + ) + .await + .unwrap() + } + + #[actix::test] + async fn create_photo() { + testx::db_t!(t); + + test_photo(&mut t, None, None, None).await; + + testx::db_rollback!(t); + } + + #[actix::test] + async fn all() { + testx::db_t!(t); + + let p1 = test_photo(&mut t, None, None, None).await; + let p2 = test_photo(&mut t, None, None, None).await; + let p3 = test_photo(&mut t, None, None, None).await; + + let all = all_photos(AllPhotos, &mut t).await.unwrap(); + + testx::db_rollback!(t); + assert_eq!(all, vec![p1, p2, p3]); + } + + #[actix::test] + async fn products_photos() { + testx::db_t!(t); + + let product_1 = test_product(&mut t).await; + let p1 = test_product_photo(&mut t, product_1.id).await; + let p2 = test_product_photo(&mut t, product_1.id).await; + let p3 = test_product_photo(&mut t, product_1.id).await; + + let product_2 = test_product(&mut t).await; + let _p4 = test_product_photo(&mut t, product_2.id).await; + let _p5 = test_product_photo(&mut t, product_2.id).await; + let _p6 = test_product_photo(&mut t, product_2.id).await; + + let product_3 = test_product(&mut t).await; + let p7 = test_product_photo(&mut t, product_3.id).await; + let p8 = test_product_photo(&mut t, product_3.id).await; + let p9 = test_product_photo(&mut t, product_3.id).await; + + let mut all = photos_for_products( + PhotosForProducts { + product_ids: vec![product_1.id, product_3.id], + }, + &mut t, + ) + .await + .unwrap() + .into_iter() + .map(|p| p.photo_id) + .collect::>(); + all.sort_by(|left, right| left.partial_cmp(right).unwrap()); + + testx::db_rollback!(t); + assert_eq!( + all, + vec![ + p1.photo_id, + p2.photo_id, + p3.photo_id, + p7.photo_id, + p8.photo_id, + p9.photo_id + ] + ); + } +} diff --git a/actors/database_manager/src/product_photos.rs b/actors/database_manager/src/product_photos.rs index c25a390..262112a 100644 --- a/actors/database_manager/src/product_photos.rs +++ b/actors/database_manager/src/product_photos.rs @@ -6,37 +6,37 @@ pub enum Error { Create, #[error("Failed to load all product photos")] All, + #[error("Failed to delete product photo")] + Delete, } #[derive(actix::Message)] #[rtype(result = "Result>")] pub struct AllProductPhotos; -crate::db_async_handler!( +db_async_handler!( AllProductPhotos, all_product_photos, Vec, inner_all_product_photos ); -pub(crate) async fn all_product_photos<'e, E>( +pub(crate) async fn all_product_photos( _msg: AllProductPhotos, - pool: E, -) -> Result> -where - E: sqlx::Executor<'e, Database = sqlx::Postgres>, -{ + pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, +) -> Result> { sqlx::query_as( r#" SELECT id, product_id, photo_id FROM product_photos +ORDER BY id ASC "#, ) .fetch_all(pool) .await .map_err(|e| { log::error!("{e:?}"); - crate::Error::ProductPhoto(Error::All) + Error::All.into() }) } @@ -54,13 +54,10 @@ db_async_handler!( inner_create_product_photo ); -pub(crate) async fn create_product_photo<'e, E>( +pub(crate) async fn create_product_photo( msg: CreateProductPhoto, - pool: E, -) -> Result -where - E: sqlx::Executor<'e, Database = sqlx::Postgres>, -{ + pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, +) -> Result { sqlx::query_as( r#" INSERT INTO product_photos(product_id, photo_id) @@ -74,6 +71,131 @@ RETURNING id, product_id, photo_id .await .map_err(|e| { log::error!("{:?}", e); - crate::Error::ProductPhoto(Error::Create) + Error::Create.into() }) } + +#[derive(actix::Message)] +#[rtype(result = "Result>")] +pub struct DeleteProductPhoto { + pub id: model::ProductPhotoId, +} + +db_async_handler!( + DeleteProductPhoto, + delete_product_photo, + Option, + inner_delete_product_photo +); + +pub(crate) async fn delete_product_photo( + msg: DeleteProductPhoto, + pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, +) -> Result> { + sqlx::query_as( + r#" +DELETE FROM product_photos +WHERE id = $1 +RETURNING id, product_id, photo_id + "#, + ) + .bind(msg.id) + .fetch_optional(pool) + .await + .map_err(|e| { + log::error!("{:?}", e); + Error::Delete.into() + }) +} + +#[cfg(test)] +mod tests { + use config::UpdateConfig; + use model::*; + use uuid::Uuid; + + pub struct NoOpts; + + impl UpdateConfig for NoOpts {} + + use crate::*; + + async fn test_product(t: &mut sqlx::Transaction<'_, sqlx::Postgres>) -> Product { + create_product( + CreateProduct { + name: ProductName::new(format!("{}", Uuid::new_v4())), + short_description: ProductShortDesc::new(format!("{}", Uuid::new_v4())), + long_description: ProductLongDesc::new(format!("{}", Uuid::new_v4())), + category: None, + price: Price::from_u32(4687), + deliver_days_flag: Days(vec![Day::Friday, Day::Sunday]), + }, + t, + ) + .await + .unwrap() + } + + async fn test_photo(t: &mut sqlx::Transaction<'_, sqlx::Postgres>) -> Photo { + crate::create_photo( + CreatePhoto { + local_path: LocalPath::new(format!("{}", Uuid::new_v4())), + file_name: FileName::new(format!("{}", Uuid::new_v4())), + unique_name: UniqueName::new(format!("{}", Uuid::new_v4())), + }, + t, + ) + .await + .unwrap() + } + + async fn test_product_photo(t: &mut sqlx::Transaction<'_, sqlx::Postgres>) -> ProductPhoto { + let product = test_product(t).await; + let photo = test_photo(t).await; + create_product_photo( + CreateProductPhoto { + product_id: product.id, + photo_id: photo.id, + }, + t, + ) + .await + .unwrap() + } + + #[actix::test] + async fn create_photo() { + testx::db_t!(t); + + test_product_photo(&mut t).await; + + testx::db_rollback!(t); + } + + #[actix::test] + async fn delete() { + testx::db_t!(t); + + let p1 = test_product_photo(&mut t).await; + let p2 = test_product_photo(&mut t).await; + let p3 = test_product_photo(&mut t).await; + + let deleted = delete_product_photo(DeleteProductPhoto { id: p2.id }, &mut t) + .await + .unwrap(); + + testx::db_rollback!(t); + assert_ne!(deleted, Some(p1)); + assert_eq!(deleted, Some(p2)); + assert_ne!(deleted, Some(p3)); + } + + #[actix::test] + async fn create() { + testx::db_t!(t); + + test_product_photo(&mut t).await; + + testx::db_rollback!(t); + } +} diff --git a/actors/database_manager/src/products.rs b/actors/database_manager/src/products.rs index e039a71..182ec73 100644 --- a/actors/database_manager/src/products.rs +++ b/actors/database_manager/src/products.rs @@ -133,7 +133,7 @@ RETURNING id, .await .map_err(|e| { log::error!("{e:?}"); - eprintln!("{e:?}"); + dbg!(e); crate::Error::Product(Error::Create) }) } @@ -152,7 +152,7 @@ pub struct UpdateProduct { crate::db_async_handler!(UpdateProduct, update_product, Product, inner_update_product); -pub(crate) async fn update_product<'e, E>(msg: UpdateProduct, pool: E) -> Result +pub(crate) async fn update_product<'e, E>(msg: UpdateProduct, pool: E) -> Result where E: sqlx::Executor<'e, Database = sqlx::Postgres>, { @@ -315,3 +315,86 @@ WHERE ) .await } + +#[cfg(test)] +mod tests { + use config::UpdateConfig; + use model::*; + use uuid::Uuid; + + pub struct NoOpts; + + impl UpdateConfig for NoOpts {} + + use crate::*; + + 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 { + super::create_product( + CreateProduct { + name: ProductName::new(name.unwrap_or_else(|| format!("{}", Uuid::new_v4()))), + short_description: ProductShortDesc::new( + short_description.unwrap_or_else(|| format!("{}", Uuid::new_v4())), + ), + long_description: ProductLongDesc::new( + long_description.unwrap_or_else(|| format!("{}", Uuid::new_v4())), + ), + category, + price: Price::from_u32(price.unwrap_or(4687)), + deliver_days_flag: deliver_days_flag + .unwrap_or_else(|| Days(vec![Day::Friday, Day::Sunday])), + }, + t, + ) + .await + .unwrap() + } + + #[actix::test] + async fn create_product() { + testx::db_t!(t); + + test_product(&mut t, None, None, None, None, None, None).await; + + testx::db_rollback!(t); + } + + #[actix::test] + async fn all() { + testx::db_t!(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]); + } + + #[actix::test] + async fn find() { + testx::db_t!(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); + } +} diff --git a/actors/database_manager/src/shopping_cart_items.rs b/actors/database_manager/src/shopping_cart_items.rs index 3ecd6c3..692dce1 100644 --- a/actors/database_manager/src/shopping_cart_items.rs +++ b/actors/database_manager/src/shopping_cart_items.rs @@ -1,5 +1,4 @@ use model::*; -use sqlx::PgPool; use super::Result; use crate::db_async_handler; @@ -34,23 +33,32 @@ pub struct AllShoppingCartItems; db_async_handler!( AllShoppingCartItems, all_shopping_cart_items, - Vec + Vec, + inner_all_shopping_cart_items ); pub(crate) async fn all_shopping_cart_items( _msg: AllShoppingCartItems, - pool: PgPool, + t: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> Result> { sqlx::query_as( r#" -SELECT id, product_id, shopping_cart_id, quantity, quantity_unit +SELECT shopping_cart_items.id, + shopping_cart_items.product_id, + shopping_cart_items.shopping_cart_id, + shopping_cart_items.quantity, + shopping_cart_items.quantity_unit FROM shopping_cart_items +INNER JOIN shopping_carts + ON shopping_cart_items.shopping_cart_id = shopping_carts.id +ORDER BY shopping_cart_items.id ASC "#, ) - .fetch_all(&pool) + .fetch_all(t) .await .map_err(|e| { log::error!("{e:?}"); + dbg!(e); super::Error::ShoppingCartItem(Error::All) }) } @@ -65,12 +73,13 @@ pub struct AccountShoppingCartItems { db_async_handler!( AccountShoppingCartItems, account_shopping_cart_items, - Vec + Vec, + inner_account_shopping_cart_items ); pub(crate) async fn account_shopping_cart_items( msg: AccountShoppingCartItems, - pool: PgPool, + t: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> Result> { match msg.shopping_cart_id { Some(shopping_cart_id) => sqlx::query_as( @@ -103,7 +112,7 @@ WHERE shopping_carts.buyer_id = $1 ) .bind(msg.account_id), } - .fetch_all(&pool) + .fetch_all(t) .await .map_err(|e| { log::error!("{e:?}"); @@ -123,12 +132,13 @@ pub struct CreateShoppingCartItem { db_async_handler!( CreateShoppingCartItem, create_shopping_cart_item, - ShoppingCartItem + ShoppingCartItem, + inner_create_shopping_cart_item ); pub(crate) async fn create_shopping_cart_item( msg: CreateShoppingCartItem, - db: PgPool, + t: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> Result { sqlx::query_as( r#" @@ -141,10 +151,11 @@ RETURNING id, product_id, shopping_cart_id, quantity, quantity_unit .bind(msg.shopping_cart_id) .bind(msg.quantity) .bind(msg.quantity_unit) - .fetch_one(&db) + .fetch_one(t) .await .map_err(|e| { log::error!("{e:?}"); + dbg!(&e); super::Error::ShoppingCartItem(Error::CantCreate) }) } @@ -162,12 +173,13 @@ pub struct UpdateShoppingCartItem { db_async_handler!( UpdateShoppingCartItem, update_shopping_cart_item, - ShoppingCartItem + ShoppingCartItem, + inner_update_shopping_cart_item ); pub(crate) async fn update_shopping_cart_item( msg: UpdateShoppingCartItem, - db: PgPool, + t: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> Result { sqlx::query_as( r#" @@ -182,7 +194,7 @@ RETURNING id, product_id, shopping_cart_id, quantity, quantity_unit .bind(msg.shopping_cart_id) .bind(msg.quantity) .bind(msg.quantity_unit) - .fetch_one(&db) + .fetch_one(t) .await .map_err(|e| { log::error!("{e:?}"); @@ -199,12 +211,13 @@ pub struct DeleteShoppingCartItem { db_async_handler!( DeleteShoppingCartItem, delete_shopping_cart_item, - Option + Option, + inner_delete_shopping_cart_item ); pub(crate) async fn delete_shopping_cart_item( msg: DeleteShoppingCartItem, - db: PgPool, + t: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> Result> { sqlx::query_as( r#" @@ -214,7 +227,7 @@ RETURNING id, product_id, shopping_cart_id, quantity, quantity_unit "#, ) .bind(msg.id) - .fetch_optional(&db) + .fetch_optional(t) .await .map_err(|e| { log::error!("{e:?}"); @@ -231,12 +244,13 @@ pub struct FindShoppingCartItem { db_async_handler!( FindShoppingCartItem, find_shopping_cart_item, - ShoppingCartItem + ShoppingCartItem, + inner_find_shopping_cart_item ); pub(crate) async fn find_shopping_cart_item( msg: FindShoppingCartItem, - db: PgPool, + t: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> Result { sqlx::query_as( r#" @@ -246,7 +260,7 @@ WHERE id = $1 "#, ) .bind(msg.id) - .fetch_one(&db) + .fetch_one(t) .await .map_err(|e| { log::error!("{e:?}"); @@ -257,18 +271,19 @@ WHERE id = $1 #[derive(actix::Message)] #[rtype(result = "Result>")] pub struct ActiveCartItemByProduct { - pub product_id: model::ProductId, + pub product_id: ProductId, } db_async_handler!( ActiveCartItemByProduct, active_cart_item_by_product, - Option + Option, + inner_active_cart_item_by_product ); pub(crate) async fn active_cart_item_by_product( msg: ActiveCartItemByProduct, - db: PgPool, + t: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> Result> { sqlx::query_as( r#" @@ -278,13 +293,16 @@ SELECT shopping_cart_items.id, shopping_cart_items.quantity, shopping_cart_items.quantity_unit FROM shopping_cart_items -INNER JOIN shopping_carts ON shopping_cart_items.shopping_cart_id = shopping_carts.id -WHERE product_id = $1 AND shopping_carts.state = $2 +INNER JOIN shopping_carts + ON shopping_cart_items.shopping_cart_id = shopping_carts.id +WHERE product_id = $1 + AND shopping_carts.state = $2 +ORDER BY shopping_cart_items.id ASC "#, ) .bind(msg.product_id) .bind(model::ShoppingCartState::Active) - .fetch_optional(&db) + .fetch_optional(t) .await .map_err(|e| { log::error!("{e:?}"); @@ -305,20 +323,25 @@ db_async_handler!( inner_cart_items ); -pub(crate) async fn cart_items<'e, E>(msg: CartItems, pool: E) -> Result> -where - E: sqlx::Executor<'e, Database = sqlx::Postgres>, -{ +pub(crate) async fn cart_items( + msg: CartItems, + t: &mut sqlx::Transaction<'_, sqlx::Postgres>, +) -> Result> { let shopping_cart_id = msg.shopping_cart_id; sqlx::query_as( r#" -SELECT id, product_id, shopping_cart_id, quantity, quantity_unit +SELECT id, + product_id, + shopping_cart_id, + quantity, + quantity_unit FROM shopping_cart_items WHERE shopping_cart_id = $1 +ORDER BY shopping_cart_items.id ASC "#, ) .bind(msg.shopping_cart_id) - .fetch_all(pool) + .fetch_all(t) .await .map_err(|e| { log::error!("{e:?}"); @@ -334,11 +357,16 @@ pub struct RemoveCartItem { pub product_id: Option, } -db_async_handler!(RemoveCartItem, remove_cart_item, Option); +db_async_handler!( + RemoveCartItem, + remove_cart_item, + Option, + inner_remove_cart_item +); pub(crate) async fn remove_cart_item( msg: RemoveCartItem, - pool: PgPool, + t: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> Result> { match (msg.shopping_cart_item_id, msg.product_id) { (Some(shopping_cart_item_id), None) => sqlx::query_as( @@ -371,7 +399,7 @@ RETURNING id, product_id, shopping_cart_id, quantity, quantity_unit .bind(product_id), _ => return Err(crate::Error::ShoppingCartItem(Error::NoIdentity)), } - .fetch_optional(&pool) + .fetch_optional(t) .await .map_err(|e| { log::error!("{e:?}"); @@ -381,3 +409,280 @@ RETURNING id, product_id, shopping_cart_id, quantity, quantity_unit }) }) } + +#[cfg(test)] +mod tests { + use config::UpdateConfig; + use fake::Fake; + use model::*; + use uuid::Uuid; + + pub struct NoOpts; + + impl UpdateConfig for NoOpts {} + + use crate::*; + + async fn test_product(t: &mut sqlx::Transaction<'_, sqlx::Postgres>) -> Product { + create_product( + CreateProduct { + name: ProductName::new(format!("{}", Uuid::new_v4())), + short_description: ProductShortDesc::new(format!("{}", Uuid::new_v4())), + long_description: ProductLongDesc::new(format!("{}", Uuid::new_v4())), + category: None, + price: Price::from_u32(4687), + deliver_days_flag: Days(vec![Day::Friday, Day::Sunday]), + }, + t, + ) + .await + .unwrap() + } + + async fn test_account( + t: &mut sqlx::Transaction<'_, sqlx::Postgres>, + login: Option, + email: Option, + hash: Option, + ) -> FullAccount { + use fake::faker::internet::en; + let login: String = login.unwrap_or_else(|| en::Username().fake()); + let email: String = email.unwrap_or_else(|| en::FreeEmail().fake()); + let hash: String = hash.unwrap_or_else(|| en::Password(10..20).fake()); + + create_account( + CreateAccount { + email: Email::new(email), + login: Login::new(login), + pass_hash: PassHash::new(hash), + role: Role::Admin, + }, + t, + ) + .await + .unwrap() + } + + async fn test_shopping_cart( + t: &mut sqlx::Transaction<'_, sqlx::Postgres>, + buyer_id: Option, + state: ShoppingCartState, + ) -> ShoppingCart { + let buyer_id = match buyer_id { + Some(id) => id, + _ => test_account(&mut *t, None, None, None).await.id, + }; + + sqlx::query( + r#" +UPDATE shopping_carts +SET state = 'closed' +WHERE buyer_id = $1 + "#, + ) + .bind(buyer_id) + .execute(&mut *t) + .await + .unwrap(); + + let cart = create_shopping_cart( + CreateShoppingCart { + buyer_id, + payment_method: PaymentMethod::PaymentOnTheSpot, + }, + &mut *t, + ) + .await + .unwrap(); + + update_shopping_cart( + UpdateShoppingCart { + id: cart.id, + buyer_id: cart.buyer_id, + payment_method: cart.payment_method, + state, + checkout_notes: None, + }, + &mut *t, + ) + .await + .unwrap() + } + + async fn test_shopping_cart_item( + t: &mut sqlx::Transaction<'_, sqlx::Postgres>, + shopping_cart_id: Option, + product_id: Option, + ) -> ShoppingCartItem { + let shopping_cart_id = match shopping_cart_id { + Some(id) => id, + _ => { + test_shopping_cart(&mut *t, None, ShoppingCartState::Closed) + .await + .id + } + }; + let product_id = match product_id { + Some(id) => id, + _ => test_product(&mut *t).await.id, + }; + create_shopping_cart_item( + CreateShoppingCartItem { + product_id, + shopping_cart_id, + quantity: Quantity::from_u32(496879), + quantity_unit: QuantityUnit::Gram, + }, + t, + ) + .await + .unwrap() + } + + #[actix::test] + async fn create() { + testx::db_t!(t); + + test_shopping_cart_item(&mut t, None, None).await; + + testx::db_rollback!(t); + } + + #[actix::test] + async fn all() { + testx::db_t!(t); + + let account_id = test_account(&mut t, None, None, None).await.id; + + let mut items = Vec::with_capacity(9); + + let cart1 = test_shopping_cart(&mut t, Some(account_id), ShoppingCartState::Closed).await; + items.push(test_shopping_cart_item(&mut t, Some(cart1.id), None).await); + items.push(test_shopping_cart_item(&mut t, Some(cart1.id), None).await); + items.push(test_shopping_cart_item(&mut t, Some(cart1.id), None).await); + + let cart2 = test_shopping_cart(&mut t, Some(account_id), ShoppingCartState::Active).await; + items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None).await); + items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None).await); + items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None).await); + + let cart3 = test_shopping_cart(&mut t, Some(account_id), ShoppingCartState::Closed).await; + items.push(test_shopping_cart_item(&mut t, Some(cart3.id), None).await); + items.push(test_shopping_cart_item(&mut t, Some(cart3.id), None).await); + items.push(test_shopping_cart_item(&mut t, Some(cart3.id), None).await); + + let all = all_shopping_cart_items(AllShoppingCartItems, &mut t) + .await + .unwrap(); + + testx::db_rollback!(t); + assert_eq!(all, items) + } + + #[actix::test] + async fn account_cart_with_cart_id() { + testx::db_t!(t); + + let account_id = test_account(&mut t, None, None, None).await.id; + + let mut items = Vec::with_capacity(9); + + let cart1 = test_shopping_cart(&mut t, Some(account_id), ShoppingCartState::Closed).await; + test_shopping_cart_item(&mut t, Some(cart1.id), None).await; + test_shopping_cart_item(&mut t, Some(cart1.id), None).await; + test_shopping_cart_item(&mut t, Some(cart1.id), None).await; + + let cart2 = test_shopping_cart(&mut t, Some(account_id), ShoppingCartState::Active).await; + items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None).await); + items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None).await); + items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None).await); + + let cart3 = test_shopping_cart(&mut t, Some(account_id), ShoppingCartState::Closed).await; + test_shopping_cart_item(&mut t, Some(cart3.id), None).await; + test_shopping_cart_item(&mut t, Some(cart3.id), None).await; + test_shopping_cart_item(&mut t, Some(cart3.id), None).await; + + let all = account_shopping_cart_items( + AccountShoppingCartItems { + account_id, + shopping_cart_id: Some(cart2.id), + }, + &mut t, + ) + .await + .unwrap(); + + testx::db_rollback!(t); + assert_eq!(all, items) + } + + #[actix::test] + async fn account_cart_without_cart_id() { + testx::db_t!(t); + + let account_id = test_account(&mut t, None, None, None).await.id; + + let mut items = Vec::with_capacity(9); + + let cart1 = test_shopping_cart(&mut t, Some(account_id), ShoppingCartState::Closed).await; + items.push(test_shopping_cart_item(&mut t, Some(cart1.id), None).await); + items.push(test_shopping_cart_item(&mut t, Some(cart1.id), None).await); + items.push(test_shopping_cart_item(&mut t, Some(cart1.id), None).await); + + let cart2 = test_shopping_cart(&mut t, Some(account_id), ShoppingCartState::Active).await; + items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None).await); + items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None).await); + items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None).await); + + let cart3 = test_shopping_cart(&mut t, Some(account_id), ShoppingCartState::Closed).await; + items.push(test_shopping_cart_item(&mut t, Some(cart3.id), None).await); + items.push(test_shopping_cart_item(&mut t, Some(cart3.id), None).await); + items.push(test_shopping_cart_item(&mut t, Some(cart3.id), None).await); + + let all = account_shopping_cart_items( + AccountShoppingCartItems { + account_id, + shopping_cart_id: None, + }, + &mut t, + ) + .await + .unwrap(); + + testx::db_rollback!(t); + assert_eq!(all, items) + } + + #[actix::test] + async fn update() { + testx::db_t!(t); + let account_id = test_account(&mut t, None, None, None).await.id; + let cart1 = test_shopping_cart(&mut t, Some(account_id), ShoppingCartState::Closed).await; + let item = test_shopping_cart_item(&mut t, Some(cart1.id), None).await; + + let updated = update_shopping_cart_item( + UpdateShoppingCartItem { + id: item.id, + product_id: item.product_id, + shopping_cart_id: item.shopping_cart_id, + quantity: Quantity::from_u32(987979879), + quantity_unit: QuantityUnit::Kilogram, + }, + &mut t, + ) + .await + .unwrap(); + + assert_ne!(item, updated); + assert_eq!( + updated, + ShoppingCartItem { + id: item.id, + product_id: item.product_id, + shopping_cart_id: item.shopping_cart_id, + quantity: Quantity::from_u32(987979879), + quantity_unit: QuantityUnit::Kilogram, + } + ); + } +} diff --git a/actors/database_manager/src/shopping_carts.rs b/actors/database_manager/src/shopping_carts.rs index 8477651..1c3b6ca 100644 --- a/actors/database_manager/src/shopping_carts.rs +++ b/actors/database_manager/src/shopping_carts.rs @@ -94,11 +94,16 @@ pub struct CreateShoppingCart { pub payment_method: PaymentMethod, } -db_async_handler!(CreateShoppingCart, create_shopping_cart, ShoppingCart); +db_async_handler!( + CreateShoppingCart, + create_shopping_cart, + ShoppingCart, + inner_create_shopping_cart +); pub(crate) async fn create_shopping_cart( msg: CreateShoppingCart, - db: PgPool, + pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> Result { sqlx::query_as( r#" @@ -109,10 +114,11 @@ RETURNING id, buyer_id, payment_method, state, checkout_notes ) .bind(msg.buyer_id) .bind(msg.payment_method) - .fetch_one(&db) + .fetch_one(pool) .await .map_err(|e| { log::error!("{e:?}"); + dbg!(e); super::Error::ShoppingCart(Error::CantCreate) }) } @@ -127,11 +133,16 @@ pub struct UpdateShoppingCart { pub checkout_notes: Option, } -db_async_handler!(UpdateShoppingCart, update_shopping_cart, ShoppingCart); +db_async_handler!( + UpdateShoppingCart, + update_shopping_cart, + ShoppingCart, + inner_update_shopping_cart +); pub(crate) async fn update_shopping_cart( msg: UpdateShoppingCart, - db: PgPool, + pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> Result { sqlx::query_as( r#" @@ -146,7 +157,7 @@ RETURNING id, buyer_id, payment_method, state, checkout_notes .bind(msg.payment_method) .bind(msg.state) .bind(msg.checkout_notes) - .fetch_one(&db) + .fetch_one(pool) .await .map_err(|e| { log::error!("{e:?}"); @@ -198,9 +209,17 @@ pub struct FindShoppingCart { pub id: ShoppingCartId, } -db_async_handler!(FindShoppingCart, find_shopping_cart, ShoppingCart); +db_async_handler!( + FindShoppingCart, + find_shopping_cart, + ShoppingCart, + inner_find_shopping_cart +); -pub(crate) async fn find_shopping_cart(msg: FindShoppingCart, db: PgPool) -> Result { +pub(crate) async fn find_shopping_cart( + msg: FindShoppingCart, + pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, +) -> Result { sqlx::query_as( r#" SELECT id, buyer_id, payment_method, state, checkout_notes @@ -209,7 +228,7 @@ WHERE id = $1 "#, ) .bind(msg.id) - .fetch_one(&db) + .fetch_one(pool) .await .map_err(|e| { log::error!("{e:?}"); @@ -267,3 +286,177 @@ WHERE buyer_id = $1 AND state = 'active' super::Error::ShoppingCart(Error::NotExists) }) } + +#[cfg(test)] +mod tests { + use config::UpdateConfig; + use fake::Fake; + use model::*; + + pub struct NoOpts; + + impl UpdateConfig for NoOpts {} + + use crate::*; + + async fn test_account( + t: &mut sqlx::Transaction<'_, sqlx::Postgres>, + login: Option, + email: Option, + hash: Option, + ) -> FullAccount { + use fake::faker::internet::en; + let login: String = login.unwrap_or_else(|| en::Username().fake()); + let email: String = email.unwrap_or_else(|| en::FreeEmail().fake()); + let hash: String = hash.unwrap_or_else(|| en::Password(10..20).fake()); + + create_account( + CreateAccount { + email: Email::new(email), + login: Login::new(login), + pass_hash: PassHash::new(hash), + role: Role::Admin, + }, + t, + ) + .await + .unwrap() + } + + async fn test_shopping_cart( + t: &mut sqlx::Transaction<'_, sqlx::Postgres>, + buyer_id: Option, + ) -> ShoppingCart { + let buyer_id = match buyer_id { + Some(id) => id, + _ => test_account(&mut *t, None, None, None).await.id, + }; + + super::create_shopping_cart( + CreateShoppingCart { + buyer_id, + payment_method: PaymentMethod::PaymentOnTheSpot, + }, + t, + ) + .await + .unwrap() + } + + #[actix::test] + async fn create_shopping_cart() { + testx::db_t!(t); + + let account = test_account(&mut t, None, None, None).await; + + let cart = super::create_shopping_cart( + CreateShoppingCart { + buyer_id: account.id, + payment_method: PaymentMethod::PaymentOnTheSpot, + }, + &mut t, + ) + .await; + + testx::db_rollback!(t); + assert!(cart.is_ok()); + } + + #[actix::test] + async fn update_shopping_cart() { + testx::db_t!(t); + + let account = test_account(&mut t, None, None, None).await; + + let original = test_shopping_cart(&mut t, Some(account.id)).await; + + let cart = super::update_shopping_cart( + UpdateShoppingCart { + id: original.id, + buyer_id: account.id, + payment_method: PaymentMethod::PayU, + state: ShoppingCartState::Closed, + checkout_notes: Some("Foo bar".into()), + }, + &mut t, + ) + .await + .unwrap(); + + testx::db_rollback!(t); + assert_ne!(cart, original); + assert_eq!( + cart, + ShoppingCart { + id: original.id, + buyer_id: account.id, + payment_method: PaymentMethod::PayU, + state: ShoppingCartState::Closed, + checkout_notes: Some("Foo bar".into()) + } + ); + } + + #[actix::test] + async fn without_cart_ensure_shopping_cart() { + testx::db_t!(t); + + let account = test_account(&mut t, None, None, None).await; + + let cart = super::ensure_active_shopping_cart( + EnsureActiveShoppingCart { + buyer_id: account.id, + }, + &mut t, + ) + .await + .unwrap(); + + let id = cart.id; + + testx::db_rollback!(t); + assert_eq!( + cart, + model::ShoppingCart { + id, + buyer_id: account.id, + payment_method: Default::default(), + state: ShoppingCartState::Active, + checkout_notes: None + } + ); + } + + #[actix::test] + async fn with_inactive_cart_ensure_shopping_cart() { + testx::db_t!(t); + + let account = test_account(&mut t, None, None, None).await; + + let original = test_shopping_cart(&mut t, Some(account.id)).await; + let _ = super::update_shopping_cart( + UpdateShoppingCart { + id: original.id, + buyer_id: account.id, + payment_method: Default::default(), + state: ShoppingCartState::Closed, + checkout_notes: None, + }, + &mut t, + ) + .await + .unwrap(); + + let cart = super::ensure_active_shopping_cart( + EnsureActiveShoppingCart { + buyer_id: account.id, + }, + &mut t, + ) + .await + .unwrap(); + + testx::db_rollback!(t); + assert_ne!(original, cart); + } +} diff --git a/actors/database_manager/src/stocks.rs b/actors/database_manager/src/stocks.rs index 3020538..f87182b 100644 --- a/actors/database_manager/src/stocks.rs +++ b/actors/database_manager/src/stocks.rs @@ -15,6 +15,8 @@ pub enum Error { Delete, #[error("Unable find stock for product")] ProductStock, + #[error("Stock does not exists")] + NotFound, } #[derive(Message)] @@ -42,6 +44,35 @@ ORDER BY id ASC }) } +#[derive(Message)] +#[rtype(result = "Result")] +pub struct FindStock { + pub id: StockId, +} + +crate::db_async_handler!(FindStock, find_stock, Stock, inner_find_stock); + +async fn find_stock( + msg: FindStock, + pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, +) -> Result { + sqlx::query_as( + r#" +SELECT id, product_id, quantity, quantity_unit +FROM stocks +WHERE id = $1 + "#, + ) + .bind(msg.id) + .fetch_one(pool) + .await + .map_err(|e| { + log::error!("{e:?}"); + dbg!(e); + crate::Error::Stock(Error::NotFound) + }) +} + #[derive(Message)] #[rtype(result = "Result")] pub struct CreateStock { @@ -70,7 +101,7 @@ RETURNING id, product_id, quantity, quantity_unit .await .map_err(|e| { log::error!("{e:?}"); - eprintln!("{e:?}"); + dbg!(e); crate::Error::Stock(Error::Create) }) } @@ -93,8 +124,8 @@ async fn update_stock( sqlx::query_as( r#" UPDATE stocks -SET product_id = $1 AND - quantity = $2 +SET product_id = $1, + quantity = $2, quantity_unit = $3 WHERE id = $4 RETURNING id, product_id, quantity, quantity_unit @@ -165,9 +196,9 @@ async fn product_stock( Ok(MultiLoad::new( pool, r#" - SELECT id, product_id, quantity, quantity_unit - FROM stocks - WHERE +SELECT id, product_id, quantity, quantity_unit +FROM stocks +WHERE "#, " product_id =", ) @@ -291,10 +322,49 @@ mod tests { ) .await .unwrap(); - // let reloaded = super::Stock + 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))); + } + + #[actix::test] + async fn update_stock() { + testx::db_t!(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 = super::update_stock( + UpdateStock { + id: second.id, + product_id: another_product.id, + quantity: Quantity::from_u32(19191), + quantity_unit: QuantityUnit::Gram, + }, + &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); } } diff --git a/migrations/20220606123105_drop_unique_active_cart.sql b/migrations/20220606123105_drop_unique_active_cart.sql new file mode 100644 index 0000000..defd8b6 --- /dev/null +++ b/migrations/20220606123105_drop_unique_active_cart.sql @@ -0,0 +1,2 @@ +ALTER TABLE shopping_carts + DROP CONSTRAINT single_active_cart; diff --git a/shared/model/src/api.rs b/shared/model/src/api.rs index b13cd58..cd09481 100644 --- a/shared/model/src/api.rs +++ b/shared/model/src/api.rs @@ -362,7 +362,7 @@ impl<'path> .drain_filter(|photo| photo.product_id == id) .map( |ProductLinkedPhoto { - id, + photo_id: id, local_path: _, file_name, product_id: _, diff --git a/shared/model/src/lib.rs b/shared/model/src/lib.rs index 4fcaa5f..b7b7758 100644 --- a/shared/model/src/lib.rs +++ b/shared/model/src/lib.rs @@ -224,7 +224,7 @@ impl Default for PaymentMethod { #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", sqlx(rename_all = "snake_case"))] -#[derive(Copy, Clone, Debug, Hash, Display, Deserialize, Serialize)] +#[derive(Copy, Clone, Debug, Hash, PartialEq, Display, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] pub enum ShoppingCartState { Active, @@ -279,7 +279,9 @@ impl Default for Audience { #[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", sqlx(transparent))] -#[derive(Serialize, Deserialize, Default, Debug, Copy, Clone, Hash, Deref, From)] +#[derive( + Default, Debug, Copy, Clone, Hash, PartialOrd, PartialEq, Serialize, Deserialize, Deref, From, +)] #[serde(transparent)] pub struct Price(NonNegative); @@ -435,7 +437,7 @@ impl<'de> serde::Deserialize<'de> for Email { #[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", sqlx(transparent))] -#[derive(Serialize, Default, Debug, PartialEq, Copy, Clone, Hash, Deref, Display)] +#[derive(Default, Debug, PartialEq, PartialOrd, Copy, Clone, Hash, Serialize, Deref, Display)] #[serde(transparent)] pub struct NonNegative(i32); @@ -533,7 +535,9 @@ impl<'de> serde::Deserialize<'de> for NonNegative { } #[cfg_attr(feature = "dummy", derive(fake::Dummy))] -#[derive(Serialize, Deserialize, Debug, Copy, Clone, Hash, Display, From)] +#[derive( + Debug, Copy, Clone, Hash, PartialOrd, PartialEq, Serialize, Deserialize, Display, From, +)] #[serde(rename_all = "lowercase")] pub enum Day { Monday = 1 << 0, @@ -595,7 +599,7 @@ impl TryFrom for Day { } #[cfg_attr(feature = "dummy", derive(fake::Dummy))] -#[derive(Serialize, Deserialize, Hash, Debug)] +#[derive(Hash, Debug, PartialOrd, PartialEq, Serialize, Deserialize)] #[serde(transparent)] pub struct Days(pub Vec); @@ -773,7 +777,7 @@ impl From for Account { #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", sqlx(transparent))] -#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, Hash, Deref, From)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Deref, From)] #[serde(transparent)] pub struct ProductId(RecordId); @@ -785,7 +789,7 @@ impl Display for ProductId { #[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", sqlx(transparent))] -#[derive(Serialize, Deserialize, Debug, Clone, Hash, Deref, Display, From)] +#[derive(Debug, Clone, Hash, PartialEq, Serialize, Deserialize, Deref, Display, From)] #[serde(transparent)] pub struct ProductName(String); @@ -801,7 +805,7 @@ impl ProductName { #[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", sqlx(transparent))] -#[derive(Serialize, Deserialize, Debug, Clone, Hash, Deref, Display, From)] +#[derive(Debug, Clone, Hash, PartialEq, Serialize, Deserialize, Deref, Display, From)] #[serde(transparent)] pub struct ProductShortDesc(String); @@ -817,7 +821,7 @@ impl ProductShortDesc { #[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", sqlx(transparent))] -#[derive(Serialize, Deserialize, Debug, Clone, Hash, Deref, Display, From)] +#[derive(Debug, Clone, Hash, PartialEq, Serialize, Deserialize, Deref, Display, From)] #[serde(transparent)] pub struct ProductLongDesc(String); @@ -836,7 +840,7 @@ impl ProductLongDesc { #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", sqlx(transparent))] -#[derive(Serialize, Deserialize, Debug, Clone, Hash, Deref, Display, From)] +#[derive(Debug, Clone, Hash, PartialEq, Serialize, Deserialize, Deref, Display, From)] #[serde(transparent)] pub struct ProductCategory(String); @@ -848,7 +852,7 @@ impl ProductCategory { #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "db", derive(sqlx::FromRow))] -#[derive(Serialize, Deserialize, Debug, Hash)] +#[derive(Debug, Hash, PartialEq, Serialize, Deserialize)] pub struct Product { pub id: ProductId, pub name: ProductName, @@ -973,7 +977,7 @@ pub struct ShoppingCartId(pub RecordId); #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "db", derive(sqlx::FromRow))] -#[derive(Serialize, Deserialize, Debug)] +#[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct ShoppingCart { pub id: ShoppingCartId, pub buyer_id: AccountId, @@ -985,12 +989,12 @@ pub struct ShoppingCart { #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", sqlx(transparent))] -#[derive(Serialize, Deserialize, PartialEq, Copy, Clone, Debug, Deref, Display)] +#[derive(PartialEq, Copy, Clone, Debug, Serialize, Deserialize, Deref, Display)] #[serde(transparent)] pub struct ShoppingCartItemId(RecordId); #[cfg_attr(feature = "db", derive(sqlx::FromRow))] -#[derive(Serialize, Deserialize, Debug)] +#[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct ShoppingCartItem { pub id: ShoppingCartItemId, pub product_id: ProductId, @@ -1047,7 +1051,7 @@ impl Token { #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", sqlx(transparent))] -#[derive(Serialize, Deserialize, Debug, Clone, Deref, Display, From)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Deref, Display, From)] pub struct AccessTokenString(String); impl AccessTokenString { @@ -1065,7 +1069,7 @@ impl From for AccessTokenString { #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", sqlx(transparent))] -#[derive(Serialize, Deserialize, Debug, Clone, Deref, Display, From)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Deref, Display, From)] pub struct RefreshTokenString(String); impl From for RefreshTokenString { @@ -1083,7 +1087,7 @@ impl RefreshTokenString { #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", sqlx(transparent))] -#[derive(Serialize, Deserialize, Debug, Hash, Deref, Display, From)] +#[derive(Debug, Hash, PartialEq, Serialize, Deserialize, Deref, Display, From)] pub struct LocalPath(String); impl LocalPath { @@ -1095,7 +1099,7 @@ impl LocalPath { #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", sqlx(transparent))] -#[derive(Serialize, Deserialize, Debug, Hash, Deref, Display, From)] +#[derive(Debug, Hash, PartialEq, Serialize, Deserialize, Deref, Display, From)] pub struct UniqueName(String); impl UniqueName { @@ -1107,7 +1111,7 @@ impl UniqueName { #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", sqlx(transparent))] -#[derive(Serialize, Deserialize, Debug, Hash, Deref, Display, From)] +#[derive(Debug, Hash, PartialEq, Serialize, Deserialize, Deref, Display, From)] pub struct FileName(String); impl FileName { @@ -1119,18 +1123,20 @@ impl FileName { #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", sqlx(transparent))] -#[derive(Serialize, Deserialize, Debug, Copy, Clone, Hash, Deref, Display, From)] +#[derive( + Debug, Copy, Clone, Hash, PartialOrd, PartialEq, Serialize, Deserialize, Deref, Display, From, +)] pub struct PhotoId(RecordId); #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", sqlx(transparent))] -#[derive(Serialize, Deserialize, Debug, Hash, Deref, Display, From)] +#[derive(Debug, Hash, Copy, Clone, PartialEq, Serialize, Deserialize, Deref, Display, From)] pub struct ProductPhotoId(RecordId); #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "db", derive(sqlx::FromRow))] -#[derive(Serialize, Deserialize, Debug, Hash)] +#[derive(Debug, Hash, PartialEq, Serialize, Deserialize)] pub struct Photo { pub id: PhotoId, pub local_path: LocalPath, @@ -1140,9 +1146,9 @@ pub struct Photo { #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "db", derive(sqlx::FromRow))] -#[derive(Serialize, Deserialize, Debug, Hash)] +#[derive(Debug, Hash, PartialEq, Serialize, Deserialize)] pub struct ProductLinkedPhoto { - pub id: PhotoId, + pub photo_id: PhotoId, pub local_path: LocalPath, pub file_name: FileName, pub unique_name: UniqueName, @@ -1151,7 +1157,7 @@ pub struct ProductLinkedPhoto { #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "db", derive(sqlx::FromRow))] -#[derive(Serialize, Deserialize, Debug)] +#[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct ProductPhoto { pub id: ProductPhotoId, pub product_id: ProductId,