Add categories

This commit is contained in:
Adrian Woźniak 2022-11-28 17:00:19 +01:00
parent 2316426a13
commit 376c1084ab
33 changed files with 941 additions and 434 deletions

View File

@ -26,6 +26,7 @@ CREATE TABLE shopping_carts (
CREATE TABLE shopping_cart_items (
id serial NOT NULL,
product_variant_id integer NOT NULL,
product_id integer NOT NULL,
shopping_cart_id integer,
quantity integer DEFAULT 0 NOT NULL,
quantity_unit "QuantityUnit" NOT NULL,

View File

@ -83,6 +83,7 @@ pub async fn modify_item(msg: modify_item::Input, db: Database) -> modify_item::
Some(item) => {
let dbm = UpdateShoppingCartItem {
id: item.id,
product_id: msg.product_id,
product_variant_id: msg.product_variant_id,
shopping_cart_id: cart.id,
quantity: msg.quantity,
@ -99,6 +100,7 @@ pub async fn modify_item(msg: modify_item::Input, db: Database) -> modify_item::
}
None => {
let dbm = CreateShoppingCartItem {
product_id: msg.product_id,
product_variant_id: msg.product_variant_id,
shopping_cart_id: cart.id,
quantity: msg.quantity,

View File

@ -37,6 +37,7 @@ impl AllShoppingCartItems {
sqlx::query_as(
r#"
SELECT shopping_cart_items.id,
shopping_cart_items.product_id,
shopping_cart_items.product_variant_id,
shopping_cart_items.shopping_cart_id,
shopping_cart_items.quantity,
@ -73,6 +74,7 @@ impl AccountShoppingCartItems {
Some(shopping_cart_id) => sqlx::query_as(
r#"
SELECT shopping_cart_items.id as id,
shopping_cart_items.product_id as product_id,
shopping_cart_items.product_variant_id as product_variant_id,
shopping_cart_items.shopping_cart_id as shopping_cart_id,
shopping_cart_items.quantity as quantity,
@ -89,6 +91,7 @@ ORDER BY shopping_cart_items.id
None => sqlx::query_as(
r#"
SELECT shopping_cart_items.id as id,
shopping_cart_items.product_id as product_id,
shopping_cart_items.product_variant_id as product_variant_id,
shopping_cart_items.shopping_cart_id as shopping_cart_id,
shopping_cart_items.quantity as quantity,
@ -113,6 +116,7 @@ ORDER BY shopping_cart_items.id
#[derive(Debug)]
pub struct CreateShoppingCartItem {
pub product_id: ProductId,
pub product_variant_id: ProductVariantId,
pub shopping_cart_id: ShoppingCartId,
pub quantity: Quantity,
@ -127,12 +131,13 @@ impl CreateShoppingCartItem {
let msg = self;
sqlx::query_as(
r#"
INSERT INTO shopping_cart_items (product_variant_id, shopping_cart_id, quantity, quantity_unit)
VALUES ($1, $2, $3, $4)
RETURNING id, product_variant_id, shopping_cart_id, quantity, quantity_unit
INSERT INTO shopping_cart_items (product_variant_id, product_id, shopping_cart_id, quantity, quantity_unit)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, product_id, product_variant_id, shopping_cart_id, quantity, quantity_unit
"#,
)
.bind(msg.product_variant_id)
.bind(msg.product_id)
.bind(msg.shopping_cart_id)
.bind(msg.quantity)
.bind(msg.quantity_unit)
@ -149,6 +154,7 @@ RETURNING id, product_variant_id, shopping_cart_id, quantity, quantity_unit
#[derive(Debug)]
pub struct UpdateShoppingCartItem {
pub id: ShoppingCartItemId,
pub product_id: ProductId,
pub product_variant_id: ProductVariantId,
pub shopping_cart_id: ShoppingCartId,
pub quantity: Quantity,
@ -164,12 +170,13 @@ impl UpdateShoppingCartItem {
sqlx::query_as(
r#"
UPDATE shopping_cart_items
SET product_variant_id = $2, shopping_cart_id = $3, quantity = $4, quantity_unit = $5
SET product_variant_id = $2, product_id = $3, shopping_cart_id = $4, quantity = $5, quantity_unit = $6
WHERE id = $1
RETURNING id, product_variant_id, shopping_cart_id, quantity, quantity_unit
RETURNING id, product_id, product_variant_id, shopping_cart_id, quantity, quantity_unit
"#,
)
.bind(msg.id)
.bind(msg.product_id)
.bind(msg.product_variant_id)
.bind(msg.shopping_cart_id)
.bind(msg.quantity)
@ -198,7 +205,7 @@ impl DeleteShoppingCartItem {
r#"
DELETE FROM shopping_cart_items
WHERE id = $1
RETURNING id, product_variant_id, shopping_cart_id, quantity, quantity_unit
RETURNING id, product_id, product_variant_id, shopping_cart_id, quantity, quantity_unit
"#,
)
.bind(msg.id)
@ -224,7 +231,7 @@ impl FindShoppingCartItem {
let msg = self;
sqlx::query_as(
r#"
SELECT id, product_variant_id, shopping_cart_id, quantity, quantity_unit
SELECT id, product_id, product_variant_id, shopping_cart_id, quantity, quantity_unit
FROM shopping_cart_items
WHERE id = $1
"#,
@ -253,6 +260,7 @@ impl ActiveCartItemByProduct {
sqlx::query_as(
r#"
SELECT shopping_cart_items.id,
shopping_cart_items.product_id,
shopping_cart_items.product_variant_id,
shopping_cart_items.shopping_cart_id,
shopping_cart_items.quantity,
@ -291,6 +299,7 @@ impl CartItems {
sqlx::query_as(
r#"
SELECT id,
product_id,
product_variant_id,
shopping_cart_id,
quantity,
@ -328,7 +337,7 @@ impl RemoveCartItem {
r#"
DELETE FROM shopping_cart_items
WHERE shopping_cart_id = $1 AND id = $2
RETURNING id, product_variant_id, shopping_cart_id, quantity, quantity_unit
RETURNING id, product_id, product_variant_id, shopping_cart_id, quantity, quantity_unit
"#,
)
.bind(msg.shopping_cart_id)
@ -337,7 +346,7 @@ RETURNING id, product_variant_id, shopping_cart_id, quantity, quantity_unit
r#"
DELETE FROM shopping_cart_items
WHERE shopping_cart_id = $1 AND id = $2 AND product_variant_id = $3
RETURNING id, product_variant_id, shopping_cart_id, quantity, quantity_unit
RETURNING id, product_id, product_id, product_variant_id, shopping_cart_id, quantity, quantity_unit
"#,
)
.bind(msg.shopping_cart_id)
@ -347,7 +356,7 @@ RETURNING id, product_variant_id, shopping_cart_id, quantity, quantity_unit
r#"
DELETE FROM shopping_cart_items
WHERE shopping_cart_id = $1 AND product_variant_id = $2
RETURNING id, product_variant_id, shopping_cart_id, quantity, quantity_unit
RETURNING id, product_id, product_variant_id, shopping_cart_id, quantity, quantity_unit
"#,
)
.bind(msg.shopping_cart_id)
@ -425,6 +434,7 @@ WHERE buyer_id = $1
t: &mut sqlx::Transaction<'_, sqlx::Postgres>,
shopping_cart_id: Option<ShoppingCartId>,
product_variant_id: Option<ProductVariantId>,
product_id: Option<ProductId>,
) -> ShoppingCartItem {
let shopping_cart_id = match shopping_cart_id {
Some(id) => id,
@ -438,7 +448,12 @@ WHERE buyer_id = $1
Some(id) => id,
_ => 1.into(),
};
let product_id = match product_id {
Some(id) => id,
_ => 1.into(),
};
CreateShoppingCartItem {
product_id,
product_variant_id,
shopping_cart_id,
quantity: Quantity::from_u32(496879),
@ -453,7 +468,7 @@ WHERE buyer_id = $1
async fn create() {
testx::db_t_ref!(t);
test_shopping_cart_item(&mut t, None, None).await;
test_shopping_cart_item(&mut t, None, None, None).await;
testx::db_rollback!(t);
}
@ -467,19 +482,19 @@ WHERE buyer_id = $1
let mut items = Vec::with_capacity(9);
let cart1 = test_shopping_cart(&mut t, Some(account_id), ShoppingCartState::Closed).await;
items.push(test_shopping_cart_item(&mut t, Some(cart1.id), None).await);
items.push(test_shopping_cart_item(&mut t, Some(cart1.id), None).await);
items.push(test_shopping_cart_item(&mut t, Some(cart1.id), None).await);
items.push(test_shopping_cart_item(&mut t, Some(cart1.id), None, None).await);
items.push(test_shopping_cart_item(&mut t, Some(cart1.id), None, None).await);
items.push(test_shopping_cart_item(&mut t, Some(cart1.id), None, None).await);
let cart2 = test_shopping_cart(&mut t, Some(account_id), ShoppingCartState::Active).await;
items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None).await);
items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None).await);
items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None).await);
items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None, None).await);
items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None, None).await);
items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None, None).await);
let cart3 = test_shopping_cart(&mut t, Some(account_id), ShoppingCartState::Closed).await;
items.push(test_shopping_cart_item(&mut t, Some(cart3.id), None).await);
items.push(test_shopping_cart_item(&mut t, Some(cart3.id), None).await);
items.push(test_shopping_cart_item(&mut t, Some(cart3.id), None).await);
items.push(test_shopping_cart_item(&mut t, Some(cart3.id), None, None).await);
items.push(test_shopping_cart_item(&mut t, Some(cart3.id), None, None).await);
items.push(test_shopping_cart_item(&mut t, Some(cart3.id), None, None).await);
let all = AllShoppingCartItems.run(&mut t).await.unwrap();
@ -496,19 +511,19 @@ WHERE buyer_id = $1
let mut items = Vec::with_capacity(9);
let cart1 = test_shopping_cart(&mut t, Some(account_id), ShoppingCartState::Closed).await;
test_shopping_cart_item(&mut t, Some(cart1.id), None).await;
test_shopping_cart_item(&mut t, Some(cart1.id), None).await;
test_shopping_cart_item(&mut t, Some(cart1.id), None).await;
test_shopping_cart_item(&mut t, Some(cart1.id), None, None).await;
test_shopping_cart_item(&mut t, Some(cart1.id), None, None).await;
test_shopping_cart_item(&mut t, Some(cart1.id), None, None).await;
let cart2 = test_shopping_cart(&mut t, Some(account_id), ShoppingCartState::Active).await;
items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None).await);
items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None).await);
items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None).await);
items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None, None).await);
items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None, None).await);
items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None, None).await);
let cart3 = test_shopping_cart(&mut t, Some(account_id), ShoppingCartState::Closed).await;
test_shopping_cart_item(&mut t, Some(cart3.id), None).await;
test_shopping_cart_item(&mut t, Some(cart3.id), None).await;
test_shopping_cart_item(&mut t, Some(cart3.id), None).await;
test_shopping_cart_item(&mut t, Some(cart3.id), None, None).await;
test_shopping_cart_item(&mut t, Some(cart3.id), None, None).await;
test_shopping_cart_item(&mut t, Some(cart3.id), None, None).await;
let all = AccountShoppingCartItems {
account_id,
@ -531,19 +546,19 @@ WHERE buyer_id = $1
let mut items = Vec::with_capacity(9);
let cart1 = test_shopping_cart(&mut t, Some(account_id), ShoppingCartState::Closed).await;
items.push(test_shopping_cart_item(&mut t, Some(cart1.id), None).await);
items.push(test_shopping_cart_item(&mut t, Some(cart1.id), None).await);
items.push(test_shopping_cart_item(&mut t, Some(cart1.id), None).await);
items.push(test_shopping_cart_item(&mut t, Some(cart1.id), None, None).await);
items.push(test_shopping_cart_item(&mut t, Some(cart1.id), None, None).await);
items.push(test_shopping_cart_item(&mut t, Some(cart1.id), None, None).await);
let cart2 = test_shopping_cart(&mut t, Some(account_id), ShoppingCartState::Active).await;
items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None).await);
items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None).await);
items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None).await);
items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None, None).await);
items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None, None).await);
items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None, None).await);
let cart3 = test_shopping_cart(&mut t, Some(account_id), ShoppingCartState::Closed).await;
items.push(test_shopping_cart_item(&mut t, Some(cart3.id), None).await);
items.push(test_shopping_cart_item(&mut t, Some(cart3.id), None).await);
items.push(test_shopping_cart_item(&mut t, Some(cart3.id), None).await);
items.push(test_shopping_cart_item(&mut t, Some(cart3.id), None, None).await);
items.push(test_shopping_cart_item(&mut t, Some(cart3.id), None, None).await);
items.push(test_shopping_cart_item(&mut t, Some(cart3.id), None, None).await);
let all = AccountShoppingCartItems {
account_id,
@ -562,10 +577,11 @@ WHERE buyer_id = $1
testx::db_t_ref!(t);
let account_id = 1.into();
let cart1 = test_shopping_cart(&mut t, Some(account_id), ShoppingCartState::Closed).await;
let item = test_shopping_cart_item(&mut t, Some(cart1.id), None).await;
let item = test_shopping_cart_item(&mut t, Some(cart1.id), None, None).await;
let updated = UpdateShoppingCartItem {
id: item.id,
product_id: item.product_id,
product_variant_id: item.product_variant_id,
shopping_cart_id: item.shopping_cart_id,
quantity: Quantity::from_u32(987979879),
@ -581,6 +597,7 @@ WHERE buyer_id = $1
ShoppingCartItem {
id: item.id,
product_variant_id: item.product_variant_id,
product_id: item.product_id,
shopping_cart_id: item.shopping_cart_id,
quantity: Quantity::from_u32(987979879),
quantity_unit: QuantityUnit::Kilogram,

View File

@ -41,6 +41,7 @@ pub mod remove_product {
pub mod modify_item {
use model::v2::ProductVariantId;
use model::ProductId;
use super::Error;
@ -48,6 +49,7 @@ pub mod modify_item {
pub struct Input {
pub buyer_id: model::AccountId,
pub product_variant_id: ProductVariantId,
pub product_id: ProductId,
pub quantity: model::Quantity,
pub quantity_unit: model::QuantityUnit,
}

View File

@ -9,11 +9,11 @@ pub mod detailed_product {
}
#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Output2 {
pub struct Details {
pub product: DetailedProduct,
}
pub type Output = Result<Output2, Error>;
pub type Output = Result<Details, Error>;
}
pub mod detailed_products {

View File

@ -54,6 +54,8 @@ pub enum Error {
FindProducts(Vec<ProductVariantId>),
#[error("Failed to load product variants {0:?}")]
FindProductVariants(Vec<ProductVariantId>),
#[error("Failed to load all categories")]
Categories,
}
pub mod rpc {

View File

@ -18,6 +18,7 @@ pub mod create_product {
pub long_description: ProductLongDesc,
pub category: Option<ProductCategory>,
pub price: Price,
pub quantity_unit: QuantityUnit,
pub deliver_days_flag: Days,
}

View File

@ -15,6 +15,7 @@ pub mod create_product_variant {
pub short_description: ProductShortDesc,
pub long_description: ProductLongDesc,
pub price: Price,
pub quantity_unit: QuantityUnit,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
@ -38,6 +39,7 @@ pub mod update_product_variant {
pub short_description: ProductShortDesc,
pub long_description: ProductLongDesc,
pub price: Price,
pub quantity_unit: QuantityUnit,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]

View File

@ -2,6 +2,7 @@ use chrono::NaiveDateTime;
use derive_more::Deref;
use serde::{Deserialize, Serialize};
use crate::v2::{CategoryId, CategoryKey, CategoryName, CategorySvg, DetailedProduct};
use crate::*;
#[derive(Serialize, Deserialize, Debug)]
@ -174,6 +175,7 @@ pub struct Order {
pub struct ShoppingCartItem {
pub id: ShoppingCartItemId,
pub product_variant_id: ProductVariantId,
pub product_id: ProductId,
pub shopping_cart_id: ShoppingCartId,
pub quantity: Quantity,
pub quantity_unit: QuantityUnit,
@ -184,6 +186,7 @@ impl From<crate::ShoppingCartItem> for ShoppingCartItem {
crate::ShoppingCartItem {
id,
product_variant_id,
product_id,
shopping_cart_id,
quantity,
quantity_unit,
@ -192,6 +195,7 @@ impl From<crate::ShoppingCartItem> for ShoppingCartItem {
Self {
id,
product_variant_id,
product_id,
shopping_cart_id,
quantity,
quantity_unit,
@ -234,11 +238,13 @@ impl From<(crate::ShoppingCart, Vec<crate::ShoppingCartItem>)> for ShoppingCart
|crate::ShoppingCartItem {
id,
product_variant_id,
product_id,
shopping_cart_id,
quantity,
quantity_unit,
}| ShoppingCartItem {
id,
product_id,
product_variant_id,
shopping_cart_id,
quantity,
@ -250,43 +256,47 @@ impl From<(crate::ShoppingCart, Vec<crate::ShoppingCartItem>)> for ShoppingCart
}
}
#[derive(Serialize, Deserialize, Debug, Hash)]
#[derive(Debug, Hash, PartialEq, Serialize, Deserialize)]
pub struct Photo {
pub id: crate::PhotoId,
pub file_name: crate::FileName,
pub id: PhotoId,
pub file_name: FileName,
pub url: String,
pub unique_name: crate::UniqueName,
pub unique_name: UniqueName,
}
#[derive(Clone, Debug, Hash, PartialOrd, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub struct Category {
pub name: String,
pub key: String,
pub svg: String,
pub id: CategoryId,
pub parent_id: Option<CategoryId>,
pub name: CategoryName,
pub key: CategoryKey,
pub svg: CategorySvg,
}
impl From<&crate::Category> for Category {
fn from(crate::Category { name, key, svg }: &crate::Category) -> Self {
fn from(&crate::Category { name, key, svg }: &crate::Category) -> Self {
Self {
name: (*name).into(),
key: (*key).into(),
svg: (*svg).into(),
id: Default::default(),
parent_id: None,
name: CategoryName::from(name),
key: CategoryKey::from(key),
svg: CategorySvg::from(svg),
}
}
}
#[derive(Serialize, Deserialize, Debug, Hash)]
pub struct Product {
pub id: crate::ProductId,
pub name: crate::ProductName,
pub short_description: crate::ProductShortDesc,
pub long_description: crate::ProductLongDesc,
pub id: ProductId,
pub name: ProductName,
pub short_description: ProductShortDesc,
pub long_description: ProductLongDesc,
pub category: Option<Category>,
pub price: crate::Price,
pub price: Price,
pub available: bool,
pub quantity_unit: crate::QuantityUnit,
pub deliver_days_flag: crate::Days,
pub quantity_unit: QuantityUnit,
pub deliver_days_flag: Days,
pub photos: Vec<Photo>,
}
@ -315,7 +325,7 @@ impl<'path>
): (
crate::Product,
&mut Vec<ProductLinkedPhoto>,
&mut Vec<crate::Stock>,
&mut Vec<Stock>,
&'path str,
),
) -> Self {
@ -325,21 +335,13 @@ impl<'path>
let (available, quantity_unit) = pos
.map(|idx| product_stocks.remove(idx))
.map(|stock| (**stock.quantity > 0, stock.quantity_unit))
.unwrap_or_else(|| (false, crate::QuantityUnit::Piece));
.unwrap_or_else(|| (false, QuantityUnit::Piece));
Self {
id,
name,
short_description,
long_description,
category: category.and_then(|name| {
crate::CATEGORIES.iter().find_map(|c| {
if c.name == name.as_str() {
Some(Category::from(c))
} else {
None
}
})
}),
category: category.and_then(CategoryMapper::api_from_product_category),
price,
available,
quantity_unit,
@ -367,32 +369,32 @@ impl<'path>
#[derive(Serialize, Deserialize, Debug, Deref)]
#[serde(transparent)]
pub struct Products(pub Vec<Product>);
pub struct Products(pub Vec<DetailedProduct>);
impl
From<(
Vec<crate::Product>,
Vec<ProductLinkedPhoto>,
Vec<Stock>,
String,
)> for Products
{
fn from(
(products, mut photos, mut products_stock, public_path): (
Vec<crate::Product>,
Vec<ProductLinkedPhoto>,
Vec<crate::Stock>,
String,
),
) -> Self {
Self(
products
.into_iter()
.map(|p| (p, &mut photos, &mut products_stock, public_path.as_str()).into())
.collect(),
)
}
}
// impl
// From<(
// Vec<DetailedProduct>,
// Vec<ProductLinkedPhoto>,
// Vec<Stock>,
// String,
// )> for Products
// {
// fn from(
// (products, mut photos, mut products_stock, public_path): (
// Vec<DetailedProduct>,
// Vec<ProductLinkedPhoto>,
// Vec<Stock>,
// String,
// ),
// ) -> Self {
// Self(
// products
// .into_iter()
// .map(|p| (p, &mut photos, &mut products_stock,
// public_path.as_str()).into()) .collect(),
// )
// }
// }
#[derive(Serialize, Deserialize, Debug)]
pub struct SignInInput {
@ -453,6 +455,7 @@ pub struct DeleteItemOutput {
#[derive(Serialize, Deserialize, Debug)]
pub struct UpdateItemInput {
pub product_variant_id: ProductVariantId,
pub product_id: ProductId,
pub quantity: Quantity,
pub quantity_unit: QuantityUnit,
}

View File

@ -14,7 +14,7 @@ use serde::de::{Error, Visitor};
use serde::{Deserialize, Deserializer, Serialize};
pub use crate::encrypt::*;
use crate::v2::ProductVariantId;
use crate::v2::{CategoryKey, CategoryName, CategorySvg, ProductVariantId};
#[derive(Debug, Hash, thiserror::Error)]
pub enum TransformError {
@ -71,53 +71,94 @@ macro_rules! category_svg {
};
}
pub const CATEGORIES: [Category; 9] = [
Category {
name: Category::CAMERAS_NAME,
key: Category::CAMERAS_KEY,
svg: category_svg!("cameras"),
},
Category {
name: Category::DRUGSTORE_NAME,
key: Category::DRUGSTORE_KEY,
svg: category_svg!("drugstore"),
},
Category {
name: Category::SPEAKERS_NAME,
key: Category::SPEAKERS_KEY,
svg: category_svg!("speakers"),
},
Category {
name: Category::PHONES_NAME,
key: Category::PHONES_KEY,
svg: category_svg!("phones"),
},
Category {
name: Category::SWEETS_NAME,
key: Category::SWEETS_KEY,
svg: category_svg!("sweets"),
},
Category {
name: Category::MEMORY_NAME,
key: Category::MEMORY_KEY,
svg: category_svg!("memory"),
},
Category {
name: Category::PANTS_NAME,
key: Category::PANTS_KEY,
svg: category_svg!("pants"),
},
Category {
name: Category::CLOTHES_NAME,
key: Category::CLOTHES_KEY,
svg: category_svg!("clothes"),
},
Category {
name: Category::PLATES_NAME,
key: Category::PLATES_KEY,
svg: category_svg!("plates"),
},
];
pub struct CategoryMapper;
impl CategoryMapper {
pub const CATEGORIES: [Category; 9] = [
Category {
name: Category::CAMERAS_NAME,
key: Category::CAMERAS_KEY,
svg: category_svg!("cameras"),
},
Category {
name: Category::DRUGSTORE_NAME,
key: Category::DRUGSTORE_KEY,
svg: category_svg!("drugstore"),
},
Category {
name: Category::SPEAKERS_NAME,
key: Category::SPEAKERS_KEY,
svg: category_svg!("speakers"),
},
Category {
name: Category::PHONES_NAME,
key: Category::PHONES_KEY,
svg: category_svg!("phones"),
},
Category {
name: Category::SWEETS_NAME,
key: Category::SWEETS_KEY,
svg: category_svg!("sweets"),
},
Category {
name: Category::MEMORY_NAME,
key: Category::MEMORY_KEY,
svg: category_svg!("memory"),
},
Category {
name: Category::PANTS_NAME,
key: Category::PANTS_KEY,
svg: category_svg!("pants"),
},
Category {
name: Category::CLOTHES_NAME,
key: Category::CLOTHES_KEY,
svg: category_svg!("clothes"),
},
Category {
name: Category::PLATES_NAME,
key: Category::PLATES_KEY,
svg: category_svg!("plates"),
},
];
pub fn api_from_product_category(name: ProductCategory) -> Option<crate::api::Category> {
Self::CATEGORIES
.iter()
.find(|category| category.name == name.as_str())
.map(|&Category { name, key, svg }| crate::api::Category {
id: Default::default(),
parent_id: None,
name: CategoryName::from(name),
key: CategoryKey::from(key),
svg: CategorySvg::from(svg),
})
}
pub fn db_into_api(
name: ProductCategory,
categories: &[crate::v2::Category],
) -> Option<crate::api::Category> {
categories
.iter()
.find(|category| category.name.as_str() == name.as_str())
.map(
|crate::v2::Category {
id,
parent_id,
name,
key,
svg,
}| crate::api::Category {
id: id.clone(),
parent_id: parent_id.clone(),
name: name.clone(),
key: key.clone(),
svg: svg.clone(),
},
)
}
}
#[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(rename_all = "snake_case"))]
@ -943,7 +984,7 @@ impl ProductLongDesc {
#[cfg_attr(feature = "db", sqlx(transparent))]
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize, Deref, Display, From)]
#[serde(transparent)]
pub struct ProductCategory(String);
pub struct ProductCategory(pub String);
impl ProductCategory {
pub fn new<S: Into<String>>(s: S) -> Self {
@ -955,121 +996,6 @@ impl ProductCategory {
}
}
pub mod v2 {
use derive_more::{Deref, Display, From};
use serde::{Deserialize, Serialize};
pub use crate::{
Day, Days, FileName, Limit, LocalPath, Offset, PhotoId, Price, ProductCategory, ProductId,
ProductLongDesc, ProductName, ProductPhotoId, ProductShortDesc, Quantity, QuantityUnit,
RecordId, ShoppingCartId, StockId, UniqueName,
};
#[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))]
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize, Deref, Display, From)]
#[serde(transparent)]
pub struct ProductVariantName(String);
impl ProductVariantName {
pub fn new<S: Into<String>>(s: S) -> Self {
Self(s.into())
}
pub fn into_inner(self) -> String {
self.0
}
pub fn as_str(&self) -> &str {
&self.0
}
}
#[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Deref, From)]
#[serde(transparent)]
pub struct ProductVariantId(pub RecordId);
#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct DetailedProductVariant {
pub id: ProductVariantId,
pub name: ProductVariantName,
pub short_description: ProductShortDesc,
pub long_description: ProductLongDesc,
pub price: Price,
pub stocks: Vec<Stock>,
pub photos: Vec<ProductLinkedPhoto>,
}
#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct DetailedProduct {
pub id: ProductId,
pub name: ProductName,
pub category: Option<ProductCategory>,
pub deliver_days_flag: Days,
pub variants: Vec<DetailedProductVariant>,
}
#[cfg_attr(feature = "db", derive(sqlx::FromRow))]
#[derive(Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct Product {
pub id: ProductId,
pub name: ProductName,
pub category: Option<ProductCategory>,
pub deliver_days_flag: Days,
}
#[cfg_attr(feature = "db", derive(sqlx::FromRow))]
#[derive(Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProductVariant {
pub id: ProductVariantId,
pub product_id: ProductId,
pub name: ProductVariantName,
pub short_description: ProductShortDesc,
pub long_description: ProductLongDesc,
pub price: Price,
}
#[cfg_attr(feature = "db", derive(sqlx::FromRow))]
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Stock {
pub id: StockId,
pub product_variant_id: ProductVariantId,
pub quantity: Quantity,
pub quantity_unit: QuantityUnit,
}
#[cfg_attr(feature = "db", derive(sqlx::FromRow))]
#[derive(Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct Photo {
pub id: PhotoId,
pub local_path: LocalPath,
pub file_name: FileName,
pub unique_name: UniqueName,
}
#[cfg_attr(feature = "db", derive(sqlx::FromRow))]
#[derive(Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProductLinkedPhoto {
pub photo_id: PhotoId,
pub local_path: LocalPath,
pub file_name: FileName,
pub unique_name: UniqueName,
pub product_variant_id: ProductVariantId,
}
#[cfg_attr(feature = "db", derive(sqlx::FromRow))]
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProductPhoto {
pub id: ProductPhotoId,
pub product_variant_id: ProductVariantId,
pub photo_id: PhotoId,
}
}
#[cfg_attr(feature = "db", derive(sqlx::FromRow))]
#[derive(Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct Product {
@ -1216,6 +1142,7 @@ pub struct ShoppingCartItemId(RecordId);
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ShoppingCartItem {
pub id: ShoppingCartItemId,
pub product_id: ProductId,
pub product_variant_id: ProductVariantId,
pub shopping_cart_id: ShoppingCartId,
pub quantity: Quantity,
@ -1568,3 +1495,233 @@ pub struct OrderAddress {
pub zip: Zip,
pub phone: Phone,
}
pub mod v2 {
use derive_more::{Deref, Display, From};
use serde::{Deserialize, Serialize};
use crate::NonNegative;
pub use crate::{
Day, Days, FileName, Limit, LocalPath, Offset, PhotoId, Price, ProductCategory, ProductId,
ProductLongDesc, ProductName, ProductPhotoId, ProductShortDesc, Quantity, QuantityUnit,
RecordId, ShoppingCartId, StockId, UniqueName,
};
#[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))]
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize, Deref, Display, From)]
#[serde(transparent)]
pub struct ProductVariantName(String);
impl ProductVariantName {
pub fn new<S: Into<String>>(s: S) -> Self {
Self(s.into())
}
pub fn into_inner(self) -> String {
self.0
}
pub fn as_str(&self) -> &str {
&self.0
}
}
#[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Deref, From)]
#[serde(transparent)]
pub struct ProductVariantId(pub RecordId);
#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct DetailedProductVariant {
pub id: ProductVariantId,
pub name: ProductVariantName,
pub short_description: ProductShortDesc,
pub long_description: ProductLongDesc,
pub price: Price,
pub quantity_unit: QuantityUnit,
pub stocks: Vec<Stock>,
pub photos: Vec<crate::api::Photo>,
pub available: bool,
}
#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct DetailedProduct {
pub id: ProductId,
pub name: ProductName,
pub category: Option<crate::api::Category>,
pub deliver_days_flag: Days,
pub variants: Vec<DetailedProductVariant>,
}
impl DetailedProduct {
pub fn variant_for(&self, variant_id: ProductVariantId) -> Option<&DetailedProductVariant> {
self.variants.iter().find(|v| v.id == variant_id)
}
}
#[cfg_attr(feature = "db", derive(sqlx::FromRow))]
#[derive(Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct Product {
pub id: ProductId,
pub name: ProductName,
pub category: Option<ProductCategory>,
pub deliver_days_flag: Days,
}
#[cfg_attr(feature = "db", derive(sqlx::FromRow))]
#[derive(Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProductVariant {
pub id: ProductVariantId,
pub product_id: ProductId,
pub name: ProductVariantName,
pub short_description: ProductShortDesc,
pub long_description: ProductLongDesc,
pub price: Price,
pub quantity_unit: QuantityUnit,
}
#[cfg_attr(feature = "db", derive(sqlx::FromRow))]
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Stock {
pub id: StockId,
pub product_variant_id: ProductVariantId,
pub quantity: Quantity,
pub quantity_unit: QuantityUnit,
}
#[cfg_attr(feature = "db", derive(sqlx::FromRow))]
#[derive(Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct Photo {
pub id: PhotoId,
pub local_path: LocalPath,
pub file_name: FileName,
pub unique_name: UniqueName,
}
#[cfg_attr(feature = "db", derive(sqlx::FromRow))]
#[derive(Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProductLinkedPhoto {
pub photo_id: PhotoId,
pub local_path: LocalPath,
pub file_name: FileName,
pub unique_name: UniqueName,
pub product_variant_id: ProductVariantId,
}
#[cfg_attr(feature = "db", derive(sqlx::FromRow))]
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProductPhoto {
pub id: ProductPhotoId,
pub product_variant_id: ProductVariantId,
pub photo_id: PhotoId,
}
#[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))]
#[derive(
Debug,
Clone,
Default,
PartialOrd,
PartialEq,
Eq,
Hash,
Serialize,
Deserialize,
Deref,
From,
Display,
)]
#[serde(transparent)]
pub struct CategoryId(NonNegative);
#[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))]
#[derive(
Debug,
Clone,
Default,
PartialOrd,
PartialEq,
Eq,
Hash,
Serialize,
Deserialize,
Deref,
From,
Display,
)]
#[serde(transparent)]
pub struct CategoryName(pub String);
impl<'s> From<&'s str> for CategoryName {
fn from(value: &'s str) -> Self {
Self(value.into())
}
}
#[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))]
#[derive(
Debug,
Clone,
Default,
PartialOrd,
PartialEq,
Eq,
Hash,
Serialize,
Deserialize,
Deref,
From,
Display,
)]
#[serde(transparent)]
pub struct CategoryKey(pub String);
impl<'s> From<&'s str> for CategoryKey {
fn from(value: &'s str) -> Self {
Self(value.into())
}
}
#[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))]
#[derive(
Debug,
Clone,
Default,
PartialOrd,
PartialEq,
Eq,
Hash,
Serialize,
Deserialize,
Deref,
From,
Display,
)]
#[serde(transparent)]
pub struct CategorySvg(pub String);
impl<'s> From<&'s str> for CategorySvg {
fn from(value: &'s str) -> Self {
Self(value.into())
}
}
#[cfg_attr(feature = "db", derive(sqlx::FromRow))]
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Category {
pub id: CategoryId,
pub parent_id: Option<CategoryId>,
pub name: CategoryName,
pub key: CategoryKey,
pub svg: CategorySvg,
}
}

