use channels::stocks::{detailed_product, detailed_products, Error}; use channels::AsyncClient; use config::SharedAppConfig; use db_utils::PgT; use model::{Limit, Offset}; use crate::db::{ AllCategories, Database, PhotosForProductVariants, ProductVariantsStock, ProductsVariants, }; use crate::{begin_t, dbm_run}; pub async fn detailed_product( input: detailed_product::Input, db: Database, _mqtt: AsyncClient, _config: SharedAppConfig, ) -> detailed_product::Output { let mut t = begin_t!(db, Error::InternalServerError); let res = inner_detailed_product(input, &mut t, Some(_mqtt), Some(_config)).await; t.commit().await.ok(); res } async fn inner_detailed_product( input: detailed_product::Input, t: &mut PgT<'_>, _mqtt: Option, config: Option, ) -> detailed_product::Output { let dbm = crate::db::FindProduct { product_id: input.product_id, }; let product = dbm_run!(dbm, &mut *t, Error::ProductNotFound(input.product_id)); let dbm = ProductsVariants { product_ids: vec![input.product_id], limit: Some(10.into()), offset: Some(0.into()), }; let variants = dbm_run!(dbm, &mut *t, Error::ProductVariants(vec![input.product_id])); let dbm = ProductVariantsStock { product_variant_ids: variants.iter().map(|p| p.id).collect(), }; let stocks = dbm_run!( dbm, t, Error::VariantStocks(variants.into_iter().map(|p| p.id).collect()) ); let dbm = PhotosForProductVariants { product_variant_ids: variants.iter().map(|p| p.id).collect(), }; let photos = dbm_run!( dbm, t, Error::VariantPhotos(variants.into_iter().map(|p| p.id).collect()) ); let dbm = AllCategories { limit: Limit::from_u32(2000), offset: Offset::from_u32(0), }; let categories = dbm_run!(dbm, t, Error::Categories); let mut variants = utils::vec_to_hash_vec(variants, 10, |p| p.product_id); let mut stocks = utils::vec_to_hash_vec(stocks, 10, |s| s.product_variant_id); let mut photos = utils::vec_to_hash_vec(photos, 10, |p| p.product_variant_id); let product = utils::map_product( product, &mut variants, &mut stocks, &mut photos, &categories, &config .as_ref() .map(|c: &SharedAppConfig| c.lock().files().public_path()) .unwrap_or_else(|| "https://example.com/".into()), ); Ok(detailed_product::Details { product }) } pub async fn detailed_products( input: detailed_products::Input, db: Database, _mqtt: AsyncClient, _config: SharedAppConfig, ) -> detailed_products::Output { let mut t = begin_t!(db, Error::InternalServerError); let res = inner_detailed_products(input, &mut t, Some(_mqtt), Some(_config)).await; t.commit().await.ok(); res } async fn inner_detailed_products( input: detailed_products::Input, t: &mut PgT<'_>, _mqtt: Option, config: Option, ) -> detailed_products::Output { let dbm = crate::db::AllProducts { limit: input.limit, offset: input.offset, }; let products = dbm_run!(dbm, &mut *t, Error::Products); let dbm = ProductsVariants { product_ids: products.iter().map(|p| p.id).collect(), limit: Some(Limit::from_u32(products.len() as u32 * 10)), offset: Some(0.into()), }; let variants = dbm_run!( dbm, &mut *t, Error::ProductVariants(products.into_iter().map(|p| p.id).collect(),) ); let dbm = ProductVariantsStock { product_variant_ids: variants.iter().map(|p| p.id).collect(), }; let stocks = match dbm.run(&mut *t).await { Ok(stocks) => stocks, Err(e) => { tracing::warn!("{}", e); return Err(Error::VariantStocks( variants.into_iter().map(|p| p.id).collect(), )); } }; let dbm = PhotosForProductVariants { product_variant_ids: variants.iter().map(|p| p.id).collect(), }; let photos = match dbm.run(t).await { Ok(photos) => photos, Err(e) => { tracing::warn!("{}", e); return Err(Error::VariantPhotos( variants.into_iter().map(|p| p.id).collect(), )); } }; let dbm = AllCategories { limit: Limit::from_u32(2000), offset: Offset::from_u32(0), }; let categories = dbm_run!(dbm, t, Error::Categories); let mut variants = utils::vec_to_hash_vec(variants, 10, |p| p.product_id); let mut stocks = utils::vec_to_hash_vec(stocks, 10, |s| s.product_variant_id); let mut photos = utils::vec_to_hash_vec(photos, 10, |p| p.product_variant_id); let products = products .into_iter() .map(|product| { utils::map_product( product, &mut variants, &mut stocks, &mut photos, &categories, &config .as_ref() .map(|config: &SharedAppConfig| config.lock().files().public_path()) .unwrap_or_else(|| "https:///example.com".into()), ) }) .collect(); Ok(detailed_products::Details { products }) } mod utils { use std::collections::HashMap; use std::hash::Hash; use model::v2::*; use model::CategoryMapper; pub fn vec_to_hash_vec Id>( v: Vec, cap: usize, f: F, ) -> HashMap> { let len = v.len(); v.into_iter().fold(HashMap::with_capacity(len), |mut h, r| { h.entry(f(&r)) .or_insert_with(|| Vec::with_capacity(cap)) .push(r); h }) } pub fn map_product( product: Product, variants: &mut HashMap>, stocks: &mut HashMap>, photos: &mut HashMap>, categories: &[Category], public_path: &str, ) -> DetailedProduct { let Product { id, name, category, deliver_days_flag, } = product; DetailedProduct { id, name, category: category.and_then(|name| CategoryMapper::db_into_api(name, categories)), deliver_days_flag, variants: variants .remove(&id) .unwrap_or(vec![]) .into_iter() .map( |ProductVariant { id, product_id: _, name, short_description, long_description, price, quantity_unit, }| { let stocks = stocks.remove(&id).unwrap_or_default(); DetailedProductVariant { id, name, short_description, long_description, price, quantity_unit, available: !stocks.is_empty(), stocks, photos: photos .remove(&id) .unwrap_or_default() .into_iter() .map( |ProductLinkedPhoto { photo_id, local_path, file_name, unique_name, product_variant_id: _, }| model::api::Photo { id: photo_id, file_name, url: format!("{public_path}/{unique_name}"), unique_name, }, ) .collect(), } }, ) .collect(), } } } #[cfg(test)] mod tests { use channels::stocks::{detailed_product, detailed_products, Error}; use config::UpdateConfig; use db_utils::PgT; use model::v2::*; use uuid::Uuid; use crate::actions::load::{inner_detailed_product, inner_detailed_products}; use crate::db::*; pub struct NoOpts; impl UpdateConfig for NoOpts {} async fn test_product(t: &mut PgT<'_>) -> Product { CreateProduct { name: ProductName::new(format!("{}", Uuid::new_v4())), category: None, deliver_days_flag: Days(vec![Day::Friday, Day::Sunday]), } .run(t) .await .unwrap() } async fn test_product_variant(product_id: ProductId, t: &mut PgT<'_>) -> ProductVariant { CreateProductVariant { product_id, name: ProductVariantName::new(format!("{}", Uuid::new_v4())), short_description: ProductShortDesc::new(format!("{}", Uuid::new_v4())), long_description: ProductLongDesc::new(format!("{}", Uuid::new_v4())), price: Default::default(), quantity_unit: QuantityUnit::Gram, } .run(t) .await .unwrap() } async fn test_photo(t: &mut PgT<'_>) -> Photo { CreatePhoto { local_path: LocalPath::new(format!("{}", Uuid::new_v4())), file_name: FileName::new(format!("{}", Uuid::new_v4())), unique_name: UniqueName::new(format!("{}", Uuid::new_v4())), } .run(t) .await .unwrap() } async fn test_product_photo( product_variant_id: ProductVariantId, photo_id: PhotoId, t: &mut PgT<'_>, ) -> ProductPhoto { CreateProductPhoto { product_variant_id, photo_id, } .run(t) .await .unwrap() } async fn n_test_photo( n: usize, product_variant_id: ProductVariantId, t: &mut PgT<'_>, ) -> Vec<(Photo, ProductPhoto)> { let mut res = Vec::with_capacity(n); for _ in 0..n { let photo = test_photo(t).await; let product_photo = test_product_photo(product_variant_id, photo.id, t).await; res.push((photo, product_photo)); } res } async fn test_stock(product_variant_id: ProductVariantId, pool: &mut PgT<'_>) -> Stock { let quantity = Quantity::from_u32(345); let quantity_unit = QuantityUnit::Piece; CreateStock { product_variant_id, quantity_unit, quantity, } .run(&mut *pool) .await .unwrap() } async fn n_test_variant( variant_count: usize, product_id: ProductId, t: &mut PgT<'_>, ) -> Vec<(ProductVariant, Stock, Vec<(Photo, ProductPhoto)>)> { let mut variants = Vec::with_capacity(variant_count); for _ in 0..variant_count { let variant = test_product_variant(product_id, t).await; let stock = test_stock(variant.id, t).await; let photos = n_test_photo(3, variant.id, t).await; variants.push((variant, stock, photos)); } variants } #[tokio::test] async fn load_details() { testx::db_t_ref!(t); let product_1 = test_product(&mut t).await; let _variants_1 = n_test_variant(3, product_1.id, &mut t).await; let product_2 = test_product(&mut t).await; let _variants_2 = n_test_variant(5, product_2.id, &mut t).await; let product_3 = test_product(&mut t).await; let _variants_2 = n_test_variant(2, product_3.id, &mut t).await; let res = inner_detailed_products( detailed_products::Input { limit: Limit::from_u32(2000), offset: Offset::from_u32(0), }, &mut t, None, None, ) .await; testx::db_rollback!(t); let mut res = res.unwrap(); assert_eq!(res.products.len(), 3); let product = res.products.remove(0); assert_eq!(product.variants.len(), 3); for variant in product.variants { assert_eq!(variant.photos.len(), 3); assert_eq!(variant.stocks.len(), 1); } let product = res.products.remove(0); assert_eq!(product.variants.len(), 5); for variant in product.variants { assert_eq!(variant.photos.len(), 3); assert_eq!(variant.stocks.len(), 1); } let product = res.products.remove(0); assert_eq!(product.variants.len(), 2); for variant in product.variants { assert_eq!(variant.photos.len(), 3); assert_eq!(variant.stocks.len(), 1); } } #[tokio::test] async fn load_detail() { testx::db_t_ref!(t); let product_1 = test_product(&mut t).await; let _variants_1 = n_test_variant(3, product_1.id, &mut t).await; let product_2 = test_product(&mut t).await; let _variants_2 = n_test_variant(5, product_2.id, &mut t).await; let product_3 = test_product(&mut t).await; let _variants_2 = n_test_variant(2, product_3.id, &mut t).await; let res1 = inner_detailed_product( detailed_product::Input { product_id: product_1.id, }, &mut t, None, None, ) .await; let res2 = inner_detailed_product( detailed_product::Input { product_id: product_3.id, }, &mut t, None, None, ) .await; let res3 = inner_detailed_product( detailed_product::Input { product_id: (-1).into(), }, &mut t, None, None, ) .await; testx::db_rollback!(t); { let res = res1.unwrap(); assert_eq!(res.product.id, product_1.id); let product = res.product; assert_eq!(product.variants.len(), 3); for variant in product.variants { assert_eq!(variant.photos.len(), 3); assert_eq!(variant.stocks.len(), 1); } } { let res = res2.unwrap(); assert_eq!(res.product.id, product_3.id); let product = res.product; assert_eq!(product.variants.len(), 2); for variant in product.variants { assert_eq!(variant.photos.len(), 3); assert_eq!(variant.stocks.len(), 1); } } assert_eq!(res3, Err(Error::ProductNotFound((-1).into()))); } }