feat: compute total price for bill #108
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.
|
// be designed to reflect the response dto that will be returned to a user.
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct LineItemView {
|
pub struct LineItemView {
|
||||||
product_name: String,
|
pub product_name: String,
|
||||||
product_id: Uuid,
|
pub product_id: Uuid,
|
||||||
bill_id: Uuid,
|
pub bill_id: Uuid,
|
||||||
created_time: OffsetDateTime,
|
pub created_time: OffsetDateTime,
|
||||||
|
|
||||||
line_item_id: Uuid,
|
pub line_item_id: Uuid,
|
||||||
|
|
||||||
quantity_major_number: i32,
|
pub quantity_major_number: i32,
|
||||||
quantity_minor_number: i32,
|
pub quantity_minor_number: i32,
|
||||||
quantity_major_unit: String,
|
pub quantity_major_unit: String,
|
||||||
quantity_minor_unit: String,
|
pub quantity_minor_unit: String,
|
||||||
|
|
||||||
price_per_unit_major: i32,
|
pub price_per_unit_major: i32,
|
||||||
price_per_unit_minor: i32,
|
pub price_per_unit_minor: i32,
|
||||||
price_per_unit_currency: String,
|
pub price_per_unit_currency: String,
|
||||||
|
|
||||||
deleted: bool,
|
pub deleted: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for LineItemView {
|
impl Default for LineItemView {
|
||||||
|
|
|
@ -10,6 +10,7 @@ use crate::db::{migrate::RunMigrations, sqlx_postgres::Postgres};
|
||||||
mod bill_id_exists;
|
mod bill_id_exists;
|
||||||
mod bill_view;
|
mod bill_view;
|
||||||
mod errors;
|
mod errors;
|
||||||
|
mod get_line_items_for_bill_id;
|
||||||
mod line_item_id_exists;
|
mod line_item_id_exists;
|
||||||
mod line_item_view;
|
mod line_item_view;
|
||||||
mod next_token_id;
|
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
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
pub mod bill_id_exists;
|
pub mod bill_id_exists;
|
||||||
pub mod errors;
|
pub mod errors;
|
||||||
|
pub mod get_line_items_for_bill_id;
|
||||||
pub mod line_item_id_exists;
|
pub mod line_item_id_exists;
|
||||||
pub mod next_token_id;
|
pub mod next_token_id;
|
||||||
pub mod store_id_exists;
|
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_bill_service;
|
||||||
pub mod add_line_item_service;
|
pub mod add_line_item_service;
|
||||||
pub mod add_store_service;
|
pub mod add_store_service;
|
||||||
|
pub mod compute_bill_total_price_service;
|
||||||
pub mod delete_bill_service;
|
pub mod delete_bill_service;
|
||||||
pub mod delete_line_item_service;
|
pub mod delete_line_item_service;
|
||||||
pub mod update_bill_service;
|
pub mod update_bill_service;
|
||||||
pub mod update_line_item_service;
|
pub mod update_line_item_service;
|
||||||
pub mod update_store_service;
|
pub mod update_store_service;
|
||||||
|
// TODO: 2. reset token number for store_id cronjob
|
||||||
|
|
||||||
#[automock]
|
#[automock]
|
||||||
pub trait BillingServicesInterface: Send + Sync {
|
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 add_line_item(&self) -> add_line_item_service::AddLineItemServiceObj;
|
||||||
fn update_line_item(&self) -> update_line_item_service::UpdateLineItemServiceObj;
|
fn update_line_item(&self) -> update_line_item_service::UpdateLineItemServiceObj;
|
||||||
fn delete_line_item(&self) -> delete_line_item_service::DeleteLineItemServiceObj;
|
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)]
|
#[derive(Clone, Builder)]
|
||||||
|
@ -40,6 +45,8 @@ pub struct BillingServices {
|
||||||
delete_line_item: delete_line_item_service::DeleteLineItemServiceObj,
|
delete_line_item: delete_line_item_service::DeleteLineItemServiceObj,
|
||||||
update_bill: update_bill_service::UpdateBillServiceObj,
|
update_bill: update_bill_service::UpdateBillServiceObj,
|
||||||
delete_bill: delete_bill_service::DeleteBillServiceObj,
|
delete_bill: delete_bill_service::DeleteBillServiceObj,
|
||||||
|
compute_total_price_for_bill:
|
||||||
|
compute_bill_total_price_service::ComputeBillTotalPriceBillServiceObj,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BillingServicesInterface for BillingServices {
|
impl BillingServicesInterface for BillingServices {
|
||||||
|
@ -68,4 +75,9 @@ impl BillingServicesInterface for BillingServices {
|
||||||
fn delete_line_item(&self) -> delete_line_item_service::DeleteLineItemServiceObj {
|
fn delete_line_item(&self) -> delete_line_item_service::DeleteLineItemServiceObj {
|
||||||
self.delete_line_item.clone()
|
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?;
|
let res = services.delete_bill().delete_bill(cmd).await?;
|
||||||
Ok(vec![BillingEvent::BillDeleted(res)])
|
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()),
|
_ => Ok(Vec::default()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -108,6 +115,9 @@ impl Aggregate for Bill {
|
||||||
match event {
|
match event {
|
||||||
BillingEvent::BillAdded(e) => *self = e.bill().clone(),
|
BillingEvent::BillAdded(e) => *self = e.bill().clone(),
|
||||||
BillingEvent::BillUpdated(e) => *self = e.new_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(),
|
BillingEvent::BillDeleted(e) => *self = e.bill().clone(),
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
|
@ -119,6 +129,8 @@ mod aggregate_tests {
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use add_bill_service::tests::mock_add_bill_service;
|
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 cqrs_es::test::TestFramework;
|
||||||
use delete_bill_service::tests::mock_delete_bill_service;
|
use delete_bill_service::tests::mock_delete_bill_service;
|
||||||
use update_bill_service::tests::mock_update_bill_service;
|
use update_bill_service::tests::mock_update_bill_service;
|
||||||
|
@ -126,6 +138,8 @@ mod aggregate_tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use crate::billing::domain::bill_deleted_event::tests::get_deleted_bill_event_from_command;
|
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::bill_updated_event::tests::get_updated_bill_event_from_command;
|
||||||
use crate::billing::domain::delete_bill_command::DeleteBillCommand;
|
use crate::billing::domain::delete_bill_command::DeleteBillCommand;
|
||||||
use crate::billing::domain::update_bill_command::UpdateBillCommand;
|
use crate::billing::domain::update_bill_command::UpdateBillCommand;
|
||||||
|
@ -133,6 +147,7 @@ mod aggregate_tests {
|
||||||
|
|
||||||
use crate::billing::domain::{
|
use crate::billing::domain::{
|
||||||
add_bill_command::*, bill_added_event::tests::get_added_bill_event_from_command,
|
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>;
|
type BillTestFramework = TestFramework<Bill>;
|
||||||
|
@ -173,6 +188,27 @@ mod aggregate_tests {
|
||||||
.then_expect_events(vec![expected]);
|
.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]
|
#[test]
|
||||||
fn test_delete_bill() {
|
fn test_delete_bill() {
|
||||||
let cmd = DeleteBillCommand::get_cmd();
|
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::{
|
use super::{
|
||||||
add_bill_command::AddBillCommand, add_line_item_command::AddLineItemCommand,
|
add_bill_command::AddBillCommand, add_line_item_command::AddLineItemCommand,
|
||||||
add_store_command::AddStoreCommand, delete_bill_command::DeleteBillCommand,
|
add_store_command::AddStoreCommand,
|
||||||
delete_line_item_command::DeleteLineItemCommand, update_bill_command::UpdateBillCommand,
|
compute_bill_total_price_command::ComputeBillTotalPriceBillCommand,
|
||||||
update_line_item_command::UpdateLineItemCommand, update_store_command::UpdateStoreCommand,
|
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)]
|
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd)]
|
||||||
|
@ -20,6 +22,7 @@ pub enum BillingCommand {
|
||||||
AddBill(AddBillCommand),
|
AddBill(AddBillCommand),
|
||||||
UpdateBill(UpdateBillCommand),
|
UpdateBill(UpdateBillCommand),
|
||||||
DeleteBill(DeleteBillCommand),
|
DeleteBill(DeleteBillCommand),
|
||||||
|
ComputeBillTotalPriceBill(ComputeBillTotalPriceBillCommand),
|
||||||
AddStore(AddStoreCommand),
|
AddStore(AddStoreCommand),
|
||||||
UpdateStore(UpdateStoreCommand),
|
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::{
|
use super::{
|
||||||
bill_added_event::BillAddedEvent, bill_deleted_event::BillDeletedEvent,
|
bill_added_event::BillAddedEvent, bill_deleted_event::BillDeletedEvent,
|
||||||
|
bill_total_price_computed_event::BillTotalPriceComputedEvent,
|
||||||
bill_updated_event::BillUpdatedEvent, line_item_added_event::LineItemAddedEvent,
|
bill_updated_event::BillUpdatedEvent, line_item_added_event::LineItemAddedEvent,
|
||||||
line_item_deleted_event::LineItemDeletedEvent, line_item_updated_event::LineItemUpdatedEvent,
|
line_item_deleted_event::LineItemDeletedEvent, line_item_updated_event::LineItemUpdatedEvent,
|
||||||
store_added_event::StoreAddedEvent, store_updated_event::StoreUpdatedEvent,
|
store_added_event::StoreAddedEvent, store_updated_event::StoreUpdatedEvent,
|
||||||
|
@ -20,6 +21,7 @@ pub enum BillingEvent {
|
||||||
BillAdded(BillAddedEvent),
|
BillAdded(BillAddedEvent),
|
||||||
BillUpdated(BillUpdatedEvent),
|
BillUpdated(BillUpdatedEvent),
|
||||||
BillDeleted(BillDeletedEvent),
|
BillDeleted(BillDeletedEvent),
|
||||||
|
BillTotalPriceComputed(BillTotalPriceComputedEvent),
|
||||||
StoreAdded(StoreAddedEvent),
|
StoreAdded(StoreAddedEvent),
|
||||||
StoreUpdated(StoreUpdatedEvent),
|
StoreUpdated(StoreUpdatedEvent),
|
||||||
}
|
}
|
||||||
|
@ -34,9 +36,10 @@ impl DomainEvent for BillingEvent {
|
||||||
BillingEvent::LineItemAdded { .. } => "BillingLineItemAdded",
|
BillingEvent::LineItemAdded { .. } => "BillingLineItemAdded",
|
||||||
BillingEvent::LineItemUpdated { .. } => "BillingLineItemUpdated",
|
BillingEvent::LineItemUpdated { .. } => "BillingLineItemUpdated",
|
||||||
BillingEvent::LineItemDeleted { .. } => "BillingLineItemDeleted",
|
BillingEvent::LineItemDeleted { .. } => "BillingLineItemDeleted",
|
||||||
BillingEvent::BillAdded { .. } => "BillingBilAdded",
|
BillingEvent::BillAdded { .. } => "BillingBillAdded",
|
||||||
BillingEvent::BillUpdated { .. } => "BillingBilUpdated",
|
BillingEvent::BillUpdated { .. } => "BillingBillUpdated",
|
||||||
BillingEvent::BillDeleted { .. } => "BillingBilDeleted",
|
BillingEvent::BillDeleted { .. } => "BillingBillDeleted",
|
||||||
|
BillingEvent::BillTotalPriceComputed { .. } => "BillingBillTotalPriceComputed",
|
||||||
BillingEvent::StoreAdded { .. } => "BillingStoreAdded",
|
BillingEvent::StoreAdded { .. } => "BillingStoreAdded",
|
||||||
BillingEvent::StoreUpdated { .. } => "BillingStoreUpdated",
|
BillingEvent::StoreUpdated { .. } => "BillingStoreUpdated",
|
||||||
};
|
};
|
||||||
|
|
|
@ -49,9 +49,15 @@ impl Default for LineItem {
|
||||||
|
|
||||||
impl LineItem {
|
impl LineItem {
|
||||||
pub fn total_price(&self) -> Price {
|
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.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(
|
Price::from_minor(
|
||||||
total_price_as_minor,
|
total_price_as_minor,
|
||||||
|
|
|
@ -12,6 +12,7 @@ pub mod add_bill_command;
|
||||||
pub mod add_line_item_command;
|
pub mod add_line_item_command;
|
||||||
pub mod add_store_command;
|
pub mod add_store_command;
|
||||||
pub mod commands;
|
pub mod commands;
|
||||||
|
pub mod compute_bill_total_price_command;
|
||||||
pub mod delete_bill_command;
|
pub mod delete_bill_command;
|
||||||
pub mod delete_line_item_command;
|
pub mod delete_line_item_command;
|
||||||
pub mod update_bill_command;
|
pub mod update_bill_command;
|
||||||
|
@ -21,6 +22,7 @@ pub mod update_store_command;
|
||||||
// events;
|
// events;
|
||||||
pub mod bill_added_event;
|
pub mod bill_added_event;
|
||||||
pub mod bill_deleted_event;
|
pub mod bill_deleted_event;
|
||||||
|
pub mod bill_total_price_computed_event;
|
||||||
pub mod bill_updated_event;
|
pub mod bill_updated_event;
|
||||||
pub mod events;
|
pub mod events;
|
||||||
pub mod line_item_added_event;
|
pub mod line_item_added_event;
|
||||||
|
|
|
@ -29,6 +29,8 @@ pub struct UpdateBillCommand {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use time::macros::datetime;
|
||||||
|
|
||||||
use crate::{billing::domain::bill_aggregate::*, utils::uuid::tests::UUID};
|
use crate::{billing::domain::bill_aggregate::*, utils::uuid::tests::UUID};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -39,6 +41,7 @@ mod tests {
|
||||||
let adding_by = UUID;
|
let adding_by = UUID;
|
||||||
|
|
||||||
UpdateBillCommandBuilder::default()
|
UpdateBillCommandBuilder::default()
|
||||||
|
.created_time(datetime!(1970-01-01 0:00 UTC))
|
||||||
.adding_by(adding_by)
|
.adding_by(adding_by)
|
||||||
.store_id(store_id)
|
.store_id(store_id)
|
||||||
.total_price(None)
|
.total_price(None)
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
use std::str::FromStr;
|
use std::{ops::Add, str::FromStr};
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use cqrs_es::Aggregate;
|
use cqrs_es::Aggregate;
|
||||||
|
@ -53,7 +53,12 @@ impl Price {
|
||||||
|
|
||||||
pub fn from_minor(minor_only: usize, currency: Currency) -> Price {
|
pub fn from_minor(minor_only: usize, currency: Currency) -> Price {
|
||||||
let minor = minor_only % 100;
|
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 {
|
Price {
|
||||||
minor,
|
minor,
|
||||||
major,
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
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),
|
MILLI_LITER => Ok(Self::MilliLiter),
|
||||||
LITER => Ok(Self::Liter),
|
LITER => Ok(Self::Liter),
|
||||||
MILLI_GRAM => Ok(Self::Milligram),
|
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(
|
#[derive(
|
||||||
|
|
Loading…
Reference in a new issue