Additional tests

This commit is contained in:
Adrian Woźniak 2022-06-06 15:09:13 +02:00
parent a34f32dc15
commit 7c272d310f
No known key found for this signature in database
GPG Key ID: 0012845A89C7352B
12 changed files with 1050 additions and 106 deletions

View File

@ -138,7 +138,7 @@ WHERE account_id = $1
.await .await
{ {
log::error!("{e}"); 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 .await
.map_err(|e| { .map_err(|e| {
log::error!("{e}"); log::error!("{e}");
eprintln!("{e}"); dbg!(e);
Error::CreateAccountAddress.into() Error::CreateAccountAddress.into()
}) })
} }

View File

@ -213,6 +213,7 @@ pub struct MultiLoad<'transaction, 'transaction2, 'header, 'condition, T> {
pool: &'transaction mut sqlx::Transaction<'transaction2, sqlx::Postgres>, pool: &'transaction mut sqlx::Transaction<'transaction2, sqlx::Postgres>,
header: &'header str, header: &'header str,
condition: &'condition str, condition: &'condition str,
sort: Option<String>,
__phantom: std::marker::PhantomData<T>, __phantom: std::marker::PhantomData<T>,
} }
@ -230,10 +231,16 @@ where
pool, pool,
header, header,
condition, condition,
sort: None,
__phantom: Default::default(), __phantom: Default::default(),
} }
} }
pub fn with_sorting<S: Into<String>>(mut self, order: S) -> Self {
self.sort = Some(order.into());
self
}
pub async fn load<'query, Error, Ids>( pub async fn load<'query, Error, Ids>(
&mut self, &mut self,
len: usize, len: usize,
@ -257,13 +264,18 @@ where
}, },
) { ) {
let query: String = self.header.into(); 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 { if idx != 0 {
q.push_str(" OR"); q.push_str(" OR");
} }
q.push_str(&format!(" {} ${}", self.condition, idx + 1)); q.push_str(&format!(" {} ${}", self.condition, idx + 1));
q 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( let q = sqlx::query_as_with(
query.as_str(), query.as_str(),
ids.into_iter() ids.into_iter()

View File

@ -38,7 +38,7 @@ ORDER BY id DESC
.await .await
.map_err(|e| { .map_err(|e| {
log::error!("{e:?}"); log::error!("{e:?}");
super::Error::from(Error::All) Error::All.into()
}) })
} }
@ -102,7 +102,7 @@ WHERE id = $1
.await .await
.map_err(|e| { .map_err(|e| {
log::error!("{e:?}"); log::error!("{e:?}");
super::Error::OrderItem(Error::NotExists) Error::NotExists.into()
}) })
} }
@ -128,6 +128,6 @@ ORDER BY id DESC
.await .await
.map_err(|e| { .map_err(|e| {
log::error!("{e:?}"); log::error!("{e:?}");
super::Error::OrderItem(Error::OrderItems) Error::OrderItems.into()
}) })
} }

View File

@ -6,6 +6,8 @@ pub enum Error {
Create, Create,
#[error("Failed to fetch all photo")] #[error("Failed to fetch all photo")]
All, All,
#[error("Failed to fetch photos for products")]
PhotosForProducts,
} }
#[derive(actix::Message)] #[derive(actix::Message)]
@ -22,6 +24,7 @@ where
r#" r#"
SELECT id, local_path, file_name, unique_name SELECT id, local_path, file_name, unique_name
FROM photos FROM photos
ORDER BY id ASC
"#, "#,
) )
.fetch_all(pool) .fetch_all(pool)
@ -91,20 +94,168 @@ pub(crate) async fn photos_for_products(
let res: Vec<model::ProductLinkedPhoto> = MultiLoad::new( let res: Vec<model::ProductLinkedPhoto> = MultiLoad::new(
pool, pool,
r#" r#"
SELECT photos.id, photos.local_path, photos.file_name, SELECT photos.id AS photo_id,
product_photos.product_id, photos.unique_name FROM photos photos.local_path AS local_path,
INNER JOIN product_photos photos.file_name AS file_name,
ON photos.id = product_photos.photo_id product_photos.product_id AS product_id,
WHERE photos.unique_name AS unique_name
FROM photos
INNER JOIN product_photos
ON photos.id = product_photos.photo_id
WHERE
"#, "#,
" product_photos.product_id =", " product_photos.product_id =",
) )
.with_sorting("photos.id ASC")
.load( .load(
msg.product_ids.len(), msg.product_ids.len(),
msg.product_ids.into_iter().map(|id| *id), 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?; .await?;
log::debug!("product linked photos {:?}", res); log::debug!("product linked photos {:?}", res);
Ok(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<String>,
file_name: Option<String>,
unique_name: Option<String>,
) -> 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::<Vec<PhotoId>>();
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
]
);
}
}

