Compare commits
5 commits
dbbbb86a8c
...
dfc080f26a
Author | SHA1 | Date | |
---|---|---|---|
dfc080f26a | |||
d4acccde9d | |||
39edaead04 | |||
f0da898e62 | |||
384dae69f5 |
18 changed files with 800 additions and 24 deletions
|
@ -0,0 +1,94 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT \n product_name,\n product_id,\n line_item_id,\n quantity_minor_unit,\n quantity_minor_number,\n quantity_major_unit,\n quantity_major_number,\n created_time,\n bill_id,\n price_per_unit_minor,\n price_per_unit_major,\n price_per_unit_currency,\n deleted\n FROM cqrs_billing_line_item_query\n WHERE\n bill_id = $1;",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "product_name",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "product_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "line_item_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "quantity_minor_unit",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "quantity_minor_number",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "quantity_major_unit",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "quantity_major_number",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "created_time",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "bill_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "price_per_unit_minor",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "price_per_unit_major",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "price_per_unit_currency",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "deleted",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "538d43c832702b03da4a51e0b0794785adfb14b4b8ff0ed7c4a7079e711b8ce7"
|
||||
}
|
|
@ -0,0 +1,144 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::BillingDBPostgresAdapter;
|
||||
use crate::billing::{
|
||||
application::port::output::db::{errors::*, get_line_items_for_bill_id::*},
|
||||
domain::line_item_aggregate::LineItem,
|
||||
};
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl GetLineItemsForBillIDDBPort for BillingDBPostgresAdapter {
|
||||
async fn get_line_items_for_bill_id(&self, bill_id: Uuid) -> BillingDBResult<Vec<LineItem>> {
|
||||
let mut res = sqlx::query_as!(
|
||||
super::line_item_view::LineItemView,
|
||||
"SELECT
|
||||
product_name,
|
||||
product_id,
|
||||
line_item_id,
|
||||
quantity_minor_unit,
|
||||
quantity_minor_number,
|
||||
quantity_major_unit,
|
||||
quantity_major_number,
|
||||
created_time,
|
||||
bill_id,
|
||||
price_per_unit_minor,
|
||||
price_per_unit_major,
|
||||
price_per_unit_currency,
|
||||
deleted
|
||||
FROM cqrs_billing_line_item_query
|
||||
WHERE
|
||||
bill_id = $1;",
|
||||
bill_id
|
||||
)
|
||||
.fetch_all(&self.pool)
|
||||
.await?;
|
||||
println!("Got len: {}", res.len());
|
||||
let mut output = Vec::with_capacity(res.len());
|
||||
res.drain(0..).for_each(|r| output.push(r.into()));
|
||||
Ok(output)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
|
||||
use super::*;
|
||||
// use crate::billing::domain::add_product_command::tests::get_customizations;
|
||||
use crate::{
|
||||
billing::{
|
||||
adapters::output::db::postgres::line_item_view::LineItemView, domain::bill_aggregate::*,
|
||||
},
|
||||
types::currency::*,
|
||||
types::quantity::*,
|
||||
utils::uuid::{tests::*, *},
|
||||
};
|
||||
|
||||
async fn create_dummy_line_item(
|
||||
db: &BillingDBPostgresAdapter,
|
||||
bill_id: Uuid,
|
||||
line_item_id: Uuid,
|
||||
) {
|
||||
let view = LineItemView::default();
|
||||
let version = 0;
|
||||
sqlx::query!(
|
||||
"INSERT INTO cqrs_billing_line_item_query (
|
||||
version,
|
||||
product_name,
|
||||
product_id,
|
||||
line_item_id,
|
||||
quantity_minor_unit,
|
||||
quantity_minor_number,
|
||||
quantity_major_unit,
|
||||
quantity_major_number,
|
||||
created_time,
|
||||
bill_id,
|
||||
price_per_unit_minor,
|
||||
price_per_unit_major,
|
||||
price_per_unit_currency,
|
||||
deleted
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14
|
||||
);",
|
||||
version,
|
||||
view.product_name,
|
||||
view.product_id,
|
||||
line_item_id,
|
||||
QuantityUnit::DiscreteNumber.to_string(),
|
||||
view.quantity_minor_number,
|
||||
QuantityUnit::DiscreteNumber.to_string(),
|
||||
view.quantity_major_number,
|
||||
view.created_time,
|
||||
bill_id,
|
||||
view.price_per_unit_minor,
|
||||
view.price_per_unit_major,
|
||||
Currency::INR.to_string(),
|
||||
view.deleted,
|
||||
)
|
||||
.execute(&db.pool)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_postgres_get_line_items_for_bill_id() {
|
||||
let settings = crate::settings::tests::get_settings().await;
|
||||
settings.create_db().await;
|
||||
let db = super::BillingDBPostgresAdapter::new(
|
||||
sqlx::postgres::PgPool::connect(&settings.database.url)
|
||||
.await
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
let bill_id = UUID;
|
||||
|
||||
// state doesn't exist
|
||||
assert!(db
|
||||
.get_line_items_for_bill_id(bill_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_empty());
|
||||
|
||||
let u = GenerateUUID;
|
||||
let li_id_1 = u.get_uuid();
|
||||
let li_id_2 = u.get_uuid();
|
||||
create_dummy_line_item(&db, bill_id, li_id_1).await;
|
||||
create_dummy_line_item(&db, bill_id, li_id_2).await;
|
||||
|
||||
// state exists
|
||||
let res = db.get_line_items_for_bill_id(bill_id).await.unwrap();
|
||||
|
||||
assert_eq!(res.len(), 2);
|
||||
assert!(res
|
||||
.iter()
|
||||
.any(|li| *li.bill_id() == bill_id && *li.line_item_id() == li_id_1));
|
||||
assert!(res
|
||||
.iter()
|
||||
.any(|li| *li.bill_id() == bill_id && *li.line_item_id() == li_id_2));
|
||||
|
||||
settings.drop_db().await;
|
||||
}
|
||||
}
|
|
@ -25,23 +25,23 @@ pub const NEW_LINE_ITEM_NON_UUID: &str = "new_line_item_non_uuid-asdfa-billing";
|
|||
// be designed to reflect the response dto that will be returned to a user.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct LineItemView {
|
||||
product_name: String,
|
||||
product_id: Uuid,
|
||||
bill_id: Uuid,
|
||||
created_time: OffsetDateTime,
|
||||
pub product_name: String,
|
||||
pub product_id: Uuid,
|
||||
pub bill_id: Uuid,
|
||||
pub created_time: OffsetDateTime,
|
||||
|
||||
line_item_id: Uuid,
|
||||
pub line_item_id: Uuid,
|
||||
|
||||
quantity_major_number: i32,
|
||||
quantity_minor_number: i32,
|
||||
quantity_major_unit: String,
|
||||
quantity_minor_unit: String,
|
||||
pub quantity_major_number: i32,
|
||||
pub quantity_minor_number: i32,
|
||||
pub quantity_major_unit: String,
|
||||
pub quantity_minor_unit: String,
|
||||
|
||||
price_per_unit_major: i32,
|
||||
price_per_unit_minor: i32,
|
||||
price_per_unit_currency: String,
|
||||
pub price_per_unit_major: i32,
|
||||
pub price_per_unit_minor: i32,
|
||||
pub price_per_unit_currency: String,
|
||||
|
||||
deleted: bool,
|
||||
pub deleted: bool,
|
||||
}
|
||||
|
||||
impl Default for LineItemView {
|
||||
|
|
|
@ -10,6 +10,7 @@ use crate::db::{migrate::RunMigrations, sqlx_postgres::Postgres};
|
|||
mod bill_id_exists;
|
||||
mod bill_view;
|
||||
mod errors;
|
||||
mod get_line_items_for_bill_id;
|
||||
mod line_item_id_exists;
|
||||
mod line_item_view;
|
||||
mod next_token_id;
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use mockall::predicate::*;
|
||||
use mockall::*;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::billing::domain::line_item_aggregate::LineItem;
|
||||
|
||||
use super::errors::*;
|
||||
#[cfg(test)]
|
||||
#[allow(unused_imports)]
|
||||
pub use tests::*;
|
||||
|
||||
#[automock]
|
||||
#[async_trait::async_trait]
|
||||
pub trait GetLineItemsForBillIDDBPort: Send + Sync {
|
||||
async fn get_line_items_for_bill_id(&self, bill_id: Uuid) -> BillingDBResult<Vec<LineItem>>;
|
||||
}
|
||||
|
||||
pub type GetLineItemsForBillIDDBPortObj = std::sync::Arc<dyn GetLineItemsForBillIDDBPort>;
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn mock_get_line_items_for_bill_id_db_port_no_line_items(
|
||||
times: Option<usize>,
|
||||
) -> GetLineItemsForBillIDDBPortObj {
|
||||
let mut m = MockGetLineItemsForBillIDDBPort::new();
|
||||
if let Some(times) = times {
|
||||
m.expect_get_line_items_for_bill_id()
|
||||
.times(times)
|
||||
.returning(|_| Ok(Vec::default()));
|
||||
} else {
|
||||
m.expect_get_line_items_for_bill_id()
|
||||
.returning(|_| Ok(Vec::default()));
|
||||
}
|
||||
|
||||
Arc::new(m)
|
||||
}
|
||||
|
||||
pub fn mock_get_line_items_for_bill_id_db_port_true(
|
||||
times: Option<usize>,
|
||||
) -> GetLineItemsForBillIDDBPortObj {
|
||||
let mut m = MockGetLineItemsForBillIDDBPort::new();
|
||||
if let Some(times) = times {
|
||||
m.expect_get_line_items_for_bill_id()
|
||||
.times(times)
|
||||
.returning(|_| Ok(vec![LineItem::default()]));
|
||||
} else {
|
||||
m.expect_get_line_items_for_bill_id()
|
||||
.returning(|_| Ok(vec![LineItem::default()]));
|
||||
}
|
||||
|
||||
Arc::new(m)
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
pub mod bill_id_exists;
|
||||
pub mod errors;
|
||||
pub mod get_line_items_for_bill_id;
|
||||
pub mod line_item_id_exists;
|
||||
pub mod next_token_id;
|
||||
pub mod store_id_exists;
|
||||
|
|
|
@ -0,0 +1,210 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use derive_builder::Builder;
|
||||
use mockall::predicate::*;
|
||||
use mockall::*;
|
||||
|
||||
use super::errors::*;
|
||||
use crate::{
|
||||
billing::{
|
||||
application::port::output::db::bill_id_exists::*,
|
||||
application::port::output::db::get_line_items_for_bill_id::*,
|
||||
domain::{
|
||||
bill_aggregate::*, bill_total_price_computed_event::*,
|
||||
compute_bill_total_price_command::*,
|
||||
},
|
||||
},
|
||||
types::currency::*,
|
||||
};
|
||||
|
||||
#[automock]
|
||||
#[async_trait::async_trait]
|
||||
pub trait ComputeBillTotalPriceBillUseCase: Send + Sync {
|
||||
async fn compute_total_price_for_bill(
|
||||
&self,
|
||||
cmd: ComputeBillTotalPriceBillCommand,
|
||||
) -> BillingResult<BillTotalPriceComputedEvent>;
|
||||
}
|
||||
|
||||
pub type ComputeBillTotalPriceBillServiceObj = Arc<dyn ComputeBillTotalPriceBillUseCase>;
|
||||
|
||||
#[derive(Clone, Builder)]
|
||||
pub struct ComputeBillTotalPriceBillService {
|
||||
db_bill_id_exists: BillIDExistsDBPortObj,
|
||||
db_get_line_items_for_bill_id: GetLineItemsForBillIDDBPortObj,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl ComputeBillTotalPriceBillUseCase for ComputeBillTotalPriceBillService {
|
||||
async fn compute_total_price_for_bill(
|
||||
&self,
|
||||
cmd: ComputeBillTotalPriceBillCommand,
|
||||
) -> BillingResult<BillTotalPriceComputedEvent> {
|
||||
if !self.db_bill_id_exists.bill_id_exists(cmd.bill_id()).await? {
|
||||
return Err(BillingError::BillIDNotFound);
|
||||
}
|
||||
|
||||
let line_items = self
|
||||
.db_get_line_items_for_bill_id
|
||||
.get_line_items_for_bill_id(*cmd.bill_id())
|
||||
.await?;
|
||||
if line_items.is_empty() {
|
||||
todo!("Can't compute bill");
|
||||
}
|
||||
|
||||
let mut total_price = PriceBuilder::default()
|
||||
.major(0)
|
||||
.minor(0)
|
||||
.currency(Currency::INR)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
for li in line_items.iter() {
|
||||
total_price = total_price.clone() + li.total_price();
|
||||
}
|
||||
// TODO: 3. Tax?
|
||||
|
||||
Ok(BillTotalPriceComputedEventBuilder::default()
|
||||
.added_by_user(*cmd.adding_by())
|
||||
.bill_id(*cmd.bill_id())
|
||||
.total_price(total_price)
|
||||
.build()
|
||||
.unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use time::macros::datetime;
|
||||
|
||||
use super::*;
|
||||
|
||||
use crate::billing::domain::bill_total_price_computed_event::tests::get_bill_total_computed_event_from_command;
|
||||
use crate::billing::domain::line_item_aggregate::LineItemBuilder;
|
||||
use crate::tests::bdd::*;
|
||||
use crate::types::quantity::*;
|
||||
use crate::utils::uuid::tests::*;
|
||||
|
||||
pub fn mock_compute_bill_total_price_service(
|
||||
times: Option<usize>,
|
||||
cmd: ComputeBillTotalPriceBillCommand,
|
||||
) -> ComputeBillTotalPriceBillServiceObj {
|
||||
let mut m = MockComputeBillTotalPriceBillUseCase::new();
|
||||
|
||||
let res = get_bill_total_computed_event_from_command(&cmd);
|
||||
|
||||
if let Some(times) = times {
|
||||
m.expect_compute_total_price_for_bill()
|
||||
.times(times)
|
||||
.returning(move |_| Ok(res.clone()));
|
||||
} else {
|
||||
m.expect_compute_total_price_for_bill()
|
||||
.returning(move |_| Ok(res.clone()));
|
||||
}
|
||||
|
||||
Arc::new(m)
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_service() {
|
||||
let cmd = ComputeBillTotalPriceBillCommand::get_cmd();
|
||||
|
||||
let s = ComputeBillTotalPriceBillServiceBuilder::default()
|
||||
.db_bill_id_exists(mock_bill_id_exists_db_port_true(IS_CALLED_ONLY_ONCE))
|
||||
.db_get_line_items_for_bill_id(mock_get_line_items_for_bill_id_db_port_true(
|
||||
IS_CALLED_ONLY_ONCE,
|
||||
))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let res = s.compute_total_price_for_bill(cmd.clone()).await.unwrap();
|
||||
assert_eq!(res.total_price().clone(), Price::default());
|
||||
assert_eq!(res.bill_id(), cmd.bill_id());
|
||||
assert_eq!(res.added_by_user(), cmd.adding_by());
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_service_sum() {
|
||||
let cmd = ComputeBillTotalPriceBillCommand::get_cmd();
|
||||
|
||||
let li = LineItemBuilder::default()
|
||||
.created_time(datetime!(1970-01-01 0:00 UTC))
|
||||
.product_name("test_product".into())
|
||||
.product_id(UUID)
|
||||
.quantity(
|
||||
QuantityBuilder::default()
|
||||
.major(
|
||||
QuantityPartBuilder::default()
|
||||
.number(2)
|
||||
.unit(QuantityUnit::DiscreteNumber)
|
||||
.build()
|
||||
.unwrap(),
|
||||
)
|
||||
.minor(
|
||||
QuantityPartBuilder::default()
|
||||
.number(1)
|
||||
.unit(QuantityUnit::DiscreteNumber)
|
||||
.build()
|
||||
.unwrap(),
|
||||
)
|
||||
.build()
|
||||
.unwrap(),
|
||||
)
|
||||
.bill_id(UUID)
|
||||
.price_per_unit(
|
||||
PriceBuilder::default()
|
||||
.major(100)
|
||||
.minor(20)
|
||||
.currency(Currency::INR)
|
||||
.build()
|
||||
.unwrap(),
|
||||
)
|
||||
.line_item_id(UUID)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let mut m = MockGetLineItemsForBillIDDBPort::new();
|
||||
m.expect_get_line_items_for_bill_id()
|
||||
.times(IS_CALLED_ONLY_ONCE.unwrap())
|
||||
.returning(move |_| Ok(vec![li.clone(), li.clone()]));
|
||||
|
||||
let s = ComputeBillTotalPriceBillServiceBuilder::default()
|
||||
.db_bill_id_exists(mock_bill_id_exists_db_port_true(IS_CALLED_ONLY_ONCE))
|
||||
.db_get_line_items_for_bill_id(std::sync::Arc::new(m))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let res = s.compute_total_price_for_bill(cmd.clone()).await.unwrap();
|
||||
assert_eq!(
|
||||
res.total_price().clone(),
|
||||
PriceBuilder::default()
|
||||
.major(100 * 2 * 2)
|
||||
.minor(20 * 2 * 2)
|
||||
.currency(Currency::INR)
|
||||
.build()
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_service_bill_id_doesnt_exist() {
|
||||
let cmd = ComputeBillTotalPriceBillCommand::get_cmd();
|
||||
|
||||
let s = ComputeBillTotalPriceBillServiceBuilder::default()
|
||||
.db_bill_id_exists(mock_bill_id_exists_db_port_false(IS_CALLED_ONLY_ONCE))
|
||||
.db_get_line_items_for_bill_id(mock_get_line_items_for_bill_id_db_port_true(
|
||||
IS_NEVER_CALLED,
|
||||
))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
s.compute_total_price_for_bill(cmd.clone()).await,
|
||||
Err(BillingError::BillIDNotFound)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -12,11 +12,13 @@ pub mod errors;
|
|||
pub mod add_bill_service;
|
||||
pub mod add_line_item_service;
|
||||
pub mod add_store_service;
|
||||
pub mod compute_bill_total_price_service;
|
||||
pub mod delete_bill_service;
|
||||
pub mod delete_line_item_service;
|
||||
pub mod update_bill_service;
|
||||
pub mod update_line_item_service;
|
||||
pub mod update_store_service;
|
||||
// TODO: 2. reset token number for store_id cronjob
|
||||
|
||||
#[automock]
|
||||
pub trait BillingServicesInterface: Send + Sync {
|
||||
|
@ -28,6 +30,9 @@ pub trait BillingServicesInterface: Send + Sync {
|
|||
fn add_line_item(&self) -> add_line_item_service::AddLineItemServiceObj;
|
||||
fn update_line_item(&self) -> update_line_item_service::UpdateLineItemServiceObj;
|
||||
fn delete_line_item(&self) -> delete_line_item_service::DeleteLineItemServiceObj;
|
||||
fn compute_total_price_for_bill(
|
||||
&self,
|
||||
) -> compute_bill_total_price_service::ComputeBillTotalPriceBillServiceObj;
|
||||
}
|
||||
|
||||
#[derive(Clone, Builder)]
|
||||
|
@ -40,6 +45,8 @@ pub struct BillingServices {
|
|||
delete_line_item: delete_line_item_service::DeleteLineItemServiceObj,
|
||||
update_bill: update_bill_service::UpdateBillServiceObj,
|
||||
delete_bill: delete_bill_service::DeleteBillServiceObj,
|
||||
compute_total_price_for_bill:
|
||||
compute_bill_total_price_service::ComputeBillTotalPriceBillServiceObj,
|
||||
}
|
||||
|
||||
impl BillingServicesInterface for BillingServices {
|
||||
|
@ -68,4 +75,9 @@ impl BillingServicesInterface for BillingServices {
|
|||
fn delete_line_item(&self) -> delete_line_item_service::DeleteLineItemServiceObj {
|
||||
self.delete_line_item.clone()
|
||||
}
|
||||
fn compute_total_price_for_bill(
|
||||
&self,
|
||||
) -> compute_bill_total_price_service::ComputeBillTotalPriceBillServiceObj {
|
||||
self.compute_total_price_for_bill.clone()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -100,6 +100,13 @@ impl Aggregate for Bill {
|
|||
let res = services.delete_bill().delete_bill(cmd).await?;
|
||||
Ok(vec![BillingEvent::BillDeleted(res)])
|
||||
}
|
||||
BillingCommand::ComputeBillTotalPriceBill(cmd) => {
|
||||
let res = services
|
||||
.compute_total_price_for_bill()
|
||||
.compute_total_price_for_bill(cmd)
|
||||
.await?;
|
||||
Ok(vec![BillingEvent::BillTotalPriceComputed(res)])
|
||||
}
|
||||
_ => Ok(Vec::default()),
|
||||
}
|
||||
}
|
||||
|
@ -108,6 +115,9 @@ impl Aggregate for Bill {
|
|||
match event {
|
||||
BillingEvent::BillAdded(e) => *self = e.bill().clone(),
|
||||
BillingEvent::BillUpdated(e) => *self = e.new_bill().clone(),
|
||||
BillingEvent::BillTotalPriceComputed(e) => {
|
||||
self.total_price = Some(e.total_price().clone());
|
||||
}
|
||||
BillingEvent::BillDeleted(e) => *self = e.bill().clone(),
|
||||
_ => (),
|
||||
}
|
||||
|
@ -119,6 +129,8 @@ mod aggregate_tests {
|
|||
use std::sync::Arc;
|
||||
|
||||
use add_bill_service::tests::mock_add_bill_service;
|
||||
use compute_bill_total_price_service::tests::mock_compute_bill_total_price_service;
|
||||
use compute_bill_total_price_service::*;
|
||||
use cqrs_es::test::TestFramework;
|
||||
use delete_bill_service::tests::mock_delete_bill_service;
|
||||
use update_bill_service::tests::mock_update_bill_service;
|
||||
|
@ -126,6 +138,8 @@ mod aggregate_tests {
|
|||
use super::*;
|
||||
|
||||
use crate::billing::domain::bill_deleted_event::tests::get_deleted_bill_event_from_command;
|
||||
use crate::billing::domain::bill_total_price_computed_event::tests::get_bill_total_computed_event_from_command;
|
||||
use crate::billing::domain::bill_total_price_computed_event::BillTotalPriceComputedEvent;
|
||||
use crate::billing::domain::bill_updated_event::tests::get_updated_bill_event_from_command;
|
||||
use crate::billing::domain::delete_bill_command::DeleteBillCommand;
|
||||
use crate::billing::domain::update_bill_command::UpdateBillCommand;
|
||||
|
@ -133,6 +147,7 @@ mod aggregate_tests {
|
|||
|
||||
use crate::billing::domain::{
|
||||
add_bill_command::*, bill_added_event::tests::get_added_bill_event_from_command,
|
||||
bill_total_price_computed_event::tests::*, compute_bill_total_price_command::*,
|
||||
};
|
||||
|
||||
type BillTestFramework = TestFramework<Bill>;
|
||||
|
@ -173,6 +188,27 @@ mod aggregate_tests {
|
|||
.then_expect_events(vec![expected]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bill_total_price_computed() {
|
||||
let cmd = ComputeBillTotalPriceBillCommand::get_cmd();
|
||||
let expected = get_bill_total_computed_event_from_command(&cmd);
|
||||
let expected = BillingEvent::BillTotalPriceComputed(expected);
|
||||
|
||||
let mut services = MockBillingServicesInterface::new();
|
||||
services
|
||||
.expect_compute_total_price_for_bill()
|
||||
.times(IS_CALLED_ONLY_ONCE.unwrap())
|
||||
.return_const(mock_compute_bill_total_price_service(
|
||||
IS_CALLED_ONLY_ONCE,
|
||||
cmd.clone(),
|
||||
));
|
||||
|
||||
BillTestFramework::with(Arc::new(services))
|
||||
.given_no_previous_events()
|
||||
.when(BillingCommand::ComputeBillTotalPriceBill(cmd))
|
||||
.then_expect_events(vec![expected]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_bill() {
|
||||
let cmd = DeleteBillCommand::get_cmd();
|
||||
|
|
44
src/billing/domain/bill_total_price_computed_event.rs
Normal file
44
src/billing/domain/bill_total_price_computed_event.rs
Normal file
|
@ -0,0 +1,44 @@
|
|||
// 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 uuid::Uuid;
|
||||
|
||||
use super::bill_aggregate::*;
|
||||
use crate::types::currency::*;
|
||||
|
||||
#[derive(
|
||||
Clone, Debug, Builder, Serialize, Deserialize, Getters, Eq, PartialEq, Ord, PartialOrd,
|
||||
)]
|
||||
pub struct BillTotalPriceComputedEvent {
|
||||
added_by_user: Uuid,
|
||||
bill_id: Uuid,
|
||||
|
||||
total_price: Price,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use crate::billing::domain::compute_bill_total_price_command::*;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub fn get_bill_total_computed_event_from_command(
|
||||
cmd: &ComputeBillTotalPriceBillCommand,
|
||||
) -> BillTotalPriceComputedEvent {
|
||||
BillTotalPriceComputedEventBuilder::default()
|
||||
.added_by_user(cmd.adding_by().clone())
|
||||
.bill_id(*cmd.bill_id())
|
||||
.total_price(Price::default())
|
||||
.build()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_event() {
|
||||
get_bill_total_computed_event_from_command(&ComputeBillTotalPriceBillCommand::get_cmd());
|
||||
}
|
||||
}
|
|
@ -7,9 +7,11 @@ use serde::{Deserialize, Serialize};
|
|||
|
||||
use super::{
|
||||
add_bill_command::AddBillCommand, add_line_item_command::AddLineItemCommand,
|
||||
add_store_command::AddStoreCommand, delete_bill_command::DeleteBillCommand,
|
||||
delete_line_item_command::DeleteLineItemCommand, update_bill_command::UpdateBillCommand,
|
||||
update_line_item_command::UpdateLineItemCommand, update_store_command::UpdateStoreCommand,
|
||||
add_store_command::AddStoreCommand,
|
||||
compute_bill_total_price_command::ComputeBillTotalPriceBillCommand,
|
||||
delete_bill_command::DeleteBillCommand, delete_line_item_command::DeleteLineItemCommand,
|
||||
update_bill_command::UpdateBillCommand, update_line_item_command::UpdateLineItemCommand,
|
||||
update_store_command::UpdateStoreCommand,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd)]
|
||||
|
@ -20,6 +22,7 @@ pub enum BillingCommand {
|
|||
AddBill(AddBillCommand),
|
||||
UpdateBill(UpdateBillCommand),
|
||||
DeleteBill(DeleteBillCommand),
|
||||
ComputeBillTotalPriceBill(ComputeBillTotalPriceBillCommand),
|
||||
AddStore(AddStoreCommand),
|
||||
UpdateStore(UpdateStoreCommand),
|
||||
}
|
||||
|
|
66
src/billing/domain/compute_bill_total_price_command.rs
Normal file
66
src/billing/domain/compute_bill_total_price_command.rs
Normal file
|
@ -0,0 +1,66 @@
|
|||
// 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::bill_aggregate::Bill;
|
||||
use crate::types::currency::*;
|
||||
|
||||
#[derive(
|
||||
Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder,
|
||||
)]
|
||||
pub struct ComputeBillTotalPriceBillCommand {
|
||||
adding_by: Uuid,
|
||||
|
||||
#[builder(default = "OffsetDateTime::now_utc()")]
|
||||
created_time: OffsetDateTime,
|
||||
|
||||
bill_id: Uuid,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use time::macros::datetime;
|
||||
|
||||
use crate::{
|
||||
billing::{self, domain::bill_aggregate::*},
|
||||
utils::uuid::tests::UUID,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
impl ComputeBillTotalPriceBillCommand {
|
||||
pub fn get_cmd() -> Self {
|
||||
let bill_id = UUID;
|
||||
let adding_by = UUID;
|
||||
|
||||
ComputeBillTotalPriceBillCommandBuilder::default()
|
||||
.adding_by(adding_by)
|
||||
.bill_id(bill_id)
|
||||
.created_time(datetime!(1970-01-01 0:00 UTC))
|
||||
.build()
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cmd() {
|
||||
let bill_id = UUID;
|
||||
let adding_by = UUID;
|
||||
|
||||
let cmd = ComputeBillTotalPriceBillCommandBuilder::default()
|
||||
.adding_by(adding_by)
|
||||
.bill_id(bill_id)
|
||||
.created_time(datetime!(1970-01-01 0:00 UTC))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(*cmd.bill_id(), bill_id);
|
||||
assert_eq!(*cmd.adding_by(), adding_by);
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize};
|
|||
|
||||
use super::{
|
||||
bill_added_event::BillAddedEvent, bill_deleted_event::BillDeletedEvent,
|
||||
bill_total_price_computed_event::BillTotalPriceComputedEvent,
|
||||
bill_updated_event::BillUpdatedEvent, line_item_added_event::LineItemAddedEvent,
|
||||
line_item_deleted_event::LineItemDeletedEvent, line_item_updated_event::LineItemUpdatedEvent,
|
||||
store_added_event::StoreAddedEvent, store_updated_event::StoreUpdatedEvent,
|
||||
|
@ -20,6 +21,7 @@ pub enum BillingEvent {
|
|||
BillAdded(BillAddedEvent),
|
||||
BillUpdated(BillUpdatedEvent),
|
||||
BillDeleted(BillDeletedEvent),
|
||||
BillTotalPriceComputed(BillTotalPriceComputedEvent),
|
||||
StoreAdded(StoreAddedEvent),
|
||||
StoreUpdated(StoreUpdatedEvent),
|
||||
}
|
||||
|
@ -34,9 +36,10 @@ impl DomainEvent for BillingEvent {
|
|||
BillingEvent::LineItemAdded { .. } => "BillingLineItemAdded",
|
||||
BillingEvent::LineItemUpdated { .. } => "BillingLineItemUpdated",
|
||||
BillingEvent::LineItemDeleted { .. } => "BillingLineItemDeleted",
|
||||
BillingEvent::BillAdded { .. } => "BillingBilAdded",
|
||||
BillingEvent::BillUpdated { .. } => "BillingBilUpdated",
|
||||
BillingEvent::BillDeleted { .. } => "BillingBilDeleted",
|
||||
BillingEvent::BillAdded { .. } => "BillingBillAdded",
|
||||
BillingEvent::BillUpdated { .. } => "BillingBillUpdated",
|
||||
BillingEvent::BillDeleted { .. } => "BillingBillDeleted",
|
||||
BillingEvent::BillTotalPriceComputed { .. } => "BillingBillTotalPriceComputed",
|
||||
BillingEvent::StoreAdded { .. } => "BillingStoreAdded",
|
||||
BillingEvent::StoreUpdated { .. } => "BillingStoreUpdated",
|
||||
};
|
||||
|
|
|
@ -49,9 +49,15 @@ impl Default for LineItem {
|
|||
|
||||
impl LineItem {
|
||||
pub fn total_price(&self) -> Price {
|
||||
let total_price_as_minor = (self.quantity().major_as_minor().unwrap() // TODO: handle err
|
||||
let price_per_unit_as_minor =
|
||||
self.price_per_unit().major_as_minor() + self.price_per_unit().minor();
|
||||
let total_price_as_minor = if self.quantity().major().is_dividable() {
|
||||
(self.quantity().major_as_minor().unwrap() // TODO: handle err
|
||||
+ self.quantity().minor().number())
|
||||
* (self.price_per_unit().major_as_minor() + self.price_per_unit().minor());
|
||||
* price_per_unit_as_minor
|
||||
} else {
|
||||
self.quantity().major().number() * price_per_unit_as_minor
|
||||
};
|
||||
|
||||
Price::from_minor(
|
||||
total_price_as_minor,
|
||||
|
|
|
@ -12,6 +12,7 @@ pub mod add_bill_command;
|
|||
pub mod add_line_item_command;
|
||||
pub mod add_store_command;
|
||||
pub mod commands;
|
||||
pub mod compute_bill_total_price_command;
|
||||
pub mod delete_bill_command;
|
||||
pub mod delete_line_item_command;
|
||||
pub mod update_bill_command;
|
||||
|
@ -21,6 +22,7 @@ pub mod update_store_command;
|
|||
// events;
|
||||
pub mod bill_added_event;
|
||||
pub mod bill_deleted_event;
|
||||
pub mod bill_total_price_computed_event;
|
||||
pub mod bill_updated_event;
|
||||
pub mod events;
|
||||
pub mod line_item_added_event;
|
||||
|
|
|
@ -29,6 +29,8 @@ pub struct UpdateBillCommand {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use time::macros::datetime;
|
||||
|
||||
use crate::{billing::domain::bill_aggregate::*, utils::uuid::tests::UUID};
|
||||
|
||||
use super::*;
|
||||
|
@ -39,6 +41,7 @@ mod tests {
|
|||
let adding_by = UUID;
|
||||
|
||||
UpdateBillCommandBuilder::default()
|
||||
.created_time(datetime!(1970-01-01 0:00 UTC))
|
||||
.adding_by(adding_by)
|
||||
.store_id(store_id)
|
||||
.total_price(None)
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use std::str::FromStr;
|
||||
use std::{ops::Add, str::FromStr};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use cqrs_es::Aggregate;
|
||||
|
@ -53,7 +53,12 @@ impl Price {
|
|||
|
||||
pub fn from_minor(minor_only: usize, currency: Currency) -> Price {
|
||||
let minor = minor_only % 100;
|
||||
let major = (minor_only - minor) / 100;
|
||||
let major_only = minor_only - minor;
|
||||
let major = if major_only == 0 {
|
||||
0
|
||||
} else {
|
||||
(minor_only - minor) / 100
|
||||
};
|
||||
Price {
|
||||
minor,
|
||||
major,
|
||||
|
@ -62,6 +67,16 @@ impl Price {
|
|||
}
|
||||
}
|
||||
|
||||
impl Add for Price {
|
||||
type Output = Self;
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
let self_minor = self.major_as_minor() + self.minor;
|
||||
let rhs_minor = rhs.major_as_minor() + rhs.minor;
|
||||
|
||||
Self::from_minor(self_minor + rhs_minor, self.currency)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
@ -110,4 +125,70 @@ mod tests {
|
|||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_price_add() {
|
||||
let a = Price {
|
||||
minor: 10,
|
||||
major: 100,
|
||||
currency: Currency::INR,
|
||||
};
|
||||
|
||||
let b = Price {
|
||||
minor: 1,
|
||||
major: 200,
|
||||
currency: Currency::INR,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
a + b,
|
||||
Price {
|
||||
minor: 11,
|
||||
major: 300,
|
||||
currency: Currency::INR
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_price_add_zero() {
|
||||
let a = Price {
|
||||
minor: 0,
|
||||
major: 0,
|
||||
currency: Currency::INR,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
a.clone() + a,
|
||||
Price {
|
||||
minor: 0,
|
||||
major: 0,
|
||||
currency: Currency::INR
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_price_add_overflow() {
|
||||
let a = Price {
|
||||
minor: 80,
|
||||
major: 100,
|
||||
currency: Currency::INR,
|
||||
};
|
||||
|
||||
let b = Price {
|
||||
minor: 80,
|
||||
major: 200,
|
||||
currency: Currency::INR,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
a + b,
|
||||
Price {
|
||||
minor: 60,
|
||||
major: 301,
|
||||
currency: Currency::INR
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,7 +46,7 @@ impl FromStr for QuantityUnit {
|
|||
MILLI_LITER => Ok(Self::MilliLiter),
|
||||
LITER => Ok(Self::Liter),
|
||||
MILLI_GRAM => Ok(Self::Milligram),
|
||||
_ => Err("Currency unsupported".into()),
|
||||
_ => Err(format!("Quantity unsupported: {s}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -74,6 +74,15 @@ impl QuantityPart {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_dividable(&self) -> bool {
|
||||
match self.unit {
|
||||
QuantityUnit::Kilogram | QuantityUnit::Liter | QuantityUnit::Gram => true,
|
||||
QuantityUnit::Milligram | QuantityUnit::MilliLiter | QuantityUnit::DiscreteNumber => {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
|
|
Loading…
Reference in a new issue