View File

@ -28,6 +28,7 @@ CREATE TABLE product_variants (
short_description character varying NOT NULL,
long_description character varying NOT NULL,
price integer NOT NULL,
quantity_unit "QuantityUnit" NOT NULL,
CONSTRAINT non_negative CHECK ((price >= 0))
);
@ -44,3 +45,11 @@ CREATE TABLE product_photos (
product_variant_id integer REFERENCES product_variants(id) ON DELETE CASCADE NOT NULL,
photo_id integer REFERENCES photos(id) NOT NULL
);
CREATE TABLE categories (
id serial NOT NULL PRIMARY KEY,
parent_id int references categories (id) ON DELETE CASCADE,
"name" varchar not null,
"key" varchar not null,
"svg" varchar not null
);

View File

@ -2,9 +2,11 @@ use channels::stocks::{detailed_product, detailed_products, Error};
use channels::AsyncClient;
use config::SharedAppConfig;
use db_utils::PgT;
use model::Limit;
use model::{Limit, Offset};
use crate::db::{Database, PhotosForProductVariants, ProductVariantsStock, ProductsVariants};
use crate::db::{
AllCategories, Database, PhotosForProductVariants, ProductVariantsStock, ProductsVariants,
};
use crate::{begin_t, dbm_run};
pub async fn detailed_product(
@ -26,7 +28,7 @@ async fn inner_detailed_product(
input: detailed_product::Input,
t: &mut PgT<'_>,
_mqtt: Option<AsyncClient>,
_config: Option<SharedAppConfig>,
config: Option<SharedAppConfig>,
) -> detailed_product::Output {
let dbm = crate::db::FindProduct {
product_id: input.product_id,
@ -55,13 +57,29 @@ async fn inner_detailed_product(
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);
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::Output2 { product })
Ok(detailed_product::Details { product })
}
pub async fn detailed_products(
@ -83,7 +101,7 @@ async fn inner_detailed_products(
input: detailed_products::Input,
t: &mut PgT<'_>,
_mqtt: Option<AsyncClient>,
_config: Option<SharedAppConfig>,
config: Option<SharedAppConfig>,
) -> detailed_products::Output {
let dbm = crate::db::AllProducts {
limit: input.limit,
@ -124,6 +142,11 @@ async fn inner_detailed_products(
));
}
};
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);
@ -131,7 +154,19 @@ async fn inner_detailed_products(
let products = products
.into_iter()
.map(|product| utils::map_product(product, &mut variants, &mut stocks, &mut photos))
.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 })
@ -142,6 +177,7 @@ mod utils {
use std::hash::Hash;
use model::v2::*;
use model::CategoryMapper;
pub fn vec_to_hash_vec<Id: Hash + Eq, R, F: Fn(&R) -> Id>(
v: Vec<R>,
@ -162,6 +198,8 @@ mod utils {
variants: &mut HashMap<ProductId, Vec<ProductVariant>>,
stocks: &mut HashMap<ProductVariantId, Vec<Stock>>,
photos: &mut HashMap<ProductVariantId, Vec<ProductLinkedPhoto>>,
categories: &[Category],
public_path: &str,
) -> DetailedProduct {
let Product {
id,
@ -172,7 +210,7 @@ mod utils {
DetailedProduct {
id,
name,
category,
category: category.and_then(|name| CategoryMapper::db_into_api(name, categories)),
deliver_days_flag,
variants: variants
.remove(&id)
@ -186,14 +224,38 @@ mod utils {
short_description,
long_description,
price,
}| DetailedProductVariant {
id,
name,
short_description,
long_description,
price,
stocks: stocks.remove(&id).unwrap_or_default(),
photos: photos.remove(&id).unwrap_or_default(),
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(),
@ -233,6 +295,7 @@ mod tests {
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

View File

@ -3,6 +3,7 @@ use channels::AsyncClient;
use config::SharedAppConfig;
use db_utils::PgT;
use model::v2::*;
use model::CategoryMapper;
use crate::begin_t;
use crate::db::Database;
@ -61,6 +62,7 @@ async fn inner_create_product(
short_description: input.product.short_description,
long_description: input.product.long_description,
price: input.product.price,
quantity_unit: input.product.quantity_unit,
};
let variant = match dbm.run(&mut *t).await {
Ok(variant) => variant,
@ -87,7 +89,9 @@ async fn inner_create_product(
product: DetailedProduct {
id: product.id,
name: product.name,
category: product.category,
category: product
.category
.and_then(CategoryMapper::api_from_product_category),
deliver_days_flag: product.deliver_days_flag,
variants: vec![DetailedProductVariant {
id: variant.id,
@ -95,8 +99,10 @@ async fn inner_create_product(
short_description: variant.short_description,
long_description: variant.long_description,
price: variant.price,
quantity_unit: QuantityUnit::Gram,
stocks: vec![stock],
photos: vec![],
available: false,
}],
},
})
@ -259,6 +265,7 @@ mod tests {
long_description: ProductLongDesc::new("long description"),
category: None,
price: Default::default(),
quantity_unit: QuantityUnit::Gram,
deliver_days_flag: Days(vec![]),
},
stock: create_product::StockInput {
@ -286,6 +293,7 @@ mod tests {
long_description: ProductLongDesc::new("long description"),
category: None,
price: Default::default(),
quantity_unit: QuantityUnit::Gram,
deliver_days_flag: Days(vec![]),
},
stock: create_product::StockInput {

View File

@ -215,6 +215,7 @@ mod tests {
short_description: ProductShortDesc::new(fakeit::words::sentence(4)),
long_description: ProductLongDesc::new(fakeit::words::sentence(16)),
price: Price::from_u32(650),
quantity_unit: QuantityUnit::Gram,
}
.run(t)
.await

View File

@ -128,6 +128,7 @@ mod tests {
short_description: ProductShortDesc::new(fakeit::words::sentence(4)),
long_description: ProductLongDesc::new(fakeit::words::sentence(16)),
price: Price::from_u32(650),
quantity_unit: QuantityUnit::Gram,
}
.run(t)
.await

View File

@ -47,6 +47,7 @@ async fn inner_create_product_variant(
short_description: input.short_description,
long_description: input.long_description,
price: input.price,
quantity_unit: input.quantity_unit,
};
match dbm.run(t).await {
Ok(variant) => Ok(create_product_variant::Details {
@ -96,6 +97,7 @@ async fn inner_update_product_variant(
short_description: input.short_description,
long_description: input.long_description,
price: input.price,
quantity_unit: input.quantity_unit,
};
match dbm.run(t).await {
Ok(product_variant) => Ok(update_product_variant::Details { product_variant }),
@ -233,6 +235,7 @@ mod test {
short_description: ProductShortDesc::new(fakeit::words::sentence(4)),
long_description: ProductLongDesc::new(fakeit::words::sentence(16)),
price: Price::from_u32(650),
quantity_unit: QuantityUnit::Gram,
},
t,
)
@ -259,6 +262,7 @@ mod test {
short_description: short_description.clone(),
long_description: long_description.clone(),
price,
quantity_unit: QuantityUnit::Gram,
},
&mut t,
)
@ -296,6 +300,7 @@ mod test {
short_description: short_description.clone(),
long_description: long_description.clone(),
price,
quantity_unit: QuantityUnit::Gram,
},
&mut t,
)
@ -312,6 +317,7 @@ mod test {
short_description,
long_description,
price,
quantity_unit: QuantityUnit::Gram,
};
assert_eq!(res.product_variant, expected);
}

View File

@ -0,0 +1,124 @@
use db_utils::PgT;
use model::v2::{Category, CategoryId, CategoryKey, CategoryName, CategorySvg};
use model::{Limit, Offset};
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Failed to load all categories")]
All,
#[error("Failed to create category")]
Create,
}
pub type Result<T> = std::result::Result<T, Error>;
pub struct CreateCategory {
pub parent_id: Option<CategoryId>,
pub name: CategoryName,
pub key: CategoryKey,
pub svg: CategorySvg,
}
impl CreateCategory {
pub async fn run(self, t: &mut PgT<'_>) -> Result<Category> {
sqlx::query_as(
r#"
INSERT INTO categories (parent_id, name, key, svg)
VALUES ($1, $2, $3, $4)
RETURNING id,
parent_id,
name,
key,
svg
"#,
)
.bind(&self.parent_id)
.bind(&self.name)
.bind(&self.key)
.bind(&self.svg)
.fetch_one(t)
.await
.map_err(|e| {
tracing::warn!("{e:?}");
dbg!(e);
Error::Create
})
}
}
pub struct AllCategories {
pub limit: Limit,
pub offset: Offset,
}
impl AllCategories {
pub async fn run(self, t: &mut PgT<'_>) -> Result<Vec<Category>> {
sqlx::query_as(
r#"
SELECT id,
parent_id,
name,
key,
svg
FROM categories
ORDER BY id ASC
LIMIT $1 OFFSET $2
"#,
)
.bind(self.limit)
.bind(self.offset)
.fetch_all(t)
.await
.map_err(|e| {
tracing::warn!("{e:?}");
dbg!(e);
Error::All
})
}
}
#[cfg(test)]
mod tests {
use config::UpdateConfig;
use db_utils::PgT;
use model::v2::{Category, CategoryName};
use model::{Limit, Offset};
use crate::db::{AllCategories, CreateCategory, Database};
struct NoOpts;
impl UpdateConfig for NoOpts {}
async fn test_category(name: CategoryName, t: &mut PgT<'_>) -> Category {
CreateCategory {
parent_id: None,
name,
key: Default::default(),
svg: Default::default(),
}
.run(t)
.await
.unwrap()
}
#[tokio::test]
async fn load_all() {
testx::db_t_ref!(t);
test_category("Electronics".into(), &mut t).await;
test_category("Shoes".into(), &mut t).await;
test_category("Pants".into(), &mut t).await;
let res = AllCategories {
limit: Limit::from_u32(2000),
offset: Offset::from_u32(0),
}
.run(&mut t)
.await;
testx::db_rollback!(t);
assert_eq!(res.unwrap().len(), 3);
}
}

View File

@ -2,12 +2,14 @@ use config::SharedAppConfig;
use sqlx_core::pool::Pool;
use sqlx_core::postgres::Postgres;
pub mod categories;
pub mod photos;
pub mod product_photos;
pub mod product_variants;
pub mod products;
pub mod stocks;
pub use categories::*;
pub use photos::*;
pub use product_photos::*;
pub use product_variants::*;

View File

@ -184,6 +184,7 @@ mod tests {
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

View File

@ -126,6 +126,7 @@ mod tests {
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

View File

@ -33,7 +33,8 @@ SELECT id,
name,
short_description,
long_description,
price
price,
quantity_unit
FROM product_variants
WHERE
"#,
@ -63,6 +64,7 @@ pub struct CreateProductVariant {
pub short_description: ProductShortDesc,
pub long_description: ProductLongDesc,
pub price: Price,
pub quantity_unit: QuantityUnit,
}
impl CreateProductVariant {
@ -74,14 +76,16 @@ INSERT INTO product_variants (
name,
short_description,
long_description,
price
) VALUES ($1, $2, $3, $4, $5)
price,
quantity_unit
) VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id,
product_id,
name,
short_description,
long_description,
price
price,
quantity_unit
"#,
)
.bind(self.product_id)
@ -89,6 +93,7 @@ RETURNING id,
.bind(self.short_description)
.bind(self.long_description)
.bind(self.price)
.bind(self.quantity_unit)
.fetch_one(pool)
.await
.map_err(|e| {
@ -107,6 +112,7 @@ pub struct UpdateProductVariant {
pub short_description: ProductShortDesc,
pub long_description: ProductLongDesc,
pub price: Price,
pub quantity_unit: QuantityUnit,
}
impl UpdateProductVariant {
@ -118,14 +124,16 @@ SET product_id = $2,
name = $3,
short_description = $4,
long_description = $5,
price = $6
price = $6,
quantity_unit = $7
WHERE id = $1
RETURNING id,
product_id,
name,
short_description,
long_description,
price
price,
quantity_unit
"#,
)
.bind(self.product_variant_id)
@ -134,6 +142,7 @@ RETURNING id,
.bind(self.short_description)
.bind(self.long_description)
.bind(self.price)
.bind(self.quantity_unit)
.fetch_one(pool)
.await
.map_err(|e| {
@ -161,7 +170,8 @@ SELECT pv.id,
pv.name,
pv.short_description,
pv.long_description,
pv.price
pv.price,
pv.quantity_unit
FROM product_variants pv
INNER JOIN products ps
ON pv.product_id = ps.id
@ -203,7 +213,8 @@ RETURNING id,
name,
short_description,
long_description,
price
price,
quantity_unit
"#,
)
.bind(self.product_variant_id)
@ -234,6 +245,7 @@ mod tests {
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
@ -268,6 +280,7 @@ mod tests {
short_description: ProductShortDesc::new("aosdjajsodjaoisdjoajs"),
long_description: ProductLongDesc::new("jsa a98dh 9ahsd ha89shd 98aus 98asu "),
price: Default::default(),
quantity_unit: QuantityUnit::Gram,
};
let res = dbm.run(&mut t).await;

View File

@ -223,6 +223,7 @@ mod tests {
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

View File

@ -1,6 +1,6 @@
use model::api::OrderAddressInput;
use model::v2::ProductVariantId;
use model::{AccessTokenString, AddressId, PaymentMethod, RefreshTokenString};
use model::{AccessTokenString, AddressId, PaymentMethod, ProductId, RefreshTokenString};
use seed::fetch::{Header, Method, Request};
use crate::api::perform;
@ -68,11 +68,13 @@ pub async fn sign_up(input: model::api::CreateAccountInput) -> NetRes<model::api
pub async fn update_cart_item(
access_token: &AccessTokenString,
product_variant_id: ProductVariantId,
product_id: ProductId,
quantity: model::Quantity,
quantity_unit: model::QuantityUnit,
) -> NetRes<model::api::UpdateItemOutput> {
let input = model::api::UpdateItemInput {
product_variant_id,
product_id,
quantity,
quantity_unit,
};
@ -90,7 +92,7 @@ pub async fn update_cart(
access_token: AccessTokenString,
items: Vec<crate::shopping_cart::Item>,
notes: String,
payment_method: Option<model::PaymentMethod>,
payment_method: Option<PaymentMethod>,
) -> NetRes<model::api::UpdateCartOutput> {
let input = model::api::UpdateCartInput {
notes,
@ -99,10 +101,12 @@ pub async fn update_cart(
.map(
|crate::shopping_cart::Item {
product_variant_id,
product_id,
quantity,
quantity_unit,
}| model::api::UpdateItemInput {
product_variant_id,
product_id,
quantity,
quantity_unit,
},

View File

@ -1,6 +1,6 @@
use std::collections::{HashMap, HashSet};
use model::v2::{DetailedProduct, ProductVariantId};
use model::v2::{DetailedProduct, DetailedProductVariant};
use model::ProductId;
use seed::Url;
@ -104,7 +104,7 @@ impl Products {
};
}
pub fn filter_product_ids<F>(&self, filter: F) -> Vec<DetailedProduct>
pub fn filter_product_ids<F>(&self, filter: F) -> Vec<ProductId>
where
F: Fn(&DetailedProduct) -> bool,
{
@ -113,4 +113,19 @@ impl Products {
.filter_map(|id| self.products.get(id).filter(|&p| filter(p)).map(|p| p.id))
.collect()
}
pub fn for_item<'item, 'products>(
&'products self,
item: &'item shopping_cart::Item,
) -> Option<(
&'item shopping_cart::Item,
&'products DetailedProduct,
&'products DetailedProductVariant,
)> {
self.products.get(&item.product_id).and_then(|product| {
product
.variant_for(item.product_variant_id)
.map(|variant| (item, product, variant))
})
}
}

View File

@ -28,22 +28,37 @@ pub mod layout {
use seed::prelude::*;
use seed::*;
pub fn view(
pub fn view_with_categories<'category, Categories>(
model: &crate::Model,
content: Node<crate::Msg>,
categories: Option<&[model::api::Category]>,
) -> Node<crate::Msg> {
let sidebar = match categories {
Some(categories) => {
let sidebar = super::sidebar::view(model, categories);
div![
C!["flex flex-col w-64 h-screen px-4 py-8 overflow-y-auto border-r"],
sidebar
]
}
_ => empty![],
};
categories: Categories,
) -> Node<crate::Msg>
where
Categories: Iterator<Item = &'category model::api::Category>,
{
let sidebar = div![
C!["flex flex-col w-64 h-screen px-4 py-8 overflow-y-auto border-r"],
super::sidebar::view(model, categories)
];
let notifications = crate::shared::notification::view(model);
view(notifications, sidebar, content)
}
pub fn view_without_categories(
model: &crate::Model,
content: Node<crate::Msg>,
) -> Node<crate::Msg> {
let notifications = crate::shared::notification::view(model);
view(notifications, empty![], content)
}
fn view(
notifications: Node<crate::Msg>,
sidebar: Node<crate::Msg>,
content: Node<crate::Msg>,
) -> Node<crate::Msg> {
div![
C!["flex"],
sidebar,
@ -59,8 +74,14 @@ pub mod sidebar {
use crate::pages::Urls;
pub fn view<Msg>(model: &crate::Model, categories: &[model::api::Category]) -> Node<Msg> {
let categories = categories.iter().map(|category| item(model, category));
pub fn view<'category, Msg, Categories>(
model: &crate::Model,
categories: Categories,
) -> Node<Msg>
where
Categories: Iterator<Item = &'category model::api::Category>,
{
let categories = categories.map(|category| item(model, category));
div![
C!["flex flex-col justify-between mt-6"],

View File

@ -149,11 +149,12 @@ pub fn view(model: &crate::Model, page: &CheckoutPage) -> Node<crate::Msg> {
div![
crate::shared::view::public_navbar::view(model, &page.products),
super::layout::view(model, content, None)
super::layout::view_without_categories(model, content)
]
}
mod left_side {
use model::v2::{DetailedProduct, DetailedProductVariant};
use rusty_money::Money;
use seed::prelude::*;
use seed::*;
@ -177,11 +178,23 @@ mod left_side {
.values()
.filter_map(|item: &Item| {
page.products
.product_variants
.get(&item.product_variant_id)
.map(|product| (item, product))
.products
.get(&item.product_id)
.map(|product: &DetailedProduct| (item, product))
})
.map(|(item, variant)| product_view(model, variant, item));
.filter_map(|(item, product): (&Item, &DetailedProduct)| {
product
.variants
.iter()
.find(|v| v.id == item.product_variant_id)
.zip(Some((item, product)))
})
.map(
|(variant, (item, product)): (
&DetailedProductVariant,
(&Item, &DetailedProduct),
)| { product_view(model, product, variant, item) },
);
div![
C!["w-full mx-auto text-gray-800 font-light mb-6 border-b border-gray-200 pb-6"],
products
@ -190,17 +203,18 @@ mod left_side {
fn product_view(
model: &crate::Model,
product: &model::api::v2::ProductVariant,
_product: &DetailedProduct,
variant: &DetailedProductVariant,
item: &Item,
) -> Node<Msg> {
let img = product
let img = variant
.photos
.first()
.as_ref()
.map(|photo| photo.url.as_str())
.unwrap_or_default();
let price = Money::from_minor(
**(product.price * item.quantity) as i64,
**(variant.price * item.quantity) as i64,
model.config.currency,
)
.to_string();
@ -217,7 +231,7 @@ mod left_side {
C!["flex-grow pl-3"],
h6![
C!["font-semibold uppercase text-gray-600"],
product.name.as_str()
variant.name.as_str()
],
p![C!["text-gray-400"], "x ", **item.quantity]
],
@ -242,14 +256,22 @@ mod left_side {
.values()
.filter_map(|item: &Item| {
page.products
.product_variants
.get(&item.product_variant_id)
.products
.get(&item.product_id)
.map(|product| (item, product))
})
.filter_map(|(item, product): (&Item, &DetailedProduct)| {
product
.variants
.iter()
.find(|v| v.id == item.product_variant_id)
.zip(Some((item, product)))
})
.map(
|(item, product): (&Item, &model::api::v2::ProductVariant)| {
**(product.price * item.quantity)
},
|(variant, (item, _product)): (
&DetailedProductVariant,
(&Item, &DetailedProduct),
)| { **(variant.price * item.quantity) },
)
.sum::<i32>() as i64;

View File

@ -1,7 +1,7 @@
use std::collections::HashSet;
use model::v2::ProductVariantId;
use model::Quantity;
use model::v2::{DetailedProduct, DetailedProductVariant};
use model::{ProductId, Quantity};
use seed::app::Orders;
use seed::prelude::*;
use seed::*;
@ -15,7 +15,7 @@ use crate::shopping_cart::CartMsg;
pub struct ListingPage {
pub errors: Vec<String>,
pub filters: HashSet<String>,
pub visible_product_variants: Vec<ProductVariantId>,
pub visible_products: Vec<ProductId>,
pub products: Products,
}
@ -32,7 +32,7 @@ pub fn init(url: Url, orders: &mut impl Orders<ListingMsg>) -> ListingPage {
products: Products::default(),
filters: url_to_filters(url),
errors: vec![],
visible_product_variants: vec![],
visible_products: vec![],
}
}
@ -55,16 +55,16 @@ pub fn page_changed(url: Url, model: &mut ListingPage) {
let ids = {
model
.products
.filter_product_variant_ids(|product| filter_product(&*model, product))
.filter_product_ids(|product| filter_product(&*model, product))
};
model.visible_product_variants = ids;
model.visible_products = ids;
}
fn filter_product(model: &ListingPage, product: &model::api::v2::ProductVariant) -> bool {
product
fn filter_product(model: &ListingPage, variant: &DetailedProduct) -> bool {
variant
.category
.as_ref()
.filter(|c| model.filters.contains(c.key.as_str()))
.filter(|c| model.filters.contains(c.name.as_str()))
.is_some()
}
@ -84,8 +84,8 @@ pub fn update(msg: ListingMsg, model: &mut ListingPage, orders: &mut impl Orders
model.products.update(products.0);
let ids = model
.products
.filter_product_variant_ids(|product| filter_product(model, product));
model.visible_product_variants = ids;
.filter_product_ids(|product| filter_product(model, product));
model.visible_products = ids;
}
ListingMsg::ProductsFetched(NetRes::Error(_))
| ListingMsg::ProductsFetched(NetRes::Http(_)) => {
@ -95,17 +95,17 @@ pub fn update(msg: ListingMsg, model: &mut ListingPage, orders: &mut impl Orders
}
pub fn view(model: &crate::Model, page: &ListingPage) -> Node<crate::Msg> {
let products: Vec<Node<crate::Msg>> = if page.visible_product_variants.is_empty() {
let products: Vec<Vec<Node<crate::Msg>>> = if page.visible_products.is_empty() {
page.products
.product_variant_ids
.product_ids
.iter()
.filter_map(|id| page.products.product_variants.get(id))
.filter_map(|id| page.products.products.get(id))
.map(|p| product(model, p))
.collect()
} else {
page.visible_product_variants
page.visible_products
.iter()
.filter_map(|id| page.products.product_variants.get(id))
.filter_map(|id| page.products.products.get(id))
.map(|p| product(model, p))
.collect()
};
@ -118,17 +118,28 @@ pub fn view(model: &crate::Model, page: &ListingPage) -> Node<crate::Msg> {
div![
crate::shared::view::public_navbar::view(model, &page.products),
super::layout::view(model, content, Some(&page.products.categories))
super::layout::view_with_categories(model, content, page.products.categories.iter())
]
}
fn product(model: &crate::Model, product: &model::api::v2::ProductVariant) -> Node<crate::Msg> {
use rusty_money::Money;
fn product(model: &crate::Model, product: &DetailedProduct) -> Vec<Node<crate::Msg>> {
product
.variants
.iter()
.map(|variant| product_variant(model, product, variant))
.collect()
}
let price = Money::from_minor(**product.price as i64, model.config.currency).to_string();
let _description = product.short_description.as_str();
let name = product.name.as_str();
let img = product
fn product_variant(
model: &crate::Model,
product: &DetailedProduct,
variant: &DetailedProductVariant,
) -> Node<crate::Msg> {
use rusty_money::Money;
let price = Money::from_minor(**variant.price as i64, model.config.currency).to_string();
let _description = variant.short_description.as_str();
let name = variant.name.as_str();
let img = variant
.photos
.first()
.map(|photo| photo.url.as_str())
@ -136,10 +147,11 @@ fn product(model: &crate::Model, product: &model::api::v2::ProductVariant) -> No
let url = Urls::new(&model.url)
.product()
.add_path_part((*product.id as i32).to_string());
.add_path_part((*variant.id as i32).to_string());
let quantity_unit = product.quantity_unit;
let product_variant_id = product.id;
let quantity_unit = variant.quantity_unit;
let product_variant_id = variant.id;
let product_id = product.id;
div![
C!["w-full px-4 lg:px-0"],
@ -176,6 +188,7 @@ fn product(model: &crate::Model, product: &model::api::v2::ProductVariant) -> No
ev.stop_propagation();
crate::Msg::from(CartMsg::AddItem {
product_variant_id,
product_id,
quantity_unit,
quantity: Quantity::try_from(1).unwrap_or_default()
})

View File

@ -1,3 +1,4 @@
use model::v2::{DetailedProduct, DetailedProductVariant, ProductVariantId};
use seed::prelude::*;
use seed::*;
@ -12,6 +13,7 @@ pub enum ProductMsg {
#[derive(Debug, Default)]
pub struct ProductPage {
pub product_id: Option<model::ProductId>,
pub product_variant_id: Option<ProductVariantId>,
pub products: crate::model::Products,
pub selected_image: usize,
}
@ -28,6 +30,7 @@ pub fn init(mut url: Url, orders: &mut impl Orders<crate::Msg>) -> ProductPage {
});
ProductPage {
product_id: Some(product_id),
product_variant_id: None,
products: Default::default(),
selected_image: 0,
}
@ -61,16 +64,24 @@ pub fn view(model: &crate::Model, page: &ProductPage) -> Node<crate::Msg> {
None => return empty!(),
Some(product) => product,
};
let large_photo = product
let variant: &DetailedProductVariant = match page
.product_variant_id
.and_then(|id| product.variant_for(id))
{
Some(v) => v,
_ => return empty![],
};
let large_photo = variant
.photos
.get(page.selected_image)
.map(image)
.unwrap_or_else(|| empty![]);
let small_photos = {
if product.photos.len() <= 1 {
if variant.photos.len() <= 1 {
empty![]
} else {
let photos = product
let photos = variant
.photos
.iter()
.enumerate()
@ -79,13 +90,13 @@ pub fn view(model: &crate::Model, page: &ProductPage) -> Node<crate::Msg> {
}
};
let description = product
let description = variant
.long_description
.as_str()
.split('\n')
.map(|s| div![s]);
let delivery = delivery_available(product, model);
let delivery = delivery_available(product, variant, model);
let content = div![
C!["max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-6"],
@ -106,11 +117,11 @@ pub fn view(model: &crate::Model, page: &ProductPage) -> Node<crate::Msg> {
C!["mb-2 leading-tight tracking-tight font-bold text-gray-800 text-2xl md:text-3xl"],
product.name.as_str()
],
div![model.i18n.t("Price per"), " ", model.i18n.t(product.quantity_unit.name()).to_lowercase()],
div![model.i18n.t("Price per"), " ", model.i18n.t(variant.quantity_unit.name()).to_lowercase()],
div![
delivery
],
action_section(product, model)
action_section(product, variant, model)
],
],
div![
@ -122,39 +133,47 @@ pub fn view(model: &crate::Model, page: &ProductPage) -> Node<crate::Msg> {
div![
crate::shared::view::public_navbar::view(model, &page.products),
super::layout::view(model, content, Some(&page.products.categories))
super::layout::view_with_categories(model, content, page.products.categories.iter())
]
}
fn action_section(product: &model::api::Product, model: &crate::Model) -> Node<ProductMsg> {
if product.available {
fn action_section(
_product: &DetailedProduct,
variant: &DetailedProductVariant,
model: &crate::Model,
) -> Node<ProductMsg> {
if variant.available {
div![
C!["flex py-4 space-x-4"],
div![
C!["relative"],
label![
C!["text-center left-0 pt-2 right-0 absolute block text-xs uppercase text-gray-400 tracking-wide font-semibold"],
attrs!["for" => "quantity"],
model.i18n.t("Qty")
],
input![C![""], attrs!["id" => "quantity", "type" => "number"]]
],
button![
C!["px-6 py-3 text-sm text-white bg-indigo-500 rounded-lg outline-none hover:bg-indigo-600 ring-indigo-300"],
model.i18n.t("Add to Cart"),
ev("click", move |ev| {
ev.prevent_default();
ev.stop_propagation();
None as Option<ProductMsg>
})
]
]
C!["flex py-4 space-x-4"],
div![
C!["relative"],
label![
C!["text-center left-0 pt-2 right-0 absolute block text-xs uppercase text-gray-400 tracking-wide font-semibold"],
attrs!["for" => "quantity"],
model.i18n.t("Qty")
],
input![C![""], attrs!["id" => "quantity", "type" => "number"]]
],
button![
C!["px-6 py-3 text-sm text-white bg-indigo-500 rounded-lg outline-none hover:bg-indigo-600 ring-indigo-300"],
model.i18n.t("Add to Cart"),
ev("click", move |ev| {
ev.prevent_default();
ev.stop_propagation();
None as Option<ProductMsg>
})
]
]
} else {
div![C!["text-sm text-gray-400 "], model.i18n.t("Out of stock")]
}
}
fn delivery_available(product: &model::api::Product, model: &crate::Model) -> Node<ProductMsg> {
fn delivery_available(
product: &DetailedProduct,
_variant: &DetailedProductVariant,
model: &crate::Model,
) -> Node<ProductMsg> {
match product.deliver_days_flag.len() {
0 => return empty![],
7 => return div![model.i18n.t("Delivery all week")],

View File

@ -1,3 +1,4 @@
use model::v2::{DetailedProduct, DetailedProductVariant};
use model::Quantity;
use seed::prelude::*;
use seed::*;
@ -63,7 +64,7 @@ pub fn view(model: &crate::Model, page: &ShoppingCartPage) -> Node<crate::Msg> {
div![
crate::shared::view::public_navbar::view(model, &page.products),
super::layout::view(model, content, None)
super::layout::view_without_categories(model, content)
]
}
@ -153,6 +154,7 @@ mod summary_left {
}
mod summary_right {
use model::v2::{DetailedProduct, DetailedProductVariant};
use rusty_money::Money;
use seed::prelude::*;
use seed::*;
@ -165,17 +167,13 @@ mod summary_right {
.cart
.items
.values()
.filter_map(|item: &crate::shopping_cart::Item| {
page.products
.product_variants
.get(&item.product_variant_id)
.map(|product| (item, product))
})
.filter_map(|item: &crate::shopping_cart::Item| page.products.for_item(item))
.map(
|(item, product): (
|(item, _product, variant): (
&crate::shopping_cart::Item,
&model::api::v2::ProductVariant,
)| { **(product.price * item.quantity) },
&DetailedProduct,
&DetailedProductVariant,
)| { **(variant.price * item.quantity) },
)
.sum::<i32>() as i64;
div![
@ -390,16 +388,13 @@ fn products_body(model: &crate::Model, page: &ShoppingCartPage) -> Node<crate::M
.cart
.items
.values()
.filter_map(|item: &crate::shopping_cart::Item| {
page.products
.product_variants
.get(&item.product_variant_id)
.map(|product| (item, product))
})
.filter_map(|item: &crate::shopping_cart::Item| page.products.for_item(item))
.map(
|(item, product): (&crate::shopping_cart::Item, &model::api::v2::ProductVariant)| {
item_view(model, item, product)
},
|(item, product, variant): (
&crate::shopping_cart::Item,
&DetailedProduct,
&DetailedProductVariant,
)| { item_view(model, item, product, variant) },
);
tbody![items]
}
@ -407,18 +402,20 @@ fn products_body(model: &crate::Model, page: &ShoppingCartPage) -> Node<crate::M
fn item_view(
model: &crate::Model,
item: &crate::shopping_cart::Item,
product: &model::api::v2::ProductVariant,
product: &DetailedProduct,
variant: &DetailedProductVariant,
) -> Node<crate::Msg> {
use rusty_money::Money;
let img = product
let img = variant
.photos
.first()
.map(|photo| photo.url.as_str())
.unwrap_or_default();
let product_variant_id = product.id;
let quantity_unit = product.quantity_unit;
let product_variant_id = variant.id;
let product_id = product.id;
let quantity_unit = variant.quantity_unit;
let product_url = Urls::new(&model.url)
.product()
.add_path_part(product.id.to_string());
@ -476,6 +473,7 @@ fn item_view(
Some(crate::Msg::from(CartMsg::ModifyItem {
product_variant_id,
product_id,
quantity_unit,
quantity,
}))
@ -488,7 +486,7 @@ fn item_view(
C!["hidden text-right md:table-cell"],
span![
C!["text-sm lg:text-base font-medium"],
Money::from_minor(**product.price as i64, model.config.currency).to_string()
Money::from_minor(**variant.price as i64, model.config.currency).to_string()
]
],
td![
@ -496,7 +494,7 @@ fn item_view(
span![
C!["text-sm lg:text-base font-medium"],
Money::from_minor(
**(product.price * item.quantity) as i64,
**(variant.price * item.quantity) as i64,
model.config.currency
)
.to_string()

View File

@ -62,7 +62,7 @@ pub fn view(model: &crate::Model, page: &SignInPage) -> Node<crate::Msg> {
]
.map_msg(Into::into);
div![super::layout::view(model, content, None)]
div![super::layout::view_without_categories(model, content)]
}
fn sign_in_form(model: &crate::Model, _page: &SignInPage) -> Node<LogInMsg> {

View File

@ -114,7 +114,7 @@ pub fn view(model: &crate::Model, page: &SignUpPage) -> Node<crate::Msg> {
]
]
.map_msg(Into::into);
div![super::layout::view(model, content, None)]
div![super::layout::view_without_categories(model, content)]
}
fn sign_up_form(model: &crate::Model, _page: &SignUpPage) -> Node<RegisterMsg> {

View File

@ -108,36 +108,26 @@ pub mod public_navbar {
}
pub mod cart_dropdown {
use model::ProductId;
use model::v2::{DetailedProduct, DetailedProductVariant};
use seed::prelude::*;
use seed::*;
use crate::shopping_cart::Item;
use crate::{Model, Msg};
macro_rules! filter_products {
($model: expr, $products: expr) => {
$model
.cart
.items
.values()
.filter_map(|item: &Item| filter_product(item, $products, item.product_id))
};
}
macro_rules! filter_product_variants {
($model: expr, $products: expr) => {
$model
.cart
.items
.values()
.filter_map(|item: &Item| filter_product(item, $products, item.product_variant_id))
.filter_map(|item: &Item| $products.for_item(item))
};
}
pub fn view(model: &Model, products: &crate::model::Products) -> Node<Msg> {
let items =
filter_product_variants!(model, products).map(|(it, product)| item(model, it, product));
let items = filter_product_variants!(model, products)
.map(|(it, product, variant)| item(model, it, product, variant));
div![
C![
"absolute w-full rounded-b border-t-0 z-10",
@ -151,15 +141,20 @@ pub mod cart_dropdown {
]
}
fn item(model: &Model, item: &Item, product: &model::api::Product) -> Node<Msg> {
let img = product
fn item(
model: &Model,
item: &Item,
product: &DetailedProduct,
variant: &DetailedProductVariant,
) -> Node<Msg> {
let img = variant
.photos
.first()
.map(|photo| photo.url.as_str())
.map(|photo| photo.unique_name.as_str())
.unwrap_or_default();
let price = rusty_money::Money::from_minor(
**(product.price * item.quantity) as i64,
**(variant.price * item.quantity) as i64,
model.config.currency,
)
.to_string();
@ -169,7 +164,7 @@ pub mod cart_dropdown {
div![
C!["flex-auto text-sm w-32"],
div![C!["font-bold"], product.name.as_str()],
div![C!["truncate"], product.short_description.as_str()],
div![C!["truncate"], variant.short_description.as_str()],
div![
C!["text-gray-400"],
model.i18n.t("Qty:"),
@ -213,7 +208,11 @@ pub mod cart_dropdown {
fn checkout(model: &Model, products: &crate::model::Products) -> Node<Msg> {
let sum: i32 = filter_product_variants!(model, products)
.map(|(item, product): (&Item, &model::api::Product)| **item.quantity * **product.price)
.map(
|(item, _product, variant): (&Item, &DetailedProduct, &DetailedProductVariant)| {
**item.quantity * **variant.price
},
)
.sum();
let sum = rusty_money::Money::from_minor(sum as i64, model.config.currency);
@ -230,15 +229,4 @@ pub mod cart_dropdown {
]
]
}
fn filter_product<'item, 'product>(
item: &'item Item,
products: &'product crate::model::Products,
product_id: ProductId,
) -> Option<(&'item Item, &'product model::api::Product)> {
products
.products
.get(&product_id)
.map(|product| (item, product))
}
}

View File

@ -12,11 +12,13 @@ pub enum CartMsg {
quantity: Quantity,
quantity_unit: QuantityUnit,
product_variant_id: ProductVariantId,
product_id: ProductId,
},
ModifyItem {
quantity: Quantity,
quantity_unit: QuantityUnit,
product_variant_id: ProductVariantId,
product_id: ProductId,
},
Remove(ProductVariantId),
Hover,
@ -34,12 +36,14 @@ impl From<CartMsg> for Msg {
}
}
pub type Items = indexmap::IndexMap<ProductId, Item>;
pub type Items = indexmap::IndexMap<ProductVariantId, Item>;
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
pub struct Item {
#[serde(rename = "i")]
#[serde(rename = "v")]
pub product_variant_id: ProductVariantId,
#[serde(rename = "p")]
pub product_id: ProductId,
#[serde(rename = "q")]
pub quantity: Quantity,
#[serde(rename = "u")]
@ -70,6 +74,7 @@ pub fn update(msg: CartMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
quantity,
quantity_unit,
product_variant_id,
product_id,
} => {
{
let items: &mut Items = &mut model.cart.items;
@ -77,6 +82,7 @@ pub fn update(msg: CartMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
quantity: Quantity::from_u32(0),
quantity_unit,
product_variant_id,
product_id,
});
entry.quantity = entry.quantity + quantity;
entry.quantity_unit = quantity_unit;
@ -85,6 +91,7 @@ pub fn update(msg: CartMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
sync_cart(model, orders);
}
CartMsg::ModifyItem {
product_id,
product_variant_id,
quantity_unit,
quantity,
@ -97,6 +104,7 @@ pub fn update(msg: CartMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
quantity,
quantity_unit,
product_variant_id,
product_id,
});
entry.quantity = quantity;
entry.quantity_unit = quantity_unit;
@ -127,6 +135,7 @@ pub fn update(msg: CartMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
model::api::ShoppingCartItem {
id: _,
product_variant_id,
product_id,
shopping_cart_id: _,
quantity,
quantity_unit,
@ -135,6 +144,7 @@ pub fn update(msg: CartMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
product_variant_id,
Item {
product_variant_id,
product_id,
quantity,
quantity_unit,
},