feat: compute total price for bill #108

Merged
realaravinth merged 5 commits from billing into master 2024-09-18 17:27:22 +05:30
18 changed files with 800 additions and 24 deletions

View file

@ -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"
}

View file

@ -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;
}
}

View file

@ -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 {

View file

@ -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;

View file

@ -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)
}
}

View file

@ -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;

View file

@ -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)
);
}
}

View file

@ -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()
}
}

View file

@ -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();

View 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());
}
}

View file

@ -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),
}

View 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);
}
}

View file

@ -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",
};

View file

@ -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,

View file

@ -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;

View file

@ -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)

View file

@ -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
}
);
}
}

View file

@ -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(