bazzar/crates/database_manager/src/products.rs

446 lines
11 KiB
Rust

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<Vec<model::Product>>")]
pub struct AllProducts;
crate::db_async_handler!(AllProducts, all, Vec<Product>, inner_all);
pub(crate) async fn all<'e, E>(_msg: AllProducts, pool: E) -> Result<Vec<model::Product>>
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<model::Product>")]
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<model::Product>
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<model::Product>")]
pub struct CreateProduct {
pub name: ProductName,
pub short_description: ProductShortDesc,
pub long_description: ProductLongDesc,
pub category: Option<ProductCategory>,
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<model::Product>
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<model::Product>")]
pub struct UpdateProduct {
pub id: ProductId,
pub name: ProductName,
pub short_description: ProductShortDesc,
pub long_description: ProductLongDesc,
pub category: Option<ProductCategory>,
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<Product>
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<Option<model::Product>>")]
pub struct DeleteProduct {
pub product_id: ProductId,
}
crate::db_async_handler!(
DeleteProduct,
delete_product,
Option<model::Product>,
inner_delete_product
);
pub(crate) async fn delete_product<'e, E>(msg: DeleteProduct, pool: E) -> Result<Option<Product>>
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<Vec<model::Product>>")]
pub struct ShoppingCartProducts {
pub shopping_cart_id: model::ShoppingCartId,
}
crate::db_async_handler!(
ShoppingCartProducts,
shopping_cart_products,
Vec<model::Product>,
inner_shopping_cart_products
);
pub(crate) async fn shopping_cart_products<'e, E>(
msg: ShoppingCartProducts,
pool: E,
) -> Result<Vec<Product>>
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<Vec<model::Product>>")]
pub struct FindProducts {
pub product_ids: Vec<ProductId>,
}
crate::db_async_handler!(
FindProducts,
find_products,
Vec<Product>,
inner_find_products
);
pub(crate) async fn find_products(
msg: FindProducts,
pool: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<Vec<Product>> {
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<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() {
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,
}
);
}
}