feat: add quantity to Product aggregate #36
12 changed files with 252 additions and 64 deletions
|
@ -17,6 +17,8 @@ CREATE TABLE IF NOT EXISTS cqrs_inventory_product_query
|
||||||
price_major INTEGER NOT NULL,
|
price_major INTEGER NOT NULL,
|
||||||
price_currency TEXT NOT NULL,
|
price_currency TEXT NOT NULL,
|
||||||
|
|
||||||
|
quantity_number INTEGER NOT NULL,
|
||||||
|
quantity_unit TEXT NOT NULL,
|
||||||
|
|
||||||
category_id UUID NOT NULL,
|
category_id UUID NOT NULL,
|
||||||
|
|
||||||
|
|
|
@ -60,7 +60,7 @@ mod tests {
|
||||||
assert!(!db.category_id_exists(&category).await.unwrap());
|
assert!(!db.category_id_exists(&category).await.unwrap());
|
||||||
|
|
||||||
create_dummy_category_record(&category, &db).await;
|
create_dummy_category_record(&category, &db).await;
|
||||||
|
|
||||||
// state exists
|
// state exists
|
||||||
assert!(db.category_id_exists(&category).await.unwrap());
|
assert!(db.category_id_exists(&category).await.unwrap());
|
||||||
|
|
||||||
|
|
|
@ -53,6 +53,7 @@ pub mod tests {
|
||||||
.image(cmd.image().as_ref().map(|s| s.to_string()))
|
.image(cmd.image().as_ref().map(|s| s.to_string()))
|
||||||
.sku_able(cmd.sku_able().clone())
|
.sku_able(cmd.sku_able().clone())
|
||||||
.category_id(cmd.category_id().clone())
|
.category_id(cmd.category_id().clone())
|
||||||
|
.quantity(cmd.quantity().clone())
|
||||||
.product_id(UUID.clone())
|
.product_id(UUID.clone())
|
||||||
.price(cmd.price().clone())
|
.price(cmd.price().clone())
|
||||||
.build()
|
.build()
|
||||||
|
@ -81,9 +82,11 @@ pub mod tests {
|
||||||
price_major,
|
price_major,
|
||||||
price_minor,
|
price_minor,
|
||||||
price_currency,
|
price_currency,
|
||||||
sku_able
|
sku_able,
|
||||||
|
quantity_unit,
|
||||||
|
quantity_number
|
||||||
) VALUES (
|
) VALUES (
|
||||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10
|
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12
|
||||||
);",
|
);",
|
||||||
1,
|
1,
|
||||||
p.name(),
|
p.name(),
|
||||||
|
@ -94,7 +97,9 @@ pub mod tests {
|
||||||
p.price().major().clone() as i32,
|
p.price().major().clone() as i32,
|
||||||
p.price().minor().clone() as i32,
|
p.price().minor().clone() as i32,
|
||||||
p.price().currency().to_string(),
|
p.price().currency().to_string(),
|
||||||
p.sku_able().clone()
|
p.sku_able().clone(),
|
||||||
|
p.quantity().unit().to_string(),
|
||||||
|
p.quantity().number().clone() as i32,
|
||||||
)
|
)
|
||||||
.execute(&db.pool)
|
.execute(&db.pool)
|
||||||
.await
|
.await
|
||||||
|
|
|
@ -62,6 +62,7 @@ mod tests {
|
||||||
.category_id(cmd.category_id().clone())
|
.category_id(cmd.category_id().clone())
|
||||||
.product_id(UUID.clone())
|
.product_id(UUID.clone())
|
||||||
.price(cmd.price().clone())
|
.price(cmd.price().clone())
|
||||||
|
.quantity(cmd.quantity().clone())
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ use super::errors::*;
|
||||||
use super::InventoryDBPostgresAdapter;
|
use super::InventoryDBPostgresAdapter;
|
||||||
use crate::inventory::domain::events::InventoryEvent;
|
use crate::inventory::domain::events::InventoryEvent;
|
||||||
use crate::inventory::domain::product_aggregate::{
|
use crate::inventory::domain::product_aggregate::{
|
||||||
Currency, PriceBuilder, Product, ProductBuilder,
|
Currency, PriceBuilder, Product, ProductBuilder, QuantityBuilder, QuantityUnit,
|
||||||
};
|
};
|
||||||
use crate::utils::parse_aggregate_id::parse_aggregate_id;
|
use crate::utils::parse_aggregate_id::parse_aggregate_id;
|
||||||
|
|
||||||
|
@ -34,6 +34,9 @@ pub struct ProductView {
|
||||||
price_major: i32,
|
price_major: i32,
|
||||||
price_currency: String,
|
price_currency: String,
|
||||||
|
|
||||||
|
quantity_unit: String,
|
||||||
|
quantity_number: i32,
|
||||||
|
|
||||||
category_id: Uuid,
|
category_id: Uuid,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,6 +49,12 @@ impl From<ProductView> for Product {
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
let quantity = QuantityBuilder::default()
|
||||||
|
.number(v.quantity_number as usize)
|
||||||
|
.unit(QuantityUnit::from_str(&v.quantity_unit).unwrap())
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
ProductBuilder::default()
|
ProductBuilder::default()
|
||||||
.name(v.name)
|
.name(v.name)
|
||||||
.description(v.description)
|
.description(v.description)
|
||||||
|
@ -53,6 +62,7 @@ impl From<ProductView> for Product {
|
||||||
.sku_able(v.sku_able)
|
.sku_able(v.sku_able)
|
||||||
.price(price)
|
.price(price)
|
||||||
.category_id(v.category_id)
|
.category_id(v.category_id)
|
||||||
|
.quantity(quantity)
|
||||||
.product_id(v.product_id)
|
.product_id(v.product_id)
|
||||||
.build()
|
.build()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
@ -77,6 +87,9 @@ impl View<Product> for ProductView {
|
||||||
self.price_minor = val.price().minor().clone() as i32;
|
self.price_minor = val.price().minor().clone() as i32;
|
||||||
self.price_major = val.price().major().clone() as i32;
|
self.price_major = val.price().major().clone() as i32;
|
||||||
self.price_currency = val.price().currency().to_string();
|
self.price_currency = val.price().currency().to_string();
|
||||||
|
|
||||||
|
self.quantity_number = val.quantity().number().clone() as i32;
|
||||||
|
self.quantity_unit = val.quantity().unit().to_string();
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
|
@ -102,7 +115,9 @@ impl ViewRepository<ProductView, Product> for InventoryDBPostgresAdapter {
|
||||||
price_major,
|
price_major,
|
||||||
price_minor,
|
price_minor,
|
||||||
price_currency,
|
price_currency,
|
||||||
sku_able
|
sku_able,
|
||||||
|
quantity_unit,
|
||||||
|
quantity_number
|
||||||
FROM
|
FROM
|
||||||
cqrs_inventory_product_query
|
cqrs_inventory_product_query
|
||||||
WHERE
|
WHERE
|
||||||
|
@ -135,7 +150,9 @@ impl ViewRepository<ProductView, Product> for InventoryDBPostgresAdapter {
|
||||||
price_major,
|
price_major,
|
||||||
price_minor,
|
price_minor,
|
||||||
price_currency,
|
price_currency,
|
||||||
sku_able
|
sku_able,
|
||||||
|
quantity_unit,
|
||||||
|
quantity_number
|
||||||
FROM
|
FROM
|
||||||
cqrs_inventory_product_query
|
cqrs_inventory_product_query
|
||||||
WHERE
|
WHERE
|
||||||
|
@ -188,9 +205,11 @@ impl ViewRepository<ProductView, Product> for InventoryDBPostgresAdapter {
|
||||||
price_major,
|
price_major,
|
||||||
price_minor,
|
price_minor,
|
||||||
price_currency,
|
price_currency,
|
||||||
sku_able
|
sku_able,
|
||||||
|
quantity_unit,
|
||||||
|
quantity_number
|
||||||
) VALUES (
|
) VALUES (
|
||||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10
|
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12
|
||||||
);",
|
);",
|
||||||
version,
|
version,
|
||||||
view.name,
|
view.name,
|
||||||
|
@ -201,7 +220,9 @@ impl ViewRepository<ProductView, Product> for InventoryDBPostgresAdapter {
|
||||||
view.price_major,
|
view.price_major,
|
||||||
view.price_minor,
|
view.price_minor,
|
||||||
view.price_currency,
|
view.price_currency,
|
||||||
view.sku_able
|
view.sku_able,
|
||||||
|
view.quantity_unit,
|
||||||
|
view.quantity_number,
|
||||||
)
|
)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
|
@ -222,7 +243,9 @@ impl ViewRepository<ProductView, Product> for InventoryDBPostgresAdapter {
|
||||||
price_major = $7,
|
price_major = $7,
|
||||||
price_minor = $8,
|
price_minor = $8,
|
||||||
price_currency = $9,
|
price_currency = $9,
|
||||||
sku_able = $10;",
|
sku_able = $10,
|
||||||
|
quantity_unit = $11,
|
||||||
|
quantity_number = $12;",
|
||||||
version,
|
version,
|
||||||
view.name,
|
view.name,
|
||||||
view.description,
|
view.description,
|
||||||
|
@ -232,7 +255,9 @@ impl ViewRepository<ProductView, Product> for InventoryDBPostgresAdapter {
|
||||||
view.price_major,
|
view.price_major,
|
||||||
view.price_minor,
|
view.price_minor,
|
||||||
view.price_currency,
|
view.price_currency,
|
||||||
view.sku_able
|
view.sku_able,
|
||||||
|
view.quantity_unit,
|
||||||
|
view.quantity_number
|
||||||
)
|
)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
|
|
|
@ -59,6 +59,7 @@ impl AddProductUseCase for AddProductService {
|
||||||
.sku_able(cmd.sku_able().clone())
|
.sku_able(cmd.sku_able().clone())
|
||||||
.price(cmd.price().clone())
|
.price(cmd.price().clone())
|
||||||
.category_id(cmd.category_id().clone())
|
.category_id(cmd.category_id().clone())
|
||||||
|
.quantity(cmd.quantity().clone())
|
||||||
.product_id(product_id)
|
.product_id(product_id)
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -80,6 +81,7 @@ impl AddProductUseCase for AddProductService {
|
||||||
.price(product.price().clone())
|
.price(product.price().clone())
|
||||||
.category_id(product.category_id().clone())
|
.category_id(product.category_id().clone())
|
||||||
.product_id(product.product_id().clone())
|
.product_id(product.product_id().clone())
|
||||||
|
.quantity(product.quantity().clone())
|
||||||
.build()
|
.build()
|
||||||
.unwrap())
|
.unwrap())
|
||||||
}
|
}
|
||||||
|
@ -89,8 +91,6 @@ impl AddProductUseCase for AddProductService {
|
||||||
pub mod tests {
|
pub mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use crate::inventory::domain::add_product_command::tests::get_command;
|
use crate::inventory::domain::add_product_command::tests::get_command;
|
||||||
use crate::utils::uuid::tests::UUID;
|
use crate::utils::uuid::tests::UUID;
|
||||||
use crate::{tests::bdd::*, utils::uuid::tests::mock_get_uuid};
|
use crate::{tests::bdd::*, utils::uuid::tests::mock_get_uuid};
|
||||||
|
@ -109,6 +109,7 @@ pub mod tests {
|
||||||
.category_id(cmd.category_id().clone())
|
.category_id(cmd.category_id().clone())
|
||||||
.product_id(UUID.clone())
|
.product_id(UUID.clone())
|
||||||
.price(cmd.price().clone())
|
.price(cmd.price().clone())
|
||||||
|
.quantity(cmd.quantity().clone())
|
||||||
.added_by_user(cmd.adding_by().clone())
|
.added_by_user(cmd.adding_by().clone())
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -146,6 +147,7 @@ pub mod tests {
|
||||||
assert_eq!(res.added_by_user(), cmd.adding_by());
|
assert_eq!(res.added_by_user(), cmd.adding_by());
|
||||||
assert_eq!(res.category_id(), cmd.category_id());
|
assert_eq!(res.category_id(), cmd.category_id());
|
||||||
assert_eq!(res.product_id(), &UUID);
|
assert_eq!(res.product_id(), &UUID);
|
||||||
|
assert_eq!(res.quantity(), cmd.quantity());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
|
|
|
@ -27,11 +27,11 @@ impl From<InventoryDBError> for InventoryError {
|
||||||
InventoryDBError::DuplicateStoreID => {
|
InventoryDBError::DuplicateStoreID => {
|
||||||
error!("DuplicateStoreID");
|
error!("DuplicateStoreID");
|
||||||
Self::InternalError
|
Self::InternalError
|
||||||
},
|
}
|
||||||
InventoryDBError::DuplicateProductID => {
|
InventoryDBError::DuplicateProductID => {
|
||||||
error!("DuplicateProductID");
|
error!("DuplicateProductID");
|
||||||
Self::InternalError
|
Self::InternalError
|
||||||
},
|
}
|
||||||
InventoryDBError::DuplicateCategoryID => {
|
InventoryDBError::DuplicateCategoryID => {
|
||||||
error!("DuplicateCategoryID");
|
error!("DuplicateCategoryID");
|
||||||
Self::InternalError
|
Self::InternalError
|
||||||
|
|
|
@ -8,7 +8,7 @@ use derive_more::{Display, Error};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use super::product_aggregate::Price;
|
use super::product_aggregate::{Price, Quantity};
|
||||||
|
|
||||||
#[derive(Debug, Error, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Debug, Error, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
pub enum AddProductCommandError {
|
pub enum AddProductCommandError {
|
||||||
|
@ -24,6 +24,7 @@ pub struct UnvalidatedAddProductCommand {
|
||||||
image: Option<String>,
|
image: Option<String>,
|
||||||
category_id: Uuid,
|
category_id: Uuid,
|
||||||
sku_able: bool,
|
sku_able: bool,
|
||||||
|
quantity: Quantity,
|
||||||
price: Price,
|
price: Price,
|
||||||
adding_by: Uuid,
|
adding_by: Uuid,
|
||||||
}
|
}
|
||||||
|
@ -36,6 +37,7 @@ pub struct AddProductCommand {
|
||||||
category_id: Uuid,
|
category_id: Uuid,
|
||||||
sku_able: bool,
|
sku_able: bool,
|
||||||
price: Price,
|
price: Price,
|
||||||
|
quantity: Quantity,
|
||||||
adding_by: Uuid,
|
adding_by: Uuid,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,6 +77,7 @@ impl UnvalidatedAddProductCommand {
|
||||||
category_id: self.category_id,
|
category_id: self.category_id,
|
||||||
sku_able: self.sku_able,
|
sku_able: self.sku_able,
|
||||||
price: self.price,
|
price: self.price,
|
||||||
|
quantity: self.quantity,
|
||||||
adding_by: self.adding_by,
|
adding_by: self.adding_by,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -85,7 +88,9 @@ pub mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
inventory::domain::product_aggregate::{Currency, PriceBuilder},
|
inventory::domain::product_aggregate::{
|
||||||
|
Currency, PriceBuilder, QuantityBuilder, QuantityUnit,
|
||||||
|
},
|
||||||
utils::uuid::tests::UUID,
|
utils::uuid::tests::UUID,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -104,12 +109,19 @@ pub mod tests {
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
let quantity = QuantityBuilder::default()
|
||||||
|
.number(1)
|
||||||
|
.unit(QuantityUnit::DiscreteNumber)
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let cmd = UnvalidatedAddProductCommandBuilder::default()
|
let cmd = UnvalidatedAddProductCommandBuilder::default()
|
||||||
.name(name.into())
|
.name(name.into())
|
||||||
.description(description.clone())
|
.description(description.clone())
|
||||||
.image(image.clone())
|
.image(image.clone())
|
||||||
.category_id(category_id.clone())
|
.category_id(category_id.clone())
|
||||||
.adding_by(adding_by.clone())
|
.adding_by(adding_by.clone())
|
||||||
|
.quantity(quantity)
|
||||||
.sku_able(sku_able)
|
.sku_able(sku_able)
|
||||||
.price(price.clone())
|
.price(price.clone())
|
||||||
.build()
|
.build()
|
||||||
|
@ -131,6 +143,11 @@ pub mod tests {
|
||||||
.currency(Currency::INR)
|
.currency(Currency::INR)
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
let quantity = QuantityBuilder::default()
|
||||||
|
.number(1)
|
||||||
|
.unit(QuantityUnit::DiscreteNumber)
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
// description = None
|
// description = None
|
||||||
let cmd = UnvalidatedAddProductCommandBuilder::default()
|
let cmd = UnvalidatedAddProductCommandBuilder::default()
|
||||||
|
@ -139,6 +156,7 @@ pub mod tests {
|
||||||
.image(None)
|
.image(None)
|
||||||
.category_id(category_id.clone())
|
.category_id(category_id.clone())
|
||||||
.adding_by(adding_by.clone())
|
.adding_by(adding_by.clone())
|
||||||
|
.quantity(quantity.clone())
|
||||||
.sku_able(sku_able)
|
.sku_able(sku_able)
|
||||||
.price(price.clone())
|
.price(price.clone())
|
||||||
.build()
|
.build()
|
||||||
|
@ -153,6 +171,7 @@ pub mod tests {
|
||||||
assert_eq!(cmd.image(), &None);
|
assert_eq!(cmd.image(), &None);
|
||||||
assert_eq!(cmd.sku_able(), &sku_able);
|
assert_eq!(cmd.sku_able(), &sku_able);
|
||||||
assert_eq!(cmd.price(), &price);
|
assert_eq!(cmd.price(), &price);
|
||||||
|
assert_eq!(cmd.quantity(), &quantity);
|
||||||
}
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn test_description_some() {
|
fn test_description_some() {
|
||||||
|
@ -169,12 +188,18 @@ pub mod tests {
|
||||||
.currency(Currency::INR)
|
.currency(Currency::INR)
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
let quantity = QuantityBuilder::default()
|
||||||
|
.number(1)
|
||||||
|
.unit(QuantityUnit::DiscreteNumber)
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let cmd = UnvalidatedAddProductCommandBuilder::default()
|
let cmd = UnvalidatedAddProductCommandBuilder::default()
|
||||||
.name(name.into())
|
.name(name.into())
|
||||||
.description(description.clone())
|
.description(description.clone())
|
||||||
.image(image.clone())
|
.image(image.clone())
|
||||||
.category_id(category_id.clone())
|
.category_id(category_id.clone())
|
||||||
|
.quantity(quantity.clone())
|
||||||
.adding_by(adding_by.clone())
|
.adding_by(adding_by.clone())
|
||||||
.sku_able(sku_able)
|
.sku_able(sku_able)
|
||||||
.price(price.clone())
|
.price(price.clone())
|
||||||
|
@ -190,6 +215,7 @@ pub mod tests {
|
||||||
assert_eq!(cmd.image(), &image);
|
assert_eq!(cmd.image(), &image);
|
||||||
assert_eq!(cmd.sku_able(), &sku_able);
|
assert_eq!(cmd.sku_able(), &sku_able);
|
||||||
assert_eq!(cmd.price(), &price);
|
assert_eq!(cmd.price(), &price);
|
||||||
|
assert_eq!(cmd.quantity(), &quantity);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -206,6 +232,11 @@ pub mod tests {
|
||||||
.currency(Currency::INR)
|
.currency(Currency::INR)
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
let quantity = QuantityBuilder::default()
|
||||||
|
.number(1)
|
||||||
|
.unit(QuantityUnit::DiscreteNumber)
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let cmd = UnvalidatedAddProductCommandBuilder::default()
|
let cmd = UnvalidatedAddProductCommandBuilder::default()
|
||||||
.name("".into())
|
.name("".into())
|
||||||
|
@ -213,6 +244,7 @@ pub mod tests {
|
||||||
.image(image.clone())
|
.image(image.clone())
|
||||||
.category_id(category_id.clone())
|
.category_id(category_id.clone())
|
||||||
.adding_by(adding_by.clone())
|
.adding_by(adding_by.clone())
|
||||||
|
.quantity(quantity)
|
||||||
.sku_able(sku_able)
|
.sku_able(sku_able)
|
||||||
.price(price.clone())
|
.price(price.clone())
|
||||||
.build()
|
.build()
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
// aggregates
|
// aggregates
|
||||||
pub mod category_aggregate;
|
pub mod category_aggregate;
|
||||||
pub mod product_aggregate;
|
pub mod product_aggregate;
|
||||||
//pub mod stock_aggregate;
|
pub mod stock_aggregate;
|
||||||
pub mod store_aggregate;
|
pub mod store_aggregate;
|
||||||
|
|
||||||
// commands
|
// commands
|
||||||
|
|
|
@ -7,7 +7,7 @@ use derive_getters::Getters;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use super::product_aggregate::Price;
|
use super::product_aggregate::{Price, Quantity};
|
||||||
|
|
||||||
#[derive(
|
#[derive(
|
||||||
Clone, Debug, Builder, Serialize, Deserialize, Getters, Eq, PartialEq, Ord, PartialOrd,
|
Clone, Debug, Builder, Serialize, Deserialize, Getters, Eq, PartialEq, Ord, PartialOrd,
|
||||||
|
@ -19,6 +19,7 @@ pub struct ProductAddedEvent {
|
||||||
description: Option<String>,
|
description: Option<String>,
|
||||||
image: Option<String>, // string = file_name
|
image: Option<String>, // string = file_name
|
||||||
price: Price,
|
price: Price,
|
||||||
|
quantity: Quantity,
|
||||||
category_id: Uuid,
|
category_id: Uuid,
|
||||||
sku_able: bool,
|
sku_able: bool,
|
||||||
product_id: Uuid,
|
product_id: Uuid,
|
||||||
|
@ -41,6 +42,7 @@ pub mod tests {
|
||||||
.category_id(cmd.category_id().clone())
|
.category_id(cmd.category_id().clone())
|
||||||
.product_id(UUID.clone())
|
.product_id(UUID.clone())
|
||||||
.price(cmd.price().clone())
|
.price(cmd.price().clone())
|
||||||
|
.quantity(cmd.quantity().clone())
|
||||||
.added_by_user(cmd.adding_by().clone())
|
.added_by_user(cmd.adding_by().clone())
|
||||||
.build()
|
.build()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
|
|
@ -15,17 +15,93 @@ use super::{commands::InventoryCommand, events::InventoryEvent};
|
||||||
use crate::inventory::application::services::errors::*;
|
use crate::inventory::application::services::errors::*;
|
||||||
use crate::inventory::application::services::InventoryServicesInterface;
|
use crate::inventory::application::services::InventoryServicesInterface;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd)]
|
||||||
|
pub enum QuantityUnit {
|
||||||
|
Kilogram,
|
||||||
|
Gram,
|
||||||
|
DiscreteNumber, // example: 1 sofa, 2 bed, etc.
|
||||||
|
MilliLiter,
|
||||||
|
Liter,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for QuantityUnit {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::DiscreteNumber
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const KILO_GRAM: &str = "kg";
|
||||||
|
const GRAM: &str = "g";
|
||||||
|
const DISCRETE_NUMBER: &str = "discrete_number";
|
||||||
|
const MILLI_LITER: &str = "ml";
|
||||||
|
const LITER: &str = "l";
|
||||||
|
|
||||||
|
impl ToString for QuantityUnit {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
match self {
|
||||||
|
Self::Kilogram => KILO_GRAM,
|
||||||
|
Self::Gram => GRAM,
|
||||||
|
Self::DiscreteNumber => DISCRETE_NUMBER,
|
||||||
|
Self::MilliLiter => MILLI_LITER,
|
||||||
|
Self::Liter => LITER,
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for QuantityUnit {
|
||||||
|
type Err = String;
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s.trim() {
|
||||||
|
KILO_GRAM => Ok(Self::Kilogram),
|
||||||
|
GRAM => Ok(Self::Gram),
|
||||||
|
DISCRETE_NUMBER => Ok(Self::DiscreteNumber),
|
||||||
|
MILLI_LITER => Ok(Self::MilliLiter),
|
||||||
|
LITER => Ok(Self::Liter),
|
||||||
|
_ => Err("Currency unsupported".into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(
|
#[derive(
|
||||||
Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder,
|
Clone, Debug, Serialize, Default, Deserialize, Eq, PartialEq, Ord, PartialOrd, Builder, Getters,
|
||||||
)]
|
)]
|
||||||
pub struct Product {
|
pub struct Quantity {
|
||||||
name: String,
|
number: usize,
|
||||||
description: Option<String>,
|
unit: QuantityUnit,
|
||||||
image: Option<String>, // string = file_name
|
}
|
||||||
price: Price,
|
|
||||||
category_id: Uuid,
|
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd)]
|
||||||
sku_able: bool,
|
pub enum Currency {
|
||||||
product_id: Uuid,
|
INR,
|
||||||
|
}
|
||||||
|
|
||||||
|
const INR: &str = "INR";
|
||||||
|
|
||||||
|
impl ToString for Currency {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
match self {
|
||||||
|
Self::INR => INR,
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Currency {
|
||||||
|
type Err = String;
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let s = s.trim();
|
||||||
|
match s {
|
||||||
|
INR => Ok(Self::INR),
|
||||||
|
_ => Err("Currency unsupported".into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Currency {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::INR
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(
|
#[derive(
|
||||||
|
@ -37,35 +113,20 @@ pub struct Price {
|
||||||
currency: Currency,
|
currency: Currency,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd)]
|
#[derive(
|
||||||
pub enum Currency {
|
Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder,
|
||||||
INR,
|
)]
|
||||||
}
|
pub struct Product {
|
||||||
|
name: String,
|
||||||
impl ToString for Currency {
|
description: Option<String>,
|
||||||
fn to_string(&self) -> String {
|
image: Option<String>, // string = file_name
|
||||||
match self {
|
price: Price,
|
||||||
Self::INR => "INR".into(),
|
// stock = Σ (not sold SKU), if SKU is relevant. Where irrelevant; it exists independent of SKU.
|
||||||
}
|
// relevancy is determined Product.sku_able
|
||||||
}
|
quantity: Quantity,
|
||||||
}
|
category_id: Uuid,
|
||||||
|
sku_able: bool,
|
||||||
impl FromStr for Currency {
|
product_id: Uuid,
|
||||||
type Err = String;
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
let s = s.trim();
|
|
||||||
let inr = Self::INR.to_string();
|
|
||||||
match s {
|
|
||||||
inr => Ok(Self::INR),
|
|
||||||
_ => Err("Currency unsupported".into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Currency {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::INR
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
@ -107,6 +168,7 @@ impl Aggregate for Product {
|
||||||
.category_id(e.category_id().clone())
|
.category_id(e.category_id().clone())
|
||||||
.sku_able(e.sku_able().clone())
|
.sku_able(e.sku_able().clone())
|
||||||
.product_id(e.product_id().clone())
|
.product_id(e.product_id().clone())
|
||||||
|
.quantity(e.quantity().clone())
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
@ -151,15 +213,48 @@ mod aggregate_tests {
|
||||||
.then_expect_events(vec![expected]);
|
.then_expect_events(vec![expected]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn test_helper<T>(t: T, str_value: &str) -> bool
|
||||||
|
where
|
||||||
|
T: ToString + FromStr + std::fmt::Debug + PartialEq,
|
||||||
|
<T as FromStr>::Err: std::fmt::Debug,
|
||||||
|
{
|
||||||
|
println!("Testing type: {:?} against value {str_value}", t);
|
||||||
|
assert_eq!(t.to_string(), str_value.to_string());
|
||||||
|
|
||||||
|
assert_eq!(T::from_str(str_value).unwrap(), t);
|
||||||
|
|
||||||
|
assert_eq!(T::from_str(t.to_string().as_str()).unwrap(), t,);
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn currency_to_string_from_str() {
|
fn currency_to_string_from_str() {
|
||||||
assert_eq!(Currency::INR.to_string(), "INR".to_string());
|
assert!(test_helper(Currency::INR, INR));
|
||||||
|
}
|
||||||
|
|
||||||
assert_eq!(Currency::from_str("INR").unwrap(), Currency::INR);
|
#[test]
|
||||||
|
fn quantity_unit_kilogram() {
|
||||||
|
assert!(test_helper(QuantityUnit::Kilogram, KILO_GRAM));
|
||||||
|
}
|
||||||
|
|
||||||
assert_eq!(
|
#[test]
|
||||||
Currency::from_str(Currency::INR.to_string().as_str()).unwrap(),
|
fn quantity_unit_gram() {
|
||||||
Currency::INR
|
assert!(test_helper(QuantityUnit::Gram, GRAM));
|
||||||
);
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn quantity_unit_discrete_number() {
|
||||||
|
assert!(test_helper(QuantityUnit::DiscreteNumber, DISCRETE_NUMBER));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn quantity_unit_milli_liter() {
|
||||||
|
assert!(test_helper(QuantityUnit::MilliLiter, MILLI_LITER));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn quantity_unit_liter() {
|
||||||
|
assert!(test_helper(QuantityUnit::Liter, LITER));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
24
src/inventory/domain/stock_aggregate.rs
Normal file
24
src/inventory/domain/stock_aggregate.rs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
use derive_builder::Builder;
|
||||||
|
use derive_getters::Getters;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use super::product_aggregate::Quantity;
|
||||||
|
|
||||||
|
// stock keeping unit
|
||||||
|
// TODO: will implement later, have to figure out how to print SKU label and during billing.
|
||||||
|
#[derive(
|
||||||
|
Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Builder, Getters,
|
||||||
|
)]
|
||||||
|
pub struct SKU {
|
||||||
|
id: String,
|
||||||
|
product_id: Uuid,
|
||||||
|
expiry: Option<OffsetDateTime>,
|
||||||
|
sold: bool,
|
||||||
|
quantity: Quantity,
|
||||||
|
}
|
Loading…
Reference in a new issue