446 lines
11 KiB
Rust
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,
|
|
}
|
|
);
|
|
}
|
|
}
|