View File

@ -6,37 +6,37 @@ pub enum Error {
Create, Create,
#[error("Failed to load all product photos")] #[error("Failed to load all product photos")]
All, All,
#[error("Failed to delete product photo")]
Delete,
} }
#[derive(actix::Message)] #[derive(actix::Message)]
#[rtype(result = "Result<Vec<model::ProductPhoto>>")] #[rtype(result = "Result<Vec<model::ProductPhoto>>")]
pub struct AllProductPhotos; pub struct AllProductPhotos;
crate::db_async_handler!( db_async_handler!(
AllProductPhotos, AllProductPhotos,
all_product_photos, all_product_photos,
Vec<model::ProductPhoto>, Vec<model::ProductPhoto>,
inner_all_product_photos inner_all_product_photos
); );
pub(crate) async fn all_product_photos<'e, E>( pub(crate) async fn all_product_photos(
_msg: AllProductPhotos, _msg: AllProductPhotos,
pool: E, pool: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<Vec<model::ProductPhoto>> ) -> Result<Vec<model::ProductPhoto>> {
where
E: sqlx::Executor<'e, Database = sqlx::Postgres>,
{
sqlx::query_as( sqlx::query_as(
r#" r#"
SELECT id, product_id, photo_id SELECT id, product_id, photo_id
FROM product_photos FROM product_photos
ORDER BY id ASC
"#, "#,
) )
.fetch_all(pool) .fetch_all(pool)
.await .await
.map_err(|e| { .map_err(|e| {
log::error!("{e:?}"); log::error!("{e:?}");
crate::Error::ProductPhoto(Error::All) Error::All.into()
}) })
} }
@ -54,13 +54,10 @@ db_async_handler!(
inner_create_product_photo inner_create_product_photo
); );
pub(crate) async fn create_product_photo<'e, E>( pub(crate) async fn create_product_photo(
msg: CreateProductPhoto, msg: CreateProductPhoto,
pool: E, pool: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<model::ProductPhoto> ) -> Result<model::ProductPhoto> {
where
E: sqlx::Executor<'e, Database = sqlx::Postgres>,
{
sqlx::query_as( sqlx::query_as(
r#" r#"
INSERT INTO product_photos(product_id, photo_id) INSERT INTO product_photos(product_id, photo_id)
@ -74,6 +71,131 @@ RETURNING id, product_id, photo_id
.await .await
.map_err(|e| { .map_err(|e| {
log::error!("{:?}", e); log::error!("{:?}", e);
crate::Error::ProductPhoto(Error::Create) Error::Create.into()
}) })
} }
#[derive(actix::Message)]
#[rtype(result = "Result<Option<model::ProductPhoto>>")]
pub struct DeleteProductPhoto {
pub id: model::ProductPhotoId,
}
db_async_handler!(
DeleteProductPhoto,
delete_product_photo,
Option<model::ProductPhoto>,
inner_delete_product_photo
);
pub(crate) async fn delete_product_photo(
msg: DeleteProductPhoto,
pool: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<Option<model::ProductPhoto>> {
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);
}
}

View File

@ -133,7 +133,7 @@ RETURNING id,
.await .await
.map_err(|e| { .map_err(|e| {
log::error!("{e:?}"); log::error!("{e:?}");
eprintln!("{e:?}"); dbg!(e);
crate::Error::Product(Error::Create) crate::Error::Product(Error::Create)
}) })
} }
@ -152,7 +152,7 @@ pub struct UpdateProduct {
crate::db_async_handler!(UpdateProduct, update_product, Product, inner_update_product); crate::db_async_handler!(UpdateProduct, update_product, Product, inner_update_product);
pub(crate) async fn update_product<'e, E>(msg: UpdateProduct, pool: E) -> Result<model::Product> pub(crate) async fn update_product<'e, E>(msg: UpdateProduct, pool: E) -> Result<Product>
where where
E: sqlx::Executor<'e, Database = sqlx::Postgres>, E: sqlx::Executor<'e, Database = sqlx::Postgres>,
{ {
@ -315,3 +315,86 @@ WHERE
) )
.await .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<String>,
short_description: Option<String>,
long_description: Option<String>,
category: Option<ProductCategory>,
price: Option<u32>,
deliver_days_flag: Option<Days>,
) -> 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);
}
}

View File

@ -1,5 +1,4 @@
use model::*; use model::*;
use sqlx::PgPool;
use super::Result; use super::Result;
use crate::db_async_handler; use crate::db_async_handler;
@ -34,23 +33,32 @@ pub struct AllShoppingCartItems;
db_async_handler!( db_async_handler!(
AllShoppingCartItems, AllShoppingCartItems,
all_shopping_cart_items, all_shopping_cart_items,
Vec<ShoppingCartItem> Vec<ShoppingCartItem>,
inner_all_shopping_cart_items
); );
pub(crate) async fn all_shopping_cart_items( pub(crate) async fn all_shopping_cart_items(
_msg: AllShoppingCartItems, _msg: AllShoppingCartItems,
pool: PgPool, t: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<Vec<ShoppingCartItem>> { ) -> Result<Vec<ShoppingCartItem>> {
sqlx::query_as( sqlx::query_as(
r#" 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 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 .await
.map_err(|e| { .map_err(|e| {
log::error!("{e:?}"); log::error!("{e:?}");
dbg!(e);
super::Error::ShoppingCartItem(Error::All) super::Error::ShoppingCartItem(Error::All)
}) })
} }
@ -65,12 +73,13 @@ pub struct AccountShoppingCartItems {
db_async_handler!( db_async_handler!(
AccountShoppingCartItems, AccountShoppingCartItems,
account_shopping_cart_items, account_shopping_cart_items,
Vec<ShoppingCartItem> Vec<ShoppingCartItem>,
inner_account_shopping_cart_items
); );
pub(crate) async fn account_shopping_cart_items( pub(crate) async fn account_shopping_cart_items(
msg: AccountShoppingCartItems, msg: AccountShoppingCartItems,
pool: PgPool, t: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<Vec<ShoppingCartItem>> { ) -> Result<Vec<ShoppingCartItem>> {
match msg.shopping_cart_id { match msg.shopping_cart_id {
Some(shopping_cart_id) => sqlx::query_as( Some(shopping_cart_id) => sqlx::query_as(
@ -103,7 +112,7 @@ WHERE shopping_carts.buyer_id = $1
) )
.bind(msg.account_id), .bind(msg.account_id),
} }
.fetch_all(&pool) .fetch_all(t)
.await .await
.map_err(|e| { .map_err(|e| {
log::error!("{e:?}"); log::error!("{e:?}");
@ -123,12 +132,13 @@ pub struct CreateShoppingCartItem {
db_async_handler!( db_async_handler!(
CreateShoppingCartItem, CreateShoppingCartItem,
create_shopping_cart_item, create_shopping_cart_item,
ShoppingCartItem ShoppingCartItem,
inner_create_shopping_cart_item
); );
pub(crate) async fn create_shopping_cart_item( pub(crate) async fn create_shopping_cart_item(
msg: CreateShoppingCartItem, msg: CreateShoppingCartItem,
db: PgPool, t: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<ShoppingCartItem> { ) -> Result<ShoppingCartItem> {
sqlx::query_as( sqlx::query_as(
r#" r#"
@ -141,10 +151,11 @@ RETURNING id, product_id, shopping_cart_id, quantity, quantity_unit
.bind(msg.shopping_cart_id) .bind(msg.shopping_cart_id)
.bind(msg.quantity) .bind(msg.quantity)
.bind(msg.quantity_unit) .bind(msg.quantity_unit)
.fetch_one(&db) .fetch_one(t)
.await .await
.map_err(|e| { .map_err(|e| {
log::error!("{e:?}"); log::error!("{e:?}");
dbg!(&e);
super::Error::ShoppingCartItem(Error::CantCreate) super::Error::ShoppingCartItem(Error::CantCreate)
}) })
} }
@ -162,12 +173,13 @@ pub struct UpdateShoppingCartItem {
db_async_handler!( db_async_handler!(
UpdateShoppingCartItem, UpdateShoppingCartItem,
update_shopping_cart_item, update_shopping_cart_item,
ShoppingCartItem ShoppingCartItem,
inner_update_shopping_cart_item
); );
pub(crate) async fn update_shopping_cart_item( pub(crate) async fn update_shopping_cart_item(
msg: UpdateShoppingCartItem, msg: UpdateShoppingCartItem,
db: PgPool, t: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<ShoppingCartItem> { ) -> Result<ShoppingCartItem> {
sqlx::query_as( sqlx::query_as(
r#" r#"
@ -182,7 +194,7 @@ RETURNING id, product_id, shopping_cart_id, quantity, quantity_unit
.bind(msg.shopping_cart_id) .bind(msg.shopping_cart_id)
.bind(msg.quantity) .bind(msg.quantity)
.bind(msg.quantity_unit) .bind(msg.quantity_unit)
.fetch_one(&db) .fetch_one(t)
.await .await
.map_err(|e| { .map_err(|e| {
log::error!("{e:?}"); log::error!("{e:?}");
@ -199,12 +211,13 @@ pub struct DeleteShoppingCartItem {
db_async_handler!( db_async_handler!(
DeleteShoppingCartItem, DeleteShoppingCartItem,
delete_shopping_cart_item, delete_shopping_cart_item,
Option<ShoppingCartItem> Option<ShoppingCartItem>,
inner_delete_shopping_cart_item
); );
pub(crate) async fn delete_shopping_cart_item( pub(crate) async fn delete_shopping_cart_item(
msg: DeleteShoppingCartItem, msg: DeleteShoppingCartItem,
db: PgPool, t: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<Option<ShoppingCartItem>> { ) -> Result<Option<ShoppingCartItem>> {
sqlx::query_as( sqlx::query_as(
r#" r#"
@ -214,7 +227,7 @@ RETURNING id, product_id, shopping_cart_id, quantity, quantity_unit
"#, "#,
) )
.bind(msg.id) .bind(msg.id)
.fetch_optional(&db) .fetch_optional(t)
.await .await
.map_err(|e| { .map_err(|e| {
log::error!("{e:?}"); log::error!("{e:?}");
@ -231,12 +244,13 @@ pub struct FindShoppingCartItem {
db_async_handler!( db_async_handler!(
FindShoppingCartItem, FindShoppingCartItem,
find_shopping_cart_item, find_shopping_cart_item,
ShoppingCartItem ShoppingCartItem,
inner_find_shopping_cart_item
); );
pub(crate) async fn find_shopping_cart_item( pub(crate) async fn find_shopping_cart_item(
msg: FindShoppingCartItem, msg: FindShoppingCartItem,
db: PgPool, t: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<ShoppingCartItem> { ) -> Result<ShoppingCartItem> {
sqlx::query_as( sqlx::query_as(
r#" r#"
@ -246,7 +260,7 @@ WHERE id = $1
"#, "#,
) )
.bind(msg.id) .bind(msg.id)
.fetch_one(&db) .fetch_one(t)
.await .await
.map_err(|e| { .map_err(|e| {
log::error!("{e:?}"); log::error!("{e:?}");
@ -257,18 +271,19 @@ WHERE id = $1
#[derive(actix::Message)] #[derive(actix::Message)]
#[rtype(result = "Result<Option<ShoppingCartItem>>")] #[rtype(result = "Result<Option<ShoppingCartItem>>")]
pub struct ActiveCartItemByProduct { pub struct ActiveCartItemByProduct {
pub product_id: model::ProductId, pub product_id: ProductId,
} }
db_async_handler!( db_async_handler!(
ActiveCartItemByProduct, ActiveCartItemByProduct,
active_cart_item_by_product, active_cart_item_by_product,
Option<ShoppingCartItem> Option<ShoppingCartItem>,
inner_active_cart_item_by_product
); );
pub(crate) async fn active_cart_item_by_product( pub(crate) async fn active_cart_item_by_product(
msg: ActiveCartItemByProduct, msg: ActiveCartItemByProduct,
db: PgPool, t: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<Option<ShoppingCartItem>> { ) -> Result<Option<ShoppingCartItem>> {
sqlx::query_as( sqlx::query_as(
r#" r#"
@ -278,13 +293,16 @@ SELECT shopping_cart_items.id,
shopping_cart_items.quantity, shopping_cart_items.quantity,
shopping_cart_items.quantity_unit shopping_cart_items.quantity_unit
FROM shopping_cart_items FROM shopping_cart_items
INNER JOIN shopping_carts ON shopping_cart_items.shopping_cart_id = shopping_carts.id INNER JOIN shopping_carts
WHERE product_id = $1 AND shopping_carts.state = $2 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(msg.product_id)
.bind(model::ShoppingCartState::Active) .bind(model::ShoppingCartState::Active)
.fetch_optional(&db) .fetch_optional(t)
.await .await
.map_err(|e| { .map_err(|e| {
log::error!("{e:?}"); log::error!("{e:?}");
@ -305,20 +323,25 @@ db_async_handler!(
inner_cart_items inner_cart_items
); );
pub(crate) async fn cart_items<'e, E>(msg: CartItems, pool: E) -> Result<Vec<ShoppingCartItem>> pub(crate) async fn cart_items(
where msg: CartItems,
E: sqlx::Executor<'e, Database = sqlx::Postgres>, t: &mut sqlx::Transaction<'_, sqlx::Postgres>,
{ ) -> Result<Vec<ShoppingCartItem>> {
let shopping_cart_id = msg.shopping_cart_id; let shopping_cart_id = msg.shopping_cart_id;
sqlx::query_as( sqlx::query_as(
r#" 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 FROM shopping_cart_items
WHERE shopping_cart_id = $1 WHERE shopping_cart_id = $1
ORDER BY shopping_cart_items.id ASC
"#, "#,
) )
.bind(msg.shopping_cart_id) .bind(msg.shopping_cart_id)
.fetch_all(pool) .fetch_all(t)
.await .await
.map_err(|e| { .map_err(|e| {
log::error!("{e:?}"); log::error!("{e:?}");
@ -334,11 +357,16 @@ pub struct RemoveCartItem {
pub product_id: Option<ProductId>, pub product_id: Option<ProductId>,
} }
db_async_handler!(RemoveCartItem, remove_cart_item, Option<ShoppingCartItem>); db_async_handler!(
RemoveCartItem,
remove_cart_item,
Option<ShoppingCartItem>,
inner_remove_cart_item
);
pub(crate) async fn remove_cart_item( pub(crate) async fn remove_cart_item(
msg: RemoveCartItem, msg: RemoveCartItem,
pool: PgPool, t: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<Option<ShoppingCartItem>> { ) -> Result<Option<ShoppingCartItem>> {
match (msg.shopping_cart_item_id, msg.product_id) { match (msg.shopping_cart_item_id, msg.product_id) {
(Some(shopping_cart_item_id), None) => sqlx::query_as( (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), .bind(product_id),
_ => return Err(crate::Error::ShoppingCartItem(Error::NoIdentity)), _ => return Err(crate::Error::ShoppingCartItem(Error::NoIdentity)),
} }
.fetch_optional(&pool) .fetch_optional(t)
.await .await
.map_err(|e| { .map_err(|e| {
log::error!("{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<String>,
email: Option<String>,
hash: Option<String>,
) -> 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<AccountId>,
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<ShoppingCartId>,
product_id: Option<ProductId>,
) -> 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,
}
);
}
}

View File

@ -94,11 +94,16 @@ pub struct CreateShoppingCart {
pub payment_method: PaymentMethod, 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( pub(crate) async fn create_shopping_cart(
msg: CreateShoppingCart, msg: CreateShoppingCart,
db: PgPool, pool: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<ShoppingCart> { ) -> Result<ShoppingCart> {
sqlx::query_as( sqlx::query_as(
r#" r#"
@ -109,10 +114,11 @@ RETURNING id, buyer_id, payment_method, state, checkout_notes
) )
.bind(msg.buyer_id) .bind(msg.buyer_id)
.bind(msg.payment_method) .bind(msg.payment_method)
.fetch_one(&db) .fetch_one(pool)
.await .await
.map_err(|e| { .map_err(|e| {
log::error!("{e:?}"); log::error!("{e:?}");
dbg!(e);
super::Error::ShoppingCart(Error::CantCreate) super::Error::ShoppingCart(Error::CantCreate)
}) })
} }
@ -127,11 +133,16 @@ pub struct UpdateShoppingCart {
pub checkout_notes: Option<String>, pub checkout_notes: Option<String>,
} }
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( pub(crate) async fn update_shopping_cart(
msg: UpdateShoppingCart, msg: UpdateShoppingCart,
db: PgPool, pool: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<ShoppingCart> { ) -> Result<ShoppingCart> {
sqlx::query_as( sqlx::query_as(
r#" r#"
@ -146,7 +157,7 @@ RETURNING id, buyer_id, payment_method, state, checkout_notes
.bind(msg.payment_method) .bind(msg.payment_method)
.bind(msg.state) .bind(msg.state)
.bind(msg.checkout_notes) .bind(msg.checkout_notes)
.fetch_one(&db) .fetch_one(pool)
.await .await
.map_err(|e| { .map_err(|e| {
log::error!("{e:?}"); log::error!("{e:?}");
@ -198,9 +209,17 @@ pub struct FindShoppingCart {
pub id: ShoppingCartId, 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<ShoppingCart> { pub(crate) async fn find_shopping_cart(
msg: FindShoppingCart,
pool: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<ShoppingCart> {
sqlx::query_as( sqlx::query_as(
r#" r#"
SELECT id, buyer_id, payment_method, state, checkout_notes SELECT id, buyer_id, payment_method, state, checkout_notes
@ -209,7 +228,7 @@ WHERE id = $1
"#, "#,
) )
.bind(msg.id) .bind(msg.id)
.fetch_one(&db) .fetch_one(pool)
.await .await
.map_err(|e| { .map_err(|e| {
log::error!("{e:?}"); log::error!("{e:?}");
@ -267,3 +286,177 @@ WHERE buyer_id = $1 AND state = 'active'
super::Error::ShoppingCart(Error::NotExists) 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<String>,
email: Option<String>,
hash: Option<String>,
) -> 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<AccountId>,
) -> 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);
}
}

View File

@ -15,6 +15,8 @@ pub enum Error {
Delete, Delete,
#[error("Unable find stock for product")] #[error("Unable find stock for product")]
ProductStock, ProductStock,
#[error("Stock does not exists")]
NotFound,
} }
#[derive(Message)] #[derive(Message)]
@ -42,6 +44,35 @@ ORDER BY id ASC
}) })
} }
#[derive(Message)]
#[rtype(result = "Result<model::Stock>")]
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<Stock> {
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)] #[derive(Message)]
#[rtype(result = "Result<model::Stock>")] #[rtype(result = "Result<model::Stock>")]
pub struct CreateStock { pub struct CreateStock {
@ -70,7 +101,7 @@ RETURNING id, product_id, quantity, quantity_unit
.await .await
.map_err(|e| { .map_err(|e| {
log::error!("{e:?}"); log::error!("{e:?}");
eprintln!("{e:?}"); dbg!(e);
crate::Error::Stock(Error::Create) crate::Error::Stock(Error::Create)
}) })
} }
@ -93,8 +124,8 @@ async fn update_stock(
sqlx::query_as( sqlx::query_as(
r#" r#"
UPDATE stocks UPDATE stocks
SET product_id = $1 AND SET product_id = $1,
quantity = $2 quantity = $2,
quantity_unit = $3 quantity_unit = $3
WHERE id = $4 WHERE id = $4
RETURNING id, product_id, quantity, quantity_unit RETURNING id, product_id, quantity, quantity_unit
@ -165,9 +196,9 @@ async fn product_stock(
Ok(MultiLoad::new( Ok(MultiLoad::new(
pool, pool,
r#" r#"
SELECT id, product_id, quantity, quantity_unit SELECT id, product_id, quantity, quantity_unit
FROM stocks FROM stocks
WHERE WHERE
"#, "#,
" product_id =", " product_id =",
) )
@ -291,10 +322,49 @@ mod tests {
) )
.await .await
.unwrap(); .unwrap();
// let reloaded = super::Stock let reloaded = super::find_stock(FindStock { id: second.id }, &mut t).await;
testx::db_rollback!(t); testx::db_rollback!(t);
assert_eq!(deleted, Some(second)); assert_eq!(deleted, Some(second));
assert_ne!(deleted, Some(first)); 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);
} }
} }

View File

@ -0,0 +1,2 @@
ALTER TABLE shopping_carts
DROP CONSTRAINT single_active_cart;

View File

@ -362,7 +362,7 @@ impl<'path>
.drain_filter(|photo| photo.product_id == id) .drain_filter(|photo| photo.product_id == id)
.map( .map(
|ProductLinkedPhoto { |ProductLinkedPhoto {
id, photo_id: id,
local_path: _, local_path: _,
file_name, file_name,
product_id: _, product_id: _,

View File

@ -224,7 +224,7 @@ impl Default for PaymentMethod {
#[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(rename_all = "snake_case"))] #[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")] #[serde(rename_all = "snake_case")]
pub enum ShoppingCartState { pub enum ShoppingCartState {
Active, Active,
@ -279,7 +279,9 @@ impl Default for Audience {
#[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))] #[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)] #[serde(transparent)]
pub struct Price(NonNegative); 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", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))] #[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)] #[serde(transparent)]
pub struct NonNegative(i32); pub struct NonNegative(i32);
@ -533,7 +535,9 @@ impl<'de> serde::Deserialize<'de> for NonNegative {
} }
#[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[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")] #[serde(rename_all = "lowercase")]
pub enum Day { pub enum Day {
Monday = 1 << 0, Monday = 1 << 0,
@ -595,7 +599,7 @@ impl TryFrom<i32> for Day {
} }
#[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[derive(Serialize, Deserialize, Hash, Debug)] #[derive(Hash, Debug, PartialOrd, PartialEq, Serialize, Deserialize)]
#[serde(transparent)] #[serde(transparent)]
pub struct Days(pub Vec<Day>); pub struct Days(pub Vec<Day>);
@ -773,7 +777,7 @@ impl From<FullAccount> for Account {
#[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))] #[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)] #[serde(transparent)]
pub struct ProductId(RecordId); pub struct ProductId(RecordId);
@ -785,7 +789,7 @@ impl Display for ProductId {
#[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))] #[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)] #[serde(transparent)]
pub struct ProductName(String); pub struct ProductName(String);
@ -801,7 +805,7 @@ impl ProductName {
#[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))] #[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)] #[serde(transparent)]
pub struct ProductShortDesc(String); pub struct ProductShortDesc(String);
@ -817,7 +821,7 @@ impl ProductShortDesc {
#[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))] #[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)] #[serde(transparent)]
pub struct ProductLongDesc(String); pub struct ProductLongDesc(String);
@ -836,7 +840,7 @@ impl ProductLongDesc {
#[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))] #[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)] #[serde(transparent)]
pub struct ProductCategory(String); pub struct ProductCategory(String);
@ -848,7 +852,7 @@ impl ProductCategory {
#[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::FromRow))] #[cfg_attr(feature = "db", derive(sqlx::FromRow))]
#[derive(Serialize, Deserialize, Debug, Hash)] #[derive(Debug, Hash, PartialEq, Serialize, Deserialize)]
pub struct Product { pub struct Product {
pub id: ProductId, pub id: ProductId,
pub name: ProductName, pub name: ProductName,
@ -973,7 +977,7 @@ pub struct ShoppingCartId(pub RecordId);
#[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::FromRow))] #[cfg_attr(feature = "db", derive(sqlx::FromRow))]
#[derive(Serialize, Deserialize, Debug)] #[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct ShoppingCart { pub struct ShoppingCart {
pub id: ShoppingCartId, pub id: ShoppingCartId,
pub buyer_id: AccountId, pub buyer_id: AccountId,
@ -985,12 +989,12 @@ pub struct ShoppingCart {
#[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))] #[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)] #[serde(transparent)]
pub struct ShoppingCartItemId(RecordId); pub struct ShoppingCartItemId(RecordId);
#[cfg_attr(feature = "db", derive(sqlx::FromRow))] #[cfg_attr(feature = "db", derive(sqlx::FromRow))]
#[derive(Serialize, Deserialize, Debug)] #[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct ShoppingCartItem { pub struct ShoppingCartItem {
pub id: ShoppingCartItemId, pub id: ShoppingCartItemId,
pub product_id: ProductId, pub product_id: ProductId,
@ -1047,7 +1051,7 @@ impl Token {
#[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))] #[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); pub struct AccessTokenString(String);
impl AccessTokenString { impl AccessTokenString {
@ -1065,7 +1069,7 @@ impl From<RefreshTokenString> for AccessTokenString {
#[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))] #[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); pub struct RefreshTokenString(String);
impl From<AccessTokenString> for RefreshTokenString { impl From<AccessTokenString> for RefreshTokenString {
@ -1083,7 +1087,7 @@ impl RefreshTokenString {
#[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))] #[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); pub struct LocalPath(String);
impl LocalPath { impl LocalPath {
@ -1095,7 +1099,7 @@ impl LocalPath {
#[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))] #[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); pub struct UniqueName(String);
impl UniqueName { impl UniqueName {
@ -1107,7 +1111,7 @@ impl UniqueName {
#[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))] #[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); pub struct FileName(String);
impl FileName { impl FileName {
@ -1119,18 +1123,20 @@ impl FileName {
#[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))] #[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); pub struct PhotoId(RecordId);
#[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))] #[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); pub struct ProductPhotoId(RecordId);
#[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::FromRow))] #[cfg_attr(feature = "db", derive(sqlx::FromRow))]
#[derive(Serialize, Deserialize, Debug, Hash)] #[derive(Debug, Hash, PartialEq, Serialize, Deserialize)]
pub struct Photo { pub struct Photo {
pub id: PhotoId, pub id: PhotoId,
pub local_path: LocalPath, pub local_path: LocalPath,
@ -1140,9 +1146,9 @@ pub struct Photo {
#[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::FromRow))] #[cfg_attr(feature = "db", derive(sqlx::FromRow))]
#[derive(Serialize, Deserialize, Debug, Hash)] #[derive(Debug, Hash, PartialEq, Serialize, Deserialize)]
pub struct ProductLinkedPhoto { pub struct ProductLinkedPhoto {
pub id: PhotoId, pub photo_id: PhotoId,
pub local_path: LocalPath, pub local_path: LocalPath,
pub file_name: FileName, pub file_name: FileName,
pub unique_name: UniqueName, pub unique_name: UniqueName,
@ -1151,7 +1157,7 @@ pub struct ProductLinkedPhoto {
#[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::FromRow))] #[cfg_attr(feature = "db", derive(sqlx::FromRow))]
#[derive(Serialize, Deserialize, Debug)] #[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct ProductPhoto { pub struct ProductPhoto {
pub id: ProductPhotoId, pub id: ProductPhotoId,
pub product_id: ProductId, pub product_id: ProductId,