use actix::Message; #[cfg(feature = "dummy")] use fake::Fake; use model::{ Days, Price, Product, ProductCategory, ProductId, ProductLongDesc, ProductName, ProductShortDesc, }; use super::Result; use crate::MultiLoad; #[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, thiserror::Error)] pub enum Error { #[error("Unable to load all products")] All, #[error("Unable to create product")] Create, #[error("Unable to update product")] Update, #[error("Unable to delete product")] Delete, #[error("Unable to find products for shopping cart")] ShoppingCartProducts, #[error("Product with id {0} can't be found")] Single(ProductId), #[error("Failed to load products for given ids")] FindProducts, } #[derive(Message)] #[rtype(result = "Result>")] pub struct AllProducts; crate::db_async_handler!(AllProducts, all, Vec, inner_all); pub(crate) async fn all<'e, E>(_msg: AllProducts, pool: E) -> Result> where E: sqlx::Executor<'e, Database = sqlx::Postgres>, { sqlx::query_as( r#" SELECT id, name, short_description, long_description, category, price, deliver_days_flag FROM products ORDER BY id "#, ) .fetch_all(pool) .await .map_err(|e| { tracing::error!("{e:?}"); crate::Error::Product(Error::All) }) } #[derive(Message)] #[rtype(result = "Result")] pub struct FindProduct { pub product_id: model::ProductId, } crate::db_async_handler!(FindProduct, find_product, Product, inner_find_product); pub(crate) async fn find_product<'e, E>(msg: FindProduct, pool: E) -> Result where E: sqlx::Executor<'e, Database = sqlx::Postgres>, { sqlx::query_as( r#" SELECT id, name, short_description, long_description, category, price, deliver_days_flag FROM products WHERE id = $1 "#, ) .bind(msg.product_id) .fetch_one(pool) .await .map_err(|e| { tracing::error!("{e:?}"); crate::Error::Product(Error::Single(msg.product_id)) }) } #[derive(Message, Debug)] #[rtype(result = "Result")] pub struct CreateProduct { pub name: ProductName, pub short_description: ProductShortDesc, pub long_description: ProductLongDesc, pub category: Option, pub price: Price, pub deliver_days_flag: Days, } crate::db_async_handler!(CreateProduct, create_product, Product, inner_create_product); pub(crate) async fn create_product<'e, E>(msg: CreateProduct, pool: E) -> Result where E: sqlx::Executor<'e, Database = sqlx::Postgres>, { sqlx::query_as( r#" INSERT INTO products (name, short_description, long_description, category, price, deliver_days_flag) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id, name, short_description, long_description, category, price, deliver_days_flag "#, ) .bind(msg.name) .bind(msg.short_description) .bind(msg.long_description) .bind(msg.category) .bind(msg.price) .bind(msg.deliver_days_flag) .fetch_one(pool) .await .map_err(|e| { tracing::error!("{e:?}"); dbg!(e); crate::Error::Product(Error::Create) }) } #[derive(Message)] #[rtype(result = "Result")] pub struct UpdateProduct { pub id: ProductId, pub name: ProductName, pub short_description: ProductShortDesc, pub long_description: ProductLongDesc, pub category: Option, pub price: Price, pub deliver_days_flag: Days, } crate::db_async_handler!(UpdateProduct, update_product, Product, inner_update_product); pub(crate) async fn update_product<'e, E>(msg: UpdateProduct, pool: E) -> Result where E: sqlx::Executor<'e, Database = sqlx::Postgres>, { sqlx::query_as( r#" UPDATE products SET name = $2, short_description = $3, long_description = $4, category = $5, price = $6, deliver_days_flag = $7 WHERE id = $1 RETURNING id, name, short_description, long_description, category, price, deliver_days_flag "#, ) .bind(msg.id) .bind(msg.name) .bind(msg.short_description) .bind(msg.long_description) .bind(msg.category) .bind(msg.price) .bind(msg.deliver_days_flag) .fetch_one(pool) .await .map_err(|e| { tracing::error!("{e:?}"); dbg!(e); crate::Error::Product(Error::Update) }) } #[derive(Message)] #[rtype(result = "Result>")] pub struct DeleteProduct { pub product_id: ProductId, } crate::db_async_handler!( DeleteProduct, delete_product, Option, inner_delete_product ); pub(crate) async fn delete_product<'e, E>(msg: DeleteProduct, pool: E) -> Result> where E: sqlx::Executor<'e, Database = sqlx::Postgres>, { sqlx::query_as( r#" DELETE FROM products WHERE id = $1 RETURNING id, name, short_description, long_description, category, price, deliver_days_flag "#, ) .bind(msg.product_id) .fetch_optional(pool) .await .map_err(|e| { tracing::error!("{e:?}"); crate::Error::Product(Error::Delete) }) } #[derive(Message)] #[rtype(result = "Result>")] pub struct ShoppingCartProducts { pub shopping_cart_id: model::ShoppingCartId, } crate::db_async_handler!( ShoppingCartProducts, shopping_cart_products, Vec, inner_shopping_cart_products ); pub(crate) async fn shopping_cart_products<'e, E>( msg: ShoppingCartProducts, pool: E, ) -> Result> where E: sqlx::Executor<'e, Database = sqlx::Postgres>, { sqlx::query_as( r#" SELECT products.id, products.name, products.short_description, products.long_description, products.category, products.price, products.deliver_days_flag FROM products INNER JOIN shopping_cart_items ON shopping_cart_items.product_id = products.id WHERE shopping_cart_id = $1 ORDER BY products.id "#, ) .bind(msg.shopping_cart_id) .fetch_all(pool) .await .map_err(|e| { tracing::error!("{e:?}"); crate::Error::Product(Error::ShoppingCartProducts) }) } #[derive(Message)] #[rtype(result = "Result>")] pub struct FindProducts { pub product_ids: Vec, } crate::db_async_handler!( FindProducts, find_products, Vec, inner_find_products ); pub(crate) async fn find_products( msg: FindProducts, pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> Result> { MultiLoad::new( pool, r#" SELECT id, name, short_description, long_description, category, price, deliver_days_flag FROM products WHERE "#, "products.id =", ) .load( msg.product_ids.len(), msg.product_ids.into_iter().map(|id| *id), |e| { tracing::error!("{e:?}"); crate::Error::Product(Error::FindProducts) }, ) .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() { testx::db_t_ref!(t); test_product(&mut t, None, None, None, None, None, None).await; testx::db_rollback!(t); } #[actix::test] async fn all() { testx::db_t_ref!(t); let p1 = test_product(&mut t, None, None, None, None, None, None).await; let p2 = test_product(&mut t, None, None, None, None, None, None).await; let p3 = test_product(&mut t, None, None, None, None, None, None).await; let products = super::all(AllProducts, &mut t).await.unwrap(); testx::db_rollback!(t); assert_eq!(products, vec![p1, p2, p3]); } #[actix::test] async fn find() { testx::db_t_ref!(t); let p1 = test_product(&mut t, None, None, None, None, None, None).await; let p2 = test_product(&mut t, None, None, None, None, None, None).await; let p3 = test_product(&mut t, None, None, None, None, None, None).await; let product = find_product(FindProduct { product_id: p2.id }, &mut t) .await .unwrap(); testx::db_rollback!(t); assert_ne!(product, p1); assert_eq!(product, p2); assert_ne!(product, p3); } #[actix::test] async fn update() { testx::db_t_ref!(t); let original = test_product(&mut t, None, None, None, None, None, None).await; let updated = update_product( UpdateProduct { id: original.id, name: ProductName::new("a9s0dja0sjd0jas09dj"), short_description: ProductShortDesc::new("ajs9d8ua9sdu9ahsd98has"), long_description: ProductLongDesc::new("hja89sdy9yha9sdy98ayusd9hya9sy8dh"), category: None, price: Price::from_u32(823794), deliver_days_flag: Day::Tuesday | Day::Saturday, }, &mut t, ) .await .unwrap(); let reloaded = find_product( FindProduct { product_id: original.id, }, &mut t, ) .await .unwrap(); testx::db_rollback!(t); assert_ne!(updated, original); assert_eq!(updated, reloaded); assert_eq!( updated, Product { id: original.id, name: ProductName::new("a9s0dja0sjd0jas09dj"), short_description: ProductShortDesc::new("ajs9d8ua9sdu9ahsd98has"), long_description: ProductLongDesc::new("hja89sdy9yha9sdy98ayusd9hya9sy8dh"), category: None, price: Price::from_u32(823794), deliver_days_flag: Day::Tuesday | Day::Saturday, } ); } }