use actix::Message; use model::{ProductId, Quantity, QuantityUnit, Stock, StockId}; use crate::{MultiLoad, Result}; #[derive(Debug, Copy, Clone, PartialEq, serde::Serialize, thiserror::Error)] pub enum Error { #[error("Unable to load all stocks")] All, #[error("Unable to create stock")] Create, #[error("Unable to update stock")] Update, #[error("Unable to delete stock")] Delete, #[error("Unable find stock for product")] ProductStock, #[error("Stock does not exists")] NotFound, } #[derive(Message)] #[rtype(result = "Result>")] pub struct AllStocks; crate::db_async_handler!(AllStocks, all_stocks, Vec, inner_all_stocks); async fn all_stocks( _msg: AllStocks, pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> Result> { sqlx::query_as( r#" SELECT id, product_id, quantity, quantity_unit FROM stocks ORDER BY id ASC "#, ) .fetch_all(pool) .await .map_err(|e| { tracing::error!("{e:?}"); crate::Error::Stock(Error::All) }) } #[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| { tracing::error!("{e:?}"); dbg!(e); crate::Error::Stock(Error::NotFound) }) } #[derive(Message)] #[rtype(result = "Result")] pub struct CreateStock { pub product_id: ProductId, pub quantity: Quantity, pub quantity_unit: QuantityUnit, } crate::db_async_handler!(CreateStock, create_stock, Stock, inner_create_stock); async fn create_stock( msg: CreateStock, pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> Result { sqlx::query_as( r#" INSERT INTO stocks (product_id, quantity, quantity_unit) VALUES ($1, $2, $3) RETURNING id, product_id, quantity, quantity_unit "#, ) .bind(msg.product_id) .bind(msg.quantity) .bind(msg.quantity_unit) .fetch_one(pool) .await .map_err(|e| { tracing::error!("{e:?}"); dbg!(e); crate::Error::Stock(Error::Create) }) } #[derive(Message)] #[rtype(result = "Result")] pub struct UpdateStock { pub id: StockId, pub product_id: ProductId, pub quantity: Quantity, pub quantity_unit: QuantityUnit, } crate::db_async_handler!(UpdateStock, update_stock, Stock, inner_update_stock); async fn update_stock( msg: UpdateStock, pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> Result { sqlx::query_as( r#" UPDATE stocks SET product_id = $1, quantity = $2, quantity_unit = $3 WHERE id = $4 RETURNING id, product_id, quantity, quantity_unit "#, ) .bind(msg.product_id) .bind(msg.quantity) .bind(msg.quantity_unit) .bind(msg.id) .fetch_one(pool) .await .map_err(|e| { tracing::error!("{e:?}"); crate::Error::Stock(Error::Update) }) } #[derive(Message)] #[rtype(result = "Result>")] pub struct DeleteStock { pub stock_id: StockId, } crate::db_async_handler!( DeleteStock, delete_stock, Option, inner_delete_stock ); async fn delete_stock( msg: DeleteStock, pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> Result> { sqlx::query_as( r#" DELETE FROM stocks WHERE id = $1 RETURNING id, product_id, quantity, quantity_unit "#, ) .bind(msg.stock_id) .fetch_optional(pool) .await .map_err(|e| { tracing::error!("{e:?}"); crate::Error::Stock(Error::Delete) }) } #[derive(Message)] #[rtype(result = "Result>")] pub struct ProductsStock { pub product_ids: Vec, } crate::db_async_handler!( ProductsStock, product_stock, Vec, inner_product_stock ); async fn product_stock( msg: ProductsStock, pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> Result> { Ok(MultiLoad::new( pool, r#" SELECT id, product_id, quantity, quantity_unit FROM stocks WHERE "#, " product_id =", ) .load( msg.product_ids.len(), msg.product_ids.into_iter().map(|id| *id), |_e| crate::Error::Stock(Error::ProductStock), ) .await?) } #[cfg(test)] mod tests { use config::UpdateConfig; use fake::faker::lorem::en as lorem; use fake::Fake; use model::*; use uuid::Uuid; pub struct NoOpts; impl UpdateConfig for NoOpts {} use crate::*; async fn test_product(pool: &mut sqlx::Transaction<'_, sqlx::Postgres>) -> Product { create_product( CreateProduct { name: ProductName::new(format!("db stocks test product {}", Uuid::new_v4())), short_description: ProductShortDesc::new(lorem::Paragraph(1..2).fake::()), long_description: ProductLongDesc::new(lorem::Paragraph(4..5).fake::()), category: None, price: Price::from_u32(12321), deliver_days_flag: Days(vec![Day::Friday, Day::Sunday]), }, pool, ) .await .unwrap() } async fn test_stock( pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, product_id: Option, quantity: Option, quantity_unit: Option, ) -> Stock { let product_id = match product_id { Some(id) => id, _ => test_product(&mut *pool).await.id, }; let quantity = quantity.unwrap_or_else(|| Quantity::from_u32(345)); let quantity_unit = quantity_unit.unwrap_or_else(|| QuantityUnit::Piece); super::create_stock( CreateStock { product_id, quantity_unit, quantity, }, &mut *pool, ) .await .unwrap() } #[actix::test] async fn create_stock() { testx::db_t!(t); test_stock(&mut t, None, None, None).await; testx::db_rollback!(t); } #[actix::test] async fn products_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 stocks: Vec = super::product_stock( ProductsStock { product_ids: vec![first.product_id, second.product_id], }, &mut t, ) .await .unwrap(); testx::db_rollback!(t); assert_eq!(stocks, vec![first, second]); } #[actix::test] async fn all_stocks() { 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 stocks: Vec = super::all_stocks(AllStocks, &mut t).await.unwrap(); testx::db_rollback!(t); assert_eq!(stocks, vec![first, second]); } #[actix::test] async fn delete_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 deleted: Option = super::delete_stock( DeleteStock { stock_id: second.id, }, &mut t, ) .await .unwrap(); 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); } }