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, } #[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 "#, ) .fetch_all(pool) .await .map_err(|e| { log::error!("{e:?}"); crate::Error::Stock(Error::All) }) } #[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| { log::error!("{e:?}"); eprintln!("{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 AND 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| { log::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| { log::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() { let config = config::default_load(&mut NoOpts); config .lock() .database_mut() .set_url("postgres://postgres@localhost/bazzar_test"); let db = Database::build(config).await; let pool = db.pool(); let mut t = pool.begin().await.unwrap(); test_stock(&mut t, None, None, None).await; t.rollback().await.unwrap(); } #[actix::test] async fn products_stock() { let config = config::default_load(&mut NoOpts); config .lock() .database_mut() .set_url("postgres://postgres@localhost/bazzar_test"); let db = Database::build(config).await; let pool = db.pool(); let mut t = pool.begin().await.unwrap(); 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(); t.rollback().await.unwrap(); assert_eq!(stocks, vec![first, second]); } }