From dbbbb86a8c23b8683d4e19116911fc848d903bfc Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Tue, 17 Sep 2024 16:13:34 +0530 Subject: [PATCH] feat: CRUD bill aggregate --- ...de8e3c0e33e22f2d2cd67fdd671b1f94ab8ea.json | 15 + ...cbdaa293f1d363f06f5926adfd038783171c4.json | 22 ++ ...31c44587f54c3583849d07d81e87e1f773c2b.json | 22 ++ ...b8c9ce62cf39e4a516511f436cba04520fddd.json | 28 ++ ...0d382b164f80a255dfe805abcd12cd43e17f3.json | 14 + ...4da078ac5fe27b022d41948ceb5d554b7f0fa.json | 22 ++ ...06cba772713dfdc11e30e92ad4842cbbb8a62.json | 64 +++ ...35433d3f8bd1e798772c05fc156494c036ef5.json | 22 ++ ...e07612061f183c20028ea4995807de6e6fae0.json | 22 ++ ...d35cbdcf0ae01437b469f1acc93ed11702ce7.json | 15 + ...20240917094208_cqrs_billing_bill_query.sql | 31 ++ .../output/db/postgres/bill_id_exists.rs | 83 ++++ .../adapters/output/db/postgres/bill_view.rs | 371 ++++++++++++++++++ .../adapters/output/db/postgres/mod.rs | 3 + .../output/db/postgres/next_token_id.rs | 111 ++++++ src/billing/application/mod.rs | 4 +- src/billing/application/port/mod.rs | 4 +- .../port/output/db/bill_id_exists.rs | 53 +++ .../application/port/output/db/errors.rs | 19 + .../port/output/db/line_item_id_exists.rs | 57 +++ src/billing/application/port/output/db/mod.rs | 9 + .../port/output/db/next_token_id.rs | 43 ++ src/billing/application/port/output/mod.rs | 1 + .../application/services/add_bill_service.rs | 125 ++++++ .../application/services/add_store_service.rs | 8 +- .../services/delete_bill_service.rs | 103 +++++ src/billing/application/services/mod.rs | 68 ++++ .../services/update_bill_service.rs | 116 ++++++ .../services/update_store_service.rs | 12 +- src/billing/domain/add_bill_command.rs | 63 +++ src/billing/domain/bill_added_event.rs | 41 ++ src/billing/domain/bill_aggregate.rs | 193 +++++++++ src/billing/domain/bill_deleted_event.rs | 49 +++ src/billing/domain/bill_updated_event.rs | 49 +++ src/billing/domain/commands.rs | 25 ++ src/billing/domain/delete_bill_command.rs | 37 ++ src/billing/domain/events.rs | 46 +++ src/billing/domain/mod.rs | 27 ++ src/billing/domain/store_aggregate.rs | 12 +- src/billing/domain/update_bill_command.rs | 71 ++++ 40 files changed, 2060 insertions(+), 20 deletions(-) create mode 100644 .sqlx/query-0e4316d20c96a7fcfcb68a98865de8e3c0e33e22f2d2cd67fdd671b1f94ab8ea.json create mode 100644 .sqlx/query-2af9418499c040df68031e79f7bcbdaa293f1d363f06f5926adfd038783171c4.json create mode 100644 .sqlx/query-6d9b6f386f4425d45d6aa8f7c2331c44587f54c3583849d07d81e87e1f773c2b.json create mode 100644 .sqlx/query-835a3afa5e16c762d021c4ca889b8c9ce62cf39e4a516511f436cba04520fddd.json create mode 100644 .sqlx/query-859920462d8e008b34bfc7467140d382b164f80a255dfe805abcd12cd43e17f3.json create mode 100644 .sqlx/query-91a8472f079a5221a94cdf5bf1a4da078ac5fe27b022d41948ceb5d554b7f0fa.json create mode 100644 .sqlx/query-b0c2747901658847d7c765401de06cba772713dfdc11e30e92ad4842cbbb8a62.json create mode 100644 .sqlx/query-b335fc519289a42c707855b620a35433d3f8bd1e798772c05fc156494c036ef5.json create mode 100644 .sqlx/query-dbd434995778eab99e753a8fcdce07612061f183c20028ea4995807de6e6fae0.json create mode 100644 .sqlx/query-df0fde518fdde267e2513fdd2c8d35cbdcf0ae01437b469f1acc93ed11702ce7.json create mode 100644 migrations/20240917094208_cqrs_billing_bill_query.sql create mode 100644 src/billing/adapters/output/db/postgres/bill_id_exists.rs create mode 100644 src/billing/adapters/output/db/postgres/bill_view.rs create mode 100644 src/billing/adapters/output/db/postgres/next_token_id.rs create mode 100644 src/billing/application/port/output/db/bill_id_exists.rs create mode 100644 src/billing/application/port/output/db/errors.rs create mode 100644 src/billing/application/port/output/db/line_item_id_exists.rs create mode 100644 src/billing/application/port/output/db/mod.rs create mode 100644 src/billing/application/port/output/db/next_token_id.rs create mode 100644 src/billing/application/services/add_bill_service.rs create mode 100644 src/billing/application/services/delete_bill_service.rs create mode 100644 src/billing/application/services/update_bill_service.rs create mode 100644 src/billing/domain/add_bill_command.rs create mode 100644 src/billing/domain/bill_added_event.rs create mode 100644 src/billing/domain/bill_aggregate.rs create mode 100644 src/billing/domain/bill_deleted_event.rs create mode 100644 src/billing/domain/bill_updated_event.rs create mode 100644 src/billing/domain/commands.rs create mode 100644 src/billing/domain/delete_bill_command.rs create mode 100644 src/billing/domain/events.rs create mode 100644 src/billing/domain/update_bill_command.rs diff --git a/.sqlx/query-0e4316d20c96a7fcfcb68a98865de8e3c0e33e22f2d2cd67fdd671b1f94ab8ea.json b/.sqlx/query-0e4316d20c96a7fcfcb68a98865de8e3c0e33e22f2d2cd67fdd671b1f94ab8ea.json new file mode 100644 index 0000000..1ddf35f --- /dev/null +++ b/.sqlx/query-0e4316d20c96a7fcfcb68a98865de8e3c0e33e22f2d2cd67fdd671b1f94ab8ea.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO cqrs_billing_bill_next_token_id\n (token_number, store_id)\n VALUES\n ($1, $2);", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int4", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "0e4316d20c96a7fcfcb68a98865de8e3c0e33e22f2d2cd67fdd671b1f94ab8ea" +} diff --git a/.sqlx/query-2af9418499c040df68031e79f7bcbdaa293f1d363f06f5926adfd038783171c4.json b/.sqlx/query-2af9418499c040df68031e79f7bcbdaa293f1d363f06f5926adfd038783171c4.json new file mode 100644 index 0000000..d961fa1 --- /dev/null +++ b/.sqlx/query-2af9418499c040df68031e79f7bcbdaa293f1d363f06f5926adfd038783171c4.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT EXISTS (\n SELECT 1\n FROM cqrs_billing_bill_next_token_id\n WHERE\n store_id = $1\n );", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + null + ] + }, + "hash": "2af9418499c040df68031e79f7bcbdaa293f1d363f06f5926adfd038783171c4" +} diff --git a/.sqlx/query-6d9b6f386f4425d45d6aa8f7c2331c44587f54c3583849d07d81e87e1f773c2b.json b/.sqlx/query-6d9b6f386f4425d45d6aa8f7c2331c44587f54c3583849d07d81e87e1f773c2b.json new file mode 100644 index 0000000..9ce5483 --- /dev/null +++ b/.sqlx/query-6d9b6f386f4425d45d6aa8f7c2331c44587f54c3583849d07d81e87e1f773c2b.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO cqrs_billing_bill_query (\n version,\n created_time,\n store_id,\n bill_id,\n token_number,\n total_price_major,\n total_price_minor,\n total_price_currency,\n deleted\n\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9\n );", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Timestamptz", + "Uuid", + "Uuid", + "Int4", + "Int4", + "Int4", + "Text", + "Bool" + ] + }, + "nullable": [] + }, + "hash": "6d9b6f386f4425d45d6aa8f7c2331c44587f54c3583849d07d81e87e1f773c2b" +} diff --git a/.sqlx/query-835a3afa5e16c762d021c4ca889b8c9ce62cf39e4a516511f436cba04520fddd.json b/.sqlx/query-835a3afa5e16c762d021c4ca889b8c9ce62cf39e4a516511f436cba04520fddd.json new file mode 100644 index 0000000..6b96215 --- /dev/null +++ b/.sqlx/query-835a3afa5e16c762d021c4ca889b8c9ce62cf39e4a516511f436cba04520fddd.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT \n bill_id, version\n FROM\n cqrs_billing_bill_query\n WHERE\n bill_id = $1;", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "bill_id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "version", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false + ] + }, + "hash": "835a3afa5e16c762d021c4ca889b8c9ce62cf39e4a516511f436cba04520fddd" +} diff --git a/.sqlx/query-859920462d8e008b34bfc7467140d382b164f80a255dfe805abcd12cd43e17f3.json b/.sqlx/query-859920462d8e008b34bfc7467140d382b164f80a255dfe805abcd12cd43e17f3.json new file mode 100644 index 0000000..3959b3a --- /dev/null +++ b/.sqlx/query-859920462d8e008b34bfc7467140d382b164f80a255dfe805abcd12cd43e17f3.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM cqrs_billing_bill_next_token_id WHERE store_id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "859920462d8e008b34bfc7467140d382b164f80a255dfe805abcd12cd43e17f3" +} diff --git a/.sqlx/query-91a8472f079a5221a94cdf5bf1a4da078ac5fe27b022d41948ceb5d554b7f0fa.json b/.sqlx/query-91a8472f079a5221a94cdf5bf1a4da078ac5fe27b022d41948ceb5d554b7f0fa.json new file mode 100644 index 0000000..9c461be --- /dev/null +++ b/.sqlx/query-91a8472f079a5221a94cdf5bf1a4da078ac5fe27b022d41948ceb5d554b7f0fa.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT EXISTS (\n SELECT 1\n FROM cqrs_billing_bill_query\n WHERE\n bill_id = $1\n );", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + null + ] + }, + "hash": "91a8472f079a5221a94cdf5bf1a4da078ac5fe27b022d41948ceb5d554b7f0fa" +} diff --git a/.sqlx/query-b0c2747901658847d7c765401de06cba772713dfdc11e30e92ad4842cbbb8a62.json b/.sqlx/query-b0c2747901658847d7c765401de06cba772713dfdc11e30e92ad4842cbbb8a62.json new file mode 100644 index 0000000..b6bf392 --- /dev/null +++ b/.sqlx/query-b0c2747901658847d7c765401de06cba772713dfdc11e30e92ad4842cbbb8a62.json @@ -0,0 +1,64 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT \n created_time,\n store_id,\n bill_id,\n token_number,\n total_price_major,\n total_price_minor,\n total_price_currency,\n deleted\n FROM\n cqrs_billing_bill_query\n WHERE\n bill_id = $1;", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "created_time", + "type_info": "Timestamptz" + }, + { + "ordinal": 1, + "name": "store_id", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "bill_id", + "type_info": "Uuid" + }, + { + "ordinal": 3, + "name": "token_number", + "type_info": "Int4" + }, + { + "ordinal": 4, + "name": "total_price_major", + "type_info": "Int4" + }, + { + "ordinal": 5, + "name": "total_price_minor", + "type_info": "Int4" + }, + { + "ordinal": 6, + "name": "total_price_currency", + "type_info": "Text" + }, + { + "ordinal": 7, + "name": "deleted", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false, + false, + false, + true, + true, + true, + false + ] + }, + "hash": "b0c2747901658847d7c765401de06cba772713dfdc11e30e92ad4842cbbb8a62" +} diff --git a/.sqlx/query-b335fc519289a42c707855b620a35433d3f8bd1e798772c05fc156494c036ef5.json b/.sqlx/query-b335fc519289a42c707855b620a35433d3f8bd1e798772c05fc156494c036ef5.json new file mode 100644 index 0000000..e9e4784 --- /dev/null +++ b/.sqlx/query-b335fc519289a42c707855b620a35433d3f8bd1e798772c05fc156494c036ef5.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE\n cqrs_billing_bill_query\n SET\n version = $1,\n\n created_time = $2,\n store_id = $3,\n bill_id = $4,\n token_number = $5,\n total_price_major = $6,\n total_price_minor = $7,\n total_price_currency = $8,\n\n deleted = $9;", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Timestamptz", + "Uuid", + "Uuid", + "Int4", + "Int4", + "Int4", + "Text", + "Bool" + ] + }, + "nullable": [] + }, + "hash": "b335fc519289a42c707855b620a35433d3f8bd1e798772c05fc156494c036ef5" +} diff --git a/.sqlx/query-dbd434995778eab99e753a8fcdce07612061f183c20028ea4995807de6e6fae0.json b/.sqlx/query-dbd434995778eab99e753a8fcdce07612061f183c20028ea4995807de6e6fae0.json new file mode 100644 index 0000000..f45dba8 --- /dev/null +++ b/.sqlx/query-dbd434995778eab99e753a8fcdce07612061f183c20028ea4995807de6e6fae0.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT\n token_number\n FROM\n cqrs_billing_bill_next_token_id\n WHERE\n store_id = $1;", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "token_number", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "dbd434995778eab99e753a8fcdce07612061f183c20028ea4995807de6e6fae0" +} diff --git a/.sqlx/query-df0fde518fdde267e2513fdd2c8d35cbdcf0ae01437b469f1acc93ed11702ce7.json b/.sqlx/query-df0fde518fdde267e2513fdd2c8d35cbdcf0ae01437b469f1acc93ed11702ce7.json new file mode 100644 index 0000000..5c11ae6 --- /dev/null +++ b/.sqlx/query-df0fde518fdde267e2513fdd2c8d35cbdcf0ae01437b469f1acc93ed11702ce7.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE cqrs_billing_bill_next_token_id\n SET\n token_number = $1\n WHERE\n store_id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int4", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "df0fde518fdde267e2513fdd2c8d35cbdcf0ae01437b469f1acc93ed11702ce7" +} diff --git a/migrations/20240917094208_cqrs_billing_bill_query.sql b/migrations/20240917094208_cqrs_billing_bill_query.sql new file mode 100644 index 0000000..00b960c --- /dev/null +++ b/migrations/20240917094208_cqrs_billing_bill_query.sql @@ -0,0 +1,31 @@ +-- SPDX-FileCopyrightText: 2024 Aravinth Manivannan +-- +-- SPDX-License-Identifier: AGPL-3.0-or-later + +CREATE TABLE IF NOT EXISTS cqrs_billing_bill_query +( + version bigint CHECK (version >= 0) NOT NULL, + + created_time timestamp with time zone DEFAULT (CURRENT_TIMESTAMP) NOT NULL, + bill_id UUID NOT NULL UNIQUE, + + + store_id UUID NOT NULL, + token_number INTEGER NOT NULL, + + total_price_minor INTEGER DEFAULT NULL, + total_price_major INTEGER DEFAULT NULL, + total_price_currency TEXT DEFAULT NULL, + + deleted BOOLEAN NOT NULL DEFAULT FALSE, + + PRIMARY KEY (bill_id) +); + +CREATE TABLE IF NOT EXISTS cqrs_billing_bill_next_token_id +( + store_id UUID NOT NULL, + token_number INTEGER NOT NULL DEFAULT 1, + + PRIMARY KEY (store_id) +); diff --git a/src/billing/adapters/output/db/postgres/bill_id_exists.rs b/src/billing/adapters/output/db/postgres/bill_id_exists.rs new file mode 100644 index 0000000..74e2170 --- /dev/null +++ b/src/billing/adapters/output/db/postgres/bill_id_exists.rs @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use uuid::Uuid; + +use super::BillingDBPostgresAdapter; +use crate::billing::application::port::output::db::{bill_id_exists::*, errors::*}; + +#[async_trait::async_trait] +impl BillIDExistsDBPort for BillingDBPostgresAdapter { + async fn bill_id_exists(&self, bill_id: &Uuid) -> BillingDBResult { + let res = sqlx::query!( + "SELECT EXISTS ( + SELECT 1 + FROM cqrs_billing_bill_query + WHERE + bill_id = $1 + );", + bill_id + ) + .fetch_one(&self.pool) + .await?; + if let Some(x) = res.exists { + Ok(x) + } else { + Ok(false) + } + } +} + +#[cfg(test)] +pub mod tests { + + use super::*; + // use crate::billing::domain::add_product_command::tests::get_customizations; + use crate::billing::domain::bill_aggregate::*; + + async fn create_dummy_bill(bill: &Bill, db: &BillingDBPostgresAdapter) { + sqlx::query!( + "INSERT INTO cqrs_billing_bill_query ( + version, + store_id, + bill_id, + token_number, + deleted + ) VALUES ( + $1, $2, $3, $4, $5 + );", + 1, + *bill.store_id(), + *bill.bill_id(), + *bill.token_number() as i32, + bill.deleted().clone(), + ) + .execute(&db.pool) + .await + .unwrap(); + } + + #[actix_rt::test] + async fn test_postgres_product_exists() { + 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 = Bill::default(); + + // state doesn't exist + assert!(!db.bill_id_exists(bill.bill_id()).await.unwrap()); + + create_dummy_bill(&bill, &db).await; + + // state exists + assert!(db.bill_id_exists(bill.bill_id()).await.unwrap()); + + settings.drop_db().await; + } +} diff --git a/src/billing/adapters/output/db/postgres/bill_view.rs b/src/billing/adapters/output/db/postgres/bill_view.rs new file mode 100644 index 0000000..462b8a4 --- /dev/null +++ b/src/billing/adapters/output/db/postgres/bill_view.rs @@ -0,0 +1,371 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use async_trait::async_trait; +use cqrs_es::persist::{PersistenceError, ViewContext, ViewRepository}; +use cqrs_es::{EventEnvelope, Query, View}; +use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; +use uuid::Uuid; + +use super::errors::*; +use super::BillingDBPostgresAdapter; +use crate::billing::domain::bill_aggregate::Bill; +use crate::billing::domain::events::BillingEvent; +use crate::utils::parse_aggregate_id::parse_aggregate_id; + +pub const NEW_BILL_NON_UUID: &str = "billing_new_bill_non_uuid-asdfa"; + +// The view for a Bill query, for a standard http application this should +// be designed to reflect the response dto that will be returned to a user. +#[derive(Debug, Serialize, Deserialize)] +pub struct BillView { + created_time: OffsetDateTime, + store_id: Uuid, + bill_id: Uuid, + + token_number: i32, + + total_price_minor: Option, + total_price_major: Option, + total_price_currency: Option, + + deleted: bool, +} + +impl Default for BillView { + fn default() -> Self { + Self { + created_time: OffsetDateTime::now_utc(), + store_id: Default::default(), + bill_id: Default::default(), + + token_number: Default::default(), + + total_price_minor: Default::default(), + total_price_major: Default::default(), + total_price_currency: Default::default(), + + deleted: false, + } + } +} + +// This updates the view with events as they are committed. +// The logic should be minimal here, e.g., don't calculate the account balance, +// design the events to carry the balance information instead. +impl View for BillView { + fn update(&mut self, event: &EventEnvelope) { + if let BillingEvent::BillAdded(val) = &event.payload { + self.created_time = val.bill().created_time().clone(); + self.store_id = *val.bill().store_id(); + self.bill_id = *val.bill().bill_id(); + + self.token_number = *val.bill().token_number() as i32; + + self.total_price_minor = val.bill().total_price().as_ref().map(|t| *t.minor() as i32); + self.total_price_major = val.bill().total_price().as_ref().map(|t| *t.major() as i32); + self.total_price_currency = val + .bill() + .total_price() + .as_ref() + .map(|t| t.currency().to_string()); + + self.deleted = false; + } + } +} + +#[async_trait] +impl ViewRepository for BillingDBPostgresAdapter { + async fn load(&self, bill_id: &str) -> Result, PersistenceError> { + let bill_id = match parse_aggregate_id(bill_id, NEW_BILL_NON_UUID)? { + Some((val, _)) => return Ok(Some(val)), + None => Uuid::parse_str(bill_id).unwrap(), + }; + + let res = sqlx::query_as!( + BillView, + "SELECT + created_time, + store_id, + bill_id, + token_number, + total_price_major, + total_price_minor, + total_price_currency, + deleted + FROM + cqrs_billing_bill_query + WHERE + bill_id = $1;", + bill_id + ) + .fetch_one(&self.pool) + .await + .map_err(PostgresAggregateError::from)?; + Ok(Some(res)) + } + + async fn load_with_context( + &self, + bill_id: &str, + ) -> Result, PersistenceError> { + let bill_id = match parse_aggregate_id(bill_id, NEW_BILL_NON_UUID)? { + Some(val) => return Ok(Some(val)), + None => Uuid::parse_str(bill_id).unwrap(), + }; + + let res = sqlx::query_as!( + BillView, + "SELECT + created_time, + store_id, + bill_id, + token_number, + total_price_major, + total_price_minor, + total_price_currency, + deleted + FROM + cqrs_billing_bill_query + WHERE + bill_id = $1;", + &bill_id, + ) + .fetch_one(&self.pool) + .await + .map_err(PostgresAggregateError::from)?; + + struct Context { + version: i64, + bill_id: Uuid, + } + + let ctx = sqlx::query_as!( + Context, + "SELECT + bill_id, version + FROM + cqrs_billing_bill_query + WHERE + bill_id = $1;", + bill_id + ) + .fetch_one(&self.pool) + .await + .map_err(PostgresAggregateError::from)?; + + let view_context = ViewContext::new(ctx.bill_id.to_string(), ctx.version); + Ok(Some((res, view_context))) + } + + async fn update_view( + &self, + view: BillView, + context: ViewContext, + ) -> Result<(), PersistenceError> { + match context.version { + 0 => { + let version = context.version + 1; + sqlx::query!( + "INSERT INTO cqrs_billing_bill_query ( + version, + created_time, + store_id, + bill_id, + token_number, + total_price_major, + total_price_minor, + total_price_currency, + deleted + + ) VALUES ( + $1, $2, $3, $4, $5, $6, $7, $8, $9 + );", + version, + view.created_time, + view.store_id, + view.bill_id, + view.token_number, + view.total_price_major, + view.total_price_minor, + view.total_price_currency, + view.deleted, + ) + .execute(&self.pool) + .await + .map_err(PostgresAggregateError::from)?; + } + _ => { + let version = context.version + 1; + sqlx::query!( + "UPDATE + cqrs_billing_bill_query + SET + version = $1, + + created_time = $2, + store_id = $3, + bill_id = $4, + token_number = $5, + total_price_major = $6, + total_price_minor = $7, + total_price_currency = $8, + + deleted = $9;", + version, + view.created_time, + view.store_id, + view.bill_id, + view.token_number, + view.total_price_major, + view.total_price_minor, + view.total_price_currency, + view.deleted, + ) + .execute(&self.pool) + .await + .map_err(PostgresAggregateError::from)?; + } + } + + Ok(()) + } +} + +pub struct SimpleLoggingQuery {} + +// Our simplest query, this is great for debugging but absolutely useless in production. +// This query just pretty prints the events as they are processed. +#[async_trait] +impl Query for SimpleLoggingQuery { + async fn dispatch(&self, aggregate_id: &str, events: &[EventEnvelope]) { + for event in events { + let payload = serde_json::to_string_pretty(&event.payload).unwrap(); + println!("{}-{}\n{}", aggregate_id, event.sequence, payload); + } + } +} + +#[async_trait] +impl Query for BillingDBPostgresAdapter { + async fn dispatch(&self, bill_id: &str, events: &[EventEnvelope]) { + let res = self + .load_with_context(bill_id) + .await + .unwrap_or_else(|_| Some((BillView::default(), ViewContext::new(bill_id.into(), 0)))); + let (mut view, view_context): (BillView, ViewContext) = res.unwrap(); + for event in events { + view.update(event); + } + self.update_view(view, view_context).await.unwrap(); + } +} + +// Our second query, this one will be handled with Postgres `GenericQuery` +// which will serialize and persist our view after it is updated. It also +// provides a `load` method to deserialize the view on request. +//pub type BillQuery = GenericQuery; +//pub type BillQuery = Query; + +//#[cfg(test)] +//mod tests { +// use super::*; +// +// use postgres_es::PostgresCqrs; +// +// use crate::{ +// db::migrate::*, +// billing::{ +// application::services::{ +// add_category_service::tests::mock_add_category_service, add_customization_service::tests::mock_add_customization_service, add_line_item_service::tests::mock_add_line_item_service, add_product_service::tests::mock_add_product_service, add_bill_service::AddBillServiceBuilder, update_category_service::tests::mock_update_category_service, update_customization_service::tests::mock_update_customization_service, update_product_service::tests::mock_update_product_service, update_bill_service::tests::mock_update_bill_service, BillingServicesBuilder +// }, +// domain::{ +// add_category_command::AddCategoryCommand, add_customization_command, +// add_product_command::tests::get_command, add_bill_command::AddBillCommand, +// commands::BillingCommand, +// update_category_command::tests::get_update_category_command, +// update_customization_command::tests::get_update_customization_command, +// update_product_command, update_bill_command::tests::get_update_bill_cmd, +// }, +// }, +// tests::bdd::IS_NEVER_CALLED, +// utils::{random_string::GenerateRandomStringInterface, uuid::tests::UUID}, +// }; +// use std::sync::Arc; +// +// #[actix_rt::test] +// async fn pg_query() { +// let settings = crate::settings::tests::get_settings().await; +// //let settings = crate::settings::Settings::new().unwrap(); +// settings.create_db().await; +// +// let db = crate::db::sqlx_postgres::Postgres::init(&settings.database.url).await; +// db.migrate().await; +// let db = BillingDBPostgresAdapter::new(db.pool.clone()); +// +// let simple_query = SimpleLoggingQuery {}; +// +// let queries: Vec>> = +// vec![Box::new(simple_query), Box::new(db.clone())]; +// +// let services = BillingServicesBuilder::default() +// .add_bill(Arc::new( +// AddBillServiceBuilder::default() +// .db_bill_id_exists(Arc::new(db.clone())) +// .db_bill_name_exists(Arc::new(db.clone())) +// .get_uuid(Arc::new(crate::utils::uuid::GenerateUUID {})) +// .build() +// .unwrap(), +// )) +// .add_category(mock_add_category_service( +// IS_NEVER_CALLED, +// AddCategoryCommand::new("foo".into(), None, UUID, UUID).unwrap(), +// )) +// .add_product(mock_add_product_service(IS_NEVER_CALLED, get_command())) +// .add_customization(mock_add_customization_service( +// IS_NEVER_CALLED, +// add_customization_command::tests::get_command(), +// )) +// .update_product(mock_update_product_service( +// IS_NEVER_CALLED, +// update_product_command::tests::get_command(), +// )) +// .update_customization(mock_update_customization_service( +// IS_NEVER_CALLED, +// get_update_customization_command(), +// )) +// .update_category(mock_update_category_service( +// IS_NEVER_CALLED, +// get_update_category_command(), +// )) +// .update_bill(mock_update_bill_service( +// IS_NEVER_CALLED, +// get_update_bill_cmd(), +// )) +// .build() +// .unwrap(); +// +// let (cqrs, _bill_query): ( +// Arc>, +// Arc>, +// ) = ( +// Arc::new(postgres_es::postgres_cqrs( +// db.pool.clone(), +// queries, +// Arc::new(services), +// )), +// Arc::new(db.clone()), +// ); +// +// let rand = crate::utils::random_string::GenerateRandomString {}; +// let cmd = AddBillCommand::new(rand.get_random(10), None, UUID).unwrap(); +// cqrs.execute("", BillingCommand::AddBill(cmd.clone())) +// .await +// .unwrap(); +// +// settings.drop_db().await; +// } +//} diff --git a/src/billing/adapters/output/db/postgres/mod.rs b/src/billing/adapters/output/db/postgres/mod.rs index 2f5961e..a605999 100644 --- a/src/billing/adapters/output/db/postgres/mod.rs +++ b/src/billing/adapters/output/db/postgres/mod.rs @@ -7,9 +7,12 @@ use sqlx::postgres::PgPool; use crate::db::{migrate::RunMigrations, sqlx_postgres::Postgres}; +mod bill_id_exists; +mod bill_view; mod errors; mod line_item_id_exists; mod line_item_view; +mod next_token_id; mod store_id_exists; mod store_name_exists; mod store_view; diff --git a/src/billing/adapters/output/db/postgres/next_token_id.rs b/src/billing/adapters/output/db/postgres/next_token_id.rs new file mode 100644 index 0000000..fbad278 --- /dev/null +++ b/src/billing/adapters/output/db/postgres/next_token_id.rs @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use uuid::Uuid; + +use super::BillingDBPostgresAdapter; +use crate::billing::application::port::output::db::{errors::*, next_token_id::*}; + +struct TokenNumber { + token_number: i32, +} + +#[async_trait::async_trait] +impl NextTokenIDDBPort for BillingDBPostgresAdapter { + /// delete record for store_id, so that when next_token_id is called, it'll create and set it + /// to 1 + async fn reset(&self, store_id: &Uuid) -> BillingDBResult<()> { + sqlx::query!( + "DELETE FROM cqrs_billing_bill_next_token_id WHERE store_id = $1", + store_id + ) + .execute(&self.pool) + .await?; + Ok(()) + } + + /// increment token ID and return pre-increment value + async fn next_token_id(&self, store_id: &Uuid) -> BillingDBResult { + let res = sqlx::query!( + "SELECT EXISTS ( + SELECT 1 + FROM cqrs_billing_bill_next_token_id + WHERE + store_id = $1 + );", + store_id + ) + .fetch_one(&self.pool) + .await?; + if let Some(true) = res.exists { + let res = sqlx::query_as!( + TokenNumber, + "SELECT + token_number + FROM + cqrs_billing_bill_next_token_id + WHERE + store_id = $1;", + store_id + ) + .fetch_one(&self.pool) + .await?; + + sqlx::query!( + "UPDATE cqrs_billing_bill_next_token_id + SET + token_number = $1 + WHERE + store_id = $2", + res.token_number + 1, + store_id + ) + .execute(&self.pool) + .await?; + + Ok(res.token_number as usize + 1) + } else { + sqlx::query!( + "INSERT INTO cqrs_billing_bill_next_token_id + (token_number, store_id) + VALUES + ($1, $2);", + 1, + store_id + ) + .execute(&self.pool) + .await?; + + Ok(1) + } + } +} + +#[cfg(test)] +pub mod tests { + + use super::*; + // use crate::billing::domain::add_product_command::tests::get_customizations; + use crate::{billing::domain::bill_aggregate::*, utils::uuid::tests::UUID}; + + #[actix_rt::test] + async fn test_postgres_next_token_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 store_id = UUID; + + assert_eq!(db.next_token_id(&store_id).await.unwrap(), 1); + assert_eq!(db.next_token_id(&store_id).await.unwrap(), 2); + assert!(db.reset(&store_id).await.is_ok()); + assert_eq!(db.next_token_id(&store_id).await.unwrap(), 1); + + settings.drop_db().await; + } +} diff --git a/src/billing/application/mod.rs b/src/billing/application/mod.rs index 357da8f..2f75b72 100644 --- a/src/billing/application/mod.rs +++ b/src/billing/application/mod.rs @@ -2,5 +2,5 @@ // // SPDX-License-Identifier: AGPL-3.0-or-later -mod port; -mod services; +pub mod port; +pub mod services; diff --git a/src/billing/application/port/mod.rs b/src/billing/application/port/mod.rs index 9b25f58..f571c4e 100644 --- a/src/billing/application/port/mod.rs +++ b/src/billing/application/port/mod.rs @@ -2,5 +2,5 @@ // // SPDX-License-Identifier: AGPL-3.0-or-later -mod input; -mod output; +pub mod input; +pub mod output; diff --git a/src/billing/application/port/output/db/bill_id_exists.rs b/src/billing/application/port/output/db/bill_id_exists.rs new file mode 100644 index 0000000..6e2323e --- /dev/null +++ b/src/billing/application/port/output/db/bill_id_exists.rs @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use mockall::predicate::*; +use mockall::*; +use uuid::Uuid; + +use super::errors::*; +#[cfg(test)] +#[allow(unused_imports)] +pub use tests::*; + +#[automock] +#[async_trait::async_trait] +pub trait BillIDExistsDBPort: Send + Sync { + async fn bill_id_exists(&self, c: &Uuid) -> BillingDBResult; +} + +pub type BillIDExistsDBPortObj = std::sync::Arc; + +#[cfg(test)] +pub mod tests { + use super::*; + + use std::sync::Arc; + + pub fn mock_bill_id_exists_db_port_false(times: Option) -> BillIDExistsDBPortObj { + let mut m = MockBillIDExistsDBPort::new(); + if let Some(times) = times { + m.expect_bill_id_exists() + .times(times) + .returning(|_| Ok(false)); + } else { + m.expect_bill_id_exists().returning(|_| Ok(false)); + } + + Arc::new(m) + } + + pub fn mock_bill_id_exists_db_port_true(times: Option) -> BillIDExistsDBPortObj { + let mut m = MockBillIDExistsDBPort::new(); + if let Some(times) = times { + m.expect_bill_id_exists() + .times(times) + .returning(|_| Ok(true)); + } else { + m.expect_bill_id_exists().returning(|_| Ok(true)); + } + + Arc::new(m) + } +} diff --git a/src/billing/application/port/output/db/errors.rs b/src/billing/application/port/output/db/errors.rs new file mode 100644 index 0000000..1debdad --- /dev/null +++ b/src/billing/application/port/output/db/errors.rs @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use derive_more::Display; +use serde::{Deserialize, Serialize}; + +pub type BillingDBResult = Result; + +#[derive(Debug, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +pub enum BillingDBError { + DuplicateBillID, + DuplicateStoreName, + DuplicateStoreID, + StoreIDNotFound, + DuplicateLineItemID, + LineItemIDNotFound, + InternalError, +} diff --git a/src/billing/application/port/output/db/line_item_id_exists.rs b/src/billing/application/port/output/db/line_item_id_exists.rs new file mode 100644 index 0000000..f012eb2 --- /dev/null +++ b/src/billing/application/port/output/db/line_item_id_exists.rs @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use mockall::predicate::*; +use mockall::*; +use uuid::Uuid; + +use super::errors::*; +#[cfg(test)] +#[allow(unused_imports)] +pub use tests::*; + +#[automock] +#[async_trait::async_trait] +pub trait LineItemIDExistsDBPort: Send + Sync { + async fn line_item_id_exists(&self, line_item_id: &Uuid) -> BillingDBResult; +} + +pub type LineItemIDExistsDBPortObj = std::sync::Arc; + +#[cfg(test)] +pub mod tests { + use super::*; + + use std::sync::Arc; + + pub fn mock_line_item_id_exists_db_port_false( + times: Option, + ) -> LineItemIDExistsDBPortObj { + let mut m = MockLineItemIDExistsDBPort::new(); + if let Some(times) = times { + m.expect_line_item_id_exists() + .times(times) + .returning(|_| Ok(false)); + } else { + m.expect_line_item_id_exists().returning(|_| Ok(false)); + } + + Arc::new(m) + } + + pub fn mock_line_item_id_exists_db_port_true( + times: Option, + ) -> LineItemIDExistsDBPortObj { + let mut m = MockLineItemIDExistsDBPort::new(); + if let Some(times) = times { + m.expect_line_item_id_exists() + .times(times) + .returning(|_| Ok(true)); + } else { + m.expect_line_item_id_exists().returning(|_| Ok(true)); + } + + Arc::new(m) + } +} diff --git a/src/billing/application/port/output/db/mod.rs b/src/billing/application/port/output/db/mod.rs new file mode 100644 index 0000000..a044ac0 --- /dev/null +++ b/src/billing/application/port/output/db/mod.rs @@ -0,0 +1,9 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later +pub mod bill_id_exists; +pub mod errors; +pub mod line_item_id_exists; +pub mod next_token_id; +pub mod store_id_exists; +pub mod store_name_exists; diff --git a/src/billing/application/port/output/db/next_token_id.rs b/src/billing/application/port/output/db/next_token_id.rs new file mode 100644 index 0000000..3bd3ba1 --- /dev/null +++ b/src/billing/application/port/output/db/next_token_id.rs @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use mockall::predicate::*; +use mockall::*; +use uuid::Uuid; + +use super::errors::*; +#[cfg(test)] +#[allow(unused_imports)] +pub use tests::*; + +#[automock] +#[async_trait::async_trait] +pub trait NextTokenIDDBPort: Send + Sync { + /// delete record for store_id, so that when next_token_id is called, it'll create and set it + /// to 1 + async fn reset(&self, store_id: &Uuid) -> BillingDBResult<()>; + + /// increment token ID and return pre-increment value + async fn next_token_id(&self, store_id: &Uuid) -> BillingDBResult; +} + +pub type NextTokenIDDBPortObj = std::sync::Arc; + +#[cfg(test)] +pub mod tests { + use super::*; + + use std::sync::Arc; + + pub fn mock_next_token_id_db_port(times: Option) -> NextTokenIDDBPortObj { + let mut m = MockNextTokenIDDBPort::new(); + if let Some(times) = times { + m.expect_next_token_id().times(times).returning(|_| Ok(1)); + } else { + m.expect_next_token_id().returning(|_| Ok(1)); + } + + Arc::new(m) + } +} diff --git a/src/billing/application/port/output/mod.rs b/src/billing/application/port/output/mod.rs index 56f60de..59db170 100644 --- a/src/billing/application/port/output/mod.rs +++ b/src/billing/application/port/output/mod.rs @@ -1,3 +1,4 @@ // SPDX-FileCopyrightText: 2024 Aravinth Manivannan // // SPDX-License-Identifier: AGPL-3.0-or-later +pub mod db; diff --git a/src/billing/application/services/add_bill_service.rs b/src/billing/application/services/add_bill_service.rs new file mode 100644 index 0000000..c8f05fe --- /dev/null +++ b/src/billing/application/services/add_bill_service.rs @@ -0,0 +1,125 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// 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::*, next_token_id::*}, + domain::{ + add_bill_command::AddBillCommand, + bill_added_event::{BillAddedEvent, BillAddedEventBuilder}, + bill_aggregate::*, + }, +}; +use crate::utils::uuid::*; + +#[automock] +#[async_trait::async_trait] +pub trait AddBillUseCase: Send + Sync { + async fn add_bill(&self, cmd: AddBillCommand) -> BillingResult; +} + +pub type AddBillServiceObj = Arc; + +#[derive(Clone, Builder)] +pub struct AddBillService { + db_bill_id_exists: BillIDExistsDBPortObj, + db_next_token_id: NextTokenIDDBPortObj, + get_uuid: GetUUIDInterfaceObj, +} + +#[async_trait::async_trait] +impl AddBillUseCase for AddBillService { + async fn add_bill(&self, cmd: AddBillCommand) -> BillingResult { + let mut bill_id = self.get_uuid.get_uuid(); + + loop { + if self.db_bill_id_exists.bill_id_exists(&bill_id).await? { + bill_id = self.get_uuid.get_uuid(); + continue; + } else { + break; + } + } + + let token_number = self.db_next_token_id.next_token_id(cmd.store_id()).await?; + + let bill = BillBuilder::default() + .bill_id(bill_id) + .token_number(token_number) + .created_time(cmd.created_time().clone()) + .store_id(*cmd.store_id()) + .total_price(None) + .deleted(false) + .build() + .unwrap(); + + Ok(BillAddedEventBuilder::default() + .added_by_user(*cmd.adding_by()) + .bill(bill) + .build() + .unwrap()) + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + + // use crate::billing::domain::add_bill_command::tests::get_command; + use crate::utils::uuid::tests::UUID; + use crate::{tests::bdd::*, utils::uuid::tests::mock_get_uuid}; + + pub fn mock_add_bill_service(times: Option, cmd: AddBillCommand) -> AddBillServiceObj { + let mut m = MockAddBillUseCase::new(); + + let bill = BillBuilder::default() + .bill_id(UUID) + .token_number(1) + .total_price(None) + .created_time(cmd.created_time().clone()) + .store_id(*cmd.store_id()) + .deleted(false) + .build() + .unwrap(); + + let res = BillAddedEventBuilder::default() + .added_by_user(*cmd.adding_by()) + .bill(bill) + .build() + .unwrap(); + + if let Some(times) = times { + m.expect_add_bill() + .times(times) + .returning(move |_| Ok(res.clone())); + } else { + m.expect_add_bill().returning(move |_| Ok(res.clone())); + } + + Arc::new(m) + } + + #[actix_rt::test] + async fn test_service_bill_doesnt_exist() { + let cmd = AddBillCommand::get_cmd(); + + let s = AddBillServiceBuilder::default() + .db_bill_id_exists(mock_bill_id_exists_db_port_false(IS_CALLED_ONLY_ONCE)) + .db_next_token_id(mock_next_token_id_db_port(IS_CALLED_ONLY_ONCE)) + .get_uuid(mock_get_uuid(IS_CALLED_ONLY_ONCE)) + .build() + .unwrap(); + + let res = s.add_bill(cmd.clone()).await.unwrap(); + assert_eq!(res.bill().created_time(), cmd.created_time()); + assert_eq!(res.bill().store_id(), cmd.store_id()); + assert_eq!(res.added_by_user(), cmd.adding_by()); + } +} diff --git a/src/billing/application/services/add_store_service.rs b/src/billing/application/services/add_store_service.rs index 6fd2c53..678662a 100644 --- a/src/billing/application/services/add_store_service.rs +++ b/src/billing/application/services/add_store_service.rs @@ -22,7 +22,7 @@ use crate::utils::uuid::*; #[automock] #[async_trait::async_trait] pub trait AddStoreUseCase: Send + Sync { - async fn add_store(&self, cmd: AddStoreCommand) -> BillResult; + async fn add_store(&self, cmd: AddStoreCommand) -> BillingResult; } pub type AddStoreServiceObj = Arc; @@ -36,7 +36,7 @@ pub struct AddStoreService { #[async_trait::async_trait] impl AddStoreUseCase for AddStoreService { - async fn add_store(&self, cmd: AddStoreCommand) -> BillResult { + async fn add_store(&self, cmd: AddStoreCommand) -> BillingResult { let mut store_id = self.get_uuid.get_uuid(); loop { @@ -57,7 +57,7 @@ impl AddStoreUseCase for AddStoreService { .unwrap(); if self.db_store_name_exists.store_name_exists(&store).await? { - return Err(BillError::DuplicateStoreName); + return Err(BillingError::DuplicateStoreName); } Ok(StoreAddedEventBuilder::default() @@ -143,7 +143,7 @@ pub mod tests { assert_eq!( s.add_store(cmd.clone()).await, - Err(BillError::DuplicateStoreName) + Err(BillingError::DuplicateStoreName) ); } } diff --git a/src/billing/application/services/delete_bill_service.rs b/src/billing/application/services/delete_bill_service.rs new file mode 100644 index 0000000..25a4cdc --- /dev/null +++ b/src/billing/application/services/delete_bill_service.rs @@ -0,0 +1,103 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// 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::*, + domain::{bill_deleted_event::*, delete_bill_command::DeleteBillCommand}, +}; + +#[automock] +#[async_trait::async_trait] +pub trait DeleteBillUseCase: Send + Sync { + async fn delete_bill(&self, cmd: DeleteBillCommand) -> BillingResult; +} + +pub type DeleteBillServiceObj = Arc; + +#[derive(Clone, Builder)] +pub struct DeleteBillService { + db_bill_id_exists: BillIDExistsDBPortObj, +} + +#[async_trait::async_trait] +impl DeleteBillUseCase for DeleteBillService { + async fn delete_bill(&self, cmd: DeleteBillCommand) -> BillingResult { + if !self + .db_bill_id_exists + .bill_id_exists(cmd.bill().bill_id()) + .await? + { + return Err(BillingError::BillIDNotFound); + } + + Ok(BillDeletedEventBuilder::default() + .added_by_user(*cmd.adding_by()) + .bill(cmd.bill().clone()) + .build() + .unwrap()) + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + + use crate::billing::domain::bill_deleted_event::tests::get_deleted_bill_event_from_command; + use crate::tests::bdd::*; + + pub fn mock_delete_bill_service( + times: Option, + cmd: DeleteBillCommand, + ) -> DeleteBillServiceObj { + let mut m = MockDeleteBillUseCase::new(); + + let res = get_deleted_bill_event_from_command(&cmd); + + if let Some(times) = times { + m.expect_delete_bill() + .times(times) + .returning(move |_| Ok(res.clone())); + } else { + m.expect_delete_bill().returning(move |_| Ok(res.clone())); + } + + Arc::new(m) + } + + #[actix_rt::test] + async fn test_service() { + let cmd = DeleteBillCommand::get_cmd(); + + let s = DeleteBillServiceBuilder::default() + .db_bill_id_exists(mock_bill_id_exists_db_port_true(IS_CALLED_ONLY_ONCE)) + .build() + .unwrap(); + + let res = s.delete_bill(cmd.clone()).await.unwrap(); + assert_eq!(res.bill(), cmd.bill()); + assert_eq!(res.added_by_user(), cmd.adding_by()); + } + + #[actix_rt::test] + async fn test_service_bill_id_doesnt_exist() { + let cmd = DeleteBillCommand::get_cmd(); + + let s = DeleteBillServiceBuilder::default() + .db_bill_id_exists(mock_bill_id_exists_db_port_false(IS_CALLED_ONLY_ONCE)) + .build() + .unwrap(); + + assert_eq!( + s.delete_bill(cmd.clone()).await, + Err(BillingError::BillIDNotFound) + ); + } +} diff --git a/src/billing/application/services/mod.rs b/src/billing/application/services/mod.rs index 56f60de..595f3c5 100644 --- a/src/billing/application/services/mod.rs +++ b/src/billing/application/services/mod.rs @@ -1,3 +1,71 @@ // SPDX-FileCopyrightText: 2024 Aravinth Manivannan // // SPDX-License-Identifier: AGPL-3.0-or-later + +use derive_builder::Builder; +use mockall::predicate::*; +use mockall::*; + +pub mod errors; + +// services +pub mod add_bill_service; +pub mod add_line_item_service; +pub mod add_store_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; + +#[automock] +pub trait BillingServicesInterface: Send + Sync { + fn add_bill(&self) -> add_bill_service::AddBillServiceObj; + fn update_bill(&self) -> update_bill_service::UpdateBillServiceObj; + fn delete_bill(&self) -> delete_bill_service::DeleteBillServiceObj; + fn add_store(&self) -> add_store_service::AddStoreServiceObj; + fn update_store(&self) -> update_store_service::UpdateStoreServiceObj; + 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; +} + +#[derive(Clone, Builder)] +pub struct BillingServices { + add_bill: add_bill_service::AddBillServiceObj, + add_store: add_store_service::AddStoreServiceObj, + update_store: update_store_service::UpdateStoreServiceObj, + add_line_item: add_line_item_service::AddLineItemServiceObj, + update_line_item: update_line_item_service::UpdateLineItemServiceObj, + delete_line_item: delete_line_item_service::DeleteLineItemServiceObj, + update_bill: update_bill_service::UpdateBillServiceObj, + delete_bill: delete_bill_service::DeleteBillServiceObj, +} + +impl BillingServicesInterface for BillingServices { + fn add_bill(&self) -> add_bill_service::AddBillServiceObj { + self.add_bill.clone() + } + fn update_bill(&self) -> update_bill_service::UpdateBillServiceObj { + self.update_bill.clone() + } + fn delete_bill(&self) -> delete_bill_service::DeleteBillServiceObj { + self.delete_bill.clone() + } + + fn add_store(&self) -> add_store_service::AddStoreServiceObj { + self.add_store.clone() + } + fn update_store(&self) -> update_store_service::UpdateStoreServiceObj { + self.update_store.clone() + } + fn add_line_item(&self) -> add_line_item_service::AddLineItemServiceObj { + self.add_line_item.clone() + } + fn update_line_item(&self) -> update_line_item_service::UpdateLineItemServiceObj { + self.update_line_item.clone() + } + fn delete_line_item(&self) -> delete_line_item_service::DeleteLineItemServiceObj { + self.delete_line_item.clone() + } +} diff --git a/src/billing/application/services/update_bill_service.rs b/src/billing/application/services/update_bill_service.rs new file mode 100644 index 0000000..96a95f0 --- /dev/null +++ b/src/billing/application/services/update_bill_service.rs @@ -0,0 +1,116 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// 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::*, + domain::{bill_aggregate::*, bill_updated_event::*, update_bill_command::UpdateBillCommand}, +}; + +#[automock] +#[async_trait::async_trait] +pub trait UpdateBillUseCase: Send + Sync { + async fn update_bill(&self, cmd: UpdateBillCommand) -> BillingResult; +} + +pub type UpdateBillServiceObj = Arc; + +#[derive(Clone, Builder)] +pub struct UpdateBillService { + db_bill_id_exists: BillIDExistsDBPortObj, +} + +#[async_trait::async_trait] +impl UpdateBillUseCase for UpdateBillService { + async fn update_bill(&self, cmd: UpdateBillCommand) -> BillingResult { + if !self + .db_bill_id_exists + .bill_id_exists(cmd.old_bill().bill_id()) + .await? + { + return Err(BillingError::BillIDNotFound); + } + + let bill = BillBuilder::default() + .bill_id(*cmd.old_bill().bill_id()) + .token_number(*cmd.old_bill().token_number()) + .total_price(cmd.total_price().clone()) + .store_id(*cmd.store_id()) + .deleted(false) + .build() + .unwrap(); + + Ok(BillUpdatedEventBuilder::default() + .added_by_user(*cmd.adding_by()) + .new_bill(bill) + .old_bill(cmd.old_bill().clone()) + .build() + .unwrap()) + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + + use crate::billing::domain::bill_updated_event::tests::get_updated_bill_event_from_command; + use crate::tests::bdd::*; + use crate::utils::uuid::tests::*; + + pub fn mock_update_bill_service( + times: Option, + cmd: UpdateBillCommand, + ) -> UpdateBillServiceObj { + let mut m = MockUpdateBillUseCase::new(); + + let res = get_updated_bill_event_from_command(&cmd); + + if let Some(times) = times { + m.expect_update_bill() + .times(times) + .returning(move |_| Ok(res.clone())); + } else { + m.expect_update_bill().returning(move |_| Ok(res.clone())); + } + + Arc::new(m) + } + + #[actix_rt::test] + async fn test_service() { + let cmd = UpdateBillCommand::get_cmd(); + + let s = UpdateBillServiceBuilder::default() + .db_bill_id_exists(mock_bill_id_exists_db_port_true(IS_CALLED_ONLY_ONCE)) + .build() + .unwrap(); + + let res = s.update_bill(cmd.clone()).await.unwrap(); + assert_eq!(res.new_bill().total_price(), cmd.total_price()); + assert_eq!(res.new_bill().bill_id(), cmd.old_bill().bill_id()); + assert_eq!(res.old_bill(), cmd.old_bill()); + assert_eq!(res.added_by_user(), cmd.adding_by()); + } + + #[actix_rt::test] + async fn test_service_bill_id_doesnt_exist() { + let cmd = UpdateBillCommand::get_cmd(); + + let s = UpdateBillServiceBuilder::default() + .db_bill_id_exists(mock_bill_id_exists_db_port_false(IS_CALLED_ONLY_ONCE)) + .build() + .unwrap(); + + assert_eq!( + s.update_bill(cmd.clone()).await, + Err(BillingError::BillIDNotFound) + ); + } +} diff --git a/src/billing/application/services/update_store_service.rs b/src/billing/application/services/update_store_service.rs index 05d9efa..4d3f923 100644 --- a/src/billing/application/services/update_store_service.rs +++ b/src/billing/application/services/update_store_service.rs @@ -20,7 +20,7 @@ use crate::utils::uuid::*; #[automock] #[async_trait::async_trait] pub trait UpdateStoreUseCase: Send + Sync { - async fn update_store(&self, cmd: UpdateStoreCommand) -> BillResult; + async fn update_store(&self, cmd: UpdateStoreCommand) -> BillingResult; } pub type UpdateStoreServiceObj = Arc; @@ -33,13 +33,13 @@ pub struct UpdateStoreService { #[async_trait::async_trait] impl UpdateStoreUseCase for UpdateStoreService { - async fn update_store(&self, cmd: UpdateStoreCommand) -> BillResult { + async fn update_store(&self, cmd: UpdateStoreCommand) -> BillingResult { if !self .db_store_id_exists .store_id_exists(cmd.old_store().store_id()) .await? { - return Err(BillError::StoreIDNotFound); + return Err(BillingError::StoreIDNotFound); } let store = StoreBuilder::default() @@ -52,7 +52,7 @@ impl UpdateStoreUseCase for UpdateStoreService { if cmd.name() != cmd.old_store().name() { if self.db_store_name_exists.store_name_exists(&store).await? { - return Err(BillError::DuplicateStoreName); + return Err(BillingError::DuplicateStoreName); } } @@ -124,7 +124,7 @@ pub mod tests { assert_eq!( s.update_store(cmd.clone()).await, - Err(BillError::DuplicateStoreName) + Err(BillingError::DuplicateStoreName) ); } @@ -140,7 +140,7 @@ pub mod tests { assert_eq!( s.update_store(cmd.clone()).await, - Err(BillError::StoreIDNotFound) + Err(BillingError::StoreIDNotFound) ); } } diff --git a/src/billing/domain/add_bill_command.rs b/src/billing/domain/add_bill_command.rs new file mode 100644 index 0000000..21aabb5 --- /dev/null +++ b/src/billing/domain/add_bill_command.rs @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use derive_builder::Builder; +use derive_getters::Getters; +use derive_more::{Display, Error}; +use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; +use uuid::Uuid; + +use crate::types::{currency::*, quantity::*}; +use crate::utils::string::empty_string_err; + +#[derive( + Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder, +)] +pub struct AddBillCommand { + adding_by: Uuid, + + #[builder(default = "OffsetDateTime::now_utc()")] + created_time: OffsetDateTime, + + store_id: Uuid, +} + +#[cfg(test)] +mod tests { + use time::macros::datetime; + + use crate::utils::uuid::tests::UUID; + + use super::*; + + impl AddBillCommand { + pub fn get_cmd() -> Self { + let store_id = UUID; + let adding_by = UUID; + + AddBillCommandBuilder::default() + .adding_by(adding_by) + .created_time(datetime!(1970-01-01 0:00 UTC)) + .store_id(store_id) + .build() + .unwrap() + } + } + + #[test] + fn test_cmd() { + let store_id = UUID; + let adding_by = UUID; + + let cmd = AddBillCommandBuilder::default() + .adding_by(adding_by) + .store_id(store_id) + .build() + .unwrap(); + + assert_eq!(*cmd.store_id(), store_id); + assert_eq!(*cmd.adding_by(), adding_by); + } +} diff --git a/src/billing/domain/bill_added_event.rs b/src/billing/domain/bill_added_event.rs new file mode 100644 index 0000000..9bf3a46 --- /dev/null +++ b/src/billing/domain/bill_added_event.rs @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// 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::Bill; + +#[derive( + Clone, Debug, Builder, Serialize, Deserialize, Getters, Eq, PartialEq, Ord, PartialOrd, +)] +pub struct BillAddedEvent { + added_by_user: Uuid, + + bill: Bill, +} + +#[cfg(test)] +pub mod tests { + use crate::billing::domain::add_bill_command::AddBillCommand; + + use super::*; + + pub fn get_added_bill_event_from_command(cmd: &AddBillCommand) -> BillAddedEvent { + let bill = Bill::get_bill(); + + BillAddedEventBuilder::default() + .added_by_user(cmd.adding_by().clone()) + .bill(bill) + .build() + .unwrap() + } + + #[test] + fn test_event() { + get_added_bill_event_from_command(&AddBillCommand::get_cmd()); + } +} diff --git a/src/billing/domain/bill_aggregate.rs b/src/billing/domain/bill_aggregate.rs new file mode 100644 index 0000000..ec8c347 --- /dev/null +++ b/src/billing/domain/bill_aggregate.rs @@ -0,0 +1,193 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use async_trait::async_trait; +use cqrs_es::Aggregate; +use derive_builder::Builder; +use derive_getters::Getters; +use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; +use uuid::Uuid; + +use crate::types::currency::*; + +use crate::billing::{ + application::services::{errors::*, *}, + domain::{commands::*, events::*}, +}; + +#[derive( + Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Builder, Getters, +)] +pub struct Bill { + #[builder(default = "OffsetDateTime::now_utc()")] + created_time: OffsetDateTime, + // bills: Vec, + bill_id: Uuid, + token_number: usize, + #[builder(default = "None")] + total_price: Option, + store_id: Uuid, + + #[builder(default = "false")] + deleted: bool, +} + +impl Default for Bill { + fn default() -> Self { + Self { + created_time: OffsetDateTime::now_utc(), + bill_id: Default::default(), + token_number: 1, + total_price: None, + store_id: Uuid::new_v4(), + deleted: false, + } + } +} + +#[cfg(test)] +pub mod tests { + use crate::{billing::domain::add_bill_command::AddBillCommand, utils::uuid::tests::UUID}; + + use super::*; + + impl Bill { + pub fn get_bill() -> Self { + let cmd = AddBillCommand::get_cmd(); + + BillBuilder::default() + .created_time(cmd.created_time().clone()) + .bill_id(UUID) + .store_id(UUID) + .token_number(1) + .build() + .unwrap() + } + } +} +// +#[async_trait] +impl Aggregate for Bill { + type Command = BillingCommand; + type Event = BillingEvent; + type Error = BillingError; + type Services = std::sync::Arc; + + // This identifier should be unique to the system. + fn aggregate_type() -> String { + "billing.bill".to_string() + } + + // The aggregate logic goes here. Note that this will be the _bulk_ of a CQRS system + // so expect to use helper functions elsewhere to keep the code clean. + async fn handle( + &self, + command: Self::Command, + services: &Self::Services, + ) -> Result, Self::Error> { + match command { + BillingCommand::AddBill(cmd) => { + let res = services.add_bill().add_bill(cmd).await?; + Ok(vec![BillingEvent::BillAdded(res)]) + } + BillingCommand::UpdateBill(cmd) => { + let res = services.update_bill().update_bill(cmd).await?; + Ok(vec![BillingEvent::BillUpdated(res)]) + } + BillingCommand::DeleteBill(cmd) => { + let res = services.delete_bill().delete_bill(cmd).await?; + Ok(vec![BillingEvent::BillDeleted(res)]) + } + _ => Ok(Vec::default()), + } + } + + fn apply(&mut self, event: Self::Event) { + match event { + BillingEvent::BillAdded(e) => *self = e.bill().clone(), + BillingEvent::BillUpdated(e) => *self = e.new_bill().clone(), + BillingEvent::BillDeleted(e) => *self = e.bill().clone(), + _ => (), + } + } +} + +#[cfg(test)] +mod aggregate_tests { + use std::sync::Arc; + + use add_bill_service::tests::mock_add_bill_service; + use cqrs_es::test::TestFramework; + use delete_bill_service::tests::mock_delete_bill_service; + use update_bill_service::tests::mock_update_bill_service; + + use super::*; + + use crate::billing::domain::bill_deleted_event::tests::get_deleted_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::update_bill_command::UpdateBillCommand; + use crate::tests::bdd::*; + + use crate::billing::domain::{ + add_bill_command::*, bill_added_event::tests::get_added_bill_event_from_command, + }; + + type BillTestFramework = TestFramework; + + #[test] + fn test_add_bill() { + let cmd = AddBillCommand::get_cmd(); + let expected = get_added_bill_event_from_command(&cmd); + let expected = BillingEvent::BillAdded(expected); + + let mut services = MockBillingServicesInterface::new(); + services + .expect_add_bill() + .times(IS_CALLED_ONLY_ONCE.unwrap()) + .return_const(mock_add_bill_service(IS_CALLED_ONLY_ONCE, cmd.clone())); + + BillTestFramework::with(Arc::new(services)) + .given_no_previous_events() + .when(BillingCommand::AddBill(cmd)) + .then_expect_events(vec![expected]); + } + + #[test] + fn test_update_bill() { + let cmd = UpdateBillCommand::get_cmd(); + let expected = get_updated_bill_event_from_command(&cmd); + let expected = BillingEvent::BillUpdated(expected); + + let mut services = MockBillingServicesInterface::new(); + services + .expect_update_bill() + .times(IS_CALLED_ONLY_ONCE.unwrap()) + .return_const(mock_update_bill_service(IS_CALLED_ONLY_ONCE, cmd.clone())); + + BillTestFramework::with(Arc::new(services)) + .given_no_previous_events() + .when(BillingCommand::UpdateBill(cmd)) + .then_expect_events(vec![expected]); + } + + #[test] + fn test_delete_bill() { + let cmd = DeleteBillCommand::get_cmd(); + let expected = get_deleted_bill_event_from_command(&cmd); + let expected = BillingEvent::BillDeleted(expected); + + let mut services = MockBillingServicesInterface::new(); + services + .expect_delete_bill() + .times(IS_CALLED_ONLY_ONCE.unwrap()) + .return_const(mock_delete_bill_service(IS_CALLED_ONLY_ONCE, cmd.clone())); + + BillTestFramework::with(Arc::new(services)) + .given_no_previous_events() + .when(BillingCommand::DeleteBill(cmd)) + .then_expect_events(vec![expected]); + } +} diff --git a/src/billing/domain/bill_deleted_event.rs b/src/billing/domain/bill_deleted_event.rs new file mode 100644 index 0000000..5686258 --- /dev/null +++ b/src/billing/domain/bill_deleted_event.rs @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// 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::*; + +#[derive( + Clone, Debug, Builder, Serialize, Deserialize, Getters, Eq, PartialEq, Ord, PartialOrd, +)] +pub struct BillDeletedEvent { + added_by_user: Uuid, + + bill: Bill, +} + +#[cfg(test)] +pub mod tests { + use crate::billing::domain::delete_bill_command::DeleteBillCommand; + + use super::*; + + pub fn get_deleted_bill_event_from_command(cmd: &DeleteBillCommand) -> BillDeletedEvent { + let deleted_bill = BillBuilder::default() + .created_time(cmd.bill().created_time().clone()) + .store_id(*cmd.bill().store_id()) + .bill_id(*cmd.bill().bill_id()) + .token_number(1) + .deleted(true) + .build() + .unwrap(); + + BillDeletedEventBuilder::default() + .added_by_user(cmd.adding_by().clone()) + .bill(deleted_bill) + .build() + .unwrap() + } + + #[test] + fn test_event() { + let event = get_deleted_bill_event_from_command(&DeleteBillCommand::get_cmd()); + assert!(event.bill().deleted()); + } +} diff --git a/src/billing/domain/bill_updated_event.rs b/src/billing/domain/bill_updated_event.rs new file mode 100644 index 0000000..5622045 --- /dev/null +++ b/src/billing/domain/bill_updated_event.rs @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// 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::*; + +#[derive( + Clone, Debug, Builder, Serialize, Deserialize, Getters, Eq, PartialEq, Ord, PartialOrd, +)] +pub struct BillUpdatedEvent { + added_by_user: Uuid, + + new_bill: Bill, + old_bill: Bill, +} + +#[cfg(test)] +pub mod tests { + use crate::billing::domain::update_bill_command::UpdateBillCommand; + + use super::*; + + pub fn get_updated_bill_event_from_command(cmd: &UpdateBillCommand) -> BillUpdatedEvent { + let new_bill = BillBuilder::default() + .created_time(cmd.created_time().clone()) + .token_number(1) + .store_id(*cmd.store_id()) + .bill_id(*cmd.old_bill().bill_id()) + .build() + .unwrap(); + + BillUpdatedEventBuilder::default() + .added_by_user(cmd.adding_by().clone()) + .old_bill(cmd.old_bill().clone()) + .new_bill(new_bill) + .build() + .unwrap() + } + + #[test] + fn test_event() { + get_updated_bill_event_from_command(&UpdateBillCommand::get_cmd()); + } +} diff --git a/src/billing/domain/commands.rs b/src/billing/domain/commands.rs new file mode 100644 index 0000000..3d92164 --- /dev/null +++ b/src/billing/domain/commands.rs @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use mockall::predicate::*; +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, +}; + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd)] +pub enum BillingCommand { + AddLineItem(AddLineItemCommand), + UpdateLineItem(UpdateLineItemCommand), + DeleteLineItem(DeleteLineItemCommand), + AddBill(AddBillCommand), + UpdateBill(UpdateBillCommand), + DeleteBill(DeleteBillCommand), + AddStore(AddStoreCommand), + UpdateStore(UpdateStoreCommand), +} diff --git a/src/billing/domain/delete_bill_command.rs b/src/billing/domain/delete_bill_command.rs new file mode 100644 index 0000000..88a6d89 --- /dev/null +++ b/src/billing/domain/delete_bill_command.rs @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// 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::Bill; + +#[derive( + Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Builder, Getters, +)] +pub struct DeleteBillCommand { + adding_by: Uuid, + bill: Bill, +} + +#[cfg(test)] +mod tests { + use crate::utils::uuid::tests::UUID; + + use super::*; + + impl DeleteBillCommand { + pub fn get_cmd() -> Self { + let adding_by = UUID; + + DeleteBillCommandBuilder::default() + .adding_by(adding_by) + .bill(Bill::get_bill()) + .build() + .unwrap() + } + } +} diff --git a/src/billing/domain/events.rs b/src/billing/domain/events.rs new file mode 100644 index 0000000..f947d4c --- /dev/null +++ b/src/billing/domain/events.rs @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use cqrs_es::DomainEvent; +use serde::{Deserialize, Serialize}; + +use super::{ + bill_added_event::BillAddedEvent, bill_deleted_event::BillDeletedEvent, + 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, +}; + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd)] +pub enum BillingEvent { + LineItemAdded(LineItemAddedEvent), + LineItemUpdated(LineItemUpdatedEvent), + LineItemDeleted(LineItemDeletedEvent), + BillAdded(BillAddedEvent), + BillUpdated(BillUpdatedEvent), + BillDeleted(BillDeletedEvent), + StoreAdded(StoreAddedEvent), + StoreUpdated(StoreUpdatedEvent), +} + +impl DomainEvent for BillingEvent { + fn event_version(&self) -> String { + "1.0".to_string() + } + + fn event_type(&self) -> String { + let e: &str = match self { + BillingEvent::LineItemAdded { .. } => "BillingLineItemAdded", + BillingEvent::LineItemUpdated { .. } => "BillingLineItemUpdated", + BillingEvent::LineItemDeleted { .. } => "BillingLineItemDeleted", + BillingEvent::BillAdded { .. } => "BillingBilAdded", + BillingEvent::BillUpdated { .. } => "BillingBilUpdated", + BillingEvent::BillDeleted { .. } => "BillingBilDeleted", + BillingEvent::StoreAdded { .. } => "BillingStoreAdded", + BillingEvent::StoreUpdated { .. } => "BillingStoreUpdated", + }; + + e.to_string() + } +} diff --git a/src/billing/domain/mod.rs b/src/billing/domain/mod.rs index 56f60de..cd8aa4d 100644 --- a/src/billing/domain/mod.rs +++ b/src/billing/domain/mod.rs @@ -1,3 +1,30 @@ // SPDX-FileCopyrightText: 2024 Aravinth Manivannan // // SPDX-License-Identifier: AGPL-3.0-or-later + +// aggregates +pub mod bill_aggregate; +pub mod line_item_aggregate; +pub mod store_aggregate; + +// commands +pub mod add_bill_command; +pub mod add_line_item_command; +pub mod add_store_command; +pub mod commands; +pub mod delete_bill_command; +pub mod delete_line_item_command; +pub mod update_bill_command; +pub mod update_line_item_command; +pub mod update_store_command; + +// events; +pub mod bill_added_event; +pub mod bill_deleted_event; +pub mod bill_updated_event; +pub mod events; +pub mod line_item_added_event; +pub mod line_item_deleted_event; +pub mod line_item_updated_event; +pub mod store_added_event; +pub mod store_updated_event; diff --git a/src/billing/domain/store_aggregate.rs b/src/billing/domain/store_aggregate.rs index cb4d7dd..c73e0cb 100644 --- a/src/billing/domain/store_aggregate.rs +++ b/src/billing/domain/store_aggregate.rs @@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::billing::application::services::errors::*; -use crate::billing::application::services::BillServicesInterface; +use crate::billing::application::services::*; use super::{commands::BillingCommand, events::BillingEvent}; @@ -30,8 +30,8 @@ pub struct Store { impl Aggregate for Store { type Command = BillingCommand; type Event = BillingEvent; - type Error = BillError; - type Services = std::sync::Arc; + type Error = BillingError; + type Services = std::sync::Arc; // This identifier should be unique to the system. fn aggregate_type() -> String { @@ -83,7 +83,7 @@ mod tests { use super::*; use crate::billing::{ - application::services::{add_store_service::tests::*, *}, + application::services::add_store_service::tests::*, domain::{ add_store_command::*, commands::BillingCommand, events::BillingEvent, store_added_event::*, store_updated_event::tests::get_store_updated_event_from_command, @@ -115,7 +115,7 @@ mod tests { let cmd = AddStoreCommand::new(name.into(), address.clone(), owner).unwrap(); - let mut services = MockBillServicesInterface::new(); + let mut services = MockBillingServicesInterface::new(); services .expect_add_store() .times(IS_CALLED_ONLY_ONCE.unwrap()) @@ -132,7 +132,7 @@ mod tests { let cmd = get_update_store_cmd(); let expected = BillingEvent::StoreUpdated(get_store_updated_event_from_command(&cmd)); - let mut services = MockBillServicesInterface::new(); + let mut services = MockBillingServicesInterface::new(); services .expect_update_store() .times(IS_CALLED_ONLY_ONCE.unwrap()) diff --git a/src/billing/domain/update_bill_command.rs b/src/billing/domain/update_bill_command.rs new file mode 100644 index 0000000..d8d1ff0 --- /dev/null +++ b/src/billing/domain/update_bill_command.rs @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// 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 UpdateBillCommand { + adding_by: Uuid, + + #[builder(default = "OffsetDateTime::now_utc()")] + created_time: OffsetDateTime, + + store_id: Uuid, + token_number: usize, + total_price: Option, + + old_bill: Bill, +} + +#[cfg(test)] +mod tests { + use crate::{billing::domain::bill_aggregate::*, utils::uuid::tests::UUID}; + + use super::*; + + impl UpdateBillCommand { + pub fn get_cmd() -> Self { + let store_id = UUID; + let adding_by = UUID; + + UpdateBillCommandBuilder::default() + .adding_by(adding_by) + .store_id(store_id) + .total_price(None) + .token_number(1) + .old_bill(Bill::get_bill()) + .build() + .unwrap() + } + } + + #[test] + fn test_cmd() { + let store_id = UUID; + let adding_by = UUID; + let old_bill = Bill::get_bill(); + + let cmd = UpdateBillCommandBuilder::default() + .adding_by(adding_by) + .store_id(store_id) + .total_price(None) + .token_number(1) + .old_bill(old_bill.clone()) + .build() + .unwrap(); + + assert_eq!(*cmd.store_id(), store_id); + assert_eq!(*cmd.adding_by(), adding_by); + assert_eq!(cmd.old_bill(), &old_bill); + } +}