From 5e9aef85843cf07aa6f3a19b5de3028cb4c7950a Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Wed, 18 Dec 2024 18:09:47 +0530 Subject: [PATCH 01/63] chore: get rid of rustc panics --- .gitignore | 1 + rustc-ice-2024-10-01T11_03_41-594641.txt | 37 ------------------------ rustc-ice-2024-10-01T11_03_53-595154.txt | 37 ------------------------ rustc-ice-2024-10-01T11_04_04-595752.txt | 37 ------------------------ 4 files changed, 1 insertion(+), 111 deletions(-) delete mode 100644 rustc-ice-2024-10-01T11_03_41-594641.txt delete mode 100644 rustc-ice-2024-10-01T11_03_53-595154.txt delete mode 100644 rustc-ice-2024-10-01T11_04_04-595752.txt diff --git a/.gitignore b/.gitignore index caebde4..e0e47d2 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ devenv.local.nix # pre-commit .pre-commit-config.yaml +rustc-ice-*.txt diff --git a/rustc-ice-2024-10-01T11_03_41-594641.txt b/rustc-ice-2024-10-01T11_03_41-594641.txt deleted file mode 100644 index b13a3d8..0000000 --- a/rustc-ice-2024-10-01T11_03_41-594641.txt +++ /dev/null @@ -1,37 +0,0 @@ -thread 'rustc' panicked at compiler/rustc_middle/src/query/on_disk_cache.rs:736:21: -Failed to convert DefPathHash DefPathHash(Fingerprint(9597590199268592635, 17791879502897349522)) -stack backtrace: - 0: 0x7fbfbb48d025 - std::backtrace::Backtrace::create::hbc256ff4bc983c0b - 1: 0x7fbfb9c8c865 - std::backtrace::Backtrace::force_capture::h1ebb6042c5b424a8 - 2: 0x7fbfb8db2217 - std[11ac8361619e45fd]::panicking::update_hook::>::{closure#0} - 3: 0x7fbfb9ca3ee8 - std::panicking::rust_panic_with_hook::ha6cfc51100acb1ab - 4: 0x7fbfb9ca3cb7 - std::panicking::begin_panic_handler::{{closure}}::he56e1efe5ba67ce1 - 5: 0x7fbfb9ca1699 - std::sys::backtrace::__rust_end_short_backtrace::hcc7bf1345a5f7ae3 - 6: 0x7fbfb9ca3984 - rust_begin_unwind - 7: 0x7fbfb6ad55c3 - core::panicking::panic_fmt::h146c2ef4416c8c7b - 8: 0x7fbfba61abc5 - ::decode_span - 9: 0x7fbfba94bc1c - rustc_query_system[4b3461eb5f3eee8]::query::plumbing::try_execute_query::>, false, false, false>, rustc_query_impl[2b1ee272ecafb82c]::plumbing::QueryCtxt, true> - 10: 0x7fbfba94929a - rustc_query_impl[2b1ee272ecafb82c]::query_impl::def_span::get_query_incr::__rust_end_short_backtrace - 11: 0x7fbfbaa190c0 - rustc_middle[62c17eb200ed25ef]::query::plumbing::query_get_at::>> - 12: 0x7fbfbacaaf7d - ::check_for_duplicate_items_in_impl - 13: 0x7fbfbb39e280 - rustc_hir_analysis[198913e0276c9380]::coherence::inherent_impls_overlap::crate_inherent_impls_overlap_check - 14: 0x7fbfbb39dc6d - rustc_query_impl[2b1ee272ecafb82c]::plumbing::__rust_begin_short_backtrace::> - 15: 0x7fbfbb39d1fd - rustc_query_system[4b3461eb5f3eee8]::query::plumbing::try_execute_query::>, false, false, false>, rustc_query_impl[2b1ee272ecafb82c]::plumbing::QueryCtxt, true> - 16: 0x7fbfbb39c867 - rustc_query_impl[2b1ee272ecafb82c]::query_impl::crate_inherent_impls_overlap_check::get_query_incr::__rust_end_short_backtrace - 17: 0x7fbfba5bb6f3 - rustc_hir_analysis[198913e0276c9380]::check_crate - 18: 0x7fbfba5b1351 - rustc_interface[d7e86b952eb641e5]::passes::run_required_analyses - 19: 0x7fbfbb1bac1e - rustc_interface[d7e86b952eb641e5]::passes::analysis - 20: 0x7fbfbb1babf1 - rustc_query_impl[2b1ee272ecafb82c]::plumbing::__rust_begin_short_backtrace::> - 21: 0x7fbfbb39cecd - rustc_query_system[4b3461eb5f3eee8]::query::plumbing::try_execute_query::>, false, false, false>, rustc_query_impl[2b1ee272ecafb82c]::plumbing::QueryCtxt, true> - 22: 0x7fbfbb39cb7a - rustc_query_impl[2b1ee272ecafb82c]::query_impl::analysis::get_query_incr::__rust_end_short_backtrace - 23: 0x7fbfbb1b30bc - rustc_interface[d7e86b952eb641e5]::interface::run_compiler::, rustc_driver_impl[8c32a9dec5e4d0f0]::run_compiler::{closure#0}>::{closure#1} - 24: 0x7fbfbb2c0504 - std[11ac8361619e45fd]::sys::backtrace::__rust_begin_short_backtrace::, rustc_driver_impl[8c32a9dec5e4d0f0]::run_compiler::{closure#0}>::{closure#1}, core[dcc560a250502550]::result::Result<(), rustc_span[e2902ef3f031f316]::ErrorGuaranteed>>::{closure#0}, core[dcc560a250502550]::result::Result<(), rustc_span[e2902ef3f031f316]::ErrorGuaranteed>>::{closure#0}::{closure#0}, core[dcc560a250502550]::result::Result<(), rustc_span[e2902ef3f031f316]::ErrorGuaranteed>> - 25: 0x7fbfbb2c0b70 - <::spawn_unchecked_, rustc_driver_impl[8c32a9dec5e4d0f0]::run_compiler::{closure#0}>::{closure#1}, core[dcc560a250502550]::result::Result<(), rustc_span[e2902ef3f031f316]::ErrorGuaranteed>>::{closure#0}, core[dcc560a250502550]::result::Result<(), rustc_span[e2902ef3f031f316]::ErrorGuaranteed>>::{closure#0}::{closure#0}, core[dcc560a250502550]::result::Result<(), rustc_span[e2902ef3f031f316]::ErrorGuaranteed>>::{closure#1} as core[dcc560a250502550]::ops::function::FnOnce<()>>::call_once::{shim:vtable#0} - 26: 0x7fbfbb2c0f6b - std::sys::pal::unix::thread::Thread::new::thread_start::h1921d0f3a7850262 - 27: 0x7fbfbcbf8272 - start_thread - 28: 0x7fbfbcc73dcc - clone3 - 29: 0x0 - - - -rustc version: 1.83.0-nightly (0ee7cb5e3 2024-09-10) -platform: x86_64-unknown-linux-gnu \ No newline at end of file diff --git a/rustc-ice-2024-10-01T11_03_53-595154.txt b/rustc-ice-2024-10-01T11_03_53-595154.txt deleted file mode 100644 index b26a907..0000000 --- a/rustc-ice-2024-10-01T11_03_53-595154.txt +++ /dev/null @@ -1,37 +0,0 @@ -thread 'rustc' panicked at compiler/rustc_middle/src/query/on_disk_cache.rs:736:21: -Failed to convert DefPathHash DefPathHash(Fingerprint(9597590199268592635, 17791879502897349522)) -stack backtrace: - 0: 0x77c2cca8d025 - std::backtrace::Backtrace::create::hbc256ff4bc983c0b - 1: 0x77c2cb28c865 - std::backtrace::Backtrace::force_capture::h1ebb6042c5b424a8 - 2: 0x77c2ca3b2217 - std[11ac8361619e45fd]::panicking::update_hook::>::{closure#0} - 3: 0x77c2cb2a3ee8 - std::panicking::rust_panic_with_hook::ha6cfc51100acb1ab - 4: 0x77c2cb2a3cb7 - std::panicking::begin_panic_handler::{{closure}}::he56e1efe5ba67ce1 - 5: 0x77c2cb2a1699 - std::sys::backtrace::__rust_end_short_backtrace::hcc7bf1345a5f7ae3 - 6: 0x77c2cb2a3984 - rust_begin_unwind - 7: 0x77c2c80d55c3 - core::panicking::panic_fmt::h146c2ef4416c8c7b - 8: 0x77c2cbc1abc5 - ::decode_span - 9: 0x77c2cbf4bc1c - rustc_query_system[4b3461eb5f3eee8]::query::plumbing::try_execute_query::>, false, false, false>, rustc_query_impl[2b1ee272ecafb82c]::plumbing::QueryCtxt, true> - 10: 0x77c2cbf4929a - rustc_query_impl[2b1ee272ecafb82c]::query_impl::def_span::get_query_incr::__rust_end_short_backtrace - 11: 0x77c2cc0190c0 - rustc_middle[62c17eb200ed25ef]::query::plumbing::query_get_at::>> - 12: 0x77c2cc2aaf7d - ::check_for_duplicate_items_in_impl - 13: 0x77c2cc99e280 - rustc_hir_analysis[198913e0276c9380]::coherence::inherent_impls_overlap::crate_inherent_impls_overlap_check - 14: 0x77c2cc99dc6d - rustc_query_impl[2b1ee272ecafb82c]::plumbing::__rust_begin_short_backtrace::> - 15: 0x77c2cc99d1fd - rustc_query_system[4b3461eb5f3eee8]::query::plumbing::try_execute_query::>, false, false, false>, rustc_query_impl[2b1ee272ecafb82c]::plumbing::QueryCtxt, true> - 16: 0x77c2cc99c867 - rustc_query_impl[2b1ee272ecafb82c]::query_impl::crate_inherent_impls_overlap_check::get_query_incr::__rust_end_short_backtrace - 17: 0x77c2cbbbb6f3 - rustc_hir_analysis[198913e0276c9380]::check_crate - 18: 0x77c2cbbb1351 - rustc_interface[d7e86b952eb641e5]::passes::run_required_analyses - 19: 0x77c2cc7bac1e - rustc_interface[d7e86b952eb641e5]::passes::analysis - 20: 0x77c2cc7babf1 - rustc_query_impl[2b1ee272ecafb82c]::plumbing::__rust_begin_short_backtrace::> - 21: 0x77c2cc99cecd - rustc_query_system[4b3461eb5f3eee8]::query::plumbing::try_execute_query::>, false, false, false>, rustc_query_impl[2b1ee272ecafb82c]::plumbing::QueryCtxt, true> - 22: 0x77c2cc99cb7a - rustc_query_impl[2b1ee272ecafb82c]::query_impl::analysis::get_query_incr::__rust_end_short_backtrace - 23: 0x77c2cc7b30bc - rustc_interface[d7e86b952eb641e5]::interface::run_compiler::, rustc_driver_impl[8c32a9dec5e4d0f0]::run_compiler::{closure#0}>::{closure#1} - 24: 0x77c2cc8c0504 - std[11ac8361619e45fd]::sys::backtrace::__rust_begin_short_backtrace::, rustc_driver_impl[8c32a9dec5e4d0f0]::run_compiler::{closure#0}>::{closure#1}, core[dcc560a250502550]::result::Result<(), rustc_span[e2902ef3f031f316]::ErrorGuaranteed>>::{closure#0}, core[dcc560a250502550]::result::Result<(), rustc_span[e2902ef3f031f316]::ErrorGuaranteed>>::{closure#0}::{closure#0}, core[dcc560a250502550]::result::Result<(), rustc_span[e2902ef3f031f316]::ErrorGuaranteed>> - 25: 0x77c2cc8c0b70 - <::spawn_unchecked_, rustc_driver_impl[8c32a9dec5e4d0f0]::run_compiler::{closure#0}>::{closure#1}, core[dcc560a250502550]::result::Result<(), rustc_span[e2902ef3f031f316]::ErrorGuaranteed>>::{closure#0}, core[dcc560a250502550]::result::Result<(), rustc_span[e2902ef3f031f316]::ErrorGuaranteed>>::{closure#0}::{closure#0}, core[dcc560a250502550]::result::Result<(), rustc_span[e2902ef3f031f316]::ErrorGuaranteed>>::{closure#1} as core[dcc560a250502550]::ops::function::FnOnce<()>>::call_once::{shim:vtable#0} - 26: 0x77c2cc8c0f6b - std::sys::pal::unix::thread::Thread::new::thread_start::h1921d0f3a7850262 - 27: 0x77c2ce089272 - start_thread - 28: 0x77c2ce104dcc - clone3 - 29: 0x0 - - - -rustc version: 1.83.0-nightly (0ee7cb5e3 2024-09-10) -platform: x86_64-unknown-linux-gnu \ No newline at end of file diff --git a/rustc-ice-2024-10-01T11_04_04-595752.txt b/rustc-ice-2024-10-01T11_04_04-595752.txt deleted file mode 100644 index ae8ab3b..0000000 --- a/rustc-ice-2024-10-01T11_04_04-595752.txt +++ /dev/null @@ -1,37 +0,0 @@ -thread 'rustc' panicked at compiler/rustc_middle/src/query/on_disk_cache.rs:736:21: -Failed to convert DefPathHash DefPathHash(Fingerprint(9597590199268592635, 17791879502897349522)) -stack backtrace: - 0: 0x704a8948d025 - std::backtrace::Backtrace::create::hbc256ff4bc983c0b - 1: 0x704a87c8c865 - std::backtrace::Backtrace::force_capture::h1ebb6042c5b424a8 - 2: 0x704a86db2217 - std[11ac8361619e45fd]::panicking::update_hook::>::{closure#0} - 3: 0x704a87ca3ee8 - std::panicking::rust_panic_with_hook::ha6cfc51100acb1ab - 4: 0x704a87ca3cb7 - std::panicking::begin_panic_handler::{{closure}}::he56e1efe5ba67ce1 - 5: 0x704a87ca1699 - std::sys::backtrace::__rust_end_short_backtrace::hcc7bf1345a5f7ae3 - 6: 0x704a87ca3984 - rust_begin_unwind - 7: 0x704a84ad55c3 - core::panicking::panic_fmt::h146c2ef4416c8c7b - 8: 0x704a8861abc5 - ::decode_span - 9: 0x704a8894bc1c - rustc_query_system[4b3461eb5f3eee8]::query::plumbing::try_execute_query::>, false, false, false>, rustc_query_impl[2b1ee272ecafb82c]::plumbing::QueryCtxt, true> - 10: 0x704a8894929a - rustc_query_impl[2b1ee272ecafb82c]::query_impl::def_span::get_query_incr::__rust_end_short_backtrace - 11: 0x704a88a190c0 - rustc_middle[62c17eb200ed25ef]::query::plumbing::query_get_at::>> - 12: 0x704a88caaf7d - ::check_for_duplicate_items_in_impl - 13: 0x704a8939e280 - rustc_hir_analysis[198913e0276c9380]::coherence::inherent_impls_overlap::crate_inherent_impls_overlap_check - 14: 0x704a8939dc6d - rustc_query_impl[2b1ee272ecafb82c]::plumbing::__rust_begin_short_backtrace::> - 15: 0x704a8939d1fd - rustc_query_system[4b3461eb5f3eee8]::query::plumbing::try_execute_query::>, false, false, false>, rustc_query_impl[2b1ee272ecafb82c]::plumbing::QueryCtxt, true> - 16: 0x704a8939c867 - rustc_query_impl[2b1ee272ecafb82c]::query_impl::crate_inherent_impls_overlap_check::get_query_incr::__rust_end_short_backtrace - 17: 0x704a885bb6f3 - rustc_hir_analysis[198913e0276c9380]::check_crate - 18: 0x704a885b1351 - rustc_interface[d7e86b952eb641e5]::passes::run_required_analyses - 19: 0x704a891bac1e - rustc_interface[d7e86b952eb641e5]::passes::analysis - 20: 0x704a891babf1 - rustc_query_impl[2b1ee272ecafb82c]::plumbing::__rust_begin_short_backtrace::> - 21: 0x704a8939cecd - rustc_query_system[4b3461eb5f3eee8]::query::plumbing::try_execute_query::>, false, false, false>, rustc_query_impl[2b1ee272ecafb82c]::plumbing::QueryCtxt, true> - 22: 0x704a8939cb7a - rustc_query_impl[2b1ee272ecafb82c]::query_impl::analysis::get_query_incr::__rust_end_short_backtrace - 23: 0x704a891b30bc - rustc_interface[d7e86b952eb641e5]::interface::run_compiler::, rustc_driver_impl[8c32a9dec5e4d0f0]::run_compiler::{closure#0}>::{closure#1} - 24: 0x704a892c0504 - std[11ac8361619e45fd]::sys::backtrace::__rust_begin_short_backtrace::, rustc_driver_impl[8c32a9dec5e4d0f0]::run_compiler::{closure#0}>::{closure#1}, core[dcc560a250502550]::result::Result<(), rustc_span[e2902ef3f031f316]::ErrorGuaranteed>>::{closure#0}, core[dcc560a250502550]::result::Result<(), rustc_span[e2902ef3f031f316]::ErrorGuaranteed>>::{closure#0}::{closure#0}, core[dcc560a250502550]::result::Result<(), rustc_span[e2902ef3f031f316]::ErrorGuaranteed>> - 25: 0x704a892c0b70 - <::spawn_unchecked_, rustc_driver_impl[8c32a9dec5e4d0f0]::run_compiler::{closure#0}>::{closure#1}, core[dcc560a250502550]::result::Result<(), rustc_span[e2902ef3f031f316]::ErrorGuaranteed>>::{closure#0}, core[dcc560a250502550]::result::Result<(), rustc_span[e2902ef3f031f316]::ErrorGuaranteed>>::{closure#0}::{closure#0}, core[dcc560a250502550]::result::Result<(), rustc_span[e2902ef3f031f316]::ErrorGuaranteed>>::{closure#1} as core[dcc560a250502550]::ops::function::FnOnce<()>>::call_once::{shim:vtable#0} - 26: 0x704a892c0f6b - std::sys::pal::unix::thread::Thread::new::thread_start::h1921d0f3a7850262 - 27: 0x704a8ac0b272 - start_thread - 28: 0x704a8ac86dcc - clone3 - 29: 0x0 - - - -rustc version: 1.83.0-nightly (0ee7cb5e3 2024-09-10) -platform: x86_64-unknown-linux-gnu \ No newline at end of file From 0a12f8938f047f5b74dfed081321f7c85a89d1e0 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Wed, 18 Dec 2024 18:10:27 +0530 Subject: [PATCH 02/63] feat: constructor to create BillingServices obj --- src/billing/application/services/mod.rs | 126 ++++++++++++++++++++++++ 1 file changed, 126 insertions(+) diff --git a/src/billing/application/services/mod.rs b/src/billing/application/services/mod.rs index 4b04e5a..85e9f96 100644 --- a/src/billing/application/services/mod.rs +++ b/src/billing/application/services/mod.rs @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2024 Aravinth Manivannan // // SPDX-License-Identifier: AGPL-3.0-or-later +use std::sync::Arc; use derive_builder::Builder; use mockall::predicate::*; @@ -20,6 +21,23 @@ pub mod update_line_item_service; pub mod update_store_service; // TODO: 2. reset token number for store_id cronjob +use add_bill_service::AddBillServiceBuilder; +use add_line_item_service::AddLineItemServiceBuilder; +use add_store_service::AddStoreServiceBuilder; +use compute_bill_total_price_service::ComputeBillTotalPriceBillServiceBuilder; +use delete_bill_service::DeleteBillServiceBuilder; +use delete_line_item_service::DeleteLineItemServiceBuilder; +use update_bill_service::UpdateBillServiceBuilder; +use update_line_item_service::UpdateLineItemServiceBuilder; +use update_store_service::UpdateStoreServiceBuilder; + +use crate::billing::application::port::output::db::{ + bill_id_exists::BillIDExistsDBPortObj, + get_line_items_for_bill_id::GetLineItemsForBillIDDBPortObj, + line_item_id_exists::LineItemIDExistsDBPortObj, next_token_id::NextTokenIDDBPortObj, + store_id_exists::StoreIDExistsDBPortObj, store_name_exists::StoreNameExistsDBPortObj, +}; + #[automock] pub trait BillingServicesInterface: Send + Sync { fn add_bill(&self) -> add_bill_service::AddBillServiceObj; @@ -35,6 +53,8 @@ pub trait BillingServicesInterface: Send + Sync { ) -> compute_bill_total_price_service::ComputeBillTotalPriceBillServiceObj; } +pub type BillingServicesObj = std::sync::Arc; + #[derive(Clone, Builder)] pub struct BillingServices { add_bill: add_bill_service::AddBillServiceObj, @@ -81,3 +101,109 @@ impl BillingServicesInterface for BillingServices { self.compute_total_price_for_bill.clone() } } + +impl BillingServices { + pub fn new( + db_bill_id_exists: BillIDExistsDBPortObj, + db_line_item_id_exists: LineItemIDExistsDBPortObj, + db_store_id_exists: StoreIDExistsDBPortObj, + db_store_name_exists: StoreNameExistsDBPortObj, + db_get_line_items_for_bill_id: GetLineItemsForBillIDDBPortObj, + db_next_token_id: NextTokenIDDBPortObj, + ) -> Self { + let services = BillingServicesBuilder::default() + .add_bill(Arc::new( + AddBillServiceBuilder::default() + .db_next_token_id(db_next_token_id.clone()) + .db_bill_id_exists(db_bill_id_exists.clone()) + .build() + .unwrap(), + )) + .add_store(Arc::new( + AddStoreServiceBuilder::default() + .db_store_id_exists(db_store_id_exists.clone()) + .db_store_name_exists(db_store_name_exists.clone()) + .build() + .unwrap(), + )) + .update_store(Arc::new( + UpdateStoreServiceBuilder::default() + .db_store_id_exists(db_store_id_exists.clone()) + .db_store_name_exists(db_store_name_exists.clone()) + .build() + .unwrap(), + )) + .add_line_item(Arc::new( + AddLineItemServiceBuilder::default() + .db_line_item_id_exists(db_line_item_id_exists.clone()) + .db_bill_id_exists(db_bill_id_exists.clone()) + .build() + .unwrap(), + )) + .update_line_item(Arc::new( + UpdateLineItemServiceBuilder::default() + .db_line_item_id_exists(db_line_item_id_exists.clone()) + .db_bill_id_exists(db_bill_id_exists.clone()) + .build() + .unwrap(), + )) + .delete_line_item(Arc::new( + DeleteLineItemServiceBuilder::default() + .db_line_item_id_exists(db_line_item_id_exists.clone()) + .build() + .unwrap(), + )) + .update_bill(Arc::new( + UpdateBillServiceBuilder::default() + .db_bill_id_exists(db_bill_id_exists.clone()) + .build() + .unwrap(), + )) + .delete_bill(Arc::new( + DeleteBillServiceBuilder::default() + .db_bill_id_exists(db_bill_id_exists.clone()) + .build() + .unwrap(), + )) + .compute_total_price_for_bill(Arc::new( + ComputeBillTotalPriceBillServiceBuilder::default() + .db_bill_id_exists(db_bill_id_exists.clone()) + .db_get_line_items_for_bill_id(db_get_line_items_for_bill_id) + .build() + .unwrap(), + )) + .build() + .unwrap(); + + services + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + billing::application::port::output::db::{ + bill_id_exists::mock_bill_id_exists_db_port_true, + get_line_items_for_bill_id::mock_get_line_items_for_bill_id_db_port_no_line_items, + line_item_id_exists::mock_line_item_id_exists_db_port_true, + next_token_id::mock_next_token_id_db_port, + store_id_exists::mock_store_id_exists_db_port_true, + store_name_exists::mock_store_name_exists_db_port_true, + }, + db::migrate::*, + tests::bdd::IS_NEVER_CALLED, + }; + + #[test] + fn billing_services_builder_works() { + BillingServices::new( + mock_bill_id_exists_db_port_true(IS_NEVER_CALLED), + mock_line_item_id_exists_db_port_true(IS_NEVER_CALLED), + mock_store_id_exists_db_port_true(IS_NEVER_CALLED), + mock_store_name_exists_db_port_true(IS_NEVER_CALLED), + mock_get_line_items_for_bill_id_db_port_no_line_items(IS_NEVER_CALLED), + mock_next_token_id_db_port(IS_NEVER_CALLED), + ); + } +} From ea2931990ca88ab3d46f41b2e36bdff9029ebe4b Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Wed, 18 Dec 2024 18:10:59 +0530 Subject: [PATCH 03/63] feat: define type aliases for billing ports&adapters --- src/billing/adapters/types.rs | 45 +++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/billing/adapters/types.rs diff --git a/src/billing/adapters/types.rs b/src/billing/adapters/types.rs new file mode 100644 index 0000000..316a085 --- /dev/null +++ b/src/billing/adapters/types.rs @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later +#![allow(dead_code)] + +use std::sync::Arc; + +use actix_web::web::Data; +use cqrs_es::persist::ViewRepository; +use postgres_es::PostgresCqrs; + +use crate::billing::{ + adapters::{ + input::web::RoutesRepository, + output::db::postgres::{ + bill_view::BillView, line_item_view::LineItemView, store_view::StoreView, + BillingDBPostgresAdapter, + }, + }, + application::services::BillingServicesObj, + domain::{bill_aggregate::Bill, line_item_aggregate::LineItem, store_aggregate::Store}, +}; + +pub type WebBillingRoutesRepository = Data>; + +pub type WebBillingServiceObj = Data; + +pub type BillingBillCqrsExec = Arc>; +pub type WebBillingBillCqrsExec = Data; +pub type BillingBillCqrsView = Arc>; +pub type WebBillingBillCqrsView = Data; + +pub type BillingLineItemCqrsExec = Arc>; +pub type WebBillingLineItemCqrsExec = Data; +pub type BillingLineItemCqrsView = Arc>; +pub type WebBillingLineItemCqrsView = Data; + +pub type BillingStoreCqrsExec = Arc>; +pub type WebBillingStoreCqrsExec = Data; +pub type BillingStoreCqrsView = Arc>; +pub type WebBillingStoreCqrsView = Data; From da3f5a463fa1d9a44185719df9793bdf145bd60b Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Wed, 18 Dec 2024 18:11:48 +0530 Subject: [PATCH 04/63] feat: init cqrs executors and queries for billing views --- .../adapters/output/db/postgres/bill_view.rs | 20 +++++++++++++++++-- .../output/db/postgres/line_item_view.rs | 18 ++++++++++++++++- .../adapters/output/db/postgres/mod.rs | 8 ++++---- .../adapters/output/db/postgres/store_view.rs | 17 ++++++++++++++++ 4 files changed, 56 insertions(+), 7 deletions(-) diff --git a/src/billing/adapters/output/db/postgres/bill_view.rs b/src/billing/adapters/output/db/postgres/bill_view.rs index 80d06d8..211838e 100644 --- a/src/billing/adapters/output/db/postgres/bill_view.rs +++ b/src/billing/adapters/output/db/postgres/bill_view.rs @@ -3,16 +3,20 @@ // SPDX-License-Identifier: AGPL-3.0-or-later use std::str::FromStr; +use std::sync::Arc; use async_trait::async_trait; use cqrs_es::persist::{PersistenceError, ViewContext, ViewRepository}; use cqrs_es::{EventEnvelope, Query, View}; +use postgres_es::PostgresCqrs; use serde::{Deserialize, Serialize}; use time::OffsetDateTime; use uuid::Uuid; use super::errors::*; use super::BillingDBPostgresAdapter; +use crate::billing::adapters::types::{BillingBillCqrsExec, BillingBillCqrsView}; +use crate::billing::application::services::BillingServicesObj; use crate::billing::domain::bill_aggregate::{Bill, BillBuilder}; use crate::billing::domain::events::BillingEvent; use crate::types::currency::{self, Currency, PriceBuilder}; @@ -311,12 +315,24 @@ impl Query for BillingDBPostgresAdapter { } } +pub fn init_cqrs( + db: BillingDBPostgresAdapter, + services: BillingServicesObj, +) -> (BillingBillCqrsExec, BillingBillCqrsView) { + let queries: Vec>> = vec![Box::new(db.clone())]; + + let pool = db.pool.clone(); + + ( + Arc::new(postgres_es::postgres_cqrs(pool.clone(), queries, services)), + Arc::new(db.clone()), + ) +} + #[cfg(test)] mod tests { use super::*; - use postgres_es::PostgresCqrs; - use crate::{ billing::{ application::services::{ diff --git a/src/billing/adapters/output/db/postgres/line_item_view.rs b/src/billing/adapters/output/db/postgres/line_item_view.rs index 7559177..d3f7243 100644 --- a/src/billing/adapters/output/db/postgres/line_item_view.rs +++ b/src/billing/adapters/output/db/postgres/line_item_view.rs @@ -1,8 +1,8 @@ // SPDX-FileCopyrightText: 2024 Aravinth Manivannan // // SPDX-License-Identifier: AGPL-3.0-or-later - use std::str::FromStr; +use std::sync::Arc; use async_trait::async_trait; use cqrs_es::persist::{PersistenceError, ViewContext, ViewRepository}; @@ -13,6 +13,8 @@ use uuid::Uuid; use super::errors::*; use super::BillingDBPostgresAdapter; +use crate::billing::adapters::types::{BillingLineItemCqrsExec, BillingLineItemCqrsView}; +use crate::billing::application::services::BillingServicesObj; use crate::billing::domain::events::BillingEvent; use crate::billing::domain::line_item_aggregate::*; use crate::types::currency::*; @@ -363,6 +365,20 @@ impl Query for BillingDBPostgresAdapter { } } +pub fn init_cqrs( + db: BillingDBPostgresAdapter, + services: BillingServicesObj, +) -> (BillingLineItemCqrsExec, BillingLineItemCqrsView) { + let queries: Vec>> = vec![Box::new(db.clone())]; + + let pool = db.pool.clone(); + + ( + Arc::new(postgres_es::postgres_cqrs(pool.clone(), queries, services)), + Arc::new(db.clone()), + ) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/billing/adapters/output/db/postgres/mod.rs b/src/billing/adapters/output/db/postgres/mod.rs index c8c8104..ec0602a 100644 --- a/src/billing/adapters/output/db/postgres/mod.rs +++ b/src/billing/adapters/output/db/postgres/mod.rs @@ -8,19 +8,19 @@ use sqlx::postgres::PgPool; use crate::db::{migrate::RunMigrations, sqlx_postgres::Postgres}; mod bill_id_exists; -mod bill_view; +pub(crate) mod bill_view; mod errors; mod get_line_items_for_bill_id; mod line_item_id_exists; -mod line_item_view; +pub(crate) mod line_item_view; mod next_token_id; mod store_id_exists; mod store_name_exists; -mod store_view; +pub(crate) mod store_view; #[derive(Clone)] pub struct BillingDBPostgresAdapter { - pool: PgPool, + pub(crate) pool: PgPool, } impl BillingDBPostgresAdapter { diff --git a/src/billing/adapters/output/db/postgres/store_view.rs b/src/billing/adapters/output/db/postgres/store_view.rs index ddb9060..6e44412 100644 --- a/src/billing/adapters/output/db/postgres/store_view.rs +++ b/src/billing/adapters/output/db/postgres/store_view.rs @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2024 Aravinth Manivannan // // SPDX-License-Identifier: AGPL-3.0-or-later +use std::sync::Arc; use async_trait::async_trait; use cqrs_es::persist::{PersistenceError, ViewContext, ViewRepository}; @@ -10,6 +11,8 @@ use uuid::Uuid; use super::errors::*; use super::BillingDBPostgresAdapter; +use crate::billing::adapters::types::{BillingStoreCqrsExec, BillingStoreCqrsView}; +use crate::billing::application::services::BillingServicesObj; use crate::billing::domain::events::BillingEvent; use crate::billing::domain::store_aggregate::*; use crate::utils::parse_aggregate_id::parse_aggregate_id; @@ -216,6 +219,20 @@ impl Query for BillingDBPostgresAdapter { } } +pub fn init_cqrs( + db: BillingDBPostgresAdapter, + services: BillingServicesObj, +) -> (BillingStoreCqrsExec, BillingStoreCqrsView) { + let queries: Vec>> = vec![Box::new(db.clone())]; + + let pool = db.pool.clone(); + + ( + Arc::new(postgres_es::postgres_cqrs(pool.clone(), queries, services)), + Arc::new(db.clone()), + ) +} + #[cfg(test)] mod tests { use super::*; From 1b4f18f92b96f7fe84b7ef3133a6ec6aa7a712dc Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Wed, 18 Dec 2024 18:12:48 +0530 Subject: [PATCH 05/63] feat: load billing adapters --- src/billing/adapters/mod.rs | 81 +++++++++++++++++++++++++++ src/billing/adapters/output/db/mod.rs | 2 +- src/main.rs | 4 ++ 3 files changed, 86 insertions(+), 1 deletion(-) diff --git a/src/billing/adapters/mod.rs b/src/billing/adapters/mod.rs index 9b25f58..e453b2d 100644 --- a/src/billing/adapters/mod.rs +++ b/src/billing/adapters/mod.rs @@ -1,6 +1,87 @@ // SPDX-FileCopyrightText: 2024 Aravinth Manivannan // // SPDX-License-Identifier: AGPL-3.0-or-later +use std::sync::Arc; + +use actix_web::web::{self, Data}; +use cqrs_es::{persist::ViewRepository, EventEnvelope, Query, View}; +use postgres_es::PostgresCqrs; +use sqlx::postgres::PgPool; + +use crate::billing::{ + application::{ + port::output::db::{ + bill_id_exists::BillIDExistsDBPortObj, + get_line_items_for_bill_id::GetLineItemsForBillIDDBPortObj, + line_item_id_exists::LineItemIDExistsDBPortObj, next_token_id::NextTokenIDDBPortObj, + store_id_exists::StoreIDExistsDBPortObj, store_name_exists::StoreNameExistsDBPortObj, + }, + services::{BillingServices, BillingServicesObj}, + }, + domain::{bill_aggregate::Bill, line_item_aggregate::LineItem, store_aggregate::Store}, +}; +use crate::settings::Settings; +use output::db::postgres::{bill_view, line_item_view, store_view, BillingDBPostgresAdapter}; mod input; mod output; +mod types; + +pub fn load_adapters(pool: PgPool, settings: Settings) -> impl FnOnce(&mut web::ServiceConfig) { + // init DB + let db = BillingDBPostgresAdapter::new(pool.clone()); + let db_bill_id_exists: BillIDExistsDBPortObj = Arc::new(db.clone()); + let db_line_item_id_exists: LineItemIDExistsDBPortObj = Arc::new(db.clone()); + let db_store_id_exists: StoreIDExistsDBPortObj = Arc::new(db.clone()); + let db_store_name_exists: StoreNameExistsDBPortObj = Arc::new(db.clone()); + let db_get_line_items_for_bill_id: GetLineItemsForBillIDDBPortObj = Arc::new(db.clone()); + let db_next_token_id: NextTokenIDDBPortObj = Arc::new(db.clone()); + + // init services + + let services: BillingServicesObj = Arc::new(BillingServices::new( + db_bill_id_exists.clone(), + db_line_item_id_exists.clone(), + db_store_id_exists.clone(), + db_store_name_exists.clone(), + db_get_line_items_for_bill_id.clone(), + db_next_token_id.clone(), + )); + + let (bill_cqrs_exec, bill_cqrs_query) = bill_view::init_cqrs(db.clone(), services.clone()); + let (store_cqrs_exec, store_cqrs_query) = store_view::init_cqrs(db.clone(), services.clone()); + let (line_item_cqrs_exec, line_item_cqrs_query) = + line_item_view::init_cqrs(db.clone(), services.clone()); + + let f = move |cfg: &mut web::ServiceConfig| { + cfg.configure(input::web::load_ctx()); + + cfg.app_data(Data::new(bill_cqrs_exec.clone())); + cfg.app_data(Data::new(bill_cqrs_query.clone())); + + cfg.app_data(Data::new(store_cqrs_exec.clone())); + cfg.app_data(Data::new(store_cqrs_query.clone())); + + cfg.app_data(Data::new(line_item_cqrs_exec.clone())); + cfg.app_data(Data::new(line_item_cqrs_query.clone())); + }; + + Box::new(f) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::db::migrate::*; + + #[actix_rt::test] + async fn billing_load_adapters() { + let settings = crate::settings::tests::get_settings().await; + settings.create_db().await; + + let db = crate::db::sqlx_postgres::Postgres::init(&settings.database.url).await; + db.migrate().await; + + load_adapters(db.pool.clone(), settings.clone()); + } +} diff --git a/src/billing/adapters/output/db/mod.rs b/src/billing/adapters/output/db/mod.rs index efa3961..0378461 100644 --- a/src/billing/adapters/output/db/mod.rs +++ b/src/billing/adapters/output/db/mod.rs @@ -1,4 +1,4 @@ // SPDX-FileCopyrightText: 2024 Aravinth Manivannan // // SPDX-License-Identifier: AGPL-3.0-or-later -mod postgres; +pub(crate) mod postgres; diff --git a/src/main.rs b/src/main.rs index d595908..6ed9eb3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -58,6 +58,10 @@ async fn main() { .wrap( middleware::DefaultHeaders::new().add(("Permissions-Policy", "interest-cohort=()")), ) + .configure(billing::adapters::load_adapters( + db.pool.clone(), + settings.clone(), + )) // .configure(auth::adapter::load_adapters(db.pool.clone(), &settings)) .configure(utils::random_string::GenerateRandomString::inject()) .configure(utils::uuid::GenerateUUID::inject()) From 52ea8be4d7d91c204c9580ec6e2ea66097287e93 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Wed, 18 Dec 2024 18:14:29 +0530 Subject: [PATCH 06/63] feat: bootstrap add bill web interface --- src/billing/adapters/input/mod.rs | 1 + src/billing/adapters/input/web/bill.rs | 168 +++++++++++++++++++++++ src/billing/adapters/input/web/errors.rs | 75 ++++++++++ src/billing/adapters/input/web/mod.rs | 28 ++++ src/billing/adapters/input/web/routes.rs | 67 +++++++++ src/billing/adapters/output/mod.rs | 2 +- src/billing/mod.rs | 2 +- 7 files changed, 341 insertions(+), 2 deletions(-) create mode 100644 src/billing/adapters/input/web/bill.rs create mode 100644 src/billing/adapters/input/web/errors.rs create mode 100644 src/billing/adapters/input/web/mod.rs create mode 100644 src/billing/adapters/input/web/routes.rs diff --git a/src/billing/adapters/input/mod.rs b/src/billing/adapters/input/mod.rs index 56f60de..810dc4e 100644 --- a/src/billing/adapters/input/mod.rs +++ b/src/billing/adapters/input/mod.rs @@ -1,3 +1,4 @@ // SPDX-FileCopyrightText: 2024 Aravinth Manivannan // // SPDX-License-Identifier: AGPL-3.0-or-later +pub mod web; diff --git a/src/billing/adapters/input/web/bill.rs b/src/billing/adapters/input/web/bill.rs new file mode 100644 index 0000000..c9e58df --- /dev/null +++ b/src/billing/adapters/input/web/bill.rs @@ -0,0 +1,168 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use actix_identity::Identity; +use actix_web::{get, http::header::ContentType, post, web, HttpRequest, HttpResponse, Responder}; +use serde::{Deserialize, Serialize}; +use url::Url; +use uuid::Uuid; + +use super::errors::*; +use super::types; +use crate::billing::domain::add_store_command::AddStoreCommandBuilder; +use crate::billing::domain::{add_bill_command::*, bill_updated_event, commands::BillingCommand}; +use crate::utils::uuid::WebGetUUIDInterfaceObj; + +pub fn services(cfg: &mut web::ServiceConfig) { + cfg.service(add_bill_submit_handler); + cfg.service(add_bill_page_handler); + cfg.service(add_store_page_handler); + cfg.service(add_store_submit_handler); +} + +#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)] +struct WebAddBillPayload { + store_id: Uuid, +} + +#[allow(clippy::too_many_arguments)] +#[get("/billing/store/add")] +#[tracing::instrument(name = "add store page handler", skip())] +//async fn add_bill_page_handler(_: Identity) -> WebJsonRepsonse { +async fn add_store_page_handler() -> WebJsonRepsonse { + let page = r#" + + + + + + Document + + +
+ +
+ + + + "#; + + Ok(HttpResponse::Ok() + .insert_header(ContentType::html()) + .body(page)) +} + +const UUID: Uuid = uuid::uuid!("67e55044-10b1-426f-9247-bb680e5fe0c8"); + +#[allow(clippy::too_many_arguments)] +#[post("/billing/store/add")] +#[tracing::instrument( + name = "add store handler", + skip(billing_store_cqrs_exec, billing_store_cqrs_view, uuid_generator) +)] +async fn add_store_submit_handler( + billing_store_cqrs_exec: types::WebBillingStoreCqrsExec, + billing_store_cqrs_view: types::WebBillingStoreCqrsView, + uuid_generator: WebGetUUIDInterfaceObj, + req: HttpRequest, + // id: Identity, +) -> WebJsonRepsonse { + // let user_id = Uuid::parse_str(&id.id().unwrap()).unwrap(); + let user_id = UUID; + + let store_uuid = UUID; + let store_uuid_str = store_uuid.to_string(); + let cmd = AddStoreCommandBuilder::default() + .name("foo".into()) + .owner(user_id) + .store_id(UUID) + .address(None) + .build() + .unwrap(); + + billing_store_cqrs_exec + .execute(&store_uuid_str, BillingCommand::AddStore(cmd)) + .await + .unwrap(); + + let store = billing_store_cqrs_view + .load(&store_uuid_str) + .await + .unwrap() + .unwrap(); + + Ok(HttpResponse::Ok().json(store)) +} + +#[allow(clippy::too_many_arguments)] +#[get("/billing/bill/add")] +#[tracing::instrument(name = "add bill page handler", skip())] +//async fn add_bill_page_handler(_: Identity) -> WebJsonRepsonse { +async fn add_bill_page_handler() -> WebJsonRepsonse { + let page = r#" + + + + + + Document + + + Token Refreshed +
+ + +
+ + + + "#; + + Ok(HttpResponse::Ok() + .insert_header(ContentType::html()) + .body(page)) +} + +#[allow(clippy::too_many_arguments)] +#[post("/billing/bill/add")] +#[tracing::instrument( + name = "add bill handler", + skip(billing_bill_cqrs_exec, billing_bill_cqrs_view,) +)] +async fn add_bill_submit_handler( + billing_bill_cqrs_exec: types::WebBillingBillCqrsExec, + billing_bill_cqrs_view: types::WebBillingBillCqrsView, + req: HttpRequest, + // id: Identity, + payload: web::Form, +) -> WebJsonRepsonse { + // let user_id = Uuid::parse_str(&id.id().unwrap()).unwrap(); + let user_id = Uuid::new_v4(); + + let bill_uuid = Uuid::new_v4(); + let bill_uuid_str = bill_uuid.to_string(); + let cmd = AddBillCommandBuilder::default() + .adding_by(user_id) + .bill_id(bill_uuid) + .store_id(payload.store_id) + .build() + .unwrap(); + + billing_bill_cqrs_exec + .execute(&bill_uuid_str, BillingCommand::AddBill(cmd)) + .await + .unwrap(); + + let bill = billing_bill_cqrs_view + .load(&bill_uuid_str) + .await + .unwrap() + .unwrap(); + + Ok(HttpResponse::Ok().json(bill)) +} diff --git a/src/billing/adapters/input/web/errors.rs b/src/billing/adapters/input/web/errors.rs new file mode 100644 index 0000000..927be55 --- /dev/null +++ b/src/billing/adapters/input/web/errors.rs @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use actix_web::http::StatusCode; +use actix_web::{HttpResponse, ResponseError}; +use derive_more::Display; +use serde::{Deserialize, Serialize}; + +use crate::billing::application::services::errors::*; + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +struct ErrorResponse { + error: String, +} + +impl From for ErrorResponse { + fn from(value: WebError) -> Self { + ErrorResponse { + error: serde_json::to_string(&value).unwrap_or_else(|_| { + log::error!("Unable to serialize error"); + "Unable to serialize error".into() + }), + } + } +} + +#[derive(Debug, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +pub enum WebError { + InternalError, + BillNotFound, + StoreNotFound, + LineItemNotFound, + BadRequest, +} + +impl From for WebError { + fn from(v: BillingError) -> Self { + match v { + BillingError::BillIDNotFound => Self::BillNotFound, + BillingError::InternalError => Self::InternalError, + BillingError::DuplicateStoreName => Self::InternalError, + BillingError::DuplicateBillID => Self::InternalError, + BillingError::DuplicateLineItemID => Self::InternalError, + BillingError::DuplicateStoreID => Self::InternalError, + BillingError::StoreIDNotFound => Self::StoreNotFound, + BillingError::LineItemIDNotFound => Self::LineItemNotFound, + } + } +} + +impl ResponseError for WebError { + fn status_code(&self) -> StatusCode { + match self { + Self::InternalError => StatusCode::INTERNAL_SERVER_ERROR, + Self::StoreNotFound => StatusCode::NOT_FOUND, + Self::LineItemNotFound => StatusCode::NOT_FOUND, + Self::BillNotFound => StatusCode::NOT_FOUND, + Self::BadRequest => StatusCode::BAD_REQUEST, + } + } + + fn error_response(&self) -> actix_web::HttpResponse { + let e: ErrorResponse = self.clone().into(); + match self { + Self::InternalError => HttpResponse::InternalServerError().json(e), + Self::StoreNotFound => HttpResponse::NotFound().json(e), + Self::LineItemNotFound => HttpResponse::NotFound().json(e), + Self::BillNotFound => HttpResponse::BadRequest().json(e), + Self::BadRequest => HttpResponse::BadRequest().json(e), + } + } +} + +pub type WebJsonRepsonse = Result; diff --git a/src/billing/adapters/input/web/mod.rs b/src/billing/adapters/input/web/mod.rs new file mode 100644 index 0000000..f255fe7 --- /dev/null +++ b/src/billing/adapters/input/web/mod.rs @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use std::sync::Arc; + +use actix_web::web; + +use crate::billing::adapters::types; + +mod bill; +mod errors; +mod routes; + +pub use errors::WebJsonRepsonse; + +pub use routes::RoutesRepository; + +pub fn load_ctx() -> impl FnOnce(&mut web::ServiceConfig) { + let routes = types::WebBillingRoutesRepository::new(Arc::new(RoutesRepository::default())); + + let f = move |cfg: &mut web::ServiceConfig| { + cfg.app_data(routes); + cfg.configure(bill::services); + }; + + Box::new(f) +} diff --git a/src/billing/adapters/input/web/routes.rs b/src/billing/adapters/input/web/routes.rs new file mode 100644 index 0000000..c680d86 --- /dev/null +++ b/src/billing/adapters/input/web/routes.rs @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use url::Url; +use uuid::Uuid; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct RoutesRepository { + add_bill: String, + update_bill: String, + delete_bill: String, + compute_total_price_for_bill: String, + + add_line_item: String, + update_line_item: String, + delete_line_item: String, +} + +impl Default for RoutesRepository { + fn default() -> Self { + Self { + add_bill: "/billing/bill/add".into(), + update_bill: "/billing/bill/{bill_uuid}".into(), + delete_bill: "/billing/bill/{bill_uuid}".into(), + compute_total_price_for_bill: "/billing/bill/{bill_uuid}/compute-total".into(), + + add_line_item: "/billing/bill/{bill_id}/line_item/add".into(), + update_line_item: "/billing/bill/{bill_id}/line_item/{line_item_uuid}".into(), + delete_line_item: "/billing/bill/{bill_id}/line_item/{line_item_uuid}".into(), + } + } +} + +impl RoutesRepository { + pub fn update_bill(&self, bill_id: Uuid) -> String { + self.update_bill + .replace("{bill_uuid}", &bill_id.to_string()) + } + + pub fn delete_bill(&self, bill_id: Uuid) -> String { + self.delete_bill + .replace("{bill_uuid}", &bill_id.to_string()) + } + + pub fn compute_total_price_for_bill(&self, bill_id: Uuid) -> String { + self.compute_total_price_for_bill + .replace("{bill_uuid}", &bill_id.to_string()) + } + + pub fn add_line_item(&self, bill_id: Uuid) -> String { + self.add_line_item + .replace("{bill_uuid}", &bill_id.to_string()) + } + + pub fn update_line_item(&self, bill_id: Uuid, line_item_uuid: Uuid) -> String { + self.update_line_item + .replace("{bill_uuid}", &bill_id.to_string()) + .replace("{line_item_uuid}", &line_item_uuid.to_string()) + } + + pub fn delete_line_item(&self, bill_id: Uuid, line_item_uuid: Uuid) -> String { + self.delete_line_item + .replace("{bill_uuid}", &bill_id.to_string()) + .replace("{line_item_uuid}", &line_item_uuid.to_string()) + } +} diff --git a/src/billing/adapters/output/mod.rs b/src/billing/adapters/output/mod.rs index f895176..bb9a6b6 100644 --- a/src/billing/adapters/output/mod.rs +++ b/src/billing/adapters/output/mod.rs @@ -1,4 +1,4 @@ // SPDX-FileCopyrightText: 2024 Aravinth Manivannan // // SPDX-License-Identifier: AGPL-3.0-or-later -mod db; +pub(crate) mod db; diff --git a/src/billing/mod.rs b/src/billing/mod.rs index 7272406..10dac6e 100644 --- a/src/billing/mod.rs +++ b/src/billing/mod.rs @@ -2,6 +2,6 @@ // // SPDX-License-Identifier: AGPL-3.0-or-later -mod adapters; +pub(crate) mod adapters; mod application; mod domain; From 7bb8dff35e3e9083945cd6773cde54b3b8c43127 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Thu, 9 Jan 2025 01:05:57 +0530 Subject: [PATCH 07/63] feat: constructors for util adapters --- src/utils/random_number.rs | 6 ++++-- src/utils/random_string.rs | 6 ++++-- src/utils/uuid.rs | 6 ++++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/utils/random_number.rs b/src/utils/random_number.rs index f78ef37..3c545f2 100644 --- a/src/utils/random_number.rs +++ b/src/utils/random_number.rs @@ -32,10 +32,12 @@ impl GenerateRandomNumberInterface for GenerateRandomNumber { } impl GenerateRandomNumber { + pub fn new() -> GenerateRandomNumberInterfaceObj { + Arc::new(GenerateRandomNumber) + } pub fn inject() -> impl FnOnce(&mut web::ServiceConfig) { - let g = WebGenerateRandomNumberInterfaceObj::new(Arc::new(GenerateRandomNumber)); let f = move |cfg: &mut web::ServiceConfig| { - cfg.app_data(g); + cfg.app_data(WebGenerateRandomNumberInterfaceObj::new(Self::new())); }; Box::new(f) diff --git a/src/utils/random_string.rs b/src/utils/random_string.rs index a1fd9f1..3d2614e 100644 --- a/src/utils/random_string.rs +++ b/src/utils/random_string.rs @@ -37,10 +37,12 @@ impl GenerateRandomStringInterface for GenerateRandomString { } impl GenerateRandomString { + pub fn new() -> GenerateRandomStringInterfaceObj { + Arc::new(GenerateRandomString) + } pub fn inject() -> impl FnOnce(&mut web::ServiceConfig) { - let g = WebGenerateRandomStringInterfaceObj::new(Arc::new(GenerateRandomString)); let f = move |cfg: &mut web::ServiceConfig| { - cfg.app_data(g); + cfg.app_data(WebGenerateRandomStringInterfaceObj::new(Self::new())); }; Box::new(f) diff --git a/src/utils/uuid.rs b/src/utils/uuid.rs index 7e0ccf7..213270b 100644 --- a/src/utils/uuid.rs +++ b/src/utils/uuid.rs @@ -28,10 +28,12 @@ impl GetUUID for GenerateUUID { } impl GenerateUUID { + pub fn new() -> GetUUIDInterfaceObj { + Arc::new(GenerateUUID) + } pub fn inject() -> impl FnOnce(&mut web::ServiceConfig) { - let g = WebGetUUIDInterfaceObj::new(Arc::new(GenerateUUID)); let f = move |cfg: &mut web::ServiceConfig| { - cfg.app_data(g); + cfg.app_data(WebGetUUIDInterfaceObj::new(Self::new())); }; Box::new(f) From 07057e08249c7c0a4992ad72cd9730c7757666a3 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Thu, 9 Jan 2025 01:09:59 +0530 Subject: [PATCH 08/63] feat: rename mailer mock --- .../application/port/output/mailer/account_validation_link.rs | 2 +- src/identity/application/services/register_user/service.rs | 4 ++-- .../application/services/resend_verification_email/service.rs | 4 ++-- src/identity/application/services/set_user_admin/service.rs | 2 +- src/identity/application/services/update_email/service.rs | 4 ++-- src/identity/application/services/update_password/service.rs | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/identity/application/port/output/mailer/account_validation_link.rs b/src/identity/application/port/output/mailer/account_validation_link.rs index 533aa5c..9a380e3 100644 --- a/src/identity/application/port/output/mailer/account_validation_link.rs +++ b/src/identity/application/port/output/mailer/account_validation_link.rs @@ -30,7 +30,7 @@ pub mod tests { use std::sync::Arc; - pub fn mock_account_validation_link_db_port( + pub fn mock_account_validation_link_mailer_port( times: Option, ) -> AccountValidationLinkOutMailerPortObj { let mut m = MockAccountValidationLinkOutMailerPort::new(); diff --git a/src/identity/application/services/register_user/service.rs b/src/identity/application/services/register_user/service.rs index 3acb971..469f875 100644 --- a/src/identity/application/services/register_user/service.rs +++ b/src/identity/application/services/register_user/service.rs @@ -127,7 +127,7 @@ mod tests { IS_CALLED_ONLY_ONCE, RETURNS_RANDOM_STRING.into(), )) - .mailer_account_validation_link_adapter(mock_account_validation_link_db_port( + .mailer_account_validation_link_adapter(mock_account_validation_link_mailer_port( IS_CALLED_ONLY_ONCE, )) .get_uuid(mock_get_uuid(IS_CALLED_ONLY_ONCE)) @@ -172,7 +172,7 @@ mod tests { IS_NEVER_CALLED, RETURNS_RANDOM_STRING.into(), )) - .mailer_account_validation_link_adapter(mock_account_validation_link_db_port( + .mailer_account_validation_link_adapter(mock_account_validation_link_mailer_port( IS_NEVER_CALLED, )) .get_uuid(mock_get_uuid(IS_NEVER_CALLED)) diff --git a/src/identity/application/services/resend_verification_email/service.rs b/src/identity/application/services/resend_verification_email/service.rs index 57f9259..2306323 100644 --- a/src/identity/application/services/resend_verification_email/service.rs +++ b/src/identity/application/services/resend_verification_email/service.rs @@ -77,7 +77,7 @@ mod tests { IS_CALLED_ONLY_ONCE, RETURNS_FALSE, )) - .mailer_account_validation_link_adapter(mock_account_validation_link_db_port( + .mailer_account_validation_link_adapter(mock_account_validation_link_mailer_port( IS_CALLED_ONLY_ONCE, )) .build() @@ -105,7 +105,7 @@ mod tests { secret.into(), )) .db_email_exists_adapter(mock_email_exists_db_port(IS_CALLED_ONLY_ONCE, RETURNS_TRUE)) - .mailer_account_validation_link_adapter(mock_account_validation_link_db_port( + .mailer_account_validation_link_adapter(mock_account_validation_link_mailer_port( IS_NEVER_CALLED, )) .build() diff --git a/src/identity/application/services/set_user_admin/service.rs b/src/identity/application/services/set_user_admin/service.rs index a6e34b0..b3f2448 100644 --- a/src/identity/application/services/set_user_admin/service.rs +++ b/src/identity/application/services/set_user_admin/service.rs @@ -4,7 +4,7 @@ use super::*; -struct SetUserAdminService; +pub struct SetUserAdminService; #[async_trait::async_trait] impl SetUserAdminUseCase for SetUserAdminService { diff --git a/src/identity/application/services/update_email/service.rs b/src/identity/application/services/update_email/service.rs index c6089b8..6ee0d02 100644 --- a/src/identity/application/services/update_email/service.rs +++ b/src/identity/application/services/update_email/service.rs @@ -100,7 +100,7 @@ mod tests { IS_CALLED_ONLY_ONCE, RETURNS_RANDOM_STRING.into(), )) - .mailer_account_validation_link_adapter(mock_account_validation_link_db_port( + .mailer_account_validation_link_adapter(mock_account_validation_link_mailer_port( IS_CALLED_ONLY_ONCE, )) .build() @@ -124,7 +124,7 @@ mod tests { IS_NEVER_CALLED, RETURNS_RANDOM_STRING.into(), )) - .mailer_account_validation_link_adapter(mock_account_validation_link_db_port( + .mailer_account_validation_link_adapter(mock_account_validation_link_mailer_port( IS_NEVER_CALLED, )) .build() diff --git a/src/identity/application/services/update_password/service.rs b/src/identity/application/services/update_password/service.rs index 1730c12..feee380 100644 --- a/src/identity/application/services/update_password/service.rs +++ b/src/identity/application/services/update_password/service.rs @@ -4,7 +4,7 @@ use super::*; -struct UpdatePasswordService; +pub struct UpdatePasswordService; #[async_trait::async_trait] impl UpdatePasswordUseCase for UpdatePasswordService { From 7d7bd551c3f4ef2e478f0c53acadafa7581aaa23 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Thu, 9 Jan 2025 01:10:36 +0530 Subject: [PATCH 09/63] hotfix: make lettre init sync for use within actix framework init --- .../adapters/output/mailer/lettre/account_validation_link.rs | 2 +- src/identity/adapters/output/mailer/lettre/mod.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/identity/adapters/output/mailer/lettre/account_validation_link.rs b/src/identity/adapters/output/mailer/lettre/account_validation_link.rs index 2e631e9..80a7cb3 100644 --- a/src/identity/adapters/output/mailer/lettre/account_validation_link.rs +++ b/src/identity/adapters/output/mailer/lettre/account_validation_link.rs @@ -51,7 +51,7 @@ mod tests { let validation_secret = "dafsdfasecret"; let settings = crate::settings::tests::get_settings().await; - let m = LettreMailer::new(&settings).await; + let m = LettreMailer::new(&settings); m.account_validation_link(email, username, validation_secret) .await diff --git a/src/identity/adapters/output/mailer/lettre/mod.rs b/src/identity/adapters/output/mailer/lettre/mod.rs index f0e998b..cbb5778 100644 --- a/src/identity/adapters/output/mailer/lettre/mod.rs +++ b/src/identity/adapters/output/mailer/lettre/mod.rs @@ -16,13 +16,13 @@ pub struct LettreMailer { } impl LettreMailer { - pub async fn new(s: &Settings) -> Self { + pub fn new(s: &Settings) -> Self { let mailer: AsyncSmtpTransport = AsyncSmtpTransport::::from_url(s.email.url.as_str()) .unwrap() .build(); - assert!(mailer.test_connection().await.unwrap()); + // assert!(mailer.test_connection().await.unwrap()); Self { mailer, From 543f66f5c5d06b5b0d5fd5f7cb6e2edf6a3969c6 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Thu, 9 Jan 2025 01:13:21 +0530 Subject: [PATCH 10/63] fix: rename bililng cqrs types, and use struct to aggregate all execs --- src/billing/adapters/input/web/bill.rs | 12 ++++---- src/billing/adapters/mod.rs | 14 ++++++--- src/billing/adapters/types.rs | 42 ++++++++++++++++++++------ 3 files changed, 47 insertions(+), 21 deletions(-) diff --git a/src/billing/adapters/input/web/bill.rs b/src/billing/adapters/input/web/bill.rs index c9e58df..54af990 100644 --- a/src/billing/adapters/input/web/bill.rs +++ b/src/billing/adapters/input/web/bill.rs @@ -61,10 +61,10 @@ const UUID: Uuid = uuid::uuid!("67e55044-10b1-426f-9247-bb680e5fe0c8"); #[post("/billing/store/add")] #[tracing::instrument( name = "add store handler", - skip(billing_store_cqrs_exec, billing_store_cqrs_view, uuid_generator) + skip(billing_cqrs_exec, billing_store_cqrs_view, uuid_generator) )] async fn add_store_submit_handler( - billing_store_cqrs_exec: types::WebBillingStoreCqrsExec, + billing_cqrs_exec: types::WebBillingCqrsExec, billing_store_cqrs_view: types::WebBillingStoreCqrsView, uuid_generator: WebGetUUIDInterfaceObj, req: HttpRequest, @@ -83,7 +83,7 @@ async fn add_store_submit_handler( .build() .unwrap(); - billing_store_cqrs_exec + billing_cqrs_exec .execute(&store_uuid_str, BillingCommand::AddStore(cmd)) .await .unwrap(); @@ -132,10 +132,10 @@ async fn add_bill_page_handler() -> WebJsonRepsonse { #[post("/billing/bill/add")] #[tracing::instrument( name = "add bill handler", - skip(billing_bill_cqrs_exec, billing_bill_cqrs_view,) + skip(billing_cqrs_exec, billing_bill_cqrs_view,) )] async fn add_bill_submit_handler( - billing_bill_cqrs_exec: types::WebBillingBillCqrsExec, + billing_cqrs_exec: types::WebBillingCqrsExec, billing_bill_cqrs_view: types::WebBillingBillCqrsView, req: HttpRequest, // id: Identity, @@ -153,7 +153,7 @@ async fn add_bill_submit_handler( .build() .unwrap(); - billing_bill_cqrs_exec + billing_cqrs_exec .execute(&bill_uuid_str, BillingCommand::AddBill(cmd)) .await .unwrap(); diff --git a/src/billing/adapters/mod.rs b/src/billing/adapters/mod.rs index e453b2d..94d2c89 100644 --- a/src/billing/adapters/mod.rs +++ b/src/billing/adapters/mod.rs @@ -53,17 +53,21 @@ pub fn load_adapters(pool: PgPool, settings: Settings) -> impl FnOnce(&mut web:: let (line_item_cqrs_exec, line_item_cqrs_query) = line_item_view::init_cqrs(db.clone(), services.clone()); + let billing_cqrs_exec = types::BillingCqrsExecBuilder::default() + .bill(bill_cqrs_exec) + .line_item(line_item_cqrs_exec) + .store(store_cqrs_exec) + .build() + .unwrap(); + let f = move |cfg: &mut web::ServiceConfig| { cfg.configure(input::web::load_ctx()); - cfg.app_data(Data::new(bill_cqrs_exec.clone())); cfg.app_data(Data::new(bill_cqrs_query.clone())); - - cfg.app_data(Data::new(store_cqrs_exec.clone())); cfg.app_data(Data::new(store_cqrs_query.clone())); - - cfg.app_data(Data::new(line_item_cqrs_exec.clone())); cfg.app_data(Data::new(line_item_cqrs_query.clone())); + + cfg.app_data(Data::new(Arc::new(billing_cqrs_exec))); }; Box::new(f) diff --git a/src/billing/adapters/types.rs b/src/billing/adapters/types.rs index 316a085..f5c3d59 100644 --- a/src/billing/adapters/types.rs +++ b/src/billing/adapters/types.rs @@ -1,7 +1,3 @@ -// SPDX-FileCopyrightText: 2024 Aravinth Manivannan -// -// SPDX-License-Identifier: AGPL-3.0-or-later - // SPDX-FileCopyrightText: 2024 Aravinth Manivannan // // SPDX-License-Identifier: AGPL-3.0-or-later @@ -10,7 +6,8 @@ use std::sync::Arc; use actix_web::web::Data; -use cqrs_es::persist::ViewRepository; +use cqrs_es::{persist::ViewRepository, AggregateError}; +use derive_builder::Builder; use postgres_es::PostgresCqrs; use crate::billing::{ @@ -21,8 +18,11 @@ use crate::billing::{ BillingDBPostgresAdapter, }, }, - application::services::BillingServicesObj, - domain::{bill_aggregate::Bill, line_item_aggregate::LineItem, store_aggregate::Store}, + application::services::{errors::BillingError, BillingServicesObj}, + domain::{ + bill_aggregate::Bill, commands::BillingCommand, line_item_aggregate::LineItem, + store_aggregate::Store, + }, }; pub type WebBillingRoutesRepository = Data>; @@ -30,16 +30,38 @@ pub type WebBillingRoutesRepository = Data>; pub type WebBillingServiceObj = Data; pub type BillingBillCqrsExec = Arc>; -pub type WebBillingBillCqrsExec = Data; pub type BillingBillCqrsView = Arc>; pub type WebBillingBillCqrsView = Data; pub type BillingLineItemCqrsExec = Arc>; -pub type WebBillingLineItemCqrsExec = Data; pub type BillingLineItemCqrsView = Arc>; pub type WebBillingLineItemCqrsView = Data; pub type BillingStoreCqrsExec = Arc>; -pub type WebBillingStoreCqrsExec = Data; pub type BillingStoreCqrsView = Arc>; pub type WebBillingStoreCqrsView = Data; + +pub type WebBillingCqrsExec = Data>; + +#[derive(Clone, Builder)] +pub struct BillingCqrsExec { + bill: BillingBillCqrsExec, + line_item: BillingLineItemCqrsExec, + store: BillingStoreCqrsExec, +} + +impl BillingCqrsExec { + pub async fn execute( + &self, + aggregate_id: &str, + command: BillingCommand, + ) -> Result<(), AggregateError> { + self.bill.execute(aggregate_id, command.clone()).await?; + self.line_item + .execute(aggregate_id, command.clone()) + .await?; + self.store.execute(aggregate_id, command).await?; + + Ok(()) + } +} From fe113a1f42697eba4606e9d13c814dd234627443 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Thu, 9 Jan 2025 01:14:09 +0530 Subject: [PATCH 11/63] feat: cqrs init utils for identity Employee & User --- .../output/db/postgres/employee_view.rs | 18 +++++++++- .../adapters/output/db/postgres/user_view.rs | 33 ++++++++++++++++++- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/identity/adapters/output/db/postgres/employee_view.rs b/src/identity/adapters/output/db/postgres/employee_view.rs index 8037ee3..f742dcf 100644 --- a/src/identity/adapters/output/db/postgres/employee_view.rs +++ b/src/identity/adapters/output/db/postgres/employee_view.rs @@ -3,6 +3,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later use std::str::FromStr; +use std::sync::Arc; use async_trait::async_trait; use cqrs_es::persist::{PersistenceError, ViewContext, ViewRepository}; @@ -13,7 +14,8 @@ use uuid::Uuid; use super::errors::*; use super::DBOutPostgresAdapter; -use crate::identity::application::services::events::IdentityEvent; +use crate::identity::adapters::types::{IdentityEmployeeCqrsExec, IdentityEmployeeCqrsView}; +use crate::identity::application::services::{events::IdentityEvent, IdentityServicesObj}; use crate::identity::domain::employee_aggregate::*; use crate::types::currency::{self, Currency, PriceBuilder}; use crate::utils::parse_aggregate_id::parse_aggregate_id; @@ -287,6 +289,20 @@ impl Query for DBOutPostgresAdapter { } } +pub fn init_cqrs( + db: DBOutPostgresAdapter, + services: IdentityServicesObj, +) -> (IdentityEmployeeCqrsExec, IdentityEmployeeCqrsView) { + let queries: Vec>> = vec![Box::new(db.clone())]; + + let pool = db.pool.clone(); + + ( + Arc::new(postgres_es::postgres_cqrs(pool.clone(), queries, services)), + Arc::new(db.clone()), + ) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/identity/adapters/output/db/postgres/user_view.rs b/src/identity/adapters/output/db/postgres/user_view.rs index c5429a1..8105a1c 100644 --- a/src/identity/adapters/output/db/postgres/user_view.rs +++ b/src/identity/adapters/output/db/postgres/user_view.rs @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2024 Aravinth Manivannan // // SPDX-License-Identifier: AGPL-3.0-or-later +use std::sync::Arc; use async_trait::async_trait; use cqrs_es::persist::GenericQuery; @@ -11,7 +12,8 @@ use uuid::Uuid; use super::errors::*; use super::DBOutPostgresAdapter; -use crate::identity::application::services::events::IdentityEvent; +use crate::identity::adapters::types::{IdentityUserCqrsExec, IdentityUserCqrsView}; +use crate::identity::application::services::{events::IdentityEvent, IdentityServicesObj}; use crate::identity::domain::aggregate::User; use crate::utils::parse_aggregate_id::parse_aggregate_id; @@ -206,7 +208,36 @@ impl Query for SimpleLoggingQuery { } } +#[async_trait] +impl Query for DBOutPostgresAdapter { + async fn dispatch(&self, user_id: &str, events: &[EventEnvelope]) { + let res = self + .load_with_context(user_id) + .await + .unwrap_or_else(|_| Some((UserView::default(), ViewContext::new(user_id.into(), 0)))); + let (mut view, view_context): (UserView, 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 UserQuery = GenericQuery; + +pub fn init_cqrs( + db: DBOutPostgresAdapter, + services: IdentityServicesObj, +) -> (IdentityUserCqrsExec, IdentityUserCqrsView) { + let queries: Vec>> = vec![Box::new(db.clone())]; + + let pool = db.pool.clone(); + + ( + Arc::new(postgres_es::postgres_cqrs(pool.clone(), queries, services)), + Arc::new(db.clone()), + ) +} From 5283f0544fcb2a8e697bef9558b907db1d3fcb86 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Thu, 9 Jan 2025 01:14:47 +0530 Subject: [PATCH 12/63] feat: delete user service publicly accessible --- src/identity/application/services/delete_user/service.rs | 2 +- src/identity/application/services/employee_register_service.rs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/identity/application/services/delete_user/service.rs b/src/identity/application/services/delete_user/service.rs index 620cafe..e8141cb 100644 --- a/src/identity/application/services/delete_user/service.rs +++ b/src/identity/application/services/delete_user/service.rs @@ -4,7 +4,7 @@ use super::*; -struct DeleteUserService; +pub struct DeleteUserService; #[async_trait::async_trait] impl DeleteUserUseCase for DeleteUserService { diff --git a/src/identity/application/services/employee_register_service.rs b/src/identity/application/services/employee_register_service.rs index 2141690..2bffc1a 100644 --- a/src/identity/application/services/employee_register_service.rs +++ b/src/identity/application/services/employee_register_service.rs @@ -7,7 +7,6 @@ use mockall::predicate::*; use mockall::*; use super::errors::*; -use super::*; use crate::identity::application::port::output::{ db::{create_verification_otp::*, emp_id_exists::*, phone_exists::*}, phone::account_validation_otp::*, From cf809e2f7418758e8e11851e60d74a64fad58645 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Thu, 9 Jan 2025 01:15:48 +0530 Subject: [PATCH 13/63] feat: define Store aggregate, commands and event in Identity domain --- src/identity/application/services/events.rs | 12 +- src/identity/domain/add_store_command.rs | 109 ++++++++++++++ src/identity/domain/mod.rs | 6 +- src/identity/domain/store_added_event.rs | 18 +++ src/identity/domain/store_aggregate.rs | 148 ++++++++++++++++++++ src/identity/domain/store_updated_event.rs | 43 ++++++ src/identity/domain/update_store_command.rs | 127 +++++++++++++++++ 7 files changed, 460 insertions(+), 3 deletions(-) create mode 100644 src/identity/domain/add_store_command.rs create mode 100644 src/identity/domain/store_added_event.rs create mode 100644 src/identity/domain/store_aggregate.rs create mode 100644 src/identity/domain/store_updated_event.rs create mode 100644 src/identity/domain/update_store_command.rs diff --git a/src/identity/application/services/events.rs b/src/identity/application/services/events.rs index fc59564..4961adf 100644 --- a/src/identity/application/services/events.rs +++ b/src/identity/application/services/events.rs @@ -14,8 +14,8 @@ use super::update_password::events::*; use crate::identity::domain::{ employee_logged_in_event::*, employee_registered_event::*, invite_accepted_event::*, login_otp_sent_event::*, organization_exited_event::*, phone_number_changed_event::*, - phone_number_verified_event::*, resend_login_otp_event::*, verification_otp_resent_event::*, - verification_otp_sent_event::*, + phone_number_verified_event::*, resend_login_otp_event::*, store_added_event::*, + store_updated_event::*, verification_otp_resent_event::*, verification_otp_sent_event::*, }; #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd)] @@ -39,6 +39,10 @@ pub enum IdentityEvent { PhoneNumberChanged(PhoneNumberChangedEvent), InviteAccepted(InviteAcceptedEvent), OrganizationExited(OrganizationExitedEvent), + + // store events + StoreAdded(StoreAddedEvent), + StoreUpdated(StoreUpdatedEvent), } //TODO: define password type that takes string and converts to hash @@ -69,6 +73,10 @@ impl DomainEvent for IdentityEvent { IdentityEvent::PhoneNumberChanged { .. } => "EmployeePhoneNumberChanged", IdentityEvent::InviteAccepted { .. } => "EmployeeInviteAccepted", IdentityEvent::OrganizationExited { .. } => "EmployeeOrganizationExited", + + // store + IdentityEvent::StoreAdded { .. } => "IdentityStoreAdded", + IdentityEvent::StoreUpdated { .. } => "IdentityStoreUpdated", }; e.to_string() diff --git a/src/identity/domain/add_store_command.rs b/src/identity/domain/add_store_command.rs new file mode 100644 index 0000000..68cb21e --- /dev/null +++ b/src/identity/domain/add_store_command.rs @@ -0,0 +1,109 @@ +// 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 uuid::Uuid; + +#[derive(Debug, Error, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +pub enum AddStoreCommandError { + NameIsEmpty, +} + +#[derive( + Clone, Builder, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, +)] +#[builder(build_fn(validate = "Self::validate"))] +pub struct AddStoreCommand { + #[builder(setter(custom))] + name: String, + #[builder(setter(custom))] + address: Option, + store_id: Uuid, + owner: Uuid, +} + +impl AddStoreCommandBuilder { + pub fn address(&mut self, address: Option) -> &mut Self { + self.address = if let Some(address) = address { + let address = address.trim(); + if address.is_empty() { + Some(None) + } else { + Some(Some(address.to_owned())) + } + } else { + Some(None) + }; + self + } + + pub fn name(&mut self, name: String) -> &mut Self { + self.name = Some(name.trim().to_owned()); + self + } + + fn validate(&self) -> Result<(), String> { + let name = self.name.as_ref().unwrap().trim().to_owned(); + if name.is_empty() { + return Err(AddStoreCommandError::NameIsEmpty.to_string()); + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::tests::bdd::*; + use crate::utils::uuid::tests::*; + + use super::*; + + #[test] + fn test_cmd() { + let name = "foo"; + let address = "bar"; + let owner = UUID; + + // address = None + let cmd = AddStoreCommandBuilder::default() + .name(name.into()) + .address(None) + .owner(owner) + .store_id(UUID) + .build() + .unwrap(); + // let cmd = AddStoreCommand::new(name.into(), None, owner, UUID).unwrap(); + assert_eq!(cmd.name(), name); + assert_eq!(cmd.address(), &None); + assert_eq!(cmd.owner(), &owner); + assert_eq!(*cmd.store_id(), UUID); + + // address = Some + let cmd = AddStoreCommandBuilder::default() + .name(name.into()) + .address(Some(address.into())) + .owner(owner) + .store_id(UUID) + .build() + .unwrap(); + // let cmd = AddStoreCommand::new(name.into(), Some(address.into()), owner, UUID).unwrap(); + assert_eq!(cmd.name(), name); + assert_eq!(cmd.address(), &Some(address.to_owned())); + assert_eq!(cmd.owner(), &owner); + assert_eq!(*cmd.store_id(), UUID); + + // AddStoreCommandError::NameIsEmpty + + assert!(AddStoreCommandBuilder::default() + .name("".into()) + .address(Some(address.into())) + .owner(owner) + .store_id(UUID) + .build() + .is_err()) + } +} diff --git a/src/identity/domain/mod.rs b/src/identity/domain/mod.rs index 47da0f3..9acec2f 100644 --- a/src/identity/domain/mod.rs +++ b/src/identity/domain/mod.rs @@ -5,7 +5,7 @@ pub mod aggregate; pub mod employee_aggregate; //pub mod employee_commands; -// pub mod store_aggregate; +pub mod store_aggregate; // pub mod invite; // events @@ -17,15 +17,19 @@ pub mod organization_exited_event; pub mod phone_number_changed_event; pub mod phone_number_verified_event; pub mod resend_login_otp_event; +pub mod store_added_event; +pub mod store_updated_event; pub mod verification_otp_resent_event; pub mod verification_otp_sent_event; // commands pub mod accept_invite_command; +pub mod add_store_command; pub mod change_phone_number_command; pub mod employee_login_command; pub mod employee_register_command; pub mod exit_organization_command; pub mod resend_login_otp_command; pub mod resend_verification_otp_command; +pub mod update_store_command; pub mod verify_phone_number_command; diff --git a/src/identity/domain/store_added_event.rs b/src/identity/domain/store_added_event.rs new file mode 100644 index 0000000..6336da0 --- /dev/null +++ b/src/identity/domain/store_added_event.rs @@ -0,0 +1,18 @@ +// 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; + +#[derive( + Clone, Debug, Builder, Serialize, Deserialize, Getters, Eq, PartialEq, Ord, PartialOrd, +)] +pub struct StoreAddedEvent { + name: String, + address: Option, + owner: Uuid, + store_id: Uuid, +} diff --git a/src/identity/domain/store_aggregate.rs b/src/identity/domain/store_aggregate.rs new file mode 100644 index 0000000..65a9701 --- /dev/null +++ b/src/identity/domain/store_aggregate.rs @@ -0,0 +1,148 @@ +// 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 uuid::Uuid; + +use crate::identity::application::services::{errors::*, events::*, *}; + +#[derive( + Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Builder, Getters, +)] +pub struct Store { + name: String, + address: Option, + owner: Uuid, + store_id: Uuid, + #[builder(default = "false")] + deleted: bool, +} + +#[async_trait] +impl Aggregate for Store { + type Command = IdentityCommand; + type Event = IdentityEvent; + type Error = IdentityError; + type Services = std::sync::Arc; + + // This identifier should be unique to the system. + fn aggregate_type() -> String { + "billing.store".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 { + IdentityCommand::AddStore(cmd) => { + let res = services.add_store().add_store(cmd).await?; + Ok(vec![IdentityEvent::StoreAdded(res)]) + } + IdentityCommand::UpdateStore(cmd) => { + let res = services.update_store().update_store(cmd).await?; + Ok(vec![IdentityEvent::StoreUpdated(res)]) + } + + _ => Ok(Vec::default()), + } + } + + fn apply(&mut self, event: Self::Event) { + match event { + IdentityEvent::StoreAdded(e) => { + self.name = e.name().into(); + self.address = e.address().as_ref().map(|s| s.to_string()); + self.owner = *e.owner(); + self.store_id = *e.store_id(); + self.deleted = false; + } + IdentityEvent::StoreUpdated(e) => *self = e.new_store().clone(), + _ => (), + } + } +} +// +//#[cfg(test)] +//mod tests { +// use std::sync::Arc; +// +// use cqrs_es::test::TestFramework; +// use update_store_service::tests::mock_update_store_service; +// +// use super::*; +// use crate::billing::{ +// application::services::add_store_service::tests::*, +// domain::{ +// add_store_command::*, commands::IdentityCommand, events::IdentityEvent, +// store_added_event::*, store_updated_event::tests::get_store_updated_event_from_command, +// update_store_command::tests::get_update_store_cmd, +// }, +// }; +// use crate::tests::bdd::*; +// use crate::utils::uuid::tests::*; +// +// // A test framework that will apply our events and command +// // and verify that the logic works as expected. +// type StoreTestFramework = TestFramework; +// +// #[test] +// fn test_create_store() { +// let name = "store_name"; +// let address = Some("store_address".to_string()); +// let owner = UUID; +// let store_id = UUID; +// +// let expected = StoreAddedEventBuilder::default() +// .name(name.into()) +// .address(address.clone()) +// .store_id(store_id) +// .owner(owner) +// .build() +// .unwrap(); +// let expected = IdentityEvent::StoreAdded(expected); +// +// let cmd = AddStoreCommandBuilder::default() +// .name(name.into()) +// .address(address.clone()) +// .owner(owner) +// .store_id(UUID) +// .build() +// .unwrap(); +// +// let mut services = MockIdentityServicesInterface::new(); +// services +// .expect_add_store() +// .times(IS_CALLED_ONLY_ONCE.unwrap()) +// .return_const(mock_add_store_service(IS_CALLED_ONLY_ONCE, cmd.clone())); +// +// StoreTestFramework::with(Arc::new(services)) +// .given_no_previous_events() +// .when(IdentityCommand::AddStore(cmd)) +// .then_expect_events(vec![expected]); +// } +// +// #[test] +// fn test_update_store() { +// let cmd = get_update_store_cmd(); +// let expected = IdentityEvent::StoreUpdated(get_store_updated_event_from_command(&cmd)); +// +// let mut services = MockIdentityServicesInterface::new(); +// services +// .expect_update_store() +// .times(IS_CALLED_ONLY_ONCE.unwrap()) +// .return_const(mock_update_store_service(IS_CALLED_ONLY_ONCE, cmd.clone())); +// +// StoreTestFramework::with(Arc::new(services)) +// .given_no_previous_events() +// .when(IdentityCommand::UpdateStore(cmd)) +// .then_expect_events(vec![expected]); +// } +//} diff --git a/src/identity/domain/store_updated_event.rs b/src/identity/domain/store_updated_event.rs new file mode 100644 index 0000000..fb415ce --- /dev/null +++ b/src/identity/domain/store_updated_event.rs @@ -0,0 +1,43 @@ +// 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::store_aggregate::*; + +#[derive( + Clone, Debug, Builder, Serialize, Deserialize, Getters, Eq, PartialEq, Ord, PartialOrd, +)] +pub struct StoreUpdatedEvent { + added_by_user: Uuid, + old_store: Store, + new_store: Store, +} + +#[cfg(test)] +pub mod tests { + use crate::identity::domain::update_store_command::UpdateStoreCommand; + + use super::*; + + pub fn get_store_updated_event_from_command(cmd: &UpdateStoreCommand) -> StoreUpdatedEvent { + let new_store = StoreBuilder::default() + .name(cmd.name().into()) + .address(cmd.address().as_ref().map(|s| s.to_string())) + .owner(*cmd.owner()) + .store_id(*cmd.old_store().store_id()) + .build() + .unwrap(); + + StoreUpdatedEventBuilder::default() + .new_store(new_store) + .old_store(cmd.old_store().clone()) + .added_by_user(*cmd.adding_by()) + .build() + .unwrap() + } +} diff --git a/src/identity/domain/update_store_command.rs b/src/identity/domain/update_store_command.rs new file mode 100644 index 0000000..53c2e2c --- /dev/null +++ b/src/identity/domain/update_store_command.rs @@ -0,0 +1,127 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use derive_getters::Getters; +use derive_more::{Display, Error}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use super::store_aggregate::*; + +#[derive(Debug, Error, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +pub enum UpdateStoreCommandError { + NameIsEmpty, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)] +pub struct UpdateStoreCommand { + name: String, + address: Option, + owner: Uuid, + old_store: Store, + adding_by: Uuid, +} + +impl UpdateStoreCommand { + pub fn new( + name: String, + address: Option, + owner: Uuid, + old_store: Store, + adding_by: Uuid, + ) -> Result { + let address: Option = if let Some(address) = address { + let address = address.trim(); + if address.is_empty() { + None + } else { + Some(address.to_owned()) + } + } else { + None + }; + + let name = name.trim().to_owned(); + if name.is_empty() { + return Err(UpdateStoreCommandError::NameIsEmpty); + } + + Ok(Self { + name, + address, + owner, + old_store, + adding_by, + }) + } +} + +#[cfg(test)] +pub mod tests { + use crate::utils::uuid::tests::UUID; + + use super::*; + + pub fn get_update_store_cmd() -> UpdateStoreCommand { + let name = "foo"; + let address = "bar"; + let owner = UUID; + let adding_by = UUID; + let old_store = Store::default(); + + UpdateStoreCommand::new( + name.into(), + Some(address.into()), + owner, + old_store.clone(), + adding_by, + ) + .unwrap() + } + + #[test] + fn test_cmd() { + let name = "foo"; + let address = "bar"; + let owner = UUID; + let old_store = Store::default(); + let adding_by = Uuid::new_v4(); + + // address = None + let cmd = UpdateStoreCommand::new(name.into(), None, owner, old_store.clone(), adding_by) + .unwrap(); + assert_eq!(cmd.name(), name); + assert_eq!(cmd.address(), &None); + assert_eq!(cmd.owner(), &owner); + assert_eq!(cmd.old_store(), &old_store); + assert_eq!(cmd.adding_by(), &adding_by); + + // address = Some + let cmd = UpdateStoreCommand::new( + name.into(), + Some(address.into()), + owner, + old_store.clone(), + adding_by, + ) + .unwrap(); + assert_eq!(cmd.name(), name); + assert_eq!(cmd.address(), &Some(address.to_owned())); + assert_eq!(cmd.owner(), &owner); + assert_eq!(cmd.old_store(), &old_store); + assert_eq!(cmd.adding_by(), &adding_by); + + // UpdateStoreCommandError::NameIsEmpty + assert_eq!( + UpdateStoreCommand::new( + "".into(), + Some(address.into()), + owner, + old_store.clone(), + adding_by + ), + Err(UpdateStoreCommandError::NameIsEmpty) + ) + } +} From 636179fb5c43074f7f989afdd80bd63c654eec95 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Thu, 9 Jan 2025 01:16:17 +0530 Subject: [PATCH 14/63] feat: define store_name_exists port for identity --- .../application/port/output/db/errors.rs | 3 ++ .../application/port/output/db/mod.rs | 1 + .../port/output/db/store_name_exists.rs | 54 +++++++++++++++++++ 3 files changed, 58 insertions(+) create mode 100644 src/identity/application/port/output/db/store_name_exists.rs diff --git a/src/identity/application/port/output/db/errors.rs b/src/identity/application/port/output/db/errors.rs index 93df497..4b4eeb7 100644 --- a/src/identity/application/port/output/db/errors.rs +++ b/src/identity/application/port/output/db/errors.rs @@ -18,4 +18,7 @@ pub enum OutDBPortError { PhoneNumberNotFound, EmpLoginOTPNotFound, EmpVerificationOTPNotFound, + DuplicateStoreName, + DuplicateStoreID, + StoreIDNotFound, } diff --git a/src/identity/application/port/output/db/mod.rs b/src/identity/application/port/output/db/mod.rs index b6f884d..ad3bc00 100644 --- a/src/identity/application/port/output/db/mod.rs +++ b/src/identity/application/port/output/db/mod.rs @@ -19,6 +19,7 @@ pub mod get_verification_secret; pub mod invite_id_exists; pub mod phone_exists; pub mod store_id_exists; +pub mod store_name_exists; pub mod user_id_exists; //pub mod verification_otp_exists; pub mod verification_secret_exists; diff --git a/src/identity/application/port/output/db/store_name_exists.rs b/src/identity/application/port/output/db/store_name_exists.rs new file mode 100644 index 0000000..bf0d9d9 --- /dev/null +++ b/src/identity/application/port/output/db/store_name_exists.rs @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use mockall::predicate::*; +use mockall::*; + +use crate::identity::domain::store_aggregate::Store; + +use super::errors::*; +#[cfg(test)] +#[allow(unused_imports)] +pub use tests::*; + +#[automock] +#[async_trait::async_trait] +pub trait StoreNameExistsDBPort: Send + Sync { + async fn store_name_exists(&self, s: &Store) -> OutDBPortResult; +} + +pub type StoreNameExistsDBPortObj = std::sync::Arc; + +#[cfg(test)] +pub mod tests { + use super::*; + + use std::sync::Arc; + + pub fn mock_store_name_exists_db_port_false(times: Option) -> StoreNameExistsDBPortObj { + let mut m = MockStoreNameExistsDBPort::new(); + if let Some(times) = times { + m.expect_store_name_exists() + .times(times) + .returning(|_| Ok(false)); + } else { + m.expect_store_name_exists().returning(|_| Ok(false)); + } + + Arc::new(m) + } + + pub fn mock_store_name_exists_db_port_true(times: Option) -> StoreNameExistsDBPortObj { + let mut m = MockStoreNameExistsDBPort::new(); + if let Some(times) = times { + m.expect_store_name_exists() + .times(times) + .returning(|_| Ok(true)); + } else { + m.expect_store_name_exists().returning(|_| Ok(true)); + } + + Arc::new(m) + } +} From 4538132c749649d24266345c4901b1d46543864c Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Thu, 9 Jan 2025 01:16:40 +0530 Subject: [PATCH 15/63] feat: handle Store-related errors from Identity DB port --- src/identity/application/services/errors.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/identity/application/services/errors.rs b/src/identity/application/services/errors.rs index 2730859..253a70b 100644 --- a/src/identity/application/services/errors.rs +++ b/src/identity/application/services/errors.rs @@ -29,6 +29,9 @@ pub enum IdentityError { EmployeeNotFound, InviteNotFound, StoreNotFound, + DuplicateStoreName, + StoreIDNotFound, + DuplicateStoreID, } pub type IdentityCommandResult = Result; @@ -74,6 +77,9 @@ impl From for IdentityError { OutDBPortError::PhoneNumberNotFound => Self::PhoneNumberNotFound, OutDBPortError::EmpLoginOTPNotFound => Self::LoginOTPNotFound, OutDBPortError::EmpVerificationOTPNotFound => Self::VerificationOTPNotFound, + OutDBPortError::DuplicateStoreName => Self::DuplicateStoreName, + OutDBPortError::DuplicateStoreID => Self::DuplicateStoreID, + OutDBPortError::StoreIDNotFound => Self::StoreIDNotFound, } } } From 3d57a26d3495c09ed232b194fdfeb56b942491ab Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Thu, 9 Jan 2025 01:17:30 +0530 Subject: [PATCH 16/63] feat: define store addapters for identity and impl Query for Store aggregate --- ...23105314_cqrs_identity_store_query.sql.sql | 16 + .../adapters/output/db/postgres/mod.rs | 4 +- .../output/db/postgres/store_id_exists.rs | 87 ++++ .../output/db/postgres/store_name_exists.rs | 81 ++++ .../adapters/output/db/postgres/store_view.rs | 371 ++++++++++++++++++ 5 files changed, 558 insertions(+), 1 deletion(-) create mode 100644 migrations/20241223105314_cqrs_identity_store_query.sql.sql create mode 100644 src/identity/adapters/output/db/postgres/store_id_exists.rs create mode 100644 src/identity/adapters/output/db/postgres/store_name_exists.rs create mode 100644 src/identity/adapters/output/db/postgres/store_view.rs diff --git a/migrations/20241223105314_cqrs_identity_store_query.sql.sql b/migrations/20241223105314_cqrs_identity_store_query.sql.sql new file mode 100644 index 0000000..25da194 --- /dev/null +++ b/migrations/20241223105314_cqrs_identity_store_query.sql.sql @@ -0,0 +1,16 @@ +--- SPDX-FileCopyrightText: 2024 Aravinth Manivannan +-- +-- SPDX-License-Identifier: AGPL-3.0-or-later + +CREATE TABLE IF NOT EXISTS cqrs_identity_store_query +( + version bigint CHECK (version >= 0) NOT NULL, + + name TEXT NOT NULL, + address TEXT, + owner UUID NOT NULL, + store_id UUID NOT NULL UNIQUE, + deleted BOOLEAN NOT NULL DEFAULT FALSE, + + PRIMARY KEY (store_id) +); diff --git a/src/identity/adapters/output/db/postgres/mod.rs b/src/identity/adapters/output/db/postgres/mod.rs index 5092ebd..9738fc7 100644 --- a/src/identity/adapters/output/db/postgres/mod.rs +++ b/src/identity/adapters/output/db/postgres/mod.rs @@ -15,6 +15,7 @@ pub mod email_exists; pub mod employee_view; mod errors; pub mod get_verification_secret; +pub mod store_view; pub mod user_id_exists; pub mod user_view; pub mod verification_secret_exists; @@ -31,7 +32,8 @@ pub mod phone_exists; //pub mod get_invite; //pub mod invite_id_exists; -//pub mod store_id_exists; +pub mod store_id_exists; +pub mod store_name_exists; #[derive(Clone)] pub struct DBOutPostgresAdapter { diff --git a/src/identity/adapters/output/db/postgres/store_id_exists.rs b/src/identity/adapters/output/db/postgres/store_id_exists.rs new file mode 100644 index 0000000..286e47b --- /dev/null +++ b/src/identity/adapters/output/db/postgres/store_id_exists.rs @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use uuid::Uuid; + +use super::DBOutPostgresAdapter; +use crate::identity::application::port::output::db::{errors::*, store_id_exists::*}; +use crate::identity::domain::store_aggregate::*; + +#[async_trait::async_trait] +impl StoreIDExistsDBPort for DBOutPostgresAdapter { + async fn store_id_exists(&self, store_id: &Uuid) -> OutDBPortResult { + let res = sqlx::query!( + "SELECT EXISTS ( + SELECT 1 + FROM cqrs_identity_store_query + WHERE + store_id = $1 + );", + store_id + ) + .fetch_one(&self.pool) + .await?; + if let Some(x) = res.exists { + Ok(x) + } else { + Ok(false) + } + } +} + +#[cfg(test)] +pub mod tests { + use uuid::Uuid; + + use crate::utils::uuid::tests::UUID; + + use super::*; + + pub async fn create_dummy_store_record(s: &Store, db: &DBOutPostgresAdapter) { + sqlx::query!( + "INSERT INTO cqrs_identity_store_query + (version, name, address, store_id, owner, deleted) + VALUES ($1, $2, $3, $4, $5 ,$6);", + 1, + s.name(), + s.address().as_ref().map(|s| s.as_str()), + s.store_id(), + s.owner(), + false + ) + .execute(&db.pool) + .await + .unwrap(); + } + + #[actix_rt::test] + async fn test_postgres_store_exists() { + let store_id = Uuid::new_v4(); + let settings = crate::settings::tests::get_settings().await; + settings.create_db().await; + let db = super::DBOutPostgresAdapter::new( + sqlx::postgres::PgPool::connect(&settings.database.url) + .await + .unwrap(), + ); + + let store = StoreBuilder::default() + .name("store_name".into()) + .owner(UUID) + .address(Some("store_address".into())) + .store_id(store_id) + .build() + .unwrap(); + + // state doesn't exist + assert!(!db.store_id_exists(store.store_id()).await.unwrap()); + + create_dummy_store_record(&store, &db).await; + + // state exists + assert!(db.store_id_exists(store.store_id()).await.unwrap()); + + settings.drop_db().await; + } +} diff --git a/src/identity/adapters/output/db/postgres/store_name_exists.rs b/src/identity/adapters/output/db/postgres/store_name_exists.rs new file mode 100644 index 0000000..ccb4d56 --- /dev/null +++ b/src/identity/adapters/output/db/postgres/store_name_exists.rs @@ -0,0 +1,81 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use super::DBOutPostgresAdapter; +use crate::identity::application::port::output::db::{errors::*, store_name_exists::*}; +use crate::identity::domain::store_aggregate::*; + +#[async_trait::async_trait] +impl StoreNameExistsDBPort for DBOutPostgresAdapter { + async fn store_name_exists(&self, s: &Store) -> OutDBPortResult { + let res = sqlx::query!( + "SELECT EXISTS ( + SELECT 1 + FROM cqrs_identity_store_query + WHERE + name = $1 + AND + deleted = false + );", + s.name(), + ) + .fetch_one(&self.pool) + .await?; + if let Some(x) = res.exists { + Ok(x) + } else { + Ok(false) + } + } +} + +#[cfg(test)] +mod tests { + use uuid::Uuid; + + use crate::utils::uuid::tests::UUID; + + use super::*; + use crate::identity::adapters::output::db::postgres::store_id_exists::tests::create_dummy_store_record; + + #[actix_rt::test] + async fn test_postgres_store_exists() { + let store_id = Uuid::new_v4(); + let settings = crate::settings::tests::get_settings().await; + settings.create_db().await; + let db = super::DBOutPostgresAdapter::new( + sqlx::postgres::PgPool::connect(&settings.database.url) + .await + .unwrap(), + ); + + let store = StoreBuilder::default() + .name("store_name".into()) + .owner(UUID) + .address(Some("store_address".into())) + .store_id(store_id) + .build() + .unwrap(); + + // state doesn't exist + assert!(!db.store_name_exists(&store).await.unwrap()); + + create_dummy_store_record(&store, &db).await; + + // state exists + assert!(db.store_name_exists(&store).await.unwrap()); + + // Set store.deleted = true; now db.store_name_exists must return false + sqlx::query!( + "UPDATE cqrs_identity_store_query SET deleted = true WHERE store_id = $1;", + store.store_id() + ) + .execute(&db.pool) + .await + .unwrap(); + assert!(!db.store_name_exists(&store).await.unwrap()); + + settings.drop_db().await; + } +} diff --git a/src/identity/adapters/output/db/postgres/store_view.rs b/src/identity/adapters/output/db/postgres/store_view.rs new file mode 100644 index 0000000..82c0b45 --- /dev/null +++ b/src/identity/adapters/output/db/postgres/store_view.rs @@ -0,0 +1,371 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later +use std::sync::Arc; + +use async_trait::async_trait; +use cqrs_es::persist::{PersistenceError, ViewContext, ViewRepository}; +use cqrs_es::{EventEnvelope, Query, View}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use super::errors::*; +use super::DBOutPostgresAdapter; +use crate::identity::adapters::types::{IdentityStoreCqrsExec, IdentityStoreCqrsView}; +use crate::identity::application::services::{ + events::IdentityEvent, IdentityCommand, IdentityServicesObj, +}; +use crate::identity::domain::store_aggregate::*; +use crate::utils::parse_aggregate_id::parse_aggregate_id; + +pub const NEW_STORE_NON_UUID: &str = "identity_new_store_non_uuid-asdfa"; + +// The view for a Store query, for a standard http application this should +// be designed to reflect the response dto that will be returned to a user. +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct StoreView { + name: String, + address: Option, + store_id: Uuid, + owner: Uuid, + deleted: bool, +} + +impl From for Store { + fn from(value: StoreView) -> Self { + StoreBuilder::default() + .name(value.name) + .address(value.address) + .store_id(value.store_id) + .owner(value.owner) + .deleted(value.deleted) + .build() + .unwrap() + } +} + +// 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 StoreView { + fn update(&mut self, event: &EventEnvelope) { + match &event.payload { + IdentityEvent::StoreAdded(val) => { + self.name = val.name().into(); + self.address = val.address().clone(); + self.store_id = *val.store_id(); + self.owner = *val.owner(); + self.deleted = false; + } + IdentityEvent::StoreUpdated(e) => { + let val = e.new_store(); + self.name = val.name().into(); + self.address = val.address().clone(); + self.store_id = *val.store_id(); + self.owner = *val.owner(); + } + _ => (), + } + } +} + +#[async_trait] +impl ViewRepository for DBOutPostgresAdapter { + async fn load(&self, store_id: &str) -> Result, PersistenceError> { + let store_id = match parse_aggregate_id(store_id, NEW_STORE_NON_UUID)? { + Some((val, _)) => return Ok(Some(val)), + None => Uuid::parse_str(store_id).unwrap(), + }; + + let res = sqlx::query_as!( + StoreView, + "SELECT + name, address, store_id, owner, deleted + FROM + cqrs_identity_store_query + WHERE + store_id = $1;", + store_id + ) + .fetch_one(&self.pool) + .await + .map_err(PostgresAggregateError::from)?; + Ok(Some(res)) + } + + async fn load_with_context( + &self, + store_id: &str, + ) -> Result, PersistenceError> { + let store_id = match parse_aggregate_id(store_id, NEW_STORE_NON_UUID)? { + Some(val) => return Ok(Some(val)), + None => Uuid::parse_str(store_id).unwrap(), + }; + + let res = sqlx::query_as!( + StoreView, + "SELECT + name, address, store_id, owner, deleted + FROM + cqrs_identity_store_query + WHERE + store_id = $1;", + &store_id, + ) + .fetch_one(&self.pool) + .await + .map_err(PostgresAggregateError::from)?; + + struct Context { + version: i64, + store_id: Uuid, + } + + let ctx = sqlx::query_as!( + Context, + "SELECT + store_id, version + FROM + cqrs_identity_store_query + WHERE + store_id = $1;", + store_id + ) + .fetch_one(&self.pool) + .await + .map_err(PostgresAggregateError::from)?; + + let view_context = ViewContext::new(ctx.store_id.to_string(), ctx.version); + Ok(Some((res, view_context))) + } + + async fn update_view( + &self, + view: StoreView, + context: ViewContext, + ) -> Result<(), PersistenceError> { + match context.version { + 0 => { + let version = context.version + 1; + sqlx::query!( + "INSERT INTO cqrs_identity_store_query ( + version, name, address, store_id, owner, deleted + ) VALUES ( + $1, $2, $3, $4, $5, $6 + );", + version, + view.name, + view.address, + view.store_id, + view.owner, + view.deleted, + ) + .execute(&self.pool) + .await + .map_err(PostgresAggregateError::from)?; + } + _ => { + let version = context.version + 1; + sqlx::query!( + "UPDATE + cqrs_identity_store_query + SET + version = $1, + name = $2, + address = $3, + owner = $4, + deleted = $5;", + version, + view.name, + view.address, + view.owner, + 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 DBOutPostgresAdapter { + async fn dispatch(&self, store_id: &str, events: &[EventEnvelope]) { + let res = self + .load_with_context(store_id) + .await + .unwrap_or_else(|_| Some((StoreView::default(), ViewContext::new(store_id.into(), 0)))); + let (mut view, view_context): (StoreView, ViewContext) = res.unwrap(); + for event in events { + view.update(event); + } + self.update_view(view, view_context).await.unwrap(); + } +} + +pub fn init_cqrs( + db: DBOutPostgresAdapter, + services: IdentityServicesObj, +) -> (IdentityStoreCqrsExec, IdentityStoreCqrsView) { + let queries: Vec>> = vec![Box::new(db.clone())]; + + let pool = db.pool.clone(); + + ( + Arc::new(postgres_es::postgres_cqrs(pool.clone(), queries, services)), + Arc::new(db.clone()), + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + use postgres_es::PostgresCqrs; + + use crate::{ + db::migrate::*, + identity::{ + application::services::{ + add_store_service::AddStoreServiceBuilder, update_store_service::*, + MockIdentityServicesInterface, + }, + domain::add_store_command::*, + // domain::commands::IdentityCommand, + domain::update_store_command::*, + }, + tests::bdd::*, + utils::{random_string::GenerateRandomStringInterface, uuid::tests::UUID}, + }; + use std::sync::Arc; + + #[actix_rt::test] + async fn pg_query_identity_store_view() { + 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 = DBOutPostgresAdapter::new(db.pool.clone()); + + let simple_query = SimpleLoggingQuery {}; + + let queries: Vec>> = + vec![Box::new(simple_query), Box::new(db.clone())]; + + let mut mock_services = MockIdentityServicesInterface::new(); + + let db2 = db.clone(); + mock_services + .expect_add_store() + .times(IS_CALLED_ONLY_ONCE.unwrap()) + .returning(move || { + Arc::new( + AddStoreServiceBuilder::default() + .db_store_id_exists(Arc::new(db2.clone())) + .db_store_name_exists(Arc::new(db2.clone())) + .build() + .unwrap(), + ) + }); + + let db2 = db.clone(); + mock_services + .expect_update_store() + .times(IS_CALLED_ONLY_ONCE.unwrap()) + .returning(move || { + Arc::new( + UpdateStoreServiceBuilder::default() + .db_store_id_exists(Arc::new(db2.clone())) + .db_store_name_exists(Arc::new(db2.clone())) + .build() + .unwrap(), + ) + }); + + let (cqrs, store_query): ( + Arc>, + Arc>, + ) = ( + Arc::new(postgres_es::postgres_cqrs( + db.pool.clone(), + queries, + Arc::new(mock_services), + )), + Arc::new(db.clone()), + ); + + let rand = crate::utils::random_string::GenerateRandomString {}; + let cmd = AddStoreCommandBuilder::default() + .name(rand.get_random(10)) + .address(None) + .owner(UUID) + .store_id(UUID) + .build() + .unwrap(); + cqrs.execute( + &cmd.store_id().to_string(), + IdentityCommand::AddStore(cmd.clone()), + ) + .await + .unwrap(); + + let store = store_query + .load(&(*cmd.store_id()).to_string()) + .await + .unwrap() + .unwrap(); + let store: Store = store.into(); + assert_eq!(store.name(), cmd.name()); + assert_eq!(store.address(), cmd.address()); + assert_eq!(store.owner(), cmd.owner()); + assert_eq!(store.store_id(), cmd.store_id()); + assert!(!store.deleted()); + + let update_store_cmd = UpdateStoreCommand::new( + rand.get_random(10), + Some(rand.get_random(10)), + UUID, + store, + UUID, + ) + .unwrap(); + cqrs.execute( + &cmd.store_id().to_string(), + IdentityCommand::UpdateStore(update_store_cmd.clone()), + ) + .await + .unwrap(); + let store = store_query + .load(&(*cmd.store_id()).to_string()) + .await + .unwrap() + .unwrap(); + let store: Store = store.into(); + assert_eq!(store.name(), update_store_cmd.name()); + assert_eq!(store.address(), update_store_cmd.address()); + assert_eq!(store.owner(), update_store_cmd.owner()); + assert_eq!(store.store_id(), update_store_cmd.old_store().store_id()); + assert!(!store.deleted()); + + settings.drop_db().await; + } +} From 6dbeded24a435b47257c3ddc65793827e092cfdf Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Thu, 9 Jan 2025 01:17:50 +0530 Subject: [PATCH 17/63] feat: impl add and update store services for identity domain --- .../application/services/add_store_service.rs | 152 ++++++++++++++++++ .../services/update_store_service.rs | 146 +++++++++++++++++ 2 files changed, 298 insertions(+) create mode 100644 src/identity/application/services/add_store_service.rs create mode 100644 src/identity/application/services/update_store_service.rs diff --git a/src/identity/application/services/add_store_service.rs b/src/identity/application/services/add_store_service.rs new file mode 100644 index 0000000..9dac6ff --- /dev/null +++ b/src/identity/application/services/add_store_service.rs @@ -0,0 +1,152 @@ +// 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::identity::{ + application::port::output::db::{store_id_exists::*, store_name_exists::*}, + domain::{ + add_store_command::*, + store_added_event::{StoreAddedEvent, StoreAddedEventBuilder}, + store_aggregate::*, + }, +}; + +#[automock] +#[async_trait::async_trait] +pub trait AddStoreUseCase: Send + Sync { + async fn add_store(&self, cmd: AddStoreCommand) -> IdentityResult; +} + +pub type AddStoreServiceObj = Arc; + +#[derive(Clone, Builder)] +pub struct AddStoreService { + db_store_id_exists: StoreIDExistsDBPortObj, + db_store_name_exists: StoreNameExistsDBPortObj, +} + +#[async_trait::async_trait] +impl AddStoreUseCase for AddStoreService { + async fn add_store(&self, cmd: AddStoreCommand) -> IdentityResult { + if self + .db_store_id_exists + .store_id_exists(cmd.store_id()) + .await? + { + return Err(IdentityError::DuplicateStoreID); + } + + let store = StoreBuilder::default() + .name(cmd.name().into()) + .address(cmd.address().as_ref().map(|s| s.to_string())) + .owner(*cmd.owner()) + .store_id(*cmd.store_id()) + .build() + .unwrap(); + + if self.db_store_name_exists.store_name_exists(&store).await? { + return Err(IdentityError::DuplicateStoreName); + } + + Ok(StoreAddedEventBuilder::default() + .name(store.name().into()) + .address(store.address().as_ref().map(|s| s.to_string())) + .owner(*cmd.owner()) + .store_id(*cmd.store_id()) + .build() + .unwrap()) + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + + use crate::tests::bdd::*; + use crate::utils::uuid::tests::*; + + pub fn mock_add_store_service( + times: Option, + cmd: AddStoreCommand, + ) -> AddStoreServiceObj { + let mut m = MockAddStoreUseCase::new(); + + let res = StoreAddedEventBuilder::default() + .name(cmd.name().into()) + .address(cmd.address().as_ref().map(|s| s.to_string())) + .owner(*cmd.owner()) + .store_id(*cmd.store_id()) + .build() + .unwrap(); + + if let Some(times) = times { + m.expect_add_store() + .times(times) + .returning(move |_| Ok(res.clone())); + } else { + m.expect_add_store().returning(move |_| Ok(res.clone())); + } + + Arc::new(m) + } + + #[actix_rt::test] + async fn test_service_store_id_doesnt_exist() { + let name = "foo"; + let address = "bar"; + let owner = UUID; + + let cmd = AddStoreCommandBuilder::default() + .name(name.into()) + .address(Some(address.into())) + .owner(owner) + .store_id(UUID) + .build() + .unwrap(); + + let s = AddStoreServiceBuilder::default() + .db_store_id_exists(mock_store_id_exists_db_port_false(IS_CALLED_ONLY_ONCE)) + .db_store_name_exists(mock_store_name_exists_db_port_false(IS_CALLED_ONLY_ONCE)) + .build() + .unwrap(); + + let res = s.add_store(cmd.clone()).await.unwrap(); + assert_eq!(res.name(), cmd.name()); + assert_eq!(res.address(), cmd.address()); + assert_eq!(res.owner(), cmd.owner()); + assert_eq!(res.store_id(), cmd.store_id()); + } + + #[actix_rt::test] + async fn test_service_store_name_exists() { + let name = "foo"; + let address = "bar"; + let owner = UUID; + + let cmd = AddStoreCommandBuilder::default() + .name(name.into()) + .address(Some(address.into())) + .owner(owner) + .store_id(UUID) + .build() + .unwrap(); + + let s = AddStoreServiceBuilder::default() + .db_store_id_exists(mock_store_id_exists_db_port_false(IS_CALLED_ONLY_ONCE)) + .db_store_name_exists(mock_store_name_exists_db_port_true(IS_CALLED_ONLY_ONCE)) + .build() + .unwrap(); + + assert_eq!( + s.add_store(cmd.clone()).await, + Err(IdentityError::DuplicateStoreName) + ); + } +} diff --git a/src/identity/application/services/update_store_service.rs b/src/identity/application/services/update_store_service.rs new file mode 100644 index 0000000..6e3c609 --- /dev/null +++ b/src/identity/application/services/update_store_service.rs @@ -0,0 +1,146 @@ +// 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::identity::{ + application::port::output::db::{store_id_exists::*, store_name_exists::*}, + domain::{ + store_aggregate::*, store_updated_event::*, update_store_command::UpdateStoreCommand, + }, +}; +use crate::utils::uuid::*; + +#[automock] +#[async_trait::async_trait] +pub trait UpdateStoreUseCase: Send + Sync { + async fn update_store(&self, cmd: UpdateStoreCommand) -> IdentityResult; +} + +pub type UpdateStoreServiceObj = Arc; + +#[derive(Clone, Builder)] +pub struct UpdateStoreService { + db_store_id_exists: StoreIDExistsDBPortObj, + db_store_name_exists: StoreNameExistsDBPortObj, +} + +#[async_trait::async_trait] +impl UpdateStoreUseCase for UpdateStoreService { + async fn update_store(&self, cmd: UpdateStoreCommand) -> IdentityResult { + if !self + .db_store_id_exists + .store_id_exists(cmd.old_store().store_id()) + .await? + { + return Err(IdentityError::StoreIDNotFound); + } + + let store = StoreBuilder::default() + .name(cmd.name().into()) + .address(cmd.address().as_ref().map(|s| s.to_string())) + .owner(*cmd.owner()) + .store_id(*cmd.old_store().store_id()) + .build() + .unwrap(); + + if cmd.name() != cmd.old_store().name() { + if self.db_store_name_exists.store_name_exists(&store).await? { + return Err(IdentityError::DuplicateStoreName); + } + } + + Ok(StoreUpdatedEventBuilder::default() + .added_by_user(*cmd.adding_by()) + .new_store(store) + .old_store(cmd.old_store().clone()) + .build() + .unwrap()) + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + + use crate::identity::domain::store_updated_event::tests::get_store_updated_event_from_command; + use crate::identity::domain::update_store_command::tests::get_update_store_cmd; + use crate::tests::bdd::*; + use crate::utils::uuid::tests::*; + + pub fn mock_update_store_service( + times: Option, + cmd: UpdateStoreCommand, + ) -> UpdateStoreServiceObj { + let mut m = MockUpdateStoreUseCase::new(); + + let res = get_store_updated_event_from_command(&cmd); + + if let Some(times) = times { + m.expect_update_store() + .times(times) + .returning(move |_| Ok(res.clone())); + } else { + m.expect_update_store().returning(move |_| Ok(res.clone())); + } + + Arc::new(m) + } + + #[actix_rt::test] + async fn test_service() { + let cmd = get_update_store_cmd(); + + let s = UpdateStoreServiceBuilder::default() + .db_store_id_exists(mock_store_id_exists_db_port_true(IS_CALLED_ONLY_ONCE)) + .db_store_name_exists(mock_store_name_exists_db_port_false(IS_CALLED_ONLY_ONCE)) + .build() + .unwrap(); + + let res = s.update_store(cmd.clone()).await.unwrap(); + assert_eq!(res.new_store().name(), cmd.name()); + assert_eq!(res.new_store().address(), cmd.address()); + assert_eq!(res.new_store().owner(), cmd.owner()); + assert_eq!(res.new_store().store_id(), cmd.old_store().store_id()); + assert_eq!(res.old_store(), cmd.old_store()); + assert_eq!(res.added_by_user(), cmd.adding_by()); + } + + #[actix_rt::test] + async fn test_service_store_name_exists() { + let cmd = get_update_store_cmd(); + + let s = UpdateStoreServiceBuilder::default() + .db_store_id_exists(mock_store_id_exists_db_port_true(IS_CALLED_ONLY_ONCE)) + .db_store_name_exists(mock_store_name_exists_db_port_true(IS_CALLED_ONLY_ONCE)) + .build() + .unwrap(); + + assert_eq!( + s.update_store(cmd.clone()).await, + Err(IdentityError::DuplicateStoreName) + ); + } + + #[actix_rt::test] + async fn test_service_store_id_doesnt_exist() { + let cmd = get_update_store_cmd(); + + let s = UpdateStoreServiceBuilder::default() + .db_store_id_exists(mock_store_id_exists_db_port_false(IS_CALLED_ONLY_ONCE)) + .db_store_name_exists(mock_store_name_exists_db_port_false(IS_NEVER_CALLED)) + .build() + .unwrap(); + + assert_eq!( + s.update_store(cmd.clone()).await, + Err(IdentityError::StoreIDNotFound) + ); + } +} From 335714fa3298e07fdb78f80d5a3ec867a6341946 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Thu, 9 Jan 2025 01:18:47 +0530 Subject: [PATCH 18/63] feat: define type aliases for identity domain --- src/identity/adapters/types.rs | 58 ++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 src/identity/adapters/types.rs diff --git a/src/identity/adapters/types.rs b/src/identity/adapters/types.rs new file mode 100644 index 0000000..3895547 --- /dev/null +++ b/src/identity/adapters/types.rs @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later +#![allow(dead_code)] + +use std::sync::Arc; + +use actix_web::web::Data; +use cqrs_es::{persist::ViewRepository, AggregateError}; +use derive_builder::Builder; +use postgres_es::PostgresCqrs; + +use crate::identity::{ + adapters::{ + // input::web::RoutesRepository, + output::db::postgres::{ + employee_view::EmployeeView, store_view::StoreView, user_view::UserView, + DBOutPostgresAdapter, + }, + }, + application::services::{errors::IdentityError, IdentityCommand, IdentityServicesObj}, + domain::{aggregate::User, employee_aggregate::Employee, store_aggregate::Store}, +}; +// +////pub type WebIdentityRoutesRepository = Data>; + +pub type IdentityUserCqrsExec = Arc>; +pub type IdentityUserCqrsView = Arc>; +pub type WebIdentityUserCqrsView = Data; + +pub type IdentityStoreCqrsExec = Arc>; +pub type IdentityStoreCqrsView = Arc>; +pub type WebIdentityStoreCqrsView = Data; + +pub type IdentityEmployeeCqrsExec = Arc>; +pub type IdentityEmployeeCqrsView = Arc>; +pub type WebIdentityEmployeeCqrsView = Data; + +pub type WebIdentityCqrsExec = Data>; +// +#[derive(Clone, Builder)] +pub struct IdentityCqrsExec { + user: IdentityUserCqrsExec, + store: IdentityStoreCqrsExec, +} + +impl IdentityCqrsExec { + pub async fn execute( + &self, + aggregate_id: &str, + command: IdentityCommand, + ) -> Result<(), AggregateError> { + self.user.execute(aggregate_id, command.clone()).await?; + self.store.execute(aggregate_id, command).await?; + + Ok(()) + } +} From 4b0df692f1ad607f7cf806f8d49d256807eba4ab Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Thu, 9 Jan 2025 01:53:44 +0530 Subject: [PATCH 19/63] fix: rm accept invite command,event,service and ports --- ...3cb7da1a5ffefa1b09705eb4ba8abe94ae135.json | 19 ++++++ ...ea24f735604de370130c8752f6e64b0a39659.json | 22 +++++++ ...44ddf4b1459c1487ed7dffd57fb5ea7cf97aa.json | 22 +++++++ ...ac9eb5204f79e1742b827cfb1048f061351df.json | 46 ++++++++++++++ ...a5d5a49d01bc8c44e578baad926f5f163c0d2.json | 28 +++++++++ ...4c0c23a5cc8a7d51a270bdab73182b12e204c.json | 18 ++++++ .../output/db/postgres/employee_view.rs | 2 +- .../application/port/output/db/get_invite.rs | 2 +- .../application/port/output/db/mod.rs | 4 +- src/identity/application/services/events.rs | 19 ++++-- src/identity/domain/employee_aggregate.rs | 60 +++++++++---------- src/identity/domain/mod.rs | 4 +- 12 files changed, 204 insertions(+), 42 deletions(-) create mode 100644 .sqlx/query-7c8fdc12c2a80c166d1696913273cb7da1a5ffefa1b09705eb4ba8abe94ae135.json create mode 100644 .sqlx/query-840678753d65298539ef0a07bf7ea24f735604de370130c8752f6e64b0a39659.json create mode 100644 .sqlx/query-a02ae03e94d928feece2996fbd344ddf4b1459c1487ed7dffd57fb5ea7cf97aa.json create mode 100644 .sqlx/query-a5b2bc2a46dfc083c6dc9b12945ac9eb5204f79e1742b827cfb1048f061351df.json create mode 100644 .sqlx/query-aa9219ef478854cc7e2afad1e80a5d5a49d01bc8c44e578baad926f5f163c0d2.json create mode 100644 .sqlx/query-b640ed9c2d601b3a78f3cac4b9a4c0c23a5cc8a7d51a270bdab73182b12e204c.json diff --git a/.sqlx/query-7c8fdc12c2a80c166d1696913273cb7da1a5ffefa1b09705eb4ba8abe94ae135.json b/.sqlx/query-7c8fdc12c2a80c166d1696913273cb7da1a5ffefa1b09705eb4ba8abe94ae135.json new file mode 100644 index 0000000..d86ec9d --- /dev/null +++ b/.sqlx/query-7c8fdc12c2a80c166d1696913273cb7da1a5ffefa1b09705eb4ba8abe94ae135.json @@ -0,0 +1,19 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO cqrs_identity_store_query (\n version, name, address, store_id, owner, deleted\n ) VALUES (\n $1, $2, $3, $4, $5, $6\n );", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Text", + "Text", + "Uuid", + "Uuid", + "Bool" + ] + }, + "nullable": [] + }, + "hash": "7c8fdc12c2a80c166d1696913273cb7da1a5ffefa1b09705eb4ba8abe94ae135" +} diff --git a/.sqlx/query-840678753d65298539ef0a07bf7ea24f735604de370130c8752f6e64b0a39659.json b/.sqlx/query-840678753d65298539ef0a07bf7ea24f735604de370130c8752f6e64b0a39659.json new file mode 100644 index 0000000..df55af5 --- /dev/null +++ b/.sqlx/query-840678753d65298539ef0a07bf7ea24f735604de370130c8752f6e64b0a39659.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT EXISTS (\n SELECT 1\n FROM cqrs_identity_store_query\n WHERE\n name = $1\n AND\n deleted = false\n );", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + null + ] + }, + "hash": "840678753d65298539ef0a07bf7ea24f735604de370130c8752f6e64b0a39659" +} diff --git a/.sqlx/query-a02ae03e94d928feece2996fbd344ddf4b1459c1487ed7dffd57fb5ea7cf97aa.json b/.sqlx/query-a02ae03e94d928feece2996fbd344ddf4b1459c1487ed7dffd57fb5ea7cf97aa.json new file mode 100644 index 0000000..f08cb3a --- /dev/null +++ b/.sqlx/query-a02ae03e94d928feece2996fbd344ddf4b1459c1487ed7dffd57fb5ea7cf97aa.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT EXISTS (\n SELECT 1\n FROM cqrs_identity_store_query\n WHERE\n store_id = $1\n );", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + null + ] + }, + "hash": "a02ae03e94d928feece2996fbd344ddf4b1459c1487ed7dffd57fb5ea7cf97aa" +} diff --git a/.sqlx/query-a5b2bc2a46dfc083c6dc9b12945ac9eb5204f79e1742b827cfb1048f061351df.json b/.sqlx/query-a5b2bc2a46dfc083c6dc9b12945ac9eb5204f79e1742b827cfb1048f061351df.json new file mode 100644 index 0000000..b2a5733 --- /dev/null +++ b/.sqlx/query-a5b2bc2a46dfc083c6dc9b12945ac9eb5204f79e1742b827cfb1048f061351df.json @@ -0,0 +1,46 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT \n name, address, store_id, owner, deleted\n FROM\n cqrs_identity_store_query\n WHERE\n store_id = $1;", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "address", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "store_id", + "type_info": "Uuid" + }, + { + "ordinal": 3, + "name": "owner", + "type_info": "Uuid" + }, + { + "ordinal": 4, + "name": "deleted", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + true, + false, + false, + false + ] + }, + "hash": "a5b2bc2a46dfc083c6dc9b12945ac9eb5204f79e1742b827cfb1048f061351df" +} diff --git a/.sqlx/query-aa9219ef478854cc7e2afad1e80a5d5a49d01bc8c44e578baad926f5f163c0d2.json b/.sqlx/query-aa9219ef478854cc7e2afad1e80a5d5a49d01bc8c44e578baad926f5f163c0d2.json new file mode 100644 index 0000000..f6c272c --- /dev/null +++ b/.sqlx/query-aa9219ef478854cc7e2afad1e80a5d5a49d01bc8c44e578baad926f5f163c0d2.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT \n store_id, version\n FROM\n cqrs_identity_store_query\n WHERE\n store_id = $1;", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "store_id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "version", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false + ] + }, + "hash": "aa9219ef478854cc7e2afad1e80a5d5a49d01bc8c44e578baad926f5f163c0d2" +} diff --git a/.sqlx/query-b640ed9c2d601b3a78f3cac4b9a4c0c23a5cc8a7d51a270bdab73182b12e204c.json b/.sqlx/query-b640ed9c2d601b3a78f3cac4b9a4c0c23a5cc8a7d51a270bdab73182b12e204c.json new file mode 100644 index 0000000..1c0b052 --- /dev/null +++ b/.sqlx/query-b640ed9c2d601b3a78f3cac4b9a4c0c23a5cc8a7d51a270bdab73182b12e204c.json @@ -0,0 +1,18 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE\n cqrs_identity_store_query\n SET\n version = $1,\n name = $2,\n address = $3,\n owner = $4,\n deleted = $5;", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Text", + "Text", + "Uuid", + "Bool" + ] + }, + "nullable": [] + }, + "hash": "b640ed9c2d601b3a78f3cac4b9a4c0c23a5cc8a7d51a270bdab73182b12e204c" +} diff --git a/src/identity/adapters/output/db/postgres/employee_view.rs b/src/identity/adapters/output/db/postgres/employee_view.rs index f742dcf..8a918ce 100644 --- a/src/identity/adapters/output/db/postgres/employee_view.rs +++ b/src/identity/adapters/output/db/postgres/employee_view.rs @@ -102,7 +102,7 @@ impl View for EmployeeView { IdentityEvent::PhoneNumberVerified(e) => self.phone_verified = true, IdentityEvent::VerificationOTPResent(e) => (), IdentityEvent::PhoneNumberChanged(e) => unimplemented!(), - IdentityEvent::InviteAccepted(e) => self.store_id = Some(*e.store_id()), + // IdentityEvent::InviteAccepted(e) => self.store_id = Some(*e.store_id()), IdentityEvent::OrganizationExited(e) => self.store_id = None, _ => (), diff --git a/src/identity/application/port/output/db/get_invite.rs b/src/identity/application/port/output/db/get_invite.rs index 1a8131e..9a4c582 100644 --- a/src/identity/application/port/output/db/get_invite.rs +++ b/src/identity/application/port/output/db/get_invite.rs @@ -17,7 +17,7 @@ pub use tests::*; #[automock] #[async_trait::async_trait] pub trait GetInviteOutDBPort: Send + Sync { - async fn get_invite(&self, invitge_id: Uuid) -> OutDBPortResult>; + async fn get_invite(&self, invite_id: Uuid) -> OutDBPortResult>; } pub type GetInviteOutDBPortObj = std::sync::Arc; diff --git a/src/identity/application/port/output/db/mod.rs b/src/identity/application/port/output/db/mod.rs index ad3bc00..264bc84 100644 --- a/src/identity/application/port/output/db/mod.rs +++ b/src/identity/application/port/output/db/mod.rs @@ -12,11 +12,11 @@ pub mod email_exists; pub mod emp_id_exists; pub mod errors; pub mod get_emp_id_from_phone_number; -pub mod get_invite; +//pub mod get_invite; pub mod get_login_otp; pub mod get_verification_otp; pub mod get_verification_secret; -pub mod invite_id_exists; +//pub mod invite_id_exists; pub mod phone_exists; pub mod store_id_exists; pub mod store_name_exists; diff --git a/src/identity/application/services/events.rs b/src/identity/application/services/events.rs index 4961adf..21f930d 100644 --- a/src/identity/application/services/events.rs +++ b/src/identity/application/services/events.rs @@ -12,10 +12,17 @@ use super::update_email::events::*; use super::update_password::events::*; use crate::identity::domain::{ - employee_logged_in_event::*, employee_registered_event::*, invite_accepted_event::*, - login_otp_sent_event::*, organization_exited_event::*, phone_number_changed_event::*, - phone_number_verified_event::*, resend_login_otp_event::*, store_added_event::*, - store_updated_event::*, verification_otp_resent_event::*, verification_otp_sent_event::*, + employee_logged_in_event::*, + employee_registered_event::*, //invite_accepted_event::*, + login_otp_sent_event::*, + organization_exited_event::*, + phone_number_changed_event::*, + phone_number_verified_event::*, + resend_login_otp_event::*, + store_added_event::*, + store_updated_event::*, + verification_otp_resent_event::*, + verification_otp_sent_event::*, }; #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd)] @@ -37,7 +44,7 @@ pub enum IdentityEvent { PhoneNumberVerified(PhoneNumberVerifiedEvent), VerificationOTPResent(VerificationOTPResentEvent), PhoneNumberChanged(PhoneNumberChangedEvent), - InviteAccepted(InviteAcceptedEvent), + // InviteAccepted(InviteAcceptedEvent), OrganizationExited(OrganizationExitedEvent), // store events @@ -71,7 +78,7 @@ impl DomainEvent for IdentityEvent { IdentityEvent::PhoneNumberVerified { .. } => "EmployeePhoneNumberVerified", IdentityEvent::VerificationOTPResent { .. } => "EmployeeVerificationOTPResent", IdentityEvent::PhoneNumberChanged { .. } => "EmployeePhoneNumberChanged", - IdentityEvent::InviteAccepted { .. } => "EmployeeInviteAccepted", + // IdentityEvent::InviteAccepted { .. } => "EmployeeInviteAccepted", IdentityEvent::OrganizationExited { .. } => "EmployeeOrganizationExited", // store diff --git a/src/identity/domain/employee_aggregate.rs b/src/identity/domain/employee_aggregate.rs index 585854f..6214ec6 100644 --- a/src/identity/domain/employee_aggregate.rs +++ b/src/identity/domain/employee_aggregate.rs @@ -128,12 +128,12 @@ impl Aggregate for Employee { .await?, )]) } - IdentityCommand::EmployeeAcceptInvite(cmd) => Ok(vec![IdentityEvent::InviteAccepted( - services - .employee_accept_invite_service() - .accept_invite(cmd) - .await?, - )]), + // IdentityCommand::EmployeeAcceptInvite(cmd) => Ok(vec![IdentityEvent::InviteAccepted( + // services + // .employee_accept_invite_service() + // .accept_invite(cmd) + // .await?, + // )]), IdentityCommand::EmployeeExitOrganization(cmd) => { Ok(vec![IdentityEvent::OrganizationExited( services @@ -158,7 +158,7 @@ impl Aggregate for Employee { IdentityEvent::PhoneNumberVerified(e) => self.phone_verified = true, IdentityEvent::VerificationOTPResent(e) => (), IdentityEvent::PhoneNumberChanged(e) => unimplemented!(), - IdentityEvent::InviteAccepted(e) => self.store_id = Some(*e.store_id()), + // IdentityEvent::InviteAccepted(e) => self.store_id = Some(*e.store_id()), IdentityEvent::OrganizationExited(e) => self.store_id = None, _ => (), @@ -171,7 +171,7 @@ mod tests { use std::sync::Arc; use cqrs_es::test::TestFramework; - use employee_accept_invite_service::EmployeeAcceptInviteService; + // use employee_accept_invite_service::EmployeeAcceptInviteService; use employee_exit_organization_service::EmployeeExitOrganizationService; use employee_login_service::EmployeeLoginService; use employee_resend_login_otp_service::EmployeeResendLoginOTPService; @@ -179,13 +179,13 @@ mod tests { use employee_verify_phone_number_service::EmployeeVerifyPhoneNumberService; use crate::identity::domain::{ - accept_invite_command::AcceptInviteCommand, + // accept_invite_command::AcceptInviteCommand, employee_logged_in_event::{EmployeeInitLoggedInEvent, EmployeeLoggedInEvent}, employee_login_command::{EmployeeFinishLoginCommand, EmployeeInitLoginCommand}, employee_register_command::EmployeeRegisterCommand, employee_registered_event::EmployeeRegisteredEvent, exit_organization_command::ExitOrganizationCommand, - invite_accepted_event::InviteAcceptedEvent, + // invite_accepted_event::InviteAcceptedEvent, organization_exited_event::OrganizationExitedEvent, phone_number_verified_event::PhoneNumberVerifiedEvent, resend_login_otp_command::ResendLoginOTPCommand, @@ -327,26 +327,26 @@ mod tests { .then_expect_events(vec![expected]); } - #[test] - fn test_accept_invite() { - let cmd = AcceptInviteCommand::get_cmd(); - let expected = InviteAcceptedEvent::get_event(&cmd); - let expected = IdentityEvent::InviteAccepted(expected); - - let mut services = MockIdentityServicesInterface::new(); - services - .expect_employee_accept_invite_service() - .times(IS_CALLED_ONLY_ONCE.unwrap()) - .return_const(EmployeeAcceptInviteService::mock_service( - IS_CALLED_ONLY_ONCE, - cmd.clone(), - )); - - EmployeeTestFramework::with(Arc::new(services)) - .given_no_previous_events() - .when(IdentityCommand::EmployeeAcceptInvite(cmd)) - .then_expect_events(vec![expected]); - } + // #[test] + // fn test_accept_invite() { + // let cmd = AcceptInviteCommand::get_cmd(); + // let expected = InviteAcceptedEvent::get_event(&cmd); + // let expected = IdentityEvent::InviteAccepted(expected); + // + // let mut services = MockIdentityServicesInterface::new(); + // services + // .expect_employee_accept_invite_service() + // .times(IS_CALLED_ONLY_ONCE.unwrap()) + // .return_const(EmployeeAcceptInviteService::mock_service( + // IS_CALLED_ONLY_ONCE, + // cmd.clone(), + // )); + // + // EmployeeTestFramework::with(Arc::new(services)) + // .given_no_previous_events() + // .when(IdentityCommand::EmployeeAcceptInvite(cmd)) + // .then_expect_events(vec![expected]); + // } #[test] fn test_exit_organization() { diff --git a/src/identity/domain/mod.rs b/src/identity/domain/mod.rs index 9acec2f..e79ba9e 100644 --- a/src/identity/domain/mod.rs +++ b/src/identity/domain/mod.rs @@ -11,7 +11,7 @@ pub mod store_aggregate; // events pub mod employee_logged_in_event; pub mod employee_registered_event; -pub mod invite_accepted_event; +//pub mod invite_accepted_event; pub mod login_otp_sent_event; pub mod organization_exited_event; pub mod phone_number_changed_event; @@ -23,7 +23,7 @@ pub mod verification_otp_resent_event; pub mod verification_otp_sent_event; // commands -pub mod accept_invite_command; +//pub mod accept_invite_command; pub mod add_store_command; pub mod change_phone_number_command; pub mod employee_login_command; From 0d5540771c515f105b6ab95f8a68cbb896a1422d Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Thu, 9 Jan 2025 20:59:03 +0530 Subject: [PATCH 20/63] feat: load twilio settings --- config/default.toml | 5 ++++ src/settings/mod.rs | 4 +++ src/settings/phone.rs | 60 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+) create mode 100644 src/settings/phone.rs diff --git a/config/default.toml b/config/default.toml index 2cbacf8..2e75fdb 100644 --- a/config/default.toml +++ b/config/default.toml @@ -33,3 +33,8 @@ reply_to="Vanikam Support " [meili] #url = "http://localhost:7700" #api_key = "" + + +[phone] +twilio_account_id = "" +twilio_auth_token = "" diff --git a/src/settings/mod.rs b/src/settings/mod.rs index fb2dee8..c5e70be 100644 --- a/src/settings/mod.rs +++ b/src/settings/mod.rs @@ -14,11 +14,13 @@ use validator::Validate; pub mod database; pub mod email; pub mod meili; +pub mod phone; pub mod server; use database::{DBType, Database}; use email::Email; use meili::Meili; +use phone::Phone; use server::Server; #[derive(Debug, Clone, Validate, Deserialize, Eq, PartialEq)] @@ -32,6 +34,7 @@ pub struct Settings { pub server: Server, pub email: Email, pub meili: Meili, + pub phone: Phone, } impl Settings { @@ -52,6 +55,7 @@ impl Settings { s = Database::env_override(s); s = Email::env_override(s); s = Meili::env_override(s); + s = Phone::env_override(s); Server::env_override(s) } diff --git a/src/settings/phone.rs b/src/settings/phone.rs new file mode 100644 index 0000000..cd7b5d7 --- /dev/null +++ b/src/settings/phone.rs @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use std::env; + +use config::{builder::DefaultState, ConfigBuilder}; +use lettre::message::Mailbox; +use serde::{Deserialize, Serialize}; +use url::Url; +use validator::{Validate, ValidationError}; + +#[derive(Debug, Clone, Serialize, Deserialize, Validate, Eq, PartialEq)] +pub struct Phone { + pub twilio_account_id: String, + pub twilio_auth_token: String, +} + +impl Phone { + pub fn env_override(mut s: ConfigBuilder) -> ConfigBuilder { + for (parameter, env_var_name) in [ + ("phone.twilio_account_id", "VANIKAM_phone_TWILIO_ACCOUNT_ID"), + ("phone.twilio_auth_token", "VANIKAM_phone_TWILIO_AUTH_TOKEN"), + ] + .iter() + { + if let Ok(val) = env::var(env_var_name) { + log::debug!("Overriding [{parameter}] with environment variable {env_var_name}"); + s = s.set_override(parameter, val).unwrap(); + } + } + + s + } +} + +#[cfg(test)] +mod tests { + use crate::env_helper; + + use super::*; + + #[test] + fn test_phone_env_override() { + let init_settings = crate::settings::Settings::new().unwrap(); + + env_helper!( + init_settings, + "VANIKAM_phone_TWILIO_AUTH_TOKEN", + "foo".to_string(), + phone.twilio_auth_token + ); + env_helper!( + init_settings, + "VANIKAM_phone_TWILIO_ACCOUNT_ID", + "foo".to_string(), + phone.twilio_account_id + ); + } +} From 970a4d45819368d27195f45c38c1d9bb28253e4f Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Thu, 9 Jan 2025 20:59:49 +0530 Subject: [PATCH 21/63] feat: bootstrap twilio client --- Cargo.lock | 16 ++++++ Cargo.toml | 5 +- src/identity/adapters/output/mod.rs | 1 + src/identity/adapters/output/phone/mod.rs | 5 ++ .../output/phone/twilio/account_login_otp.rs | 57 +++++++++++++++++++ .../phone/twilio/account_validation_otp.rs | 57 +++++++++++++++++++ .../adapters/output/phone/twilio/errors.rs | 1 + .../adapters/output/phone/twilio/mod.rs | 29 ++++++++++ twilio_client/.gitignore | 1 + twilio_client/Cargo.toml | 15 +++++ twilio_client/src/lib.rs | 43 ++++++++++++++ 11 files changed, 228 insertions(+), 2 deletions(-) create mode 100644 src/identity/adapters/output/phone/mod.rs create mode 100644 src/identity/adapters/output/phone/twilio/account_login_otp.rs create mode 100644 src/identity/adapters/output/phone/twilio/account_validation_otp.rs create mode 100644 src/identity/adapters/output/phone/twilio/errors.rs create mode 100644 src/identity/adapters/output/phone/twilio/mod.rs create mode 100644 twilio_client/.gitignore create mode 100644 twilio_client/Cargo.toml create mode 100644 twilio_client/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index da8c266..1aa0aa5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4172,6 +4172,21 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "twilio_client" +version = "0.1.0" +dependencies = [ + "actix-rt", + "derive-getters", + "derive_builder 0.20.2", + "derive_more 0.99.18", + "log", + "reqwest", + "serde", + "serde_json", + "url", +] + [[package]] name = "typenum" version = "1.17.0" @@ -4445,6 +4460,7 @@ dependencies = [ "time", "tracing", "tracing-actix-web", + "twilio_client", "url", "uuid", "validator 0.18.1", diff --git a/Cargo.toml b/Cargo.toml index 8326ebd..3a015ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [workspace] exclude = ["utils/db-migrations"] #, "utils/cache-bust"] -members = [".", "mailpit_client"] +members = [".", "mailpit_client", "twilio_client"] [dependencies] actix-identity = "0.8.0" @@ -36,7 +36,8 @@ tracing = { version = "0.1.40", features = ["log"] } tracing-actix-web = "0.7.10" url = { version = "2.5.0", features = ["serde"] } uuid = { version = "1.10.0", features = ["v4", "serde"] } -validator = { version = "0.18.1", features = ["derive"] } +validator = { version = "0.19.0", features = ["derive"] } +twilio_client = { path = "./twilio_client" } [dev-dependencies] #reqwest = { version = "0.12.4", features = ["json"] } diff --git a/src/identity/adapters/output/mod.rs b/src/identity/adapters/output/mod.rs index 7462528..abf311a 100644 --- a/src/identity/adapters/output/mod.rs +++ b/src/identity/adapters/output/mod.rs @@ -4,3 +4,4 @@ pub mod db; pub mod mailer; +pub mod phone; diff --git a/src/identity/adapters/output/phone/mod.rs b/src/identity/adapters/output/phone/mod.rs new file mode 100644 index 0000000..0e92c5c --- /dev/null +++ b/src/identity/adapters/output/phone/mod.rs @@ -0,0 +1,5 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +pub mod twilio; diff --git a/src/identity/adapters/output/phone/twilio/account_login_otp.rs b/src/identity/adapters/output/phone/twilio/account_login_otp.rs new file mode 100644 index 0000000..5d79153 --- /dev/null +++ b/src/identity/adapters/output/phone/twilio/account_login_otp.rs @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use lettre::{message::header::ContentType, AsyncTransport, Message}; + +use super::*; +use crate::identity::application::port::output::phone::{account_login_otp::*, errors::*}; +use crate::identity::domain::employee_aggregate::PhoneNumber; + +#[async_trait::async_trait] +impl AccountLoginOTPOutPhonePort for Phone { + async fn account_login_otp(&self, to: &PhoneNumber, otp: usize) -> OutPhonePortResult<()> { + unimplemented!() + } +} +#[cfg(test)] +mod tests { + use super::*; + use url::Url; + + use mailpit_client::*; + + use serde::Deserialize; + + #[actix_rt::test] + async fn test_phone_account_validation_otp() { + // let username = "batman"; + // let email = "batman@account_validation_otp.example.com"; + // let validation_secret = "dafsdfasecret"; + // + // let settings = crate::settings::tests::get_settings().await; + // let m = Lettrephone::new(&settings); + // + // m.account_validation_otp(email, username, validation_secret) + // .await + // .unwrap(); + // + // let mailpit_url = + // std::env::var("MAILPIT_URL").expect("Please set mailpit instance URL in MAILPIT_URL"); + // let mc = MailPitHTTPClientBuilder::default() + // .url(Url::parse(&mailpit_url).unwrap()) + // .build() + // .unwrap(); + // + // let mailpit_email = mc.get_email_addressed_to(email).await; + // + // assert!(mailpit_email.text().contains(validation_secret)); + // assert!(mailpit_email.text().contains(username)); + // assert!(mailpit_email + // .to() + // .iter() + // .any(|t| t.address() == email && t.name() == username)); + // + // mc.delete_email(mailpit_email).await; + } +} diff --git a/src/identity/adapters/output/phone/twilio/account_validation_otp.rs b/src/identity/adapters/output/phone/twilio/account_validation_otp.rs new file mode 100644 index 0000000..49ece96 --- /dev/null +++ b/src/identity/adapters/output/phone/twilio/account_validation_otp.rs @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use lettre::{message::header::ContentType, AsyncTransport, Message}; + +use super::*; +use crate::identity::application::port::output::phone::{account_validation_otp::*, errors::*}; +use crate::identity::domain::employee_aggregate::PhoneNumber; + +#[async_trait::async_trait] +impl AccountValidationOTPOutPhonePort for Phone { + async fn account_validation_otp(&self, to: &PhoneNumber, otp: usize) -> OutPhonePortResult<()> { + unimplemented!() + } +} +#[cfg(test)] +mod tests { + use super::*; + use url::Url; + + use mailpit_client::*; + + use serde::Deserialize; + + #[actix_rt::test] + async fn test_phone_account_validation_otp() { + // let username = "batman"; + // let email = "batman@account_validation_otp.example.com"; + // let validation_secret = "dafsdfasecret"; + // + // let settings = crate::settings::tests::get_settings().await; + // let m = Lettrephone::new(&settings); + // + // m.account_validation_otp(email, username, validation_secret) + // .await + // .unwrap(); + // + // let mailpit_url = + // std::env::var("MAILPIT_URL").expect("Please set mailpit instance URL in MAILPIT_URL"); + // let mc = MailPitHTTPClientBuilder::default() + // .url(Url::parse(&mailpit_url).unwrap()) + // .build() + // .unwrap(); + // + // let mailpit_email = mc.get_email_addressed_to(email).await; + // + // assert!(mailpit_email.text().contains(validation_secret)); + // assert!(mailpit_email.text().contains(username)); + // assert!(mailpit_email + // .to() + // .iter() + // .any(|t| t.address() == email && t.name() == username)); + // + // mc.delete_email(mailpit_email).await; + } +} diff --git a/src/identity/adapters/output/phone/twilio/errors.rs b/src/identity/adapters/output/phone/twilio/errors.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/identity/adapters/output/phone/twilio/errors.rs @@ -0,0 +1 @@ + diff --git a/src/identity/adapters/output/phone/twilio/mod.rs b/src/identity/adapters/output/phone/twilio/mod.rs new file mode 100644 index 0000000..48ac0b8 --- /dev/null +++ b/src/identity/adapters/output/phone/twilio/mod.rs @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later +use std::sync::Arc; + +use twilio_client::{TwilioClient, TwilioClientBuilder}; + +use crate::settings::Settings; + +mod account_login_otp; +mod account_validation_otp; +mod errors; + +#[derive(Clone)] +pub struct Phone { + tx: TwilioClient, +} + +impl Phone { + pub fn new(s: &Settings) -> Self { + let tx = TwilioClientBuilder::default() + .auth_token(s.phone.twilio_auth_token.clone()) + .account_id(s.phone.twilio_account_id.clone()) + .build() + .unwrap(); + + Self { tx } + } +} diff --git a/twilio_client/.gitignore b/twilio_client/.gitignore new file mode 100644 index 0000000..2f7896d --- /dev/null +++ b/twilio_client/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/twilio_client/Cargo.toml b/twilio_client/Cargo.toml new file mode 100644 index 0000000..bae2389 --- /dev/null +++ b/twilio_client/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "twilio_client" +version = "0.1.0" +edition = "2024" + +[dependencies] +reqwest = { version = "0.12.4", features = ["json", "native-tls-vendored"] } +serde = { version = "1.0.201", features = ["derive"] } +serde_json = "1.0.117" +actix-rt = "2.9.0" +derive-getters = "0.5.0" +derive_more = "0.99.17" +log = "0.4.21" +derive_builder = "0.20.0" +url = { version = "2.5.0", features = ["serde"] } diff --git a/twilio_client/src/lib.rs b/twilio_client/src/lib.rs new file mode 100644 index 0000000..a005bab --- /dev/null +++ b/twilio_client/src/lib.rs @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use derive_builder::Builder; +#[allow(unused_imports)] +use log::*; +#[allow(unused_imports)] +#[cfg(test)] +use println as info; +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use url::Url; + +#[derive(Clone, Debug, Builder)] +pub struct TwilioClient { + auth_token: String, + account_id: String, + #[builder(default = "Client::default()")] + client: Client, +} + +impl TwilioClient { + async fn send_sms(from: &str, to: &str, msg: &str) -> Result<(), String> { + unimplemented!() + // Ok(()) + } +} + +//pub fn add(left: u64, right: u64) -> u64 { +// left + right +//} +// +//#[cfg(test)] +//mod tests { +// use super::*; +// +// #[test] +// fn it_works() { +// let result = add(2, 2); +// assert_eq!(result, 4); +// } +//} From 2a85f88e04e94c5e2e7786936c681a88f4bc8f39 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Thu, 9 Jan 2025 21:01:53 +0530 Subject: [PATCH 22/63] feat: load identity adapters into actix web --- src/identity/adapters/mod.rs | 91 +++++ src/identity/adapters/types.rs | 6 +- src/identity/application/services/mod.rs | 433 ++++++++++++++++++----- src/identity/mod.rs | 2 +- src/main.rs | 8 + 5 files changed, 456 insertions(+), 84 deletions(-) diff --git a/src/identity/adapters/mod.rs b/src/identity/adapters/mod.rs index c325d4d..a337b8f 100644 --- a/src/identity/adapters/mod.rs +++ b/src/identity/adapters/mod.rs @@ -2,5 +2,96 @@ // // SPDX-License-Identifier: AGPL-3.0-or-later +use std::sync::Arc; + +use actix_web::web::{self, Data}; +use cqrs_es::{persist::ViewRepository, EventEnvelope, Query, View}; +use postgres_es::PostgresCqrs; +use sqlx::postgres::PgPool; + +use crate::identity::{ + application::{ + services::{IdentityServices, IdentityServicesObj}, + }, + domain::{aggregate::User, employee_aggregate::Employee, store_aggregate::Store}, +}; +use crate::settings::Settings; +use output::{ + db::postgres::{employee_view, store_view, user_view, DBOutPostgresAdapter}, + mailer::lettre::LettreMailer, + phone::twilio::Phone, +}; + mod input; pub mod output; +mod types; + +pub fn load_adapters(pool: PgPool, settings: Settings) -> impl FnOnce(&mut web::ServiceConfig) { + // init DB + let db = DBOutPostgresAdapter::new(pool.clone()); + let mailer = LettreMailer::new(&settings); + let phone = Phone::new(&settings); + + let services: IdentityServicesObj = IdentityServices::new( + Arc::new(db.clone()), + Arc::new(db.clone()), + Arc::new(db.clone()), + Arc::new(db.clone()), + Arc::new(db.clone()), + Arc::new(db.clone()), + Arc::new(db.clone()), + Arc::new(db.clone()), + Arc::new(db.clone()), + Arc::new(db.clone()), + Arc::new(db.clone()), + Arc::new(db.clone()), + Arc::new(db.clone()), + Arc::new(db.clone()), + Arc::new(db.clone()), + Arc::new(db.clone()), + Arc::new(db.clone()), + Arc::new(mailer.clone()), + Arc::new(phone.clone()), + Arc::new(phone.clone()), + crate::utils::uuid::GenerateUUID::new(), + crate::utils::random_string::GenerateRandomString::new(), + crate::utils::random_number::GenerateRandomNumber::new(), + ); + + let (user_cqrs_exec, user_cqrs_query) = user_view::init_cqrs(db.clone(), services.clone()); + let (store_cqrs_exec, store_cqrs_query) = store_view::init_cqrs(db.clone(), services.clone()); + let (employee_cqrs_exec, employee_cqrs_query) = + employee_view::init_cqrs(db.clone(), services.clone()); + + let f = move |cfg: &mut web::ServiceConfig| { + cfg.configure(input::web::load_ctx()); + + cfg.app_data(Data::new(user_cqrs_exec.clone())); + cfg.app_data(Data::new(user_cqrs_query.clone())); + + cfg.app_data(Data::new(store_cqrs_exec.clone())); + cfg.app_data(Data::new(store_cqrs_query.clone())); + + cfg.app_data(Data::new(employee_cqrs_exec.clone())); + cfg.app_data(Data::new(employee_cqrs_query.clone())); + }; + + Box::new(f) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::db::migrate::*; + + #[actix_rt::test] + async fn identity_load_adapters() { + let settings = crate::settings::tests::get_settings().await; + settings.create_db().await; + + let db = crate::db::sqlx_postgres::Postgres::init(&settings.database.url).await; + db.migrate().await; + + load_adapters(db.pool.clone(), settings.clone()); + } +} diff --git a/src/identity/adapters/types.rs b/src/identity/adapters/types.rs index 3895547..e1b85ab 100644 --- a/src/identity/adapters/types.rs +++ b/src/identity/adapters/types.rs @@ -12,7 +12,7 @@ use postgres_es::PostgresCqrs; use crate::identity::{ adapters::{ - // input::web::RoutesRepository, + input::web::RoutesRepository, output::db::postgres::{ employee_view::EmployeeView, store_view::StoreView, user_view::UserView, DBOutPostgresAdapter, @@ -21,8 +21,8 @@ use crate::identity::{ application::services::{errors::IdentityError, IdentityCommand, IdentityServicesObj}, domain::{aggregate::User, employee_aggregate::Employee, store_aggregate::Store}, }; -// -////pub type WebIdentityRoutesRepository = Data>; + +pub type WebIdentityRoutesRepository = Data>; pub type IdentityUserCqrsExec = Arc>; pub type IdentityUserCqrsView = Arc>; diff --git a/src/identity/application/services/mod.rs b/src/identity/application/services/mod.rs index d468359..d11ba06 100644 --- a/src/identity/application/services/mod.rs +++ b/src/identity/application/services/mod.rs @@ -1,6 +1,8 @@ +use std::hash::RandomState; // SPDX-FileCopyrightText: 2024 Aravinth Manivannan // // SPDX-License-Identifier: AGPL-3.0-or-later +use std::sync::Arc; use derive_builder::Builder; use mockall::predicate::*; @@ -8,7 +10,7 @@ use mockall::*; use serde::{Deserialize, Serialize}; pub mod delete_user; -pub mod employee_accept_invite_service; +//pub mod employee_accept_invite_service; pub mod employee_exit_organization_service; pub mod employee_login_service; pub mod employee_register_service; @@ -25,10 +27,46 @@ pub mod set_user_admin; pub mod update_email; pub mod update_password; +pub mod add_store_service; +pub mod update_store_service; + +use add_store_service::*; +use delete_user::{service::*, *}; +//use employee_accept_invite_service::*; +use employee_exit_organization_service::*; +use employee_login_service::*; +use employee_register_service::*; +use employee_resend_login_otp_service::*; +use employee_resend_verification_otp_service::*; +use employee_verify_phone_number_service::*; +use login::{service::*, *}; +use mark_user_verified::{service::*, *}; +use register_user::{service::*, *}; +use resend_verification_email::{service::*, *}; +use set_user_admin::{service::*, *}; +use update_email::{service::*, *}; +use update_password::{service::*, *}; +use update_store_service::*; + +use errors::*; +use events::*; + use crate::identity::domain::{ - accept_invite_command::*, change_phone_number_command::*, employee_login_command::*, - employee_register_command::*, exit_organization_command::*, resend_login_otp_command::*, - resend_verification_otp_command::*, verify_phone_number_command::*, + // accept_invite_command::*, + add_store_command::*, + change_phone_number_command::*, + employee_login_command::*, + employee_register_command::*, + exit_organization_command::*, + resend_login_otp_command::*, + resend_verification_otp_command::*, + update_store_command::*, + verify_phone_number_command::*, +}; +use crate::utils::{ + random_number::{self, *}, + random_string::*, + uuid::*, }; use delete_user::command::*; use login::command::*; @@ -39,6 +77,18 @@ use set_user_admin::command::*; use update_email::command::*; use update_password::command::*; +use crate::identity::application::port::output::{ + db::{ + create_login_otp::*, create_verification_otp::*, create_verification_secret::*, + delete_login_otp::*, delete_verification_otp::*, delete_verification_secret::*, + email_exists::*, emp_id_exists::*, get_emp_id_from_phone_number::*, get_login_otp::*, + get_verification_otp::*, get_verification_secret::*, phone_exists::*, store_id_exists::*, + store_name_exists::*, user_id_exists::*, verification_secret_exists::*, + }, + mailer::account_validation_link::*, + phone::{account_login_otp::*, account_validation_otp::*}, +}; + #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd)] pub enum IdentityCommand { RegisterUser(RegisterUserCommand), @@ -57,125 +107,348 @@ pub enum IdentityCommand { EmployeeVerifyPhoneNumber(VerifyPhoneNumberCommand), EmployeeResendVerificationOTP(ResendVerificationOTPCommand), EmployeeChangePhoneNumber(ChangePhoneNumberCommand), - EmployeeAcceptInvite(AcceptInviteCommand), + // EmployeeAcceptInvite(AcceptInviteCommand), EmployeeExitOrganization(ExitOrganizationCommand), + + AddStore(AddStoreCommand), + UpdateStore(UpdateStoreCommand), } #[automock] pub trait IdentityServicesInterface: Send + Sync { - fn delete_user(&self) -> delete_user::DeleteUserServiceObj; - fn login(&self) -> login::LoginServiceObj; - fn mark_user_verified(&self) -> mark_user_verified::MarkUserVerifiedServiceObj; - fn register_user(&self) -> register_user::RegisterUserServiceObj; - fn resend_verification_email( - &self, - ) -> resend_verification_email::ResendVerificationEmailServiceObj; - fn set_user_admin(&self) -> set_user_admin::SetUserAdminServiceObj; - fn update_email(&self) -> update_email::UpdateEmailServiceObj; - fn update_password(&self) -> update_password::UpdatePasswordServiceObj; + fn delete_user(&self) -> DeleteUserServiceObj; + fn login(&self) -> LoginServiceObj; + fn mark_user_verified(&self) -> MarkUserVerifiedServiceObj; + fn register_user(&self) -> RegisterUserServiceObj; + fn resend_verification_email(&self) -> ResendVerificationEmailServiceObj; + fn set_user_admin(&self) -> SetUserAdminServiceObj; + fn update_email(&self) -> UpdateEmailServiceObj; + fn update_password(&self) -> UpdatePasswordServiceObj; // employee - fn employee_accept_invite_service( - &self, - ) -> employee_accept_invite_service::EmployeeAcceptInviteServiceObj; - fn employee_exit_organization_service( - &self, - ) -> employee_exit_organization_service::EmployeeExitOrganizationServiceObj; - fn employee_login_service(&self) -> employee_login_service::EmployeeLoginServiceObj; - fn employee_register_service(&self) -> employee_register_service::EmployeeRegisterServiceObj; - fn employee_resend_login_otp_service( - &self, - ) -> employee_resend_login_otp_service::EmployeeResendLoginOTPServiceObj; - fn employee_resend_verification_otp_service( - &self, - ) -> employee_resend_verification_otp_service::EmployeeResendVerificationOTPServiceObj; - fn employee_verify_phone_number_service( - &self, - ) -> employee_verify_phone_number_service::EmployeeVerifyPhoneNumberServiceObj; + // fn employee_accept_invite_service(&self) -> EmployeeAcceptInviteServiceObj; + fn employee_exit_organization_service(&self) -> EmployeeExitOrganizationServiceObj; + fn employee_login_service(&self) -> EmployeeLoginServiceObj; + fn employee_register_service(&self) -> EmployeeRegisterServiceObj; + fn employee_resend_login_otp_service(&self) -> EmployeeResendLoginOTPServiceObj; + fn employee_resend_verification_otp_service(&self) -> EmployeeResendVerificationOTPServiceObj; + fn employee_verify_phone_number_service(&self) -> EmployeeVerifyPhoneNumberServiceObj; + + // store + fn add_store(&self) -> AddStoreServiceObj; + fn update_store(&self) -> UpdateStoreServiceObj; } +pub type IdentityServicesObj = Arc; + #[derive(Clone, Builder)] pub struct IdentityServices { - delete_user: delete_user::DeleteUserServiceObj, - login: login::LoginServiceObj, - mark_user_verified: mark_user_verified::MarkUserVerifiedServiceObj, - register_user: register_user::RegisterUserServiceObj, - resend_verification_email: resend_verification_email::ResendVerificationEmailServiceObj, - set_user_admin: set_user_admin::SetUserAdminServiceObj, - update_email: update_email::UpdateEmailServiceObj, - update_password: update_password::UpdatePasswordServiceObj, + delete_user: DeleteUserServiceObj, + login: LoginServiceObj, + mark_user_verified: MarkUserVerifiedServiceObj, + register_user: RegisterUserServiceObj, + resend_verification_email: ResendVerificationEmailServiceObj, + set_user_admin: SetUserAdminServiceObj, + update_email: UpdateEmailServiceObj, + update_password: UpdatePasswordServiceObj, - employee_accept_invite_service: employee_accept_invite_service::EmployeeAcceptInviteServiceObj, - employee_exit_organization_service: - employee_exit_organization_service::EmployeeExitOrganizationServiceObj, - employee_login_service: employee_login_service::EmployeeLoginServiceObj, - employee_register_service: employee_register_service::EmployeeRegisterServiceObj, - employee_resend_login_otp_service: - employee_resend_login_otp_service::EmployeeResendLoginOTPServiceObj, - employee_resend_verification_otp_service: - employee_resend_verification_otp_service::EmployeeResendVerificationOTPServiceObj, - employee_verify_phone_number_service: - employee_verify_phone_number_service::EmployeeVerifyPhoneNumberServiceObj, + // employee_accept_invite_service: EmployeeAcceptInviteServiceObj, + employee_exit_organization_service: EmployeeExitOrganizationServiceObj, + employee_login_service: EmployeeLoginServiceObj, + employee_register_service: EmployeeRegisterServiceObj, + employee_resend_login_otp_service: EmployeeResendLoginOTPServiceObj, + employee_resend_verification_otp_service: EmployeeResendVerificationOTPServiceObj, + employee_verify_phone_number_service: EmployeeVerifyPhoneNumberServiceObj, + + add_store: AddStoreServiceObj, + update_store: UpdateStoreServiceObj, } impl IdentityServicesInterface for IdentityServices { - fn delete_user(&self) -> delete_user::DeleteUserServiceObj { + fn delete_user(&self) -> DeleteUserServiceObj { self.delete_user.clone() } - fn login(&self) -> login::LoginServiceObj { + fn login(&self) -> LoginServiceObj { self.login.clone() } - fn mark_user_verified(&self) -> mark_user_verified::MarkUserVerifiedServiceObj { + fn mark_user_verified(&self) -> MarkUserVerifiedServiceObj { self.mark_user_verified.clone() } - fn register_user(&self) -> register_user::RegisterUserServiceObj { + fn register_user(&self) -> RegisterUserServiceObj { self.register_user.clone() } - fn resend_verification_email( - &self, - ) -> resend_verification_email::ResendVerificationEmailServiceObj { + fn resend_verification_email(&self) -> ResendVerificationEmailServiceObj { self.resend_verification_email.clone() } - fn set_user_admin(&self) -> set_user_admin::SetUserAdminServiceObj { + fn set_user_admin(&self) -> SetUserAdminServiceObj { self.set_user_admin.clone() } - fn update_email(&self) -> update_email::UpdateEmailServiceObj { + fn update_email(&self) -> UpdateEmailServiceObj { self.update_email.clone() } - fn update_password(&self) -> update_password::UpdatePasswordServiceObj { + fn update_password(&self) -> UpdatePasswordServiceObj { self.update_password.clone() } // employee - fn employee_accept_invite_service( - &self, - ) -> employee_accept_invite_service::EmployeeAcceptInviteServiceObj { - self.employee_accept_invite_service.clone() - } - fn employee_exit_organization_service( - &self, - ) -> employee_exit_organization_service::EmployeeExitOrganizationServiceObj { + // fn employee_accept_invite_service(&self) -> EmployeeAcceptInviteServiceObj { + // self.employee_accept_invite_service.clone() + // } + fn employee_exit_organization_service(&self) -> EmployeeExitOrganizationServiceObj { self.employee_exit_organization_service.clone() } - fn employee_login_service(&self) -> employee_login_service::EmployeeLoginServiceObj { + fn employee_login_service(&self) -> EmployeeLoginServiceObj { self.employee_login_service.clone() } - fn employee_register_service(&self) -> employee_register_service::EmployeeRegisterServiceObj { + fn employee_register_service(&self) -> EmployeeRegisterServiceObj { self.employee_register_service.clone() } - fn employee_resend_login_otp_service( - &self, - ) -> employee_resend_login_otp_service::EmployeeResendLoginOTPServiceObj { + fn employee_resend_login_otp_service(&self) -> EmployeeResendLoginOTPServiceObj { self.employee_resend_login_otp_service.clone() } - fn employee_resend_verification_otp_service( - &self, - ) -> employee_resend_verification_otp_service::EmployeeResendVerificationOTPServiceObj { + fn employee_resend_verification_otp_service(&self) -> EmployeeResendVerificationOTPServiceObj { self.employee_resend_verification_otp_service.clone() } - fn employee_verify_phone_number_service( - &self, - ) -> employee_verify_phone_number_service::EmployeeVerifyPhoneNumberServiceObj { + fn employee_verify_phone_number_service(&self) -> EmployeeVerifyPhoneNumberServiceObj { self.employee_verify_phone_number_service.clone() } + + fn add_store(&self) -> AddStoreServiceObj { + self.add_store.clone() + } + fn update_store(&self) -> UpdateStoreServiceObj { + self.update_store.clone() + } +} + +impl IdentityServices { + pub fn new( + out_db_create_login_otp: CreateLoginOTPOutDBPortObj, + out_db_create_verification_otp: CreateVerificationOTPOutDBPortObj, + out_db_create_verification_secret: CreateVerificationSecretOutDBPortObj, + out_db_delete_login_otp: DeleteLoginOTPOutDBPortObj, + out_db_delete_verification_otp: DeleteVerificationOTPOutDBPortObj, + out_db_delete_verification_secret: DeleteVerificationSecretOutDBPortObj, + out_db_email_exists: EmailExistsOutDBPortObj, + out_db_emp_id_exists: EmpIDExistsOutDBPortObj, + out_db_get_emp_id_from_phone_number: GetEmpIDFromPhoneNumberOutDBPortObj, + out_db_get_login_otp: GetLoginOTPOutDBPortObj, + out_db_get_verification_otp: GetVerificationOTPOutDBPortObj, + out_db_get_verification_secret: GetVerificationSecretOutDBPortObj, + out_db_phone_exists: PhoneNumberExistsOutDBPortObj, + out_db_store_id_exists: StoreIDExistsDBPortObj, + out_db_store_name_exists: StoreNameExistsDBPortObj, + out_db_user_id_exists: UserIDExistsOutDBPortObj, + out_db_verification_secret_exists: VerificationSecretExistsOutDBPortObj, + + out_mailer_account_validating_link: AccountValidationLinkOutMailerPortObj, + + out_phone_account_validation_otp: AccountValidationOTPOutPhonePortObj, + out_phone_account_login_otp: AccountLoginOTPOutPhonePortObj, + + get_uuid: GetUUIDInterfaceObj, + random_string: GenerateRandomStringInterfaceObj, + random_number: GenerateRandomNumberInterfaceObj, + ) -> IdentityServicesObj { + let delete_user: DeleteUserServiceObj = Arc::new(DeleteUserService); + let login: LoginServiceObj = Arc::new(LoginService {}); + + let mark_user_verified: MarkUserVerifiedServiceObj = Arc::new( + MarkUserVerifiedServiceBuilder::default() + .db_verification_secret_exists_adapter(out_db_verification_secret_exists.clone()) + .db_delete_verification_secret_adapter(out_db_delete_verification_secret.clone()) + .build() + .unwrap(), + ); + let register_user: RegisterUserServiceObj = Arc::new( + RegisterUserServiceBuilder::default() + .db_email_exists_adapter(out_db_email_exists.clone()) + .db_user_id_exists_adapter(out_db_user_id_exists.clone()) + .db_create_verification_secret_adapter(out_db_create_verification_secret.clone()) + .mailer_account_validation_link_adapter(out_mailer_account_validating_link.clone()) + .get_uuid(get_uuid.clone()) + .random_string_adapter(random_string.clone()) + .build() + .unwrap(), + ); + + let resend_verification_email: ResendVerificationEmailServiceObj = Arc::new( + ResendVerificationEmailServiceBuilder::default() + .db_email_exists_adapter(out_db_email_exists.clone()) + .db_get_verification_secret_adapter(out_db_get_verification_secret.clone()) + .mailer_account_validation_link_adapter(out_mailer_account_validating_link.clone()) + .build() + .unwrap(), + ); + + let set_user_admin: SetUserAdminServiceObj = Arc::new(SetUserAdminService); + + let update_email: UpdateEmailServiceObj = Arc::new( + UpdateEmailServiceBuilder::default() + .db_email_exists_adapter(out_db_email_exists.clone()) + .db_create_verification_secret_adapter(out_db_create_verification_secret.clone()) + .mailer_account_validation_link_adapter(out_mailer_account_validating_link.clone()) + .random_string_adapter(random_string.clone()) + .build() + .unwrap(), + ); + + let update_password: UpdatePasswordServiceObj = Arc::new(UpdatePasswordService); + + // let employee_accept_invite_service: EmployeeAcceptInviteServiceObj = Arc::new( + // EmployeeAcceptInviteServiceBuilder::default() + // .db_get_invite_adapter(unimplemented!()) + // .db_emp_id_exists_adapter(out_db_emp_id_exists.clone()) + // .build() + // .unwrap(), + // ); + let employee_exit_organization_service: EmployeeExitOrganizationServiceObj = Arc::new( + EmployeeExitOrganizationServiceBuilder::default() + .db_emp_id_exists_adapter(out_db_emp_id_exists.clone()) + .db_store_id_exists_adapter(out_db_store_id_exists.clone()) + .build() + .unwrap(), + ); + + let employee_login_service: EmployeeLoginServiceObj = Arc::new( + EmployeeLoginServiceBuilder::default() + .db_phone_exists_adapter(out_db_phone_exists.clone()) + .db_get_emp_id_from_phone_number_adapter( + out_db_get_emp_id_from_phone_number.clone(), + ) + .db_create_login_otp_adapter(out_db_create_login_otp.clone()) + .db_delete_login_otp(out_db_delete_login_otp.clone()) + .db_get_login_otp(out_db_get_login_otp.clone()) + .random_number_adapter(random_number.clone()) + .phone_login_otp_adapter(out_phone_account_login_otp.clone()) + .build() + .unwrap(), + ); + + let employee_register_service: EmployeeRegisterServiceObj = Arc::new( + EmployeeRegisterUserServiceBuilder::default() + .db_phone_exists_adapter(out_db_phone_exists.clone()) + .db_emp_id_exists_adapter(out_db_emp_id_exists.clone()) + .db_create_verification_otp_adapter(out_db_create_verification_otp.clone()) + .phone_account_validation_otp_adapter(out_phone_account_validation_otp.clone()) + .random_number_adapter(random_number.clone()) + .build() + .unwrap(), + ); + + let employee_resend_login_otp_service: EmployeeResendLoginOTPServiceObj = Arc::new( + EmployeeResendLoginOTPServiceBuilder::default() + .db_phone_exists_adapter(out_db_phone_exists.clone()) + .db_get_emp_id_from_phone_number_adapter( + out_db_get_emp_id_from_phone_number.clone(), + ) + .db_get_login_otp(out_db_get_login_otp.clone()) + .phone_login_otp_adapter(out_phone_account_login_otp.clone()) + .build() + .unwrap(), + ); + + let employee_resend_verification_otp_service: EmployeeResendVerificationOTPServiceObj = + Arc::new( + EmployeeResendVerificationOTPServiceBuilder::default() + .db_phone_exists_adapter(out_db_phone_exists.clone()) + .db_get_emp_id_from_phone_number_adapter( + out_db_get_emp_id_from_phone_number.clone(), + ) + .db_get_verification_otp(out_db_get_verification_otp.clone()) + .phone_account_validation_otp_adapter(out_phone_account_validation_otp.clone()) + .build() + .unwrap(), + ); + + let employee_verify_phone_number_service: EmployeeVerifyPhoneNumberServiceObj = Arc::new( + EmployeeVerifyPhoneNumberServiceBuilder::default() + .db_get_emp_id_from_phone_number_adapter( + out_db_get_emp_id_from_phone_number.clone(), + ) + .db_delete_verification_otp(out_db_delete_verification_otp.clone()) + .db_get_verification_otp(out_db_get_verification_otp.clone()) + .build() + .unwrap(), + ); + + let add_store: AddStoreServiceObj = Arc::new( + AddStoreServiceBuilder::default() + .db_store_id_exists(out_db_store_id_exists.clone()) + .db_store_name_exists(out_db_store_name_exists.clone()) + .build() + .unwrap(), + ); + + let update_store: UpdateStoreServiceObj = Arc::new( + UpdateStoreServiceBuilder::default() + .db_store_id_exists(out_db_store_id_exists.clone()) + .db_store_name_exists(out_db_store_name_exists.clone()) + .build() + .unwrap(), + ); + + Arc::new(Self { + delete_user, + login, + mark_user_verified, + register_user, + resend_verification_email, + set_user_admin, + update_email, + update_password, + + // employee_accept_invite_service, + employee_exit_organization_service, + employee_login_service, + employee_register_service, + employee_resend_login_otp_service, + employee_resend_verification_otp_service, + employee_verify_phone_number_service, + + add_store, + update_store, + }) + } +} + +#[cfg(test)] +mod tests { + use random_number::tests::mock_generate_random_number; + + use crate::{ + tests::bdd::IS_NEVER_CALLED, + utils::{random_string::tests::mock_generate_random_string, uuid::tests::mock_get_uuid}, + }; + + use super::*; + + #[test] + fn services_init_works() { + let s = IdentityServices::new( + mock_create_login_otp_db_port(IS_NEVER_CALLED), + mock_create_verification_otp_db_port(IS_NEVER_CALLED), + mock_create_verification_secret_db_port(IS_NEVER_CALLED), + mock_delete_login_otp_db_port(IS_NEVER_CALLED), + mock_delete_verification_otp_db_port(IS_NEVER_CALLED), + mock_delete_verification_secret_db_port(IS_NEVER_CALLED), + mock_email_exists_db_port(IS_NEVER_CALLED, false), + mock_emp_id_exists_db_port(IS_NEVER_CALLED, false), + mock_get_emp_id_from_phone_number_db_port(IS_NEVER_CALLED), + mock_get_login_otp_db_port(IS_NEVER_CALLED), + mock_get_verification_otp_db_port(IS_NEVER_CALLED), + mock_get_verification_secret_db_port(IS_NEVER_CALLED, "".into()), + mock_phone_exists_db_port(IS_NEVER_CALLED, false), + mock_store_id_exists_db_port_true(IS_NEVER_CALLED), + mock_store_name_exists_db_port_true(IS_NEVER_CALLED), + mock_user_id_exists_db_port(IS_NEVER_CALLED, false), + mock_verification_secret_exists_db_port(IS_NEVER_CALLED, false), + mock_account_validation_link_mailer_port(IS_NEVER_CALLED), + mock_account_validation_otp_phone_port(IS_NEVER_CALLED), + mock_account_login_otp_phone_port(IS_NEVER_CALLED), + mock_get_uuid(IS_NEVER_CALLED), + mock_generate_random_string(IS_NEVER_CALLED, "".into()), + mock_generate_random_number(IS_NEVER_CALLED, 0), + ); + } } diff --git a/src/identity/mod.rs b/src/identity/mod.rs index 7272406..10dac6e 100644 --- a/src/identity/mod.rs +++ b/src/identity/mod.rs @@ -2,6 +2,6 @@ // // SPDX-License-Identifier: AGPL-3.0-or-later -mod adapters; +pub(crate) mod adapters; mod application; mod domain; diff --git a/src/main.rs b/src/main.rs index 6ed9eb3..6ee53c1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -62,6 +62,14 @@ async fn main() { db.pool.clone(), settings.clone(), )) + .configure(identity::adapters::load_adapters( + db.pool.clone(), + settings.clone(), + )) + .configure(billing::adapters::load_adapters( + db.pool.clone(), + settings.clone(), + )) // .configure(auth::adapter::load_adapters(db.pool.clone(), &settings)) .configure(utils::random_string::GenerateRandomString::inject()) .configure(utils::uuid::GenerateUUID::inject()) From 41c46577411da5d0c20b8047032ee382eb613ddc Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Thu, 9 Jan 2025 23:05:24 +0530 Subject: [PATCH 23/63] feat: bootstrap identity web adapter --- src/identity/adapters/input/mod.rs | 1 + src/identity/adapters/input/web/errors.rs | 134 ++++++++++++++++++++++ src/identity/adapters/input/web/mod.rs | 28 +++++ src/identity/adapters/input/web/routes.rs | 86 ++++++++++++++ src/identity/adapters/mod.rs | 4 +- 5 files changed, 250 insertions(+), 3 deletions(-) create mode 100644 src/identity/adapters/input/web/errors.rs create mode 100644 src/identity/adapters/input/web/mod.rs create mode 100644 src/identity/adapters/input/web/routes.rs diff --git a/src/identity/adapters/input/mod.rs b/src/identity/adapters/input/mod.rs index 56f60de..810dc4e 100644 --- a/src/identity/adapters/input/mod.rs +++ b/src/identity/adapters/input/mod.rs @@ -1,3 +1,4 @@ // SPDX-FileCopyrightText: 2024 Aravinth Manivannan // // SPDX-License-Identifier: AGPL-3.0-or-later +pub mod web; diff --git a/src/identity/adapters/input/web/errors.rs b/src/identity/adapters/input/web/errors.rs new file mode 100644 index 0000000..8321ee0 --- /dev/null +++ b/src/identity/adapters/input/web/errors.rs @@ -0,0 +1,134 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use actix_web::http::StatusCode; +use actix_web::{HttpResponse, ResponseError}; +use derive_more::Display; +use serde::{Deserialize, Serialize}; + +use crate::identity::application::services::errors::*; + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +struct ErrorResponse { + error: String, +} + +impl From for ErrorResponse { + fn from(value: WebError) -> Self { + ErrorResponse { + error: serde_json::to_string(&value).unwrap_or_else(|_| { + log::error!("Unable to serialize error"); + "Unable to serialize error".into() + }), + } + } +} + +#[derive(Debug, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +pub enum WebError { + InternalError, + BadRequest, + + DuplicateUsername, + VerificationOTPNotFound, + VerificationSecretNotFound, + PhoneNumberVerificationFailed, + LoginOTPNotFound, + LoginFailed, + DuplicateEmail, + DuplicateEmployeeID, + DuplicatePhoneNumber, + DuplicateVerificationOTP, + PhoneNumberNotFound, + EmployeeNotFound, + InviteNotFound, + StoreNotFound, + DuplicateStoreName, + StoreIDNotFound, + DuplicateStoreID, +} + +impl From for WebError { + fn from(v: IdentityError) -> Self { + match v { + IdentityError::InternalError => Self::InternalError, + IdentityError::StoreIDNotFound => Self::StoreNotFound, + IdentityError::DuplicateUsername => Self::DuplicateUsername, + IdentityError::VerificationOTPNotFound => Self::VerificationOTPNotFound, + IdentityError::VerificationSecretNotFound => Self::VerificationSecretNotFound, + IdentityError::PhoneNumberVerificationFailed => Self::PhoneNumberNotFound, + IdentityError::LoginOTPNotFound => Self::LoginOTPNotFound, + IdentityError::LoginFailed => Self::LoginFailed, + IdentityError::DuplicateEmail => Self::DuplicateEmail, + IdentityError::DuplicateEmployeeID => Self::DuplicateEmployeeID, + IdentityError::DuplicatePhoneNumber => Self::DuplicatePhoneNumber, + IdentityError::DuplicateVerificationOTP => Self::DuplicateVerificationOTP, + IdentityError::PhoneNumberNotFound => Self::PhoneNumberNotFound, + IdentityError::EmployeeNotFound => Self::EmployeeNotFound, + IdentityError::InviteNotFound => Self::InviteNotFound, + IdentityError::StoreNotFound => Self::StoreNotFound, + IdentityError::DuplicateStoreName => Self::DuplicateStoreName, + IdentityError::StoreIDNotFound => Self::StoreIDNotFound, + IdentityError::DuplicateStoreID => Self::DuplicateStoreID, + } + } +} + +impl ResponseError for WebError { + fn status_code(&self) -> StatusCode { + match self { + Self::InternalError => StatusCode::INTERNAL_SERVER_ERROR, + Self::BadRequest => StatusCode::BAD_REQUEST, + + Self::StoreIDNotFound => StatusCode::NOT_FOUND, + Self::DuplicateUsername => StatusCode::BAD_REQUEST, + Self::VerificationOTPNotFound => StatusCode::UNAUTHORIZED, + Self::VerificationSecretNotFound => StatusCode::UNAUTHORIZED, + Self::PhoneNumberVerificationFailed => StatusCode::UNAUTHORIZED, + Self::LoginOTPNotFound => StatusCode::UNAUTHORIZED, + Self::LoginFailed => StatusCode::UNAUTHORIZED, + Self::DuplicateEmail => StatusCode::BAD_REQUEST, + Self::DuplicateEmployeeID => StatusCode::INTERNAL_SERVER_ERROR, + Self::DuplicatePhoneNumber => StatusCode::BAD_REQUEST, + Self::DuplicateVerificationOTP => StatusCode::INTERNAL_SERVER_ERROR, + Self::PhoneNumberNotFound => StatusCode::UNAUTHORIZED, // (?) + Self::EmployeeNotFound => StatusCode::NOT_FOUND, + Self::InviteNotFound => StatusCode::NOT_FOUND, + Self::StoreNotFound => StatusCode::NOT_FOUND, + Self::DuplicateStoreName => StatusCode::BAD_REQUEST, + Self::StoreIDNotFound => StatusCode::NOT_FOUND, + Self::DuplicateStoreID => StatusCode::INTERNAL_SERVER_ERROR, + } + } + + fn error_response(&self) -> actix_web::HttpResponse { + let e: ErrorResponse = self.clone().into(); + match self { + Self::InternalError => HttpResponse::InternalServerError().json(e), + Self::BadRequest => HttpResponse::BadRequest().json(e), + + Self::StoreNotFound => HttpResponse::NotFound().json(e), + Self::StoreIDNotFound => HttpResponse::NotFound().json(e), + Self::DuplicateUsername => HttpResponse::BadRequest().json(e), + Self::VerificationOTPNotFound => HttpResponse::Unauthorized().json(e), + Self::VerificationSecretNotFound => HttpResponse::Unauthorized().json(e), + Self::PhoneNumberVerificationFailed => HttpResponse::Unauthorized().json(e), + Self::LoginOTPNotFound => HttpResponse::Unauthorized().json(e), + Self::LoginFailed => HttpResponse::Unauthorized().json(e), + Self::DuplicateEmail => HttpResponse::BadRequest().json(e), + Self::DuplicateEmployeeID => HttpResponse::InternalServerError().json(e), + Self::DuplicatePhoneNumber => HttpResponse::BadRequest().json(e), + Self::DuplicateVerificationOTP => HttpResponse::InternalServerError().json(e), + Self::PhoneNumberNotFound => HttpResponse::Unauthorized().json(e), // (?) + Self::EmployeeNotFound => HttpResponse::NotFound().json(e), + Self::InviteNotFound => HttpResponse::NotFound().json(e), + Self::StoreNotFound => HttpResponse::NotFound().json(e), + Self::DuplicateStoreName => HttpResponse::BadRequest().json(e), + Self::StoreIDNotFound => HttpResponse::NotFound().json(e), + Self::DuplicateStoreID => HttpResponse::InternalServerError().json(e), + } + } +} + +pub type WebJsonRepsonse = Result; diff --git a/src/identity/adapters/input/web/mod.rs b/src/identity/adapters/input/web/mod.rs new file mode 100644 index 0000000..678fe25 --- /dev/null +++ b/src/identity/adapters/input/web/mod.rs @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use std::sync::Arc; + +use actix_web::web; + +use crate::identity::adapters::types; + +mod errors; +mod owner; +mod routes; + +pub use errors::WebJsonRepsonse; + +pub use routes::RoutesRepository; + +pub fn load_ctx() -> impl FnOnce(&mut web::ServiceConfig) { + let routes = types::WebIdentityRoutesRepository::new(Arc::new(RoutesRepository::default())); + + let f = move |cfg: &mut web::ServiceConfig| { + cfg.app_data(routes); + cfg.configure(owner::services); + }; + + Box::new(f) +} diff --git a/src/identity/adapters/input/web/routes.rs b/src/identity/adapters/input/web/routes.rs new file mode 100644 index 0000000..7134c55 --- /dev/null +++ b/src/identity/adapters/input/web/routes.rs @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use url::Url; +use uuid::Uuid; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct RoutesRepository { + owner_login: String, + owner_register: String, + owner_delete: String, + owner_verify_email: String, + owner_resend_verification_email: String, + // owner_set_admin: String, + owner_update_email: String, + owner_change_password: String, + + owner_add_store: String, + owner_update_store: String, + + employee_exit_organization: String, + employee_login: String, + employee_register: String, + employee_resend_login_otp: String, + employee_resend_verification_otp: String, + employee_verify_phone_number: String, +} + +impl Default for RoutesRepository { + fn default() -> Self { + Self { + owner_login: "/owner/login".into(), + owner_register: "/owner/register".into(), + owner_delete: "/owner/user/delete".into(), + owner_verify_email: "/owner/user/verify/email".into(), + owner_resend_verification_email: "/owner/user/verify/email/resend".into(), + owner_update_email: "/owner/user/email/update".into(), + owner_change_password: "/owner/password/change".into(), + //owner_set_admin: "/owner/user/promote/admin".into(), + owner_add_store: "/owner/store".into(), + owner_update_store: "/owner/store/update".into(), + + employee_login: "/employee/login".into(), + employee_register: "/employee/register".into(), + employee_resend_login_otp: "/employee/login/resend_otp".into(), + employee_resend_verification_otp: "/employee/user/phone/verify/resend_otp".into(), + employee_verify_phone_number: "/employee/user/phone/verify".into(), + employee_exit_organization: "/employee/organization/exit".into(), + } + } +} + +impl RoutesRepository { + // pub fn update_bill(&self, bill_id: Uuid) -> String { + // self.update_bill + // .replace("{bill_uuid}", &bill_id.to_string()) + // } + + // pub fn delete_bill(&self, bill_id: Uuid) -> String { + // self.delete_bill + // .replace("{bill_uuid}", &bill_id.to_string()) + // } + + // pub fn compute_total_price_for_bill(&self, bill_id: Uuid) -> String { + // self.compute_total_price_for_bill + // .replace("{bill_uuid}", &bill_id.to_string()) + // } + + // pub fn add_line_item(&self, bill_id: Uuid) -> String { + // self.add_line_item + // .replace("{bill_uuid}", &bill_id.to_string()) + // } + + // pub fn update_line_item(&self, bill_id: Uuid, line_item_uuid: Uuid) -> String { + // self.update_line_item + // .replace("{bill_uuid}", &bill_id.to_string()) + // .replace("{line_item_uuid}", &line_item_uuid.to_string()) + // } + + // pub fn delete_line_item(&self, bill_id: Uuid, line_item_uuid: Uuid) -> String { + // self.delete_line_item + // .replace("{bill_uuid}", &bill_id.to_string()) + // .replace("{line_item_uuid}", &line_item_uuid.to_string()) + // } +} diff --git a/src/identity/adapters/mod.rs b/src/identity/adapters/mod.rs index a337b8f..e55f278 100644 --- a/src/identity/adapters/mod.rs +++ b/src/identity/adapters/mod.rs @@ -10,9 +10,7 @@ use postgres_es::PostgresCqrs; use sqlx::postgres::PgPool; use crate::identity::{ - application::{ - services::{IdentityServices, IdentityServicesObj}, - }, + application::services::{IdentityServices, IdentityServicesObj}, domain::{aggregate::User, employee_aggregate::Employee, store_aggregate::Store}, }; use crate::settings::Settings; From 88f5b602fa4e3167deb4f8ce5798167371ef144b Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Thu, 9 Jan 2025 23:05:42 +0530 Subject: [PATCH 24/63] feat: bootstrap owner web adapter handlers --- src/identity/adapters/input/web/owner.rs | 341 ++++++++++++++++++ .../adapters/input/web/owner_add_store.html | 25 ++ .../input/web/owner_change_password.html | 31 ++ .../adapters/input/web/owner_delete_user.html | 20 + .../adapters/input/web/owner_login.html | 27 ++ .../adapters/input/web/owner_register.html | 43 +++ .../input/web/owner_update_email.html | 25 ++ .../input/web/owner_update_store.html | 25 ++ .../input/web/owner_verify_email.html | 14 + 9 files changed, 551 insertions(+) create mode 100644 src/identity/adapters/input/web/owner.rs create mode 100644 src/identity/adapters/input/web/owner_add_store.html create mode 100644 src/identity/adapters/input/web/owner_change_password.html create mode 100644 src/identity/adapters/input/web/owner_delete_user.html create mode 100644 src/identity/adapters/input/web/owner_login.html create mode 100644 src/identity/adapters/input/web/owner_register.html create mode 100644 src/identity/adapters/input/web/owner_update_email.html create mode 100644 src/identity/adapters/input/web/owner_update_store.html create mode 100644 src/identity/adapters/input/web/owner_verify_email.html diff --git a/src/identity/adapters/input/web/owner.rs b/src/identity/adapters/input/web/owner.rs new file mode 100644 index 0000000..35b88bc --- /dev/null +++ b/src/identity/adapters/input/web/owner.rs @@ -0,0 +1,341 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use actix_identity::Identity; +use actix_web::{get, http::header::ContentType, post, web, HttpRequest, HttpResponse, Responder}; +use derive_builder::Builder; +use serde::{Deserialize, Serialize}; +use url::Url; +use uuid::Uuid; + +use super::errors::*; +use super::types; +//use crate::utils::uuid::WebGetUUIDInterfaceObj; + +pub fn services(cfg: &mut web::ServiceConfig) { + cfg.service(login_ui_handler); + cfg.service(login_form_submission_handler); + + cfg.service(register_ui_handler); + cfg.service(register_form_submission_handler); + + cfg.service(update_email_ui_handler); + cfg.service(update_email_form_submission_handler); + + cfg.service(change_password_ui_handler); + cfg.service(change_password_form_submission_handler); + + cfg.service(delete_user_ui_handler); + cfg.service(delete_user_form_submission_handler); + + cfg.service(verify_email_ui_handler); + cfg.service(verify_email_form_submission_handler); + cfg.service(resend_verification_email); + + cfg.service(add_store_ui_handler); + cfg.service(add_store_form_submission_handler); + + cfg.service(update_store_ui_handler); + cfg.service(update_store_form_submission_handler); +} + +// login handlers + +#[allow(clippy::too_many_arguments)] +#[get("/owner/login")] +#[tracing::instrument(name = "login UI handler", skip())] +async fn login_ui_handler() -> WebJsonRepsonse { + const LOGIN_PAGE: &str = include_str!("./owner_login.html"); + + Ok(HttpResponse::Ok() + .insert_header(ContentType::html()) + .body(LOGIN_PAGE)) +} + +#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)] +struct OwnerLoginPayload { + email: String, + password: String, +} + +#[allow(clippy::too_many_arguments)] +#[post("/owner/login")] +#[tracing::instrument( + name = "Login form submission handler" + skip(id, req, payload, identity_cqrs_exec) +)] +async fn login_form_submission_handler( + identity_cqrs_exec: types::WebIdentityCqrsExec, + req: HttpRequest, + id: Identity, + payload: web::Form, +) -> WebJsonRepsonse { + let store = ""; + + Ok(HttpResponse::Ok().json(store)) +} + +// register handlers + +#[allow(clippy::too_many_arguments)] +#[get("/owner/register")] +#[tracing::instrument(name = "register UI handler", skip())] +async fn register_ui_handler() -> WebJsonRepsonse { + const REGISTER_PAGE: &str = include_str!("./owner_register.html"); + + Ok(HttpResponse::Ok() + .insert_header(ContentType::html()) + .body(REGISTER_PAGE)) +} + +#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)] +struct OwnerRegisterPayload { + password: String, + confirm_password: String, + first_name: String, + last_name: String, + email: String, +} + +#[allow(clippy::too_many_arguments)] +#[post("/owner/register")] +#[tracing::instrument( + name = "Register form submission handler" + skip(id, req, payload, identity_cqrs_exec) +)] +async fn register_form_submission_handler( + identity_cqrs_exec: types::WebIdentityCqrsExec, + req: HttpRequest, + id: Identity, + payload: web::Form, +) -> WebJsonRepsonse { + let store = ""; + + Ok(HttpResponse::Ok().json(store)) +} + +// update email handlers + +#[allow(clippy::too_many_arguments)] +#[get("/owner/user/email/update")] +#[tracing::instrument(name = "Update email UI handler", skip())] +async fn update_email_ui_handler() -> WebJsonRepsonse { + const UPDATE_EMAIL: &str = include_str!("./owner_update_email.html"); + + Ok(HttpResponse::Ok() + .insert_header(ContentType::html()) + .body(UPDATE_EMAIL)) +} + +#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)] +struct OwnerUpdateEmailPayload { + password: String, + new_email: String, +} + +#[allow(clippy::too_many_arguments)] +#[post("/owner/user/email/update")] +#[tracing::instrument( + name = "Update email form submission handler" + skip(id, req, payload, identity_cqrs_exec) +)] +async fn update_email_form_submission_handler( + identity_cqrs_exec: types::WebIdentityCqrsExec, + req: HttpRequest, + id: Identity, + payload: web::Form, +) -> WebJsonRepsonse { + let store = ""; + + Ok(HttpResponse::Ok().json(store)) +} + +// change password handlers + +#[allow(clippy::too_many_arguments)] +#[get("/owner/user/password/update")] +#[tracing::instrument(name = "Change password UI handler", skip())] +async fn change_password_ui_handler() -> WebJsonRepsonse { + const CHANGE_PASSWORD: &str = include_str!("./owner_change_password.html"); + + Ok(HttpResponse::Ok() + .insert_header(ContentType::html()) + .body(CHANGE_PASSWORD)) +} + +#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)] +struct OwnerChangePasswordPayload { + current_password: String, + new_password: String, + confirm_new_password: String, +} + +#[allow(clippy::too_many_arguments)] +#[post("/owner/user/email/update")] +#[tracing::instrument( + name = "Change password form submission handler" + skip(id, req, payload, identity_cqrs_exec) +)] +async fn change_password_form_submission_handler( + identity_cqrs_exec: types::WebIdentityCqrsExec, + req: HttpRequest, + id: Identity, + payload: web::Form, +) -> WebJsonRepsonse { + let store = ""; + + Ok(HttpResponse::Ok().json(store)) +} + +// Delete user handlers + +#[allow(clippy::too_many_arguments)] +#[get("/owner/user/delete")] +#[tracing::instrument(name = "Delete user UI handler", skip())] +async fn delete_user_ui_handler() -> WebJsonRepsonse { + const DELETE_USER: &str = include_str!("./owner_delete_user.html"); + + Ok(HttpResponse::Ok() + .insert_header(ContentType::html()) + .body(DELETE_USER)) +} + +#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)] +struct OwnerDeleteUser { + password: String, +} + +#[allow(clippy::too_many_arguments)] +#[post("/owner/user/delete")] +#[tracing::instrument( + name = "Delete user form submission handler" + skip(id, req, payload, identity_cqrs_exec) +)] +async fn delete_user_form_submission_handler( + identity_cqrs_exec: types::WebIdentityCqrsExec, + req: HttpRequest, + id: Identity, + payload: web::Form, +) -> WebJsonRepsonse { + let store = ""; + + Ok(HttpResponse::Ok().json(store)) +} + +// Verify email handler + +#[allow(clippy::too_many_arguments)] +#[get("/owner/user/verify/email")] +#[tracing::instrument(name = "Verify email UI handler", skip())] +async fn verify_email_ui_handler() -> WebJsonRepsonse { + const VERIFY_EMAIL: &str = include_str!("./owner_verify_email.html"); + + Ok(HttpResponse::Ok() + .insert_header(ContentType::html()) + .body(VERIFY_EMAIL)) +} + +#[allow(clippy::too_many_arguments)] +#[post("/owner/user/verify/email")] +#[tracing::instrument( + name = "Verify email form submission handler" + skip(id, req, identity_cqrs_exec) +)] +async fn verify_email_form_submission_handler( + identity_cqrs_exec: types::WebIdentityCqrsExec, + req: HttpRequest, + id: Identity, +) -> WebJsonRepsonse { + let store = ""; + + Ok(HttpResponse::Ok().json(store)) +} + +#[allow(clippy::too_many_arguments)] +#[get("/owner/user/verify/email")] +#[tracing::instrument( + name = "Resend verification email handler", + skip(id, req, identity_cqrs_exec) +)] +async fn resend_verification_email( + identity_cqrs_exec: types::WebIdentityCqrsExec, + req: HttpRequest, + id: Identity, +) -> WebJsonRepsonse { + let store = ""; + + Ok(HttpResponse::Ok().json(store)) +} + +// add store handler + +#[allow(clippy::too_many_arguments)] +#[get("/owner/store")] +#[tracing::instrument(name = "Add store UI handler", skip())] +async fn add_store_ui_handler() -> WebJsonRepsonse { + const ADD_STORE: &str = include_str!("./owner_add_store.html"); + + Ok(HttpResponse::Ok() + .insert_header(ContentType::html()) + .body(ADD_STORE)) +} + +#[derive(Clone, Builder, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd)] +pub struct OnwerAddStorePayload { + name: String, + address: Option, +} + +#[allow(clippy::too_many_arguments)] +#[post("/owner/store")] +#[tracing::instrument( + name = "Add store form submission handler" + skip(id, req, identity_cqrs_exec) +)] +async fn add_store_form_submission_handler( + identity_cqrs_exec: types::WebIdentityCqrsExec, + req: HttpRequest, + id: Identity, + paylod: web::Form, +) -> WebJsonRepsonse { + let store = ""; + + Ok(HttpResponse::Ok().json(store)) +} + +// Update store handler + +#[allow(clippy::too_many_arguments)] +#[get("/owner/store/update")] +#[tracing::instrument(name = "Add store UI handler", skip())] +async fn update_store_ui_handler() -> WebJsonRepsonse { + const ADD_STORE: &str = include_str!("./owner_add_store.html"); + + Ok(HttpResponse::Ok() + .insert_header(ContentType::html()) + .body(ADD_STORE)) +} + +#[derive(Clone, Builder, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd)] +pub struct OnwerUpdatetorePayload { + name: String, + address: Option, +} + +#[allow(clippy::too_many_arguments)] +#[post("/owner/store/update")] +#[tracing::instrument( + name = "Update store form submission handler" + skip(id, req, identity_cqrs_exec) +)] +async fn update_store_form_submission_handler( + identity_cqrs_exec: types::WebIdentityCqrsExec, + req: HttpRequest, + id: Identity, + paylod: web::Form, +) -> WebJsonRepsonse { + let store = ""; + + Ok(HttpResponse::Ok().json(store)) +} diff --git a/src/identity/adapters/input/web/owner_add_store.html b/src/identity/adapters/input/web/owner_add_store.html new file mode 100644 index 0000000..e2e77bf --- /dev/null +++ b/src/identity/adapters/input/web/owner_add_store.html @@ -0,0 +1,25 @@ + + + + + + Create Store | Vanikam + + + +
+ + + + + +
+ + + diff --git a/src/identity/adapters/input/web/owner_change_password.html b/src/identity/adapters/input/web/owner_change_password.html new file mode 100644 index 0000000..242f9a6 --- /dev/null +++ b/src/identity/adapters/input/web/owner_change_password.html @@ -0,0 +1,31 @@ + + + + + + Change Password | Vanikam + + + +
+ + + + + + + + +
+ + + diff --git a/src/identity/adapters/input/web/owner_delete_user.html b/src/identity/adapters/input/web/owner_delete_user.html new file mode 100644 index 0000000..13d6e67 --- /dev/null +++ b/src/identity/adapters/input/web/owner_delete_user.html @@ -0,0 +1,20 @@ + + + + + + Delete account | Vanikam + + + +
+ + + + +
+ + diff --git a/src/identity/adapters/input/web/owner_login.html b/src/identity/adapters/input/web/owner_login.html new file mode 100644 index 0000000..095f9a7 --- /dev/null +++ b/src/identity/adapters/input/web/owner_login.html @@ -0,0 +1,27 @@ + + + + + + Login | Vanikam + + + +
+ + + + + +
+ +

New here? Click here to register!

+ + + diff --git a/src/identity/adapters/input/web/owner_register.html b/src/identity/adapters/input/web/owner_register.html new file mode 100644 index 0000000..94b5a43 --- /dev/null +++ b/src/identity/adapters/input/web/owner_register.html @@ -0,0 +1,43 @@ + + + + + + Register | Vanikam + + + +
+ + + + + + + + + + + + +
+ +

Already have an account? Click here to log in!

+ + + diff --git a/src/identity/adapters/input/web/owner_update_email.html b/src/identity/adapters/input/web/owner_update_email.html new file mode 100644 index 0000000..c205d3c --- /dev/null +++ b/src/identity/adapters/input/web/owner_update_email.html @@ -0,0 +1,25 @@ + + + + + + Update Email | Vanikam + + + +
+ + + + + +
+ + + diff --git a/src/identity/adapters/input/web/owner_update_store.html b/src/identity/adapters/input/web/owner_update_store.html new file mode 100644 index 0000000..9c58a96 --- /dev/null +++ b/src/identity/adapters/input/web/owner_update_store.html @@ -0,0 +1,25 @@ + + + + + + Update Store | Vanikam + + + +
+ + + + + +
+ + + diff --git a/src/identity/adapters/input/web/owner_verify_email.html b/src/identity/adapters/input/web/owner_verify_email.html new file mode 100644 index 0000000..3453545 --- /dev/null +++ b/src/identity/adapters/input/web/owner_verify_email.html @@ -0,0 +1,14 @@ + + + + + + Verify email | Vanikam + + + +
+ +
+ + From 4a7e64b16a5cd180c2f79dd1329146f5dc94a081 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Thu, 9 Jan 2025 23:22:57 +0530 Subject: [PATCH 25/63] fix: consistent update routes --- src/identity/adapters/input/web/owner.rs | 6 +++--- src/identity/adapters/input/web/routes.rs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/identity/adapters/input/web/owner.rs b/src/identity/adapters/input/web/owner.rs index 35b88bc..08bd05a 100644 --- a/src/identity/adapters/input/web/owner.rs +++ b/src/identity/adapters/input/web/owner.rs @@ -226,7 +226,7 @@ async fn delete_user_form_submission_handler( // Verify email handler #[allow(clippy::too_many_arguments)] -#[get("/owner/user/verify/email")] +#[get("/owner/user/email/verify")] #[tracing::instrument(name = "Verify email UI handler", skip())] async fn verify_email_ui_handler() -> WebJsonRepsonse { const VERIFY_EMAIL: &str = include_str!("./owner_verify_email.html"); @@ -237,7 +237,7 @@ async fn verify_email_ui_handler() -> WebJsonRepsonse { } #[allow(clippy::too_many_arguments)] -#[post("/owner/user/verify/email")] +#[post("/owner/user/email/verify")] #[tracing::instrument( name = "Verify email form submission handler" skip(id, req, identity_cqrs_exec) @@ -253,7 +253,7 @@ async fn verify_email_form_submission_handler( } #[allow(clippy::too_many_arguments)] -#[get("/owner/user/verify/email")] +#[get("/owner/user/email/verify/resend")] #[tracing::instrument( name = "Resend verification email handler", skip(id, req, identity_cqrs_exec) diff --git a/src/identity/adapters/input/web/routes.rs b/src/identity/adapters/input/web/routes.rs index 7134c55..5c4aa88 100644 --- a/src/identity/adapters/input/web/routes.rs +++ b/src/identity/adapters/input/web/routes.rs @@ -33,8 +33,8 @@ impl Default for RoutesRepository { owner_login: "/owner/login".into(), owner_register: "/owner/register".into(), owner_delete: "/owner/user/delete".into(), - owner_verify_email: "/owner/user/verify/email".into(), - owner_resend_verification_email: "/owner/user/verify/email/resend".into(), + owner_verify_email: "/owner/user/email/verify".into(), + owner_resend_verification_email: "/owner/user/email/verify/resend".into(), owner_update_email: "/owner/user/email/update".into(), owner_change_password: "/owner/password/change".into(), //owner_set_admin: "/owner/user/promote/admin".into(), From 517ab10942036eda9fd848160e0231646e7603c6 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Thu, 9 Jan 2025 23:23:11 +0530 Subject: [PATCH 26/63] feat: bootstrap employee routes --- src/identity/adapters/input/web/employee.rs | 160 ++++++++++++++++++ .../input/web/employee_exit_organization.html | 14 ++ .../adapters/input/web/employee_login.html | 27 +++ .../adapters/input/web/employee_register.html | 37 ++++ src/identity/adapters/input/web/mod.rs | 2 + 5 files changed, 240 insertions(+) create mode 100644 src/identity/adapters/input/web/employee.rs create mode 100644 src/identity/adapters/input/web/employee_exit_organization.html create mode 100644 src/identity/adapters/input/web/employee_login.html create mode 100644 src/identity/adapters/input/web/employee_register.html diff --git a/src/identity/adapters/input/web/employee.rs b/src/identity/adapters/input/web/employee.rs new file mode 100644 index 0000000..edbf500 --- /dev/null +++ b/src/identity/adapters/input/web/employee.rs @@ -0,0 +1,160 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use actix_identity::Identity; +use actix_web::{get, http::header::ContentType, post, web, HttpRequest, HttpResponse, Responder}; +use derive_builder::Builder; +use serde::{Deserialize, Serialize}; +use url::Url; +use uuid::Uuid; + +use super::errors::*; +use super::types; +//use crate::utils::uuid::WebGetUUIDInterfaceObj; + +pub fn services(cfg: &mut web::ServiceConfig) { + cfg.service(login_ui_handler); + cfg.service(login_form_submission_handler); + cfg.service(resend_login_otp_handler); + + cfg.service(register_ui_handler); + cfg.service(register_form_submission_handler); + cfg.service(resend_verification_otp_handler); + + cfg.service(exit_organization_ui_handler); + cfg.service(exit_organization_form_submission_handler); +} + +// login handlers + +#[allow(clippy::too_many_arguments)] +#[get("/employee/login")] +#[tracing::instrument(name = "login UI handler", skip())] +async fn login_ui_handler() -> WebJsonRepsonse { + const LOGIN_PAGE: &str = include_str!("./employee_login.html"); + + Ok(HttpResponse::Ok() + .insert_header(ContentType::html()) + .body(LOGIN_PAGE)) +} + +#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)] +struct EmployeeLoginPayload { + country_code: usize, + phone_number: u64, +} + +#[allow(clippy::too_many_arguments)] +#[post("/employee/login")] +#[tracing::instrument( + name = "Login form submission handler" + skip(id, req, payload, identity_cqrs_exec) +)] +async fn login_form_submission_handler( + identity_cqrs_exec: types::WebIdentityCqrsExec, + req: HttpRequest, + id: Identity, + payload: web::Form, +) -> WebJsonRepsonse { + let store = ""; + + Ok(HttpResponse::Ok().json(store)) +} + +#[allow(clippy::too_many_arguments)] +#[post("/employee/login/resend_otp")] +#[tracing::instrument( + name = "Resend login OTP handler", + skip(req, identity_cqrs_exec, payload) +)] +async fn resend_login_otp_handler( + identity_cqrs_exec: types::WebIdentityCqrsExec, + req: HttpRequest, + payload: web::Form, +) -> WebJsonRepsonse { + let store = ""; + + Ok(HttpResponse::Ok().json(store)) +} + +// register handlers + +#[allow(clippy::too_many_arguments)] +#[get("/employee/register")] +#[tracing::instrument(name = "register UI handler", skip())] +async fn register_ui_handler() -> WebJsonRepsonse { + const REGISTER_PAGE: &str = include_str!("./employee_register.html"); + + Ok(HttpResponse::Ok() + .insert_header(ContentType::html()) + .body(REGISTER_PAGE)) +} + +#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)] +struct EmployeeRegisterPayload { + first_name: String, + last_name: String, + + country_code: usize, + phone_number: u64, +} + +#[allow(clippy::too_many_arguments)] +#[post("/employee/register")] +#[tracing::instrument( + name = "Register form submission handler" + skip(id, req, payload, identity_cqrs_exec) +)] +async fn register_form_submission_handler( + identity_cqrs_exec: types::WebIdentityCqrsExec, + req: HttpRequest, + id: Identity, + payload: web::Form, +) -> WebJsonRepsonse { + let store = ""; + + Ok(HttpResponse::Ok().json(store)) +} + +#[allow(clippy::too_many_arguments)] +#[post("/employee/phone/verify/resend_otp")] +#[tracing::instrument(name = "Resend verification OTP", skip(id, req, identity_cqrs_exec))] +async fn resend_verification_otp_handler( + identity_cqrs_exec: types::WebIdentityCqrsExec, + req: HttpRequest, + id: Identity, +) -> WebJsonRepsonse { + let store = ""; + + Ok(HttpResponse::Ok().json(store)) +} + +// Delete user handlers + +#[allow(clippy::too_many_arguments)] +#[get("/employee/organization/exit")] +#[tracing::instrument(name = "Exit organizationUI handler", skip())] +async fn exit_organization_ui_handler() -> WebJsonRepsonse { + const EXIT_ORGANIZATION: &str = include_str!("./employee_exit_organization.html"); + + Ok(HttpResponse::Ok() + .insert_header(ContentType::html()) + .body(EXIT_ORGANIZATION)) +} + +#[allow(clippy::too_many_arguments)] +#[post("/employee/organization/exit")] +#[tracing::instrument( + name = "Exit organization form submission handler" + skip(id, req, identity_cqrs_exec) +)] +async fn exit_organization_form_submission_handler( + identity_cqrs_exec: types::WebIdentityCqrsExec, + req: HttpRequest, + id: Identity, +) -> WebJsonRepsonse { + let store = ""; + + Ok(HttpResponse::Ok().json(store)) +} diff --git a/src/identity/adapters/input/web/employee_exit_organization.html b/src/identity/adapters/input/web/employee_exit_organization.html new file mode 100644 index 0000000..801785d --- /dev/null +++ b/src/identity/adapters/input/web/employee_exit_organization.html @@ -0,0 +1,14 @@ + + + + + + Exit organization | Vanikam + + + +
+ +
+ + diff --git a/src/identity/adapters/input/web/employee_login.html b/src/identity/adapters/input/web/employee_login.html new file mode 100644 index 0000000..56a8ac9 --- /dev/null +++ b/src/identity/adapters/input/web/employee_login.html @@ -0,0 +1,27 @@ + + + + + + Login | Vanikam + + + +
+ + + + + +
+ +

New here? Click here to register!

+ + + diff --git a/src/identity/adapters/input/web/employee_register.html b/src/identity/adapters/input/web/employee_register.html new file mode 100644 index 0000000..5184221 --- /dev/null +++ b/src/identity/adapters/input/web/employee_register.html @@ -0,0 +1,37 @@ + + + + + + Register | Vanikam + + + +
+ + + + + + + + + +
+ +

Already have an account? Click here to log in!

+ + + diff --git a/src/identity/adapters/input/web/mod.rs b/src/identity/adapters/input/web/mod.rs index 678fe25..61e3046 100644 --- a/src/identity/adapters/input/web/mod.rs +++ b/src/identity/adapters/input/web/mod.rs @@ -8,6 +8,7 @@ use actix_web::web; use crate::identity::adapters::types; +mod employee; mod errors; mod owner; mod routes; @@ -22,6 +23,7 @@ pub fn load_ctx() -> impl FnOnce(&mut web::ServiceConfig) { let f = move |cfg: &mut web::ServiceConfig| { cfg.app_data(routes); cfg.configure(owner::services); + cfg.configure(employee::services); }; Box::new(f) From 579f482205eff49c895e8f1110c7260b17fb3c03 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Fri, 10 Jan 2025 11:47:44 +0530 Subject: [PATCH 27/63] feat: util script to generate handler --- utils/gen_handler.sh | 64 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100755 utils/gen_handler.sh diff --git a/utils/gen_handler.sh b/utils/gen_handler.sh new file mode 100755 index 0000000..e522890 --- /dev/null +++ b/utils/gen_handler.sh @@ -0,0 +1,64 @@ +#!/bin/bash + +help() { + echo "Usage: gen_handler.sh " +} + + + run() { + +echo "cfg.service($1_ui_handler);" +echo "cfg.service($1_form_submission_handler);" + + +echo " +#[allow(clippy::too_many_arguments)] +#[get(\"$2\")] +#[tracing::instrument(name = \"login UI handler\", skip())] +async fn login_ui_handler() -> WebJsonRepsonse { + const LOGIN_PAGE: &str = include_str!(\"./owner_login.html\"); + + Ok(HttpResponse::Ok() + .insert_header(ContentType::html()) + .body(LOGIN_PAGE)) +} + +#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)] +struct $3Payload { + password: String, +} + +#[allow(clippy::too_many_arguments)] +#[post(\"$2\")] +#[tracing::instrument( + name = \"Login form submission handler\" + skip(id, req, payload, identity_cqrs_exec) +)] +async fn login_form_submission_handler( + identity_cqrs_exec: types::WebIdentityCqrsExec, + req: HttpRequest, + id: Identity, + payload: web::Form<$3Payload>, +) -> WebJsonRepsonse { + let store = \"\"; + + Ok(HttpResponse::Ok().json(store)) +} +" +} + + + +if [ -z $1 ] +then + help +elif [ -z $2 ] +then + help +elif [ -z $3 ] +then + help +else + run $1 $2 $3 | wl-copy + run $1 $2 $3 +fi From c26f3fbf41af86548c2e51d744d35ff823b8bffe Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Fri, 10 Jan 2025 14:06:06 +0530 Subject: [PATCH 28/63] feat: InventoryServices constructor --- src/inventory/application/services/mod.rs | 200 +++++++++++++++++++--- 1 file changed, 176 insertions(+), 24 deletions(-) diff --git a/src/inventory/application/services/mod.rs b/src/inventory/application/services/mod.rs index 59a9f4b..0cf885c 100644 --- a/src/inventory/application/services/mod.rs +++ b/src/inventory/application/services/mod.rs @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2024 Aravinth Manivannan // // SPDX-License-Identifier: AGPL-3.0-or-later +use std::sync::Arc; use derive_builder::Builder; use mockall::predicate::*; @@ -18,58 +19,209 @@ pub mod update_customization_service; pub mod update_product_service; pub mod update_store_service; +use add_category_service::*; +use add_customization_service::*; +use add_product_service::*; +use add_store_service::*; +use update_category_service::*; +use update_customization_service::*; +use update_product_service::*; +use update_store_service::*; + +use super::port::output::{ + db::{ + category_id_exists::*, category_name_exists_for_store::*, customization_id_exists::*, + customization_name_exists_for_product::*, get_category::*, product_id_exists::*, + product_name_exists_for_category::*, store_id_exists::*, store_name_exists::*, + }, + full_text_search::{add_product_to_store::*, update_product::*}, +}; + #[automock] pub trait InventoryServicesInterface: Send + Sync { - fn add_store(&self) -> add_store_service::AddStoreServiceObj; - fn add_category(&self) -> add_category_service::AddCategoryServiceObj; - fn add_product(&self) -> add_product_service::AddProductServiceObj; - fn add_customization(&self) -> add_customization_service::AddCustomizationServiceObj; - fn update_product(&self) -> update_product_service::UpdateProductServiceObj; - fn update_customization(&self) -> update_customization_service::UpdateCustomizationServiceObj; - fn update_category(&self) -> update_category_service::UpdateCategoryServiceObj; - fn update_store(&self) -> update_store_service::UpdateStoreServiceObj; + fn add_store(&self) -> AddStoreServiceObj; + fn add_category(&self) -> AddCategoryServiceObj; + fn add_product(&self) -> AddProductServiceObj; + fn add_customization(&self) -> AddCustomizationServiceObj; + fn update_product(&self) -> UpdateProductServiceObj; + fn update_customization(&self) -> UpdateCustomizationServiceObj; + fn update_category(&self) -> UpdateCategoryServiceObj; + fn update_store(&self) -> UpdateStoreServiceObj; } +pub type InventoryServicesObj = Arc; + #[derive(Clone, Builder)] pub struct InventoryServices { - add_store: add_store_service::AddStoreServiceObj, - add_category: add_category_service::AddCategoryServiceObj, - add_product: add_product_service::AddProductServiceObj, - add_customization: add_customization_service::AddCustomizationServiceObj, - update_product: update_product_service::UpdateProductServiceObj, - update_customization: update_customization_service::UpdateCustomizationServiceObj, - update_category: update_category_service::UpdateCategoryServiceObj, - update_store: update_store_service::UpdateStoreServiceObj, + add_store: AddStoreServiceObj, + add_category: AddCategoryServiceObj, + add_product: AddProductServiceObj, + add_customization: AddCustomizationServiceObj, + update_product: UpdateProductServiceObj, + update_customization: UpdateCustomizationServiceObj, + update_category: UpdateCategoryServiceObj, + update_store: UpdateStoreServiceObj, } impl InventoryServicesInterface for InventoryServices { - fn add_store(&self) -> add_store_service::AddStoreServiceObj { + fn add_store(&self) -> AddStoreServiceObj { self.add_store.clone() } - fn add_category(&self) -> add_category_service::AddCategoryServiceObj { + fn add_category(&self) -> AddCategoryServiceObj { self.add_category.clone() } - fn add_product(&self) -> add_product_service::AddProductServiceObj { + fn add_product(&self) -> AddProductServiceObj { self.add_product.clone() } - fn add_customization(&self) -> add_customization_service::AddCustomizationServiceObj { + fn add_customization(&self) -> AddCustomizationServiceObj { self.add_customization.clone() } - fn update_product(&self) -> update_product_service::UpdateProductServiceObj { + fn update_product(&self) -> UpdateProductServiceObj { self.update_product.clone() } - fn update_customization(&self) -> update_customization_service::UpdateCustomizationServiceObj { + fn update_customization(&self) -> UpdateCustomizationServiceObj { self.update_customization.clone() } - fn update_category(&self) -> update_category_service::UpdateCategoryServiceObj { + fn update_category(&self) -> UpdateCategoryServiceObj { self.update_category.clone() } - fn update_store(&self) -> update_store_service::UpdateStoreServiceObj { + fn update_store(&self) -> UpdateStoreServiceObj { self.update_store.clone() } } + +impl InventoryServices { + pub fn new( + out_db_category_id_exists: CategoryIDExistsDBPortObj, + out_db_category_name_exists_for_store: CategoryNameExistsForStoreDBPortObj, + out_db_customization_id_exists: CustomizationIDExistsDBPortObj, + out_db_customization_name_exists_for_product: CustomizationNameExistsForProductDBPortObj, + out_db_get_category: GetCategoryDBPortObj, + out_db_product_id_exists: ProductIDExistsDBPortObj, + out_db_product_name_exists_for_category: ProductNameExistsForCategoryDBPortObj, + out_db_store_id_exists: StoreIDExistsDBPortObj, + out_db_store_name_exists: StoreNameExistsDBPortObj, + + out_fts_add_product_to_store: AddProductToStoreFTSPortObj, + // out_fts_update_product: updateproduct + ) -> InventoryServicesObj { + let add_store = Arc::new( + AddStoreServiceBuilder::default() + .db_store_id_exists(out_db_store_id_exists.clone()) + .db_store_name_exists(out_db_store_name_exists.clone()) + .build() + .unwrap(), + ); + + let add_category = Arc::new( + AddCategoryServiceBuilder::default() + .db_store_id_exists(out_db_store_id_exists.clone()) + .db_category_name_exists_for_store(out_db_category_name_exists_for_store.clone()) + .db_category_id_exists(out_db_category_id_exists.clone()) + .build() + .unwrap(), + ); + + let add_product = Arc::new( + AddProductServiceBuilder::default() + .db_category_id_exists(out_db_category_id_exists.clone()) + .db_product_name_exists_for_category( + out_db_product_name_exists_for_category.clone(), + ) + .db_product_id_exists(out_db_product_id_exists.clone()) + .db_get_category(out_db_get_category.clone()) + .fts_add_product(out_fts_add_product_to_store.clone()) + .build() + .unwrap(), + ); + + let add_customization = Arc::new( + AddCustomizationServiceBuilder::default() + .db_product_id_exists(out_db_product_id_exists.clone()) + .db_customization_id_exists(out_db_customization_id_exists.clone()) + .db_customization_name_exists_for_product( + out_db_customization_name_exists_for_product.clone(), + ) + .build() + .unwrap(), + ); + + let update_product = Arc::new( + UpdateProductServiceBuilder::default() + .db_category_id_exists(out_db_category_id_exists.clone()) + .db_product_name_exists_for_category( + out_db_product_name_exists_for_category.clone(), + ) + .db_product_id_exists(out_db_product_id_exists.clone()) + .build() + .unwrap(), + ); + + let update_customization = Arc::new( + UpdateCustomizationServiceBuilder::default() + .db_product_id_exists(out_db_product_id_exists.clone()) + .db_customization_id_exists(out_db_customization_id_exists.clone()) + .db_customization_name_exists_for_product( + out_db_customization_name_exists_for_product.clone(), + ) + .build() + .unwrap(), + ); + + let update_category = Arc::new( + UpdateCategoryServiceBuilder::default() + .db_store_id_exists(out_db_store_id_exists.clone()) + .db_category_name_exists_for_store(out_db_category_name_exists_for_store.clone()) + .db_category_id_exists(out_db_category_id_exists.clone()) + .build() + .unwrap(), + ); + + let update_store = Arc::new( + UpdateStoreServiceBuilder::default() + .db_store_id_exists(out_db_store_id_exists.clone()) + .db_store_name_exists(out_db_store_name_exists.clone()) + .build() + .unwrap(), + ); + + Arc::new(Self { + add_store, + add_category, + add_product, + add_customization, + update_product, + update_customization, + update_category, + update_store, + }) + } +} + +#[cfg(test)] +mod tests { + use crate::tests::bdd::IS_NEVER_CALLED; + + use super::*; + + #[test] + fn inventory_services_work() { + let s = InventoryServices::new( + mock_category_id_exists_db_port_true(IS_NEVER_CALLED), + mock_category_name_exists_for_store_db_port_true(IS_NEVER_CALLED), + mock_customization_id_exists_db_port_true(IS_NEVER_CALLED), + mock_customization_name_exists_for_product_db_port_true(IS_NEVER_CALLED), + mock_get_category_db_port(IS_NEVER_CALLED), + mock_product_id_exists_db_port_true(IS_NEVER_CALLED), + mock_product_name_exists_for_category_db_port_true(IS_NEVER_CALLED), + mock_store_id_exists_db_port_true(IS_NEVER_CALLED), + mock_store_name_exists_db_port_true(IS_NEVER_CALLED), + mock_add_product_to_store_fts_port(IS_NEVER_CALLED), + ); + } +} From 8a752140174fc0f4524793e72fa8405835171b42 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Fri, 10 Jan 2025 14:08:13 +0530 Subject: [PATCH 29/63] feat: OrderingServices constructor --- src/ordering/application/services/mod.rs | 346 +++++++++++++++++++---- 1 file changed, 295 insertions(+), 51 deletions(-) diff --git a/src/ordering/application/services/mod.rs b/src/ordering/application/services/mod.rs index d56fc69..a0ceac7 100644 --- a/src/ordering/application/services/mod.rs +++ b/src/ordering/application/services/mod.rs @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2024 Aravinth Manivannan // // SPDX-License-Identifier: AGPL-3.0-or-later +use std::sync::Arc; use derive_builder::Builder; use mockall::predicate::*; @@ -27,105 +28,348 @@ pub mod update_order_service; pub mod update_product_service; pub mod update_store_service; +use add_category_service::*; +use add_customization_service::*; +use add_kot_service::*; +use add_line_item_service::*; +use add_order_service::*; +use add_product_service::*; +use add_store_service::*; +use delete_kot_service::*; +use delete_line_item_service::*; +use delete_order_service::*; +use update_category_service::*; +use update_customization_service::*; +use update_kot_service::*; +use update_line_item_service::*; +use update_order_service::*; +use update_product_service::*; +use update_store_service::*; + +use super::port::output::{ + db::{ + category_id_exists::*, category_name_exists_for_store::*, customization_id_exists::*, + customization_name_exists_for_product::*, get_category::*, kot_id_exists::*, + line_item_id_exists::*, order_id_exists::*, product_id_exists::*, + product_name_exists_for_category::*, store_id_exists::*, store_name_exists::*, + }, + full_text_search::add_product_to_store::*, +}; + #[automock] pub trait OrderingServicesInterface: 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 add_order(&self) -> add_order_service::AddOrderServiceObj; - fn update_order(&self) -> update_order_service::UpdateOrderServiceObj; - fn delete_order(&self) -> delete_order_service::DeleteOrderServiceObj; - fn add_kot(&self) -> add_kot_service::AddKotServiceObj; - fn update_kot(&self) -> update_kot_service::UpdateKotServiceObj; - fn delete_kot(&self) -> delete_kot_service::DeleteKotServiceObj; - fn add_store(&self) -> add_store_service::AddStoreServiceObj; - fn update_store(&self) -> update_store_service::UpdateStoreServiceObj; - fn add_category(&self) -> add_category_service::AddCategoryServiceObj; - fn update_category(&self) -> update_category_service::UpdateCategoryServiceObj; - fn add_product(&self) -> add_product_service::AddProductServiceObj; - fn update_product(&self) -> update_product_service::UpdateProductServiceObj; - fn add_customization(&self) -> add_customization_service::AddCustomizationServiceObj; - fn update_customization(&self) -> update_customization_service::UpdateCustomizationServiceObj; + fn add_line_item(&self) -> AddLineItemServiceObj; + fn update_line_item(&self) -> UpdateLineItemServiceObj; + fn delete_line_item(&self) -> DeleteLineItemServiceObj; + fn add_order(&self) -> AddOrderServiceObj; + fn update_order(&self) -> UpdateOrderServiceObj; + fn delete_order(&self) -> DeleteOrderServiceObj; + fn add_kot(&self) -> AddKotServiceObj; + fn update_kot(&self) -> UpdateKotServiceObj; + fn delete_kot(&self) -> DeleteKotServiceObj; + fn add_store(&self) -> AddStoreServiceObj; + fn update_store(&self) -> UpdateStoreServiceObj; + fn add_category(&self) -> AddCategoryServiceObj; + fn update_category(&self) -> UpdateCategoryServiceObj; + fn add_product(&self) -> AddProductServiceObj; + fn update_product(&self) -> UpdateProductServiceObj; + fn add_customization(&self) -> AddCustomizationServiceObj; + fn update_customization(&self) -> UpdateCustomizationServiceObj; } #[derive(Clone, Builder)] pub struct OrderingServices { - add_line_item: add_line_item_service::AddLineItemServiceObj, - update_line_item: update_line_item_service::UpdateLineItemServiceObj, - delete_line_item: delete_line_item_service::DeleteLineItemServiceObj, - add_order: add_order_service::AddOrderServiceObj, - update_order: update_order_service::UpdateOrderServiceObj, - delete_order: delete_order_service::DeleteOrderServiceObj, - add_kot: add_kot_service::AddKotServiceObj, - update_kot: update_kot_service::UpdateKotServiceObj, - delete_kot: delete_kot_service::DeleteKotServiceObj, - add_store: add_store_service::AddStoreServiceObj, - update_store: update_store_service::UpdateStoreServiceObj, - add_category: add_category_service::AddCategoryServiceObj, - update_category: update_category_service::UpdateCategoryServiceObj, - add_product: add_product_service::AddProductServiceObj, - update_product: update_product_service::UpdateProductServiceObj, - add_customization: add_customization_service::AddCustomizationServiceObj, - update_customization: update_customization_service::UpdateCustomizationServiceObj, + add_line_item: AddLineItemServiceObj, + update_line_item: UpdateLineItemServiceObj, + delete_line_item: DeleteLineItemServiceObj, + add_order: AddOrderServiceObj, + update_order: UpdateOrderServiceObj, + delete_order: DeleteOrderServiceObj, + add_kot: AddKotServiceObj, + update_kot: UpdateKotServiceObj, + delete_kot: DeleteKotServiceObj, + add_store: AddStoreServiceObj, + update_store: UpdateStoreServiceObj, + add_category: AddCategoryServiceObj, + update_category: UpdateCategoryServiceObj, + add_product: AddProductServiceObj, + update_product: UpdateProductServiceObj, + add_customization: AddCustomizationServiceObj, + update_customization: UpdateCustomizationServiceObj, } impl OrderingServicesInterface for OrderingServices { - fn add_line_item(&self) -> add_line_item_service::AddLineItemServiceObj { + fn add_line_item(&self) -> AddLineItemServiceObj { self.add_line_item.clone() } - fn update_line_item(&self) -> update_line_item_service::UpdateLineItemServiceObj { + fn update_line_item(&self) -> UpdateLineItemServiceObj { self.update_line_item.clone() } - fn delete_line_item(&self) -> delete_line_item_service::DeleteLineItemServiceObj { + fn delete_line_item(&self) -> DeleteLineItemServiceObj { self.delete_line_item.clone() } - fn add_order(&self) -> add_order_service::AddOrderServiceObj { + fn add_order(&self) -> AddOrderServiceObj { self.add_order.clone() } - fn update_order(&self) -> update_order_service::UpdateOrderServiceObj { + fn update_order(&self) -> UpdateOrderServiceObj { self.update_order.clone() } - fn delete_order(&self) -> delete_order_service::DeleteOrderServiceObj { + fn delete_order(&self) -> DeleteOrderServiceObj { self.delete_order.clone() } - fn add_kot(&self) -> add_kot_service::AddKotServiceObj { + fn add_kot(&self) -> AddKotServiceObj { self.add_kot.clone() } - fn update_kot(&self) -> update_kot_service::UpdateKotServiceObj { + fn update_kot(&self) -> UpdateKotServiceObj { self.update_kot.clone() } - fn delete_kot(&self) -> delete_kot_service::DeleteKotServiceObj { + fn delete_kot(&self) -> DeleteKotServiceObj { self.delete_kot.clone() } - fn add_store(&self) -> add_store_service::AddStoreServiceObj { + fn add_store(&self) -> AddStoreServiceObj { self.add_store.clone() } - fn update_store(&self) -> update_store_service::UpdateStoreServiceObj { + fn update_store(&self) -> UpdateStoreServiceObj { self.update_store.clone() } - fn add_category(&self) -> add_category_service::AddCategoryServiceObj { + fn add_category(&self) -> AddCategoryServiceObj { self.add_category.clone() } - fn update_category(&self) -> update_category_service::UpdateCategoryServiceObj { + fn update_category(&self) -> UpdateCategoryServiceObj { self.update_category.clone() } - fn add_product(&self) -> add_product_service::AddProductServiceObj { + fn add_product(&self) -> AddProductServiceObj { self.add_product.clone() } - fn update_product(&self) -> update_product_service::UpdateProductServiceObj { + fn update_product(&self) -> UpdateProductServiceObj { self.update_product.clone() } - fn add_customization(&self) -> add_customization_service::AddCustomizationServiceObj { + fn add_customization(&self) -> AddCustomizationServiceObj { self.add_customization.clone() } - fn update_customization(&self) -> update_customization_service::UpdateCustomizationServiceObj { + fn update_customization(&self) -> UpdateCustomizationServiceObj { self.update_customization.clone() } } + +pub type OrderingServicesInterfaceObj = Arc; + +impl OrderingServices { + pub fn new( + out_db_category_id_exists: CategoryIDExistsDBPortObj, + out_db_category_name_exists_for_store: CategoryNameExistsForStoreDBPortObj, + out_db_customization_id_exists: CustomizationIDExistsDBPortObj, + out_db_customization_name_exists_for_product: CustomizationNameExistsForProductDBPortObj, + out_db_get_category: GetCategoryDBPortObj, + + out_db_kot_id_exists: KotIDExistsDBPortObj, + out_db_line_item_id_exists: LineItemIDExistsDBPortObj, + out_db_order_id_exists: OrderIDExistsDBPortObj, + + out_db_product_id_exists: ProductIDExistsDBPortObj, + out_db_product_name_exists_for_category: ProductNameExistsForCategoryDBPortObj, + out_db_store_id_exists: StoreIDExistsDBPortObj, + out_db_store_name_exists: StoreNameExistsDBPortObj, + + out_fts_add_product_to_store: AddProductToStoreFTSPortObj, + ) -> OrderingServicesInterfaceObj { + let add_line_item = Arc::new( + AddLineItemServiceBuilder::default() + .db_line_item_id_exists(out_db_line_item_id_exists.clone()) + .db_kot_id_exists(out_db_kot_id_exists.clone()) + .build() + .unwrap(), + ); + + let update_line_item = Arc::new( + UpdateLineItemServiceBuilder::default() + .db_line_item_id_exists(out_db_line_item_id_exists.clone()) + .db_kot_id_exists(out_db_kot_id_exists.clone()) + .build() + .unwrap(), + ); + let delete_line_item = Arc::new( + DeleteLineItemServiceBuilder::default() + .db_line_item_id_exists(out_db_line_item_id_exists.clone()) + .build() + .unwrap(), + ); + + let add_order = Arc::new( + AddOrderServiceBuilder::default() + .db_store_id_exists(out_db_store_id_exists.clone()) + .db_order_id_exists(out_db_order_id_exists.clone()) + .build() + .unwrap(), + ); + let update_order = Arc::new( + UpdateOrderServiceBuilder::default() + .db_order_id_exists(out_db_order_id_exists.clone()) + .build() + .unwrap(), + ); + + let delete_order = Arc::new( + DeleteOrderServiceBuilder::default() + .db_order_id_exists(out_db_order_id_exists.clone()) + .build() + .unwrap(), + ); + + let add_kot = Arc::new( + AddKotServiceBuilder::default() + .db_order_id_exists(out_db_order_id_exists.clone()) + .db_kot_id_exists(out_db_kot_id_exists.clone()) + .build() + .unwrap(), + ); + + let update_kot = Arc::new( + UpdateKotServiceBuilder::default() + .db_order_id_exists(out_db_order_id_exists.clone()) + .db_kot_id_exists(out_db_kot_id_exists.clone()) + .build() + .unwrap(), + ); + + let delete_kot = Arc::new( + DeleteKotServiceBuilder::default() + .db_kot_id_exists(out_db_kot_id_exists.clone()) + .build() + .unwrap(), + ); + + let add_store = Arc::new( + AddStoreServiceBuilder::default() + .db_store_id_exists(out_db_store_id_exists.clone()) + .db_store_name_exists(out_db_store_name_exists.clone()) + .build() + .unwrap(), + ); + + let add_category = Arc::new( + AddCategoryServiceBuilder::default() + .db_store_id_exists(out_db_store_id_exists.clone()) + .db_category_name_exists_for_store(out_db_category_name_exists_for_store.clone()) + .db_category_id_exists(out_db_category_id_exists.clone()) + .build() + .unwrap(), + ); + + let add_product = Arc::new( + AddProductServiceBuilder::default() + .db_category_id_exists(out_db_category_id_exists.clone()) + .db_product_name_exists_for_category( + out_db_product_name_exists_for_category.clone(), + ) + .db_product_id_exists(out_db_product_id_exists.clone()) + .db_get_category(out_db_get_category.clone()) + .fts_add_product(out_fts_add_product_to_store.clone()) + .build() + .unwrap(), + ); + + let add_customization = Arc::new( + AddCustomizationServiceBuilder::default() + .db_product_id_exists(out_db_product_id_exists.clone()) + .db_customization_id_exists(out_db_customization_id_exists.clone()) + .db_customization_name_exists_for_product( + out_db_customization_name_exists_for_product.clone(), + ) + .build() + .unwrap(), + ); + + let update_product = Arc::new( + UpdateProductServiceBuilder::default() + .db_category_id_exists(out_db_category_id_exists.clone()) + .db_product_name_exists_for_category( + out_db_product_name_exists_for_category.clone(), + ) + .db_product_id_exists(out_db_product_id_exists.clone()) + .build() + .unwrap(), + ); + + let update_customization = Arc::new( + UpdateCustomizationServiceBuilder::default() + .db_product_id_exists(out_db_product_id_exists.clone()) + .db_customization_id_exists(out_db_customization_id_exists.clone()) + .db_customization_name_exists_for_product( + out_db_customization_name_exists_for_product.clone(), + ) + .build() + .unwrap(), + ); + + let update_category = Arc::new( + UpdateCategoryServiceBuilder::default() + .db_store_id_exists(out_db_store_id_exists.clone()) + .db_category_name_exists_for_store(out_db_category_name_exists_for_store.clone()) + .db_category_id_exists(out_db_category_id_exists.clone()) + .build() + .unwrap(), + ); + + let update_store = Arc::new( + UpdateStoreServiceBuilder::default() + .db_store_id_exists(out_db_store_id_exists.clone()) + .db_store_name_exists(out_db_store_name_exists.clone()) + .build() + .unwrap(), + ); + + Arc::new(Self { + add_line_item, + update_line_item, + delete_line_item, + add_order, + update_order, + delete_order, + add_kot, + update_kot, + delete_kot, + add_store, + update_store, + add_category, + update_category, + add_product, + update_product, + add_customization, + update_customization, + }) + } +} + +#[cfg(test)] +mod tests { + use crate::tests::bdd::IS_NEVER_CALLED; + + use super::*; + + #[test] + fn ordering_services_work() { + let s = OrderingServices::new( + mock_category_id_exists_db_port_true(IS_NEVER_CALLED), + mock_category_name_exists_for_store_db_port_true(IS_NEVER_CALLED), + mock_customization_id_exists_db_port_true(IS_NEVER_CALLED), + mock_customization_name_exists_for_product_db_port_true(IS_NEVER_CALLED), + mock_get_category_db_port(IS_NEVER_CALLED), + mock_kot_id_exists_db_port_true(IS_NEVER_CALLED), + mock_line_item_id_exists_db_port_true(IS_NEVER_CALLED), + mock_order_id_exists_db_port_true(IS_NEVER_CALLED), + mock_product_id_exists_db_port_true(IS_NEVER_CALLED), + mock_product_name_exists_for_category_db_port_true(IS_NEVER_CALLED), + mock_store_id_exists_db_port_true(IS_NEVER_CALLED), + mock_store_name_exists_db_port_true(IS_NEVER_CALLED), + mock_add_product_to_store_fts_port(IS_NEVER_CALLED), + ); + } +} From 17a7e58f44fd6a667053b92954fb6cf4ffa7eb71 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Fri, 10 Jan 2025 15:47:43 +0530 Subject: [PATCH 30/63] feat: access CQRS executors from a trait obj --- src/billing/adapters/mod.rs | 16 +++++++++------- src/billing/adapters/types.rs | 20 +++++++++++++++++--- src/identity/adapters/mod.rs | 15 ++++++++++----- src/identity/adapters/types.rs | 26 +++++++++++++++++++++----- 4 files changed, 57 insertions(+), 20 deletions(-) diff --git a/src/billing/adapters/mod.rs b/src/billing/adapters/mod.rs index 94d2c89..6d3d7bc 100644 --- a/src/billing/adapters/mod.rs +++ b/src/billing/adapters/mod.rs @@ -53,12 +53,14 @@ pub fn load_adapters(pool: PgPool, settings: Settings) -> impl FnOnce(&mut web:: let (line_item_cqrs_exec, line_item_cqrs_query) = line_item_view::init_cqrs(db.clone(), services.clone()); - let billing_cqrs_exec = types::BillingCqrsExecBuilder::default() - .bill(bill_cqrs_exec) - .line_item(line_item_cqrs_exec) - .store(store_cqrs_exec) - .build() - .unwrap(); + let billing_cqrs_exec = types::WebBillingCqrsExec::new(Arc::new( + types::BillingCqrsExecBuilder::default() + .bill(bill_cqrs_exec) + .line_item(line_item_cqrs_exec) + .store(store_cqrs_exec) + .build() + .unwrap(), + )); let f = move |cfg: &mut web::ServiceConfig| { cfg.configure(input::web::load_ctx()); @@ -67,7 +69,7 @@ pub fn load_adapters(pool: PgPool, settings: Settings) -> impl FnOnce(&mut web:: cfg.app_data(Data::new(store_cqrs_query.clone())); cfg.app_data(Data::new(line_item_cqrs_query.clone())); - cfg.app_data(Data::new(Arc::new(billing_cqrs_exec))); + cfg.app_data(billing_cqrs_exec); }; Box::new(f) diff --git a/src/billing/adapters/types.rs b/src/billing/adapters/types.rs index f5c3d59..17b5fce 100644 --- a/src/billing/adapters/types.rs +++ b/src/billing/adapters/types.rs @@ -6,8 +6,11 @@ use std::sync::Arc; use actix_web::web::Data; +use async_trait::async_trait; use cqrs_es::{persist::ViewRepository, AggregateError}; use derive_builder::Builder; +use mockall::predicate::*; +use mockall::*; use postgres_es::PostgresCqrs; use crate::billing::{ @@ -41,7 +44,17 @@ pub type BillingStoreCqrsExec = Arc>; pub type BillingStoreCqrsView = Arc>; pub type WebBillingStoreCqrsView = Data; -pub type WebBillingCqrsExec = Data>; +pub type WebBillingCqrsExec = Data>; + +#[automock] +#[async_trait] +pub trait BillingCqrsExecutor { + async fn execute( + &self, + aggregate_id: &str, + command: BillingCommand, + ) -> Result<(), AggregateError>; +} #[derive(Clone, Builder)] pub struct BillingCqrsExec { @@ -50,8 +63,9 @@ pub struct BillingCqrsExec { store: BillingStoreCqrsExec, } -impl BillingCqrsExec { - pub async fn execute( +#[async_trait] +impl BillingCqrsExecutor for BillingCqrsExec { + async fn execute( &self, aggregate_id: &str, command: BillingCommand, diff --git a/src/identity/adapters/mod.rs b/src/identity/adapters/mod.rs index e55f278..010baa2 100644 --- a/src/identity/adapters/mod.rs +++ b/src/identity/adapters/mod.rs @@ -61,17 +61,22 @@ pub fn load_adapters(pool: PgPool, settings: Settings) -> impl FnOnce(&mut web:: let (employee_cqrs_exec, employee_cqrs_query) = employee_view::init_cqrs(db.clone(), services.clone()); + let identity_cqrs_exec = types::WebIdentityCqrsExec::new(Arc::new( + types::IdentityCqrsExecBuilder::default() + .user(user_cqrs_exec) + .employee(employee_cqrs_exec) + .store(store_cqrs_exec) + .build() + .unwrap(), + )); + let f = move |cfg: &mut web::ServiceConfig| { cfg.configure(input::web::load_ctx()); - cfg.app_data(Data::new(user_cqrs_exec.clone())); cfg.app_data(Data::new(user_cqrs_query.clone())); - - cfg.app_data(Data::new(store_cqrs_exec.clone())); cfg.app_data(Data::new(store_cqrs_query.clone())); - - cfg.app_data(Data::new(employee_cqrs_exec.clone())); cfg.app_data(Data::new(employee_cqrs_query.clone())); + cfg.app_data(identity_cqrs_exec.clone()); }; Box::new(f) diff --git a/src/identity/adapters/types.rs b/src/identity/adapters/types.rs index e1b85ab..f2a1d47 100644 --- a/src/identity/adapters/types.rs +++ b/src/identity/adapters/types.rs @@ -6,8 +6,11 @@ use std::sync::Arc; use actix_web::web::Data; +use async_trait::async_trait; use cqrs_es::{persist::ViewRepository, AggregateError}; use derive_builder::Builder; +use mockall::predicate::*; +use mockall::*; use postgres_es::PostgresCqrs; use crate::identity::{ @@ -36,22 +39,35 @@ pub type IdentityEmployeeCqrsExec = Arc>; pub type IdentityEmployeeCqrsView = Arc>; pub type WebIdentityEmployeeCqrsView = Data; -pub type WebIdentityCqrsExec = Data>; -// +pub type WebIdentityCqrsExec = Data>; + +#[automock] +#[async_trait] +pub trait IdentityCqrsExecutor { + async fn execute( + &self, + aggregate_id: &str, + command: IdentityCommand, + ) -> Result<(), AggregateError>; +} + #[derive(Clone, Builder)] pub struct IdentityCqrsExec { user: IdentityUserCqrsExec, store: IdentityStoreCqrsExec, + employee: IdentityEmployeeCqrsExec, } -impl IdentityCqrsExec { - pub async fn execute( +#[async_trait] +impl IdentityCqrsExecutor for IdentityCqrsExec { + async fn execute( &self, aggregate_id: &str, command: IdentityCommand, ) -> Result<(), AggregateError> { self.user.execute(aggregate_id, command.clone()).await?; - self.store.execute(aggregate_id, command).await?; + self.store.execute(aggregate_id, command.clone()).await?; + self.employee.execute(aggregate_id, command).await?; Ok(()) } From a36684d9ae28c18d4d850e6e1c2c89d8e40565bc Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Fri, 10 Jan 2025 15:49:05 +0530 Subject: [PATCH 31/63] feat: test helpers for actix web harness --- src/tests/actix_web_test_utils.rs | 57 +++++++++++++++++++++++++++++++ src/tests/mod.rs | 1 + 2 files changed, 58 insertions(+) create mode 100644 src/tests/actix_web_test_utils.rs diff --git a/src/tests/actix_web_test_utils.rs b/src/tests/actix_web_test_utils.rs new file mode 100644 index 0000000..5ac7bd9 --- /dev/null +++ b/src/tests/actix_web_test_utils.rs @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later +use std::sync::Arc; + +use crate::db::{migrate::*, sqlx_postgres::*}; +use crate::settings::Settings; +use actix_web::cookie::Key; +use actix_web::dev::Service; + +pub async fn init_db() -> (Settings, Key, Postgres) { + let settings = crate::settings::tests::get_settings().await; + settings.create_db().await; + let secret_key = Key::from(settings.server.cookie_secret.as_bytes()); + let db = crate::db::sqlx_postgres::Postgres::init(&settings.database.url).await; + db.migrate().await; + (settings, secret_key, db) +} + +#[macro_export] +macro_rules! get_test_app { + ($settings:ident, $secret_key:ident, $db:ident) => { + test::init_service( + actix_web::App::new() + .wrap(actix_identity::IdentityMiddleware::default()) + .wrap(tracing_actix_web::TracingLogger::default()) + .wrap(actix_web::middleware::Compress::default()) + .app_data(actix_web::web::Data::new($settings.clone())) + .wrap(actix_session::SessionMiddleware::new( + actix_session::storage::CookieSessionStore::default(), + $secret_key.clone(), + )) + .wrap( + actix_web::middleware::DefaultHeaders::new() + .add(("Permissions-Policy", "interest-cohort=()")), + ) + .configure(crate::utils::load_adapters::load_adapters( + $db.pool.clone(), + $settings.clone(), + )), + ) + .await + }; +} + +pub async fn page_test_runner(path: &str) { + use actix_web::http::StatusCode; + use actix_web::test; + + let (settings, secret_key, db) = init_db().await; + let app = get_test_app!(settings, secret_key, db); + + let req = test::TestRequest::get().uri(path).to_request(); + let resp = test::call_service(&app, req).await; + let status = resp.status(); + assert_eq!(status, StatusCode::OK); +} diff --git a/src/tests/mod.rs b/src/tests/mod.rs index e799cc5..7f68e0c 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -2,4 +2,5 @@ // // SPDX-License-Identifier: AGPL-3.0-or-later +pub mod actix_web_test_utils; pub mod bdd; From 753863052af102d3a76d14832fb61089c5b761bb Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Fri, 10 Jan 2025 15:49:23 +0530 Subject: [PATCH 32/63] feat: app-wide adapters loading util --- src/utils/load_adapters.rs | 24 ++++++++++++++++++++++++ src/utils/mod.rs | 1 + 2 files changed, 25 insertions(+) create mode 100644 src/utils/load_adapters.rs diff --git a/src/utils/load_adapters.rs b/src/utils/load_adapters.rs new file mode 100644 index 0000000..1fb3bd1 --- /dev/null +++ b/src/utils/load_adapters.rs @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later +use actix_web::web::{self, Data}; +use sqlx::postgres::PgPool; + +use crate::settings::Settings; + +pub fn load_adapters(pool: PgPool, settings: Settings) -> impl FnOnce(&mut web::ServiceConfig) { + let f = move |cfg: &mut web::ServiceConfig| { + cfg.configure(crate::identity::adapters::load_adapters( + pool.clone(), + settings.clone(), + )) + .configure(crate::billing::adapters::load_adapters( + pool.clone(), + settings.clone(), + )) + .configure(super::random_string::GenerateRandomString::inject()) + .configure(super::uuid::GenerateUUID::inject()); + }; + + Box::new(f) +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 2ca7fc9..8b6fd1b 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -2,6 +2,7 @@ // // SPDX-License-Identifier: AGPL-3.0-or-later +pub mod load_adapters; pub mod parse_aggregate_id; pub mod random_number; pub mod random_string; From 5352fcccfc3039ea086d78ee5a7eedcb4cf0ace4 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Fri, 10 Jan 2025 15:49:38 +0530 Subject: [PATCH 33/63] feat: codegen script to generate actix web tests --- utils/web_handler_test_generator.sh | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100755 utils/web_handler_test_generator.sh diff --git a/utils/web_handler_test_generator.sh b/utils/web_handler_test_generator.sh new file mode 100755 index 0000000..500e3f4 --- /dev/null +++ b/utils/web_handler_test_generator.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +help() { + echo "Usage: web_test_generator.sh " +} + + + run() { + echo " + #[actix_rt::test] + async fn $1_web_$2_ui_works() { + let routes = RoutesRepository::default(); + page_test_runner( &routes.$2).await; + }" +} + + + +if [ -z $1 ] +then + help +elif [ -z $2 ] +then + help +else + run $1 $2 | wl-copy + run $1 $2 +fi From 104fc1525c25ce4f6e299ea87005d25abcdd84bc Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Fri, 10 Jan 2025 15:50:17 +0530 Subject: [PATCH 34/63] feat: identity tests --- src/identity/adapters/input/web/employee.rs | 26 +++++++ src/identity/adapters/input/web/owner.rs | 75 +++++++++++++++++++-- src/identity/adapters/input/web/routes.rs | 32 ++++----- 3 files changed, 112 insertions(+), 21 deletions(-) diff --git a/src/identity/adapters/input/web/employee.rs b/src/identity/adapters/input/web/employee.rs index edbf500..3b78dcb 100644 --- a/src/identity/adapters/input/web/employee.rs +++ b/src/identity/adapters/input/web/employee.rs @@ -158,3 +158,29 @@ async fn exit_organization_form_submission_handler( Ok(HttpResponse::Ok().json(store)) } + +#[cfg(test)] +mod tests { + use super::*; + + use crate::identity::adapters::input::web::RoutesRepository; + use crate::tests::actix_web_test_utils::page_test_runner; + + #[actix_rt::test] + async fn identity_web_employee_login_ui_works() { + let routes = RoutesRepository::default(); + page_test_runner(&routes.employee_login).await; + } + + #[actix_rt::test] + async fn identity_web_employee_register_ui_works() { + let routes = RoutesRepository::default(); + page_test_runner(&routes.employee_register).await; + } + + #[actix_rt::test] + async fn identity_web_employee_exit_organization_ui_works() { + let routes = RoutesRepository::default(); + page_test_runner(&routes.employee_exit_organization).await; + } +} diff --git a/src/identity/adapters/input/web/owner.rs b/src/identity/adapters/input/web/owner.rs index 08bd05a..23f362f 100644 --- a/src/identity/adapters/input/web/owner.rs +++ b/src/identity/adapters/input/web/owner.rs @@ -121,11 +121,13 @@ async fn register_form_submission_handler( #[get("/owner/user/email/update")] #[tracing::instrument(name = "Update email UI handler", skip())] async fn update_email_ui_handler() -> WebJsonRepsonse { - const UPDATE_EMAIL: &str = include_str!("./owner_update_email.html"); + use web_ui::identity::owner_update_email::*; + + let page = OwnerUpdateEmailPage::page(); Ok(HttpResponse::Ok() .insert_header(ContentType::html()) - .body(UPDATE_EMAIL)) + .body(page)) } #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)] @@ -154,7 +156,7 @@ async fn update_email_form_submission_handler( // change password handlers #[allow(clippy::too_many_arguments)] -#[get("/owner/user/password/update")] +#[get("/owner/user/password/change")] #[tracing::instrument(name = "Change password UI handler", skip())] async fn change_password_ui_handler() -> WebJsonRepsonse { const CHANGE_PASSWORD: &str = include_str!("./owner_change_password.html"); @@ -256,12 +258,12 @@ async fn verify_email_form_submission_handler( #[get("/owner/user/email/verify/resend")] #[tracing::instrument( name = "Resend verification email handler", - skip(id, req, identity_cqrs_exec) + skip(req, identity_cqrs_exec) )] async fn resend_verification_email( identity_cqrs_exec: types::WebIdentityCqrsExec, req: HttpRequest, - id: Identity, + // id: Identity, ) -> WebJsonRepsonse { let store = ""; @@ -339,3 +341,66 @@ async fn update_store_form_submission_handler( Ok(HttpResponse::Ok().json(store)) } + +#[cfg(test)] +mod tests { + use super::*; + + use crate::identity::adapters::input::web::RoutesRepository; + use crate::tests::actix_web_test_utils::page_test_runner; + + #[actix_rt::test] + async fn identity_web_owner_login_ui_works() { + let routes = RoutesRepository::default(); + + page_test_runner(&routes.owner_login).await; + } + + #[actix_rt::test] + async fn identity_web_owner_register_ui_works() { + let routes = RoutesRepository::default(); + page_test_runner(&routes.owner_register).await; + } + + #[actix_rt::test] + async fn identity_web_owner_delete_ui_works() { + let routes = RoutesRepository::default(); + page_test_runner(&routes.owner_delete).await; + } + + #[actix_rt::test] + async fn identity_web_owner_verify_email_ui_works() { + let routes = RoutesRepository::default(); + page_test_runner(&routes.owner_verify_email).await; + } + + #[actix_rt::test] + async fn identity_web_owner_resend_verification_email_ui_works() { + let routes = RoutesRepository::default(); + page_test_runner(&routes.owner_resend_verification_email).await; + } + + #[actix_rt::test] + async fn identity_web_owner_update_email_ui_works() { + let routes = RoutesRepository::default(); + page_test_runner(&routes.owner_update_email).await; + } + + #[actix_rt::test] + async fn identity_web_owner_change_password_ui_works() { + let routes = RoutesRepository::default(); + page_test_runner(&routes.owner_change_password).await; + } + + #[actix_rt::test] + async fn identity_web_owner_add_store_ui_works() { + let routes = RoutesRepository::default(); + page_test_runner(&routes.owner_add_store).await; + } + + #[actix_rt::test] + async fn identity_web_owner_update_store_ui_works() { + let routes = RoutesRepository::default(); + page_test_runner(&routes.owner_update_store).await; + } +} diff --git a/src/identity/adapters/input/web/routes.rs b/src/identity/adapters/input/web/routes.rs index 5c4aa88..78daf50 100644 --- a/src/identity/adapters/input/web/routes.rs +++ b/src/identity/adapters/input/web/routes.rs @@ -7,24 +7,24 @@ use uuid::Uuid; #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct RoutesRepository { - owner_login: String, - owner_register: String, - owner_delete: String, - owner_verify_email: String, - owner_resend_verification_email: String, + pub owner_login: String, + pub owner_register: String, + pub owner_delete: String, + pub owner_verify_email: String, + pub owner_resend_verification_email: String, // owner_set_admin: String, - owner_update_email: String, - owner_change_password: String, + pub owner_update_email: String, + pub owner_change_password: String, - owner_add_store: String, - owner_update_store: String, + pub owner_add_store: String, + pub owner_update_store: String, - employee_exit_organization: String, - employee_login: String, - employee_register: String, - employee_resend_login_otp: String, - employee_resend_verification_otp: String, - employee_verify_phone_number: String, + pub employee_exit_organization: String, + pub employee_login: String, + pub employee_register: String, + pub employee_resend_login_otp: String, + pub employee_resend_verification_otp: String, + pub employee_verify_phone_number: String, } impl Default for RoutesRepository { @@ -36,7 +36,7 @@ impl Default for RoutesRepository { owner_verify_email: "/owner/user/email/verify".into(), owner_resend_verification_email: "/owner/user/email/verify/resend".into(), owner_update_email: "/owner/user/email/update".into(), - owner_change_password: "/owner/password/change".into(), + owner_change_password: "/owner/user/password/change".into(), //owner_set_admin: "/owner/user/promote/admin".into(), owner_add_store: "/owner/store".into(), owner_update_store: "/owner/store/update".into(), From 1c327aaffbeff4c0d91ed89b11fc9c05c3c3de5c Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Fri, 10 Jan 2025 15:51:27 +0530 Subject: [PATCH 35/63] feat: bootstrap templating engine --- Cargo.lock | 499 +++++++++++++----- Cargo.toml | 3 +- src/main.rs | 15 +- web_ui/.gitignore | 1 + web_ui/Cargo.toml | 15 + web_ui/Makefile | 2 + web_ui/src/identity/mod.rs | 17 + web_ui/src/identity/owner_update_email.rs | 67 +++ web_ui/src/identity/owner_verify_email.rs | 67 +++ web_ui/src/lib.rs | 11 + web_ui/src/log.rs | 25 + web_ui/src/utils.rs | 149 ++++++ .../identity/employee_exit_organization.html | 14 + web_ui/templates/identity/employee_login.html | 27 + .../templates/identity/employee_register.html | 37 ++ .../templates/identity/owner_add_store.html | 25 + .../identity/owner_change_password.html | 31 ++ .../templates/identity/owner_delete_user.html | 20 + web_ui/templates/identity/owner_login.html | 27 + web_ui/templates/identity/owner_register.html | 43 ++ .../identity/owner_update_email.html | 25 + .../identity/owner_update_store.html | 25 + .../identity/owner_verify_email.html | 14 + web_ui/templates/nav_segment.html | 25 + web_ui/templates/tailwind_config.html | 25 + 25 files changed, 1051 insertions(+), 158 deletions(-) create mode 100644 web_ui/.gitignore create mode 100644 web_ui/Cargo.toml create mode 100644 web_ui/Makefile create mode 100644 web_ui/src/identity/mod.rs create mode 100644 web_ui/src/identity/owner_update_email.rs create mode 100644 web_ui/src/identity/owner_verify_email.rs create mode 100644 web_ui/src/lib.rs create mode 100644 web_ui/src/log.rs create mode 100644 web_ui/src/utils.rs create mode 100644 web_ui/templates/identity/employee_exit_organization.html create mode 100644 web_ui/templates/identity/employee_login.html create mode 100644 web_ui/templates/identity/employee_register.html create mode 100644 web_ui/templates/identity/owner_add_store.html create mode 100644 web_ui/templates/identity/owner_change_password.html create mode 100644 web_ui/templates/identity/owner_delete_user.html create mode 100644 web_ui/templates/identity/owner_login.html create mode 100644 web_ui/templates/identity/owner_register.html create mode 100644 web_ui/templates/identity/owner_update_email.html create mode 100644 web_ui/templates/identity/owner_update_store.html create mode 100644 web_ui/templates/identity/owner_verify_email.html create mode 100644 web_ui/templates/nav_segment.html create mode 100644 web_ui/templates/tailwind_config.html diff --git a/Cargo.lock b/Cargo.lock index 1aa0aa5..8bb847a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -81,7 +81,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn 2.0.90", + "syn 2.0.96", ] [[package]] @@ -216,7 +216,7 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.96", ] [[package]] @@ -348,9 +348,9 @@ checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anyhow" -version = "1.0.94" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" +checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" [[package]] name = "argon2-creds" @@ -395,7 +395,7 @@ checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.96", ] [[package]] @@ -460,9 +460,9 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be" dependencies = [ "serde", ] @@ -510,9 +510,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.11.1" +version = "1.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "786a307d683a5bf92e6fd5fd69a7eb613751668d1d8d67d802846dfe367c62c8" +checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0" dependencies = [ "memchr", "serde", @@ -547,9 +547,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.4" +version = "1.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9157bbaa6b165880c27a4293a474c91cdcf265cc68cc829bf10be0964a391caf" +checksum = "ad0cf6e91fde44c773c6ee7ec6bba798504641a8bc2eb7e37a04ffbf4dfaa55a" dependencies = [ "jobserver", "libc", @@ -588,7 +588,7 @@ checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb" dependencies = [ "chrono", "chrono-tz-build", - "phf 0.11.2", + "phf 0.11.3", ] [[package]] @@ -598,8 +598,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c088aee841df9c3041febbb73934cfc39708749bf96dc827e3359cd39ef11b1" dependencies = [ "parse-zoneinfo", - "phf 0.11.2", - "phf_codegen 0.11.2", + "phf 0.11.3", + "phf_codegen 0.11.3", ] [[package]] @@ -852,7 +852,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.96", ] [[package]] @@ -900,7 +900,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.90", + "syn 2.0.96", ] [[package]] @@ -922,7 +922,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core 0.20.10", "quote", - "syn 2.0.90", + "syn 2.0.96", ] [[package]] @@ -954,7 +954,7 @@ checksum = "74ef43543e701c01ad77d3a5922755c6a1d71b22d942cb8042be4994b380caff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.96", ] [[package]] @@ -996,7 +996,7 @@ dependencies = [ "darling 0.20.10", "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.96", ] [[package]] @@ -1016,7 +1016,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core 0.20.2", - "syn 2.0.90", + "syn 2.0.96", ] [[package]] @@ -1029,7 +1029,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.90", + "syn 2.0.96", ] [[package]] @@ -1049,7 +1049,7 @@ checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.96", "unicode-xid", ] @@ -1079,7 +1079,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.96", ] [[package]] @@ -1180,6 +1180,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "erased-serde" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24e2389d65ab4fab27dc2a5de7b191e1f6617d1f1c8855c0dc569c94a4cbb18d" +dependencies = [ + "serde", + "typeid", +] + [[package]] name = "errno" version = "0.3.10" @@ -1353,7 +1363,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.96", ] [[package]] @@ -1668,15 +1678,15 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.3" +version = "0.27.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" +checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" dependencies = [ "futures-util", "http 1.2.0", "hyper", "hyper-util", - "rustls 0.23.20", + "rustls 0.23.21", "rustls-pki-types", "tokio", "tokio-rustls", @@ -1857,7 +1867,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.96", ] [[package]] @@ -1876,16 +1886,6 @@ dependencies = [ "unicode-normalization", ] -[[package]] -name = "idna" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" -dependencies = [ - "unicode-bidi", - "unicode-normalization", -] - [[package]] name = "idna" version = "1.0.3" @@ -1931,9 +1931,9 @@ dependencies = [ [[package]] name = "impl-more" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aae21c3177a27788957044151cc2800043d127acaa460a47ebb9b84dfa2c6aa0" +checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" [[package]] name = "indexmap" @@ -2067,7 +2067,7 @@ dependencies = [ "percent-encoding", "quoted_printable", "rsa", - "rustls 0.23.20", + "rustls 0.23.21", "rustls-pemfile 2.2.0", "rustls-pki-types", "sha2", @@ -2082,9 +2082,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.168" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libm" @@ -2105,9 +2105,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "litemap" @@ -2144,9 +2144,12 @@ dependencies = [ [[package]] name = "log" -version = "0.4.22" +version = "0.4.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "3d6ea2a48c204030ee31a7d7fc72c93294c92fe87ecb1789881c9543516e1a0d" +dependencies = [ + "value-bag", +] [[package]] name = "mac" @@ -2209,7 +2212,7 @@ dependencies = [ "proc-macro2", "quote", "structmeta", - "syn 2.0.90", + "syn 2.0.96", ] [[package]] @@ -2301,7 +2304,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.96", ] [[package]] @@ -2398,9 +2401,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.5" +version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] @@ -2440,7 +2443,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.96", ] [[package]] @@ -2449,6 +2452,15 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +[[package]] +name = "openssl-src" +version = "300.4.1+3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faa4eac4138c62414b5622d1b31c5c304f34b406b013c079c2bbc652fdd6678c" +dependencies = [ + "cc", +] + [[package]] name = "openssl-sys" version = "0.9.104" @@ -2457,6 +2469,7 @@ checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" dependencies = [ "cc", "libc", + "openssl-src", "pkg-config", "vcpkg", ] @@ -2537,7 +2550,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc" dependencies = [ "memchr", - "thiserror 2.0.8", + "thiserror 2.0.11", "ucd-trie", ] @@ -2561,7 +2574,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.96", ] [[package]] @@ -2586,11 +2599,11 @@ dependencies = [ [[package]] name = "phf" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ - "phf_shared 0.11.2", + "phf_shared 0.11.3", ] [[package]] @@ -2605,12 +2618,12 @@ dependencies = [ [[package]] name = "phf_codegen" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" dependencies = [ - "phf_generator 0.11.2", - "phf_shared 0.11.2", + "phf_generator 0.11.3", + "phf_shared 0.11.3", ] [[package]] @@ -2625,11 +2638,11 @@ dependencies = [ [[package]] name = "phf_generator" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ - "phf_shared 0.11.2", + "phf_shared 0.11.3", "rand", ] @@ -2639,43 +2652,43 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" dependencies = [ - "siphasher", + "siphasher 0.3.11", ] [[package]] name = "phf_shared" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" dependencies = [ - "siphasher", + "siphasher 1.0.1", ] [[package]] name = "pin-project" -version = "1.1.7" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95" +checksum = "1e2ec53ad785f4d35dac0adea7f7dc6f1bb277ad84a680c7afefeae05d1f5916" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.7" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c" +checksum = "d56a66c0c55993aa927429d0f8a0abfd74f084e4d9c192cffed01e418d83eefb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.96", ] [[package]] name = "pin-project-lite" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -2760,9 +2773,9 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] name = "predicates" -version = "3.1.2" +version = "3.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e9086cc7640c29a356d1a29fd134380bee9d8f79a17410aa76e7ad295f42c97" +checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" dependencies = [ "anstyle", "predicates-core", @@ -2770,15 +2783,15 @@ dependencies = [ [[package]] name = "predicates-core" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae8177bee8e75d6846599c6b9ff679ed51e882816914eec639944d7c9aa11931" +checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" [[package]] name = "predicates-tree" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41b740d195ed3166cd147c8047ec98db0e22ec019eb8eeb76d343b795304fb13" +checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" dependencies = [ "predicates-core", "termtree", @@ -2819,10 +2832,32 @@ dependencies = [ ] [[package]] -name = "proc-macro2" -version = "1.0.92" +name = "proc-macro-error-attr2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "proc-macro2" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] @@ -2847,9 +2882,9 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls 0.23.20", + "rustls 0.23.21", "socket2", - "thiserror 2.0.8", + "thiserror 2.0.11", "tokio", "tracing", ] @@ -2865,10 +2900,10 @@ dependencies = [ "rand", "ring", "rustc-hash", - "rustls 0.23.20", + "rustls 0.23.21", "rustls-pki-types", "slab", - "thiserror 2.0.8", + "thiserror 2.0.11", "tinyvec", "tracing", "web-time", @@ -2890,9 +2925,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.37" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] @@ -2979,9 +3014,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" -version = "0.12.9" +version = "0.12.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f" +checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da" dependencies = [ "base64 0.22.1", "bytes", @@ -3005,7 +3040,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.20", + "rustls 0.23.21", "rustls-pemfile 2.2.0", "rustls-pki-types", "serde", @@ -3017,6 +3052,7 @@ dependencies = [ "tokio-native-tls", "tokio-rustls", "tokio-util", + "tower", "tower-service", "url", "wasm-bindgen", @@ -3105,7 +3141,7 @@ dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.90", + "syn 2.0.96", "walkdir", ] @@ -3153,9 +3189,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.42" +version = "0.38.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" +checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6" dependencies = [ "bitflags", "errno", @@ -3177,9 +3213,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.20" +version = "0.23.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5065c3f250cbd332cd894be57c40fa52387247659b14a2d6041d121547903b1b" +checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8" dependencies = [ "log", "once_cell", @@ -3293,9 +3329,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1863fd3768cd83c56a7f60faa4dc0d403f1b6df0a38c3c25f44b7894e45370d5" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" dependencies = [ "core-foundation-sys", "libc", @@ -3324,7 +3360,16 @@ checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.96", +] + +[[package]] +name = "serde_fmt" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d4ddca14104cd60529e8c7f7ba71a2c8acd8f7f5cfcdc2faf97eeb7c3010a4" +dependencies = [ + "serde", ] [[package]] @@ -3413,6 +3458,12 @@ version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "slab" version = "0.4.9" @@ -3759,7 +3810,7 @@ dependencies = [ "proc-macro2", "quote", "structmeta-derive", - "syn 2.0.90", + "syn 2.0.96", ] [[package]] @@ -3770,7 +3821,7 @@ checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.96", ] [[package]] @@ -3779,6 +3830,84 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "sval" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6dc0f9830c49db20e73273ffae9b5240f63c42e515af1da1fceefb69fceafd8" + +[[package]] +name = "sval_buffer" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "429922f7ad43c0ef8fd7309e14d750e38899e32eb7e8da656ea169dd28ee212f" +dependencies = [ + "sval", + "sval_ref", +] + +[[package]] +name = "sval_dynamic" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f16ff5d839396c11a30019b659b0976348f3803db0626f736764c473b50ff4" +dependencies = [ + "sval", +] + +[[package]] +name = "sval_fmt" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c01c27a80b6151b0557f9ccbe89c11db571dc5f68113690c1e028d7e974bae94" +dependencies = [ + "itoa", + "ryu", + "sval", +] + +[[package]] +name = "sval_json" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0deef63c70da622b2a8069d8600cf4b05396459e665862e7bdb290fd6cf3f155" +dependencies = [ + "itoa", + "ryu", + "sval", +] + +[[package]] +name = "sval_nested" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a39ce5976ae1feb814c35d290cf7cf8cd4f045782fe1548d6bc32e21f6156e9f" +dependencies = [ + "sval", + "sval_buffer", + "sval_ref", +] + +[[package]] +name = "sval_ref" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb7c6ee3751795a728bc9316a092023529ffea1783499afbc5c66f5fabebb1fa" +dependencies = [ + "sval", +] + +[[package]] +name = "sval_serde" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a5572d0321b68109a343634e3a5d576bf131b82180c6c442dee06349dfc652a" +dependencies = [ + "serde", + "sval", + "sval_nested", +] + [[package]] name = "syn" version = "1.0.109" @@ -3792,9 +3921,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.90" +version = "2.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" +checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" dependencies = [ "proc-macro2", "quote", @@ -3818,7 +3947,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.96", ] [[package]] @@ -3844,12 +3973,13 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.14.0" +version = "3.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" +checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" dependencies = [ "cfg-if", "fastrand", + "getrandom", "once_cell", "rustix", "windows-sys 0.59.0", @@ -3899,9 +4029,9 @@ dependencies = [ [[package]] name = "termtree" -version = "0.4.1" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" [[package]] name = "thiserror" @@ -3914,11 +4044,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.8" +version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f5383f3e0071702bf93ab5ee99b52d26936be9dedd9413067cbdcddcb6141a" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" dependencies = [ - "thiserror-impl 2.0.8", + "thiserror-impl 2.0.11", ] [[package]] @@ -3929,18 +4059,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.96", ] [[package]] name = "thiserror-impl" -version = "2.0.8" +version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f357fcec90b3caef6623a099691be676d033b40a058ac95d2a6ade6fa0c943" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.96", ] [[package]] @@ -3995,9 +4125,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" dependencies = [ "tinyvec_macros", ] @@ -4010,9 +4140,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.42.0" +version = "1.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" +checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" dependencies = [ "backtrace", "bytes", @@ -4028,13 +4158,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.96", ] [[package]] @@ -4053,7 +4183,7 @@ version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" dependencies = [ - "rustls 0.23.20", + "rustls 0.23.21", "tokio", ] @@ -4115,6 +4245,27 @@ dependencies = [ "winnow", ] +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + [[package]] name = "tower-service" version = "0.3.3" @@ -4154,7 +4305,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.96", ] [[package]] @@ -4187,6 +4338,12 @@ dependencies = [ "url", ] +[[package]] +name = "typeid" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e13db2e0ccd5e14a544e8a246ba2312cd25223f616442d7f2cb0e3db614236e" + [[package]] name = "typenum" version = "1.17.0" @@ -4348,9 +4505,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +checksum = "b913a3b5fe84142e269d63cc62b64319ccaf89b748fc31fe025177f767a756c4" dependencies = [ "getrandom", "serde", @@ -4375,18 +4532,18 @@ dependencies = [ [[package]] name = "validator" -version = "0.18.1" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db79c75af171630a3148bd3e6d7c4f42b6a9a014c2945bc5ed0020cbb8d9478e" +checksum = "d0b4a29d8709210980a09379f27ee31549b73292c87ab9899beee1c0d3be6303" dependencies = [ - "idna 0.5.0", + "idna 1.0.3", "once_cell", "regex", "serde", "serde_derive", "serde_json", "url", - "validator_derive 0.18.2", + "validator_derive 0.19.0", ] [[package]] @@ -4407,16 +4564,16 @@ dependencies = [ [[package]] name = "validator_derive" -version = "0.18.2" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df0bcf92720c40105ac4b2dda2a4ea3aa717d4d6a862cc217da653a4bd5c6b10" +checksum = "bac855a2ce6f843beb229757e6e570a42e837bcb15e5f449dd48d5747d41bf77" dependencies = [ "darling 0.20.10", "once_cell", - "proc-macro-error", + "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.96", ] [[package]] @@ -4429,6 +4586,42 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "value-bag" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ef4c4aa54d5d05a279399bfa921ec387b7aba77caf7a682ae8d86785b8fdad2" +dependencies = [ + "value-bag-serde1", + "value-bag-sval2", +] + +[[package]] +name = "value-bag-serde1" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb773bd36fd59c7ca6e336c94454d9c66386416734817927ac93d81cb3c5b0b" +dependencies = [ + "erased-serde", + "serde", + "serde_fmt", +] + +[[package]] +name = "value-bag-sval2" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a916a702cac43a88694c97657d449775667bcd14b70419441d05b7fea4a83a" +dependencies = [ + "sval", + "sval_buffer", + "sval_dynamic", + "sval_fmt", + "sval_json", + "sval_ref", + "sval_serde", +] + [[package]] name = "vanikam" version = "0.1.0" @@ -4463,7 +4656,8 @@ dependencies = [ "twilio_client", "url", "uuid", - "validator 0.18.1", + "validator 0.19.0", + "web_ui", ] [[package]] @@ -4530,7 +4724,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.96", "wasm-bindgen-shared", ] @@ -4565,7 +4759,7 @@ checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.96", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -4609,6 +4803,21 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web_ui" +version = "0.1.0" +dependencies = [ + "derive_builder 0.20.2", + "derive_more 0.99.18", + "lazy_static", + "rust-embed", + "serde", + "serde_json", + "tera", + "tracing", + "url", +] + [[package]] name = "webpki-roots" version = "0.25.4" @@ -4832,9 +5041,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.20" +version = "0.6.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a" dependencies = [ "memchr", ] @@ -4893,7 +5102,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.96", "synstructure", ] @@ -4915,7 +5124,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.96", ] [[package]] @@ -4935,7 +5144,7 @@ checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.96", "synstructure", ] @@ -4964,7 +5173,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.96", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 3a015ad..75ab30c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [workspace] exclude = ["utils/db-migrations"] #, "utils/cache-bust"] -members = [".", "mailpit_client", "twilio_client"] +members = [".", "mailpit_client", "twilio_client", "web_ui"] [dependencies] actix-identity = "0.8.0" @@ -38,6 +38,7 @@ url = { version = "2.5.0", features = ["serde"] } uuid = { version = "1.10.0", features = ["v4", "serde"] } validator = { version = "0.19.0", features = ["derive"] } twilio_client = { path = "./twilio_client" } +web_ui = { path = "./web_ui" } [dev-dependencies] #reqwest = { version = "0.12.4", features = ["json"] } diff --git a/src/main.rs b/src/main.rs index 6ee53c1..fc62f9b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,6 +16,7 @@ mod inventory; mod ordering; mod settings; #[cfg(test)] +#[macro_use] mod tests; mod types; mod utils; @@ -33,6 +34,7 @@ async fn main() { } pretty_env_logger::init(); + web_ui::init(); let db = db::sqlx_postgres::Postgres::init(&settings.database.url).await; db.migrate().await; @@ -58,21 +60,10 @@ async fn main() { .wrap( middleware::DefaultHeaders::new().add(("Permissions-Policy", "interest-cohort=()")), ) - .configure(billing::adapters::load_adapters( + .configure(utils::load_adapters::load_adapters( db.pool.clone(), settings.clone(), )) - .configure(identity::adapters::load_adapters( - db.pool.clone(), - settings.clone(), - )) - .configure(billing::adapters::load_adapters( - db.pool.clone(), - settings.clone(), - )) - // .configure(auth::adapter::load_adapters(db.pool.clone(), &settings)) - .configure(utils::random_string::GenerateRandomString::inject()) - .configure(utils::uuid::GenerateUUID::inject()) }) .bind(&socket_addr) .unwrap() diff --git a/web_ui/.gitignore b/web_ui/.gitignore new file mode 100644 index 0000000..3fec32c --- /dev/null +++ b/web_ui/.gitignore @@ -0,0 +1 @@ +tmp/ diff --git a/web_ui/Cargo.toml b/web_ui/Cargo.toml new file mode 100644 index 0000000..3d5d3b6 --- /dev/null +++ b/web_ui/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "web_ui" +version = "0.1.0" +edition = "2024" + +[dependencies] +derive_builder = "0.20.0" +derive_more = { version = "0.99.17", features = ["error"]} +lazy_static = "1.5.0" +rust-embed = { version = "8.4.0", features = ["debug-embed"]} +serde = { version = "1.0.201", features = ["derive"] } +serde_json = "1.0.117" +tera = "1.19.0" +tracing = { version = "0.1.40", features = ["log"] } +url = { version = "2.5.0", features = ["serde"] } diff --git a/web_ui/Makefile b/web_ui/Makefile new file mode 100644 index 0000000..585864c --- /dev/null +++ b/web_ui/Makefile @@ -0,0 +1,2 @@ +check: ## Check for syntax errors on all workspaces + cargo check --tests --all-features diff --git a/web_ui/src/identity/mod.rs b/web_ui/src/identity/mod.rs new file mode 100644 index 0000000..411f72e --- /dev/null +++ b/web_ui/src/identity/mod.rs @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +pub mod owner_update_email; +pub mod owner_verify_email; + +pub fn register_templates(t: &mut tera::Tera) { + for template in [ + owner_update_email::ONWER_UPDATE_EMAIL_PAGE, + owner_verify_email::ONWER_VERIFY_EMAIL_PAGE, + ] + .iter() + { + template.register(t).expect(template.name); + } +} diff --git a/web_ui/src/identity/owner_update_email.rs b/web_ui/src/identity/owner_update_email.rs new file mode 100644 index 0000000..472497d --- /dev/null +++ b/web_ui/src/identity/owner_update_email.rs @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later +use std::cell::RefCell; + +use derive_builder::*; +use derive_more::*; +use serde::*; +use tera::Context; +use url::Url; + +pub use super::*; +use crate::utils::*; + +pub const ONWER_UPDATE_EMAIL_PAGE: TemplateFile = TemplateFile::new( + "identity.owner_update_email", + "identity/owner_update_email.html", +); + +pub fn register_templates(t: &mut tera::Tera) { + ONWER_UPDATE_EMAIL_PAGE + .register(t) + .expect(ONWER_UPDATE_EMAIL_PAGE.name); +} + +pub struct OwnerUpdateEmailPage { + ctx: RefCell, +} + +impl OwnerUpdateEmailPage { + pub fn new() -> Self { + let mut ctx = context(); + + //ctx.insert(PAYLOAD_KEY, p); + + let ctx = RefCell::new(ctx); + Self { ctx } + } + + pub fn render(&self) -> String { + TEMPLATES + .render(ONWER_UPDATE_EMAIL_PAGE.name, &self.ctx.borrow()) + .unwrap() + } + + pub fn page() -> String { + let p = Self::new(); + p.render() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn render_page_and_write() { + const FILE: &str = "./tmp/identity-owner_update_email.html"; + + let tw = TestWriterBuilder::default() + .file(FILE.into()) + .contents(OwnerUpdateEmailPage::page()) + .build() + .unwrap(); + tw.write(); + } +} diff --git a/web_ui/src/identity/owner_verify_email.rs b/web_ui/src/identity/owner_verify_email.rs new file mode 100644 index 0000000..8e5785d --- /dev/null +++ b/web_ui/src/identity/owner_verify_email.rs @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later +use std::cell::RefCell; + +use derive_builder::*; +use derive_more::*; +use serde::*; +use tera::Context; +use url::Url; + +pub use super::*; +use crate::utils::*; + +pub const ONWER_VERIFY_EMAIL_PAGE: TemplateFile = TemplateFile::new( + "identity.owner_verify_email", + "identity/owner_verify_email.html", +); + +pub fn register_templates(t: &mut tera::Tera) { + ONWER_VERIFY_EMAIL_PAGE + .register(t) + .expect(ONWER_VERIFY_EMAIL_PAGE.name); +} + +pub struct OwnerVerifyEmailPage { + ctx: RefCell, +} + +impl OwnerVerifyEmailPage { + pub fn new() -> Self { + let mut ctx = context(); + + //ctx.insert(PAYLOAD_KEY, p); + + let ctx = RefCell::new(ctx); + Self { ctx } + } + + pub fn render(&self) -> String { + TEMPLATES + .render(ONWER_VERIFY_EMAIL_PAGE.name, &self.ctx.borrow()) + .unwrap() + } + + pub fn page() -> String { + let p = Self::new(); + p.render() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn render_page_and_write() { + const FILE: &str = "./tmp/identity-owner_verify_email.html"; + + let tw = TestWriterBuilder::default() + .file(FILE.into()) + .contents(OwnerVerifyEmailPage::page()) + .build() + .unwrap(); + tw.write(); + } +} diff --git a/web_ui/src/lib.rs b/web_ui/src/lib.rs new file mode 100644 index 0000000..d02df30 --- /dev/null +++ b/web_ui/src/lib.rs @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +pub mod identity; +mod log; +mod utils; + +pub fn init() { + lazy_static::initialize(&utils::TEMPLATES); +} diff --git a/web_ui/src/log.rs b/web_ui/src/log.rs new file mode 100644 index 0000000..f1d278b --- /dev/null +++ b/web_ui/src/log.rs @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +#[allow(unused_imports)] +#[cfg(test)] +pub(crate) use println as info; +#[allow(unused_imports)] +#[cfg(test)] +pub(crate) use println as error; + +#[allow(unused_imports)] +#[cfg(test)] +pub(crate) use println as trace; + +#[allow(unused_imports)] +#[cfg(test)] +pub(crate) use println as debug; + +#[allow(unused_imports)] +#[cfg(test)] +pub(crate) use println as warn; + +#[cfg(not(test))] +pub use tracing::{debug, error, info, trace, warn}; diff --git a/web_ui/src/utils.rs b/web_ui/src/utils.rs new file mode 100644 index 0000000..f6674b6 --- /dev/null +++ b/web_ui/src/utils.rs @@ -0,0 +1,149 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use lazy_static::lazy_static; +use rust_embed::RustEmbed; +use serde::*; +use tera::*; +#[allow(unused_imports)] +#[cfg(test)] +pub use tests::*; + +pub struct TemplateFile { + pub name: &'static str, + pub path: &'static str, +} + +impl TemplateFile { + pub const fn new(name: &'static str, path: &'static str) -> Self { + Self { name, path } + } + + pub fn register(&self, t: &mut Tera) -> std::result::Result<(), tera::Error> { + t.add_raw_template(self.name, &Templates::get_template(self).expect(self.name)) + } + + #[cfg(test)] + #[allow(dead_code)] + pub fn register_from_file(&self, t: &mut Tera) -> std::result::Result<(), tera::Error> { + use std::path::Path; + t.add_template_file(Path::new("templates/").join(self.path), Some(self.name)) + } +} + +pub const PAYLOAD_KEY: &str = "payload"; + +pub const NAV_SEGMENT: TemplateFile = TemplateFile::new("nav_segment.html", "nav_segment.html"); + +pub const TAILWIND_CONFIG: TemplateFile = + TemplateFile::new("tailwind_config.html", "tailwind_config.html"); + +lazy_static! { + pub static ref TEMPLATES: Tera = { + let mut tera = Tera::default(); + for t in [NAV_SEGMENT, TAILWIND_CONFIG].iter() { + t.register(&mut tera).unwrap(); + } + tera.autoescape_on(vec![".html", ".sql"]); + crate::identity::register_templates(&mut tera); + tera + }; +} + +#[derive(RustEmbed)] +#[folder = "templates/"] +pub struct Templates; + +impl Templates { + pub fn get_template(t: &TemplateFile) -> Option { + match Self::get(t.path) { + Some(file) => Some(String::from_utf8_lossy(&file.data).into_owned()), + None => None, + } + } +} + +pub fn context() -> Context { + let ctx = Context::new(); + // let footer = Footer::new(s); + // ctx.insert("footer", &footer); + // ctx.insert("page", &PAGES); + // ctx.insert("assets", &*ASSETS); + ctx +} + +//pub fn auth_ctx(username: Option<&str>, s: &Settings) -> Context { +// use routes::GistProfilePathComponent; +// let mut profile_link = None; +// if let Some(name) = username { +// profile_link = Some( +// PAGES +// .gist +// .get_profile_route(GistProfilePathComponent { username: name }), +// ); +// } +// let mut ctx = Context::new(); +// let footer = Footer::new(s); +// ctx.insert("footer", &footer); +// ctx.insert("page", &PAGES); +// ctx.insert("assets", &*ASSETS); +// ctx.insert("loggedin_user", &profile_link); +// ctx +//} + +#[derive(Serialize)] +pub struct Footer<'a> { + // version: &'a str, + admin_email: &'a str, + source_code: &'a str, + // git_hash: &'a str, + // settings: &'a Settings, + // demo_user: &'a str, + // demo_password: &'a str, +} + +impl<'a> Footer<'a> { + pub fn new(admin_email: &'a str, source_code: &'a str) -> Self { + Self { + // version: VERSION, + source_code, + admin_email, + // git_hash: &GIT_COMMIT_HASH[..8], + // demo_user: crate::demo::DEMO_USER, + // demo_password: crate::demo::DEMO_PASSWORD, + // settings, + } + } +} + + +#[cfg(test)] +pub mod tests { + use super::*; + use derive_builder::*; + use derive_more::*; + use tracing::info; + + use std::fs; + use std::path::Path; + + #[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize, Builder)] + pub struct TestWriter { + file: String, + contents: String, + } + + impl TestWriter { + pub fn write(&self) { + let p = Path::new(&self.file); + if p.exists() { + info!("{:?} exists; removing", p.canonicalize().unwrap()); + fs::remove_file(&self.file).unwrap(); + } + + let _ = fs::write(&self.file, self.contents.as_bytes()); + info!("contents written to file: {:?}", p.canonicalize().unwrap()); + } + } +} diff --git a/web_ui/templates/identity/employee_exit_organization.html b/web_ui/templates/identity/employee_exit_organization.html new file mode 100644 index 0000000..801785d --- /dev/null +++ b/web_ui/templates/identity/employee_exit_organization.html @@ -0,0 +1,14 @@ + + + + + + Exit organization | Vanikam + + + +
+ +
+ + diff --git a/web_ui/templates/identity/employee_login.html b/web_ui/templates/identity/employee_login.html new file mode 100644 index 0000000..56a8ac9 --- /dev/null +++ b/web_ui/templates/identity/employee_login.html @@ -0,0 +1,27 @@ + + + + + + Login | Vanikam + + + +
+ + + + + +
+ +

New here? Click here to register!

+ + + diff --git a/web_ui/templates/identity/employee_register.html b/web_ui/templates/identity/employee_register.html new file mode 100644 index 0000000..5184221 --- /dev/null +++ b/web_ui/templates/identity/employee_register.html @@ -0,0 +1,37 @@ + + + + + + Register | Vanikam + + + +
+ + + + + + + + + +
+ +

Already have an account? Click here to log in!

+ + + diff --git a/web_ui/templates/identity/owner_add_store.html b/web_ui/templates/identity/owner_add_store.html new file mode 100644 index 0000000..e2e77bf --- /dev/null +++ b/web_ui/templates/identity/owner_add_store.html @@ -0,0 +1,25 @@ + + + + + + Create Store | Vanikam + + + +
+ + + + + +
+ + + diff --git a/web_ui/templates/identity/owner_change_password.html b/web_ui/templates/identity/owner_change_password.html new file mode 100644 index 0000000..242f9a6 --- /dev/null +++ b/web_ui/templates/identity/owner_change_password.html @@ -0,0 +1,31 @@ + + + + + + Change Password | Vanikam + + + +
+ + + + + + + + +
+ + + diff --git a/web_ui/templates/identity/owner_delete_user.html b/web_ui/templates/identity/owner_delete_user.html new file mode 100644 index 0000000..13d6e67 --- /dev/null +++ b/web_ui/templates/identity/owner_delete_user.html @@ -0,0 +1,20 @@ + + + + + + Delete account | Vanikam + + + +
+ + + + +
+ + diff --git a/web_ui/templates/identity/owner_login.html b/web_ui/templates/identity/owner_login.html new file mode 100644 index 0000000..095f9a7 --- /dev/null +++ b/web_ui/templates/identity/owner_login.html @@ -0,0 +1,27 @@ + + + + + + Login | Vanikam + + + +
+ + + + + +
+ +

New here? Click here to register!

+ + + diff --git a/web_ui/templates/identity/owner_register.html b/web_ui/templates/identity/owner_register.html new file mode 100644 index 0000000..94b5a43 --- /dev/null +++ b/web_ui/templates/identity/owner_register.html @@ -0,0 +1,43 @@ + + + + + + Register | Vanikam + + + +
+ + + + + + + + + + + + +
+ +

Already have an account? Click here to log in!

+ + + diff --git a/web_ui/templates/identity/owner_update_email.html b/web_ui/templates/identity/owner_update_email.html new file mode 100644 index 0000000..c205d3c --- /dev/null +++ b/web_ui/templates/identity/owner_update_email.html @@ -0,0 +1,25 @@ + + + + + + Update Email | Vanikam + + + +
+ + + + + +
+ + + diff --git a/web_ui/templates/identity/owner_update_store.html b/web_ui/templates/identity/owner_update_store.html new file mode 100644 index 0000000..9c58a96 --- /dev/null +++ b/web_ui/templates/identity/owner_update_store.html @@ -0,0 +1,25 @@ + + + + + + Update Store | Vanikam + + + +
+ + + + + +
+ + + diff --git a/web_ui/templates/identity/owner_verify_email.html b/web_ui/templates/identity/owner_verify_email.html new file mode 100644 index 0000000..3453545 --- /dev/null +++ b/web_ui/templates/identity/owner_verify_email.html @@ -0,0 +1,14 @@ + + + + + + Verify email | Vanikam + + + +
+ +
+ + diff --git a/web_ui/templates/nav_segment.html b/web_ui/templates/nav_segment.html new file mode 100644 index 0000000..21976d4 --- /dev/null +++ b/web_ui/templates/nav_segment.html @@ -0,0 +1,25 @@ + diff --git a/web_ui/templates/tailwind_config.html b/web_ui/templates/tailwind_config.html new file mode 100644 index 0000000..876a1da --- /dev/null +++ b/web_ui/templates/tailwind_config.html @@ -0,0 +1,25 @@ + + + From 6e62c8d8a9ade1fb77dc958a2ba87b5a52c951a2 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Fri, 10 Jan 2025 16:19:57 +0530 Subject: [PATCH 36/63] feat: port html pages into tera templates --- .../identity/employee_exit_organization.rs | 67 +++++++++++++++++++ web_ui/src/identity/employee_login.rs | 65 ++++++++++++++++++ web_ui/src/identity/employee_register.rs | 67 +++++++++++++++++++ web_ui/src/identity/mod.rs | 18 +++++ web_ui/src/identity/owner_add_store.rs | 65 ++++++++++++++++++ web_ui/src/identity/owner_change_password.rs | 67 +++++++++++++++++++ web_ui/src/identity/owner_delete_user.rs | 67 +++++++++++++++++++ web_ui/src/identity/owner_login.rs | 63 +++++++++++++++++ web_ui/src/identity/owner_register.rs | 65 ++++++++++++++++++ web_ui/src/identity/owner_update_store.rs | 67 +++++++++++++++++++ web_ui/src/utils.rs | 1 - 11 files changed, 611 insertions(+), 1 deletion(-) create mode 100644 web_ui/src/identity/employee_exit_organization.rs create mode 100644 web_ui/src/identity/employee_login.rs create mode 100644 web_ui/src/identity/employee_register.rs create mode 100644 web_ui/src/identity/owner_add_store.rs create mode 100644 web_ui/src/identity/owner_change_password.rs create mode 100644 web_ui/src/identity/owner_delete_user.rs create mode 100644 web_ui/src/identity/owner_login.rs create mode 100644 web_ui/src/identity/owner_register.rs create mode 100644 web_ui/src/identity/owner_update_store.rs diff --git a/web_ui/src/identity/employee_exit_organization.rs b/web_ui/src/identity/employee_exit_organization.rs new file mode 100644 index 0000000..c11c2ed --- /dev/null +++ b/web_ui/src/identity/employee_exit_organization.rs @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later +use std::cell::RefCell; + +use derive_builder::*; +use derive_more::*; +use serde::*; +use tera::Context; +use url::Url; + +pub use super::*; +use crate::utils::*; + +pub const EMPLOYEE_EXIT_ORGANIZATION_PAGE: TemplateFile = TemplateFile::new( + "identity.employee_exit_organization", + "identity/employee_exit_organization.html", +); + +pub fn register_templates(t: &mut tera::Tera) { + EMPLOYEE_EXIT_ORGANIZATION_PAGE + .register(t) + .expect(EMPLOYEE_EXIT_ORGANIZATION_PAGE.name); +} + +pub struct EmployeeExitOrganizationPage { + ctx: RefCell, +} + +impl EmployeeExitOrganizationPage { + pub fn new() -> Self { + let mut ctx = context(); + + //ctx.insert(PAYLOAD_KEY, p); + + let ctx = RefCell::new(ctx); + Self { ctx } + } + + pub fn render(&self) -> String { + TEMPLATES + .render(EMPLOYEE_EXIT_ORGANIZATION_PAGE.name, &self.ctx.borrow()) + .unwrap() + } + + pub fn page() -> String { + let p = Self::new(); + p.render() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn render_page_and_write() { + const FILE: &str = "./tmp/identity-employee_exit_organization.html"; + + let tw = TestWriterBuilder::default() + .file(FILE.into()) + .contents(EmployeeExitOrganizationPage::page()) + .build() + .unwrap(); + tw.write(); + } +} diff --git a/web_ui/src/identity/employee_login.rs b/web_ui/src/identity/employee_login.rs new file mode 100644 index 0000000..c5586fa --- /dev/null +++ b/web_ui/src/identity/employee_login.rs @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later +use std::cell::RefCell; + +use derive_builder::*; +use derive_more::*; +use serde::*; +use tera::Context; +use url::Url; + +pub use super::*; +use crate::utils::*; + +pub const EMPLOYEE_LOGIN_PAGE: TemplateFile = + TemplateFile::new("identity.employee_login", "identity/employee_login.html"); + +pub fn register_templates(t: &mut tera::Tera) { + EMPLOYEE_LOGIN_PAGE + .register(t) + .expect(EMPLOYEE_LOGIN_PAGE.name); +} + +pub struct EmployeeLoginPage { + ctx: RefCell, +} + +impl EmployeeLoginPage { + pub fn new() -> Self { + let mut ctx = context(); + + //ctx.insert(PAYLOAD_KEY, p); + + let ctx = RefCell::new(ctx); + Self { ctx } + } + + pub fn render(&self) -> String { + TEMPLATES + .render(EMPLOYEE_LOGIN_PAGE.name, &self.ctx.borrow()) + .unwrap() + } + + pub fn page() -> String { + let p = Self::new(); + p.render() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn render_page_and_write() { + const FILE: &str = "./tmp/identity-employee_login.html"; + + let tw = TestWriterBuilder::default() + .file(FILE.into()) + .contents(EmployeeLoginPage::page()) + .build() + .unwrap(); + tw.write(); + } +} diff --git a/web_ui/src/identity/employee_register.rs b/web_ui/src/identity/employee_register.rs new file mode 100644 index 0000000..a62dcb2 --- /dev/null +++ b/web_ui/src/identity/employee_register.rs @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later +use std::cell::RefCell; + +use derive_builder::*; +use derive_more::*; +use serde::*; +use tera::Context; +use url::Url; + +pub use super::*; +use crate::utils::*; + +pub const EMPLOYEE_REGISTER_PAGE: TemplateFile = TemplateFile::new( + "identity.employee_register", + "identity/employee_register.html", +); + +pub fn register_templates(t: &mut tera::Tera) { + EMPLOYEE_REGISTER_PAGE + .register(t) + .expect(EMPLOYEE_REGISTER_PAGE.name); +} + +pub struct EmployeeRegisterPage { + ctx: RefCell, +} + +impl EmployeeRegisterPage { + pub fn new() -> Self { + let mut ctx = context(); + + //ctx.insert(PAYLOAD_KEY, p); + + let ctx = RefCell::new(ctx); + Self { ctx } + } + + pub fn render(&self) -> String { + TEMPLATES + .render(EMPLOYEE_REGISTER_PAGE.name, &self.ctx.borrow()) + .unwrap() + } + + pub fn page() -> String { + let p = Self::new(); + p.render() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn render_page_and_write() { + const FILE: &str = "./tmp/identity-employee_register.html"; + + let tw = TestWriterBuilder::default() + .file(FILE.into()) + .contents(EmployeeRegisterPage::page()) + .build() + .unwrap(); + tw.write(); + } +} diff --git a/web_ui/src/identity/mod.rs b/web_ui/src/identity/mod.rs index 411f72e..fd3f0df 100644 --- a/web_ui/src/identity/mod.rs +++ b/web_ui/src/identity/mod.rs @@ -2,13 +2,31 @@ // // SPDX-License-Identifier: AGPL-3.0-or-later +pub mod employee_exit_organization; +pub mod employee_login; +pub mod employee_register; +pub mod owner_add_store; +pub mod owner_change_password; +pub mod owner_delete_user; +pub mod owner_login; +pub mod owner_register; pub mod owner_update_email; +pub mod owner_update_store; pub mod owner_verify_email; pub fn register_templates(t: &mut tera::Tera) { for template in [ owner_update_email::ONWER_UPDATE_EMAIL_PAGE, owner_verify_email::ONWER_VERIFY_EMAIL_PAGE, + owner_delete_user::ONWER_DELETE_USER_PAGE, + owner_change_password::OWNER_CHANGE_PASSWORD_PAGE, + employee_register::EMPLOYEE_REGISTER_PAGE, + owner_update_store::OWNER_UPDATE_STORE_PAGE, + owner_login::OWNER_LOGIN_PAGE, + owner_register::OWNER_REGISTER_PAGE, + owner_add_store::OWNER_ADD_STORE_PAGE, + employee_exit_organization::EMPLOYEE_EXIT_ORGANIZATION_PAGE, + employee_login::EMPLOYEE_LOGIN_PAGE, ] .iter() { diff --git a/web_ui/src/identity/owner_add_store.rs b/web_ui/src/identity/owner_add_store.rs new file mode 100644 index 0000000..2597766 --- /dev/null +++ b/web_ui/src/identity/owner_add_store.rs @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later +use std::cell::RefCell; + +use derive_builder::*; +use derive_more::*; +use serde::*; +use tera::Context; +use url::Url; + +pub use super::*; +use crate::utils::*; + +pub const OWNER_ADD_STORE_PAGE: TemplateFile = + TemplateFile::new("identity.owner_add_store", "identity/owner_add_store.html"); + +pub fn register_templates(t: &mut tera::Tera) { + OWNER_ADD_STORE_PAGE + .register(t) + .expect(OWNER_ADD_STORE_PAGE.name); +} + +pub struct OwnerAddStorePage { + ctx: RefCell, +} + +impl OwnerAddStorePage { + pub fn new() -> Self { + let mut ctx = context(); + + //ctx.insert(PAYLOAD_KEY, p); + + let ctx = RefCell::new(ctx); + Self { ctx } + } + + pub fn render(&self) -> String { + TEMPLATES + .render(OWNER_ADD_STORE_PAGE.name, &self.ctx.borrow()) + .unwrap() + } + + pub fn page() -> String { + let p = Self::new(); + p.render() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn render_page_and_write() { + const FILE: &str = "./tmp/identity-owner_add_store.html"; + + let tw = TestWriterBuilder::default() + .file(FILE.into()) + .contents(OwnerAddStorePage::page()) + .build() + .unwrap(); + tw.write(); + } +} diff --git a/web_ui/src/identity/owner_change_password.rs b/web_ui/src/identity/owner_change_password.rs new file mode 100644 index 0000000..3576dc3 --- /dev/null +++ b/web_ui/src/identity/owner_change_password.rs @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later +use std::cell::RefCell; + +use derive_builder::*; +use derive_more::*; +use serde::*; +use tera::Context; +use url::Url; + +pub use super::*; +use crate::utils::*; + +pub const OWNER_CHANGE_PASSWORD_PAGE: TemplateFile = TemplateFile::new( + "identity.owner_change_password", + "identity/owner_change_password.html", +); + +pub fn register_templates(t: &mut tera::Tera) { + OWNER_CHANGE_PASSWORD_PAGE + .register(t) + .expect(OWNER_CHANGE_PASSWORD_PAGE.name); +} + +pub struct OwnerChangePasswordPage { + ctx: RefCell, +} + +impl OwnerChangePasswordPage { + pub fn new() -> Self { + let mut ctx = context(); + + //ctx.insert(PAYLOAD_KEY, p); + + let ctx = RefCell::new(ctx); + Self { ctx } + } + + pub fn render(&self) -> String { + TEMPLATES + .render(OWNER_CHANGE_PASSWORD_PAGE.name, &self.ctx.borrow()) + .unwrap() + } + + pub fn page() -> String { + let p = Self::new(); + p.render() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn render_page_and_write() { + const FILE: &str = "./tmp/identity-owner_change_password.html"; + + let tw = TestWriterBuilder::default() + .file(FILE.into()) + .contents(OwnerChangePasswordPage::page()) + .build() + .unwrap(); + tw.write(); + } +} diff --git a/web_ui/src/identity/owner_delete_user.rs b/web_ui/src/identity/owner_delete_user.rs new file mode 100644 index 0000000..93d1025 --- /dev/null +++ b/web_ui/src/identity/owner_delete_user.rs @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later +use std::cell::RefCell; + +use derive_builder::*; +use derive_more::*; +use serde::*; +use tera::Context; +use url::Url; + +pub use super::*; +use crate::utils::*; + +pub const ONWER_DELETE_USER_PAGE: TemplateFile = TemplateFile::new( + "identity.owner_delete_user", + "identity/owner_delete_user.html", +); + +pub fn register_templates(t: &mut tera::Tera) { + ONWER_DELETE_USER_PAGE + .register(t) + .expect(ONWER_DELETE_USER_PAGE.name); +} + +pub struct OwnerDeleteUserPage { + ctx: RefCell, +} + +impl OwnerDeleteUserPage { + pub fn new() -> Self { + let mut ctx = context(); + + //ctx.insert(PAYLOAD_KEY, p); + + let ctx = RefCell::new(ctx); + Self { ctx } + } + + pub fn render(&self) -> String { + TEMPLATES + .render(ONWER_DELETE_USER_PAGE.name, &self.ctx.borrow()) + .unwrap() + } + + pub fn page() -> String { + let p = Self::new(); + p.render() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn render_page_and_write() { + const FILE: &str = "./tmp/identity-owner_delete_user.html"; + + let tw = TestWriterBuilder::default() + .file(FILE.into()) + .contents(OwnerDeleteUserPage::page()) + .build() + .unwrap(); + tw.write(); + } +} diff --git a/web_ui/src/identity/owner_login.rs b/web_ui/src/identity/owner_login.rs new file mode 100644 index 0000000..7c9f5cb --- /dev/null +++ b/web_ui/src/identity/owner_login.rs @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later +use std::cell::RefCell; + +use derive_builder::*; +use derive_more::*; +use serde::*; +use tera::Context; +use url::Url; + +pub use super::*; +use crate::utils::*; + +pub const OWNER_LOGIN_PAGE: TemplateFile = + TemplateFile::new("identity.owner_login", "identity/owner_login.html"); + +pub fn register_templates(t: &mut tera::Tera) { + OWNER_LOGIN_PAGE.register(t).expect(OWNER_LOGIN_PAGE.name); +} + +pub struct OwnerLoginPage { + ctx: RefCell, +} + +impl OwnerLoginPage { + pub fn new() -> Self { + let mut ctx = context(); + + //ctx.insert(PAYLOAD_KEY, p); + + let ctx = RefCell::new(ctx); + Self { ctx } + } + + pub fn render(&self) -> String { + TEMPLATES + .render(OWNER_LOGIN_PAGE.name, &self.ctx.borrow()) + .unwrap() + } + + pub fn page() -> String { + let p = Self::new(); + p.render() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn render_page_and_write() { + const FILE: &str = "./tmp/identity-owner_login.html"; + + let tw = TestWriterBuilder::default() + .file(FILE.into()) + .contents(OwnerLoginPage::page()) + .build() + .unwrap(); + tw.write(); + } +} diff --git a/web_ui/src/identity/owner_register.rs b/web_ui/src/identity/owner_register.rs new file mode 100644 index 0000000..2b0ed2b --- /dev/null +++ b/web_ui/src/identity/owner_register.rs @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later +use std::cell::RefCell; + +use derive_builder::*; +use derive_more::*; +use serde::*; +use tera::Context; +use url::Url; + +pub use super::*; +use crate::utils::*; + +pub const OWNER_REGISTER_PAGE: TemplateFile = + TemplateFile::new("identity.owner_register", "identity/owner_register.html"); + +pub fn register_templates(t: &mut tera::Tera) { + OWNER_REGISTER_PAGE + .register(t) + .expect(OWNER_REGISTER_PAGE.name); +} + +pub struct OwnerRegisterPage { + ctx: RefCell, +} + +impl OwnerRegisterPage { + pub fn new() -> Self { + let mut ctx = context(); + + //ctx.insert(PAYLOAD_KEY, p); + + let ctx = RefCell::new(ctx); + Self { ctx } + } + + pub fn render(&self) -> String { + TEMPLATES + .render(OWNER_REGISTER_PAGE.name, &self.ctx.borrow()) + .unwrap() + } + + pub fn page() -> String { + let p = Self::new(); + p.render() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn render_page_and_write() { + const FILE: &str = "./tmp/identity-owner_register.html"; + + let tw = TestWriterBuilder::default() + .file(FILE.into()) + .contents(OwnerRegisterPage::page()) + .build() + .unwrap(); + tw.write(); + } +} diff --git a/web_ui/src/identity/owner_update_store.rs b/web_ui/src/identity/owner_update_store.rs new file mode 100644 index 0000000..b1acb1f --- /dev/null +++ b/web_ui/src/identity/owner_update_store.rs @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later +use std::cell::RefCell; + +use derive_builder::*; +use derive_more::*; +use serde::*; +use tera::Context; +use url::Url; + +pub use super::*; +use crate::utils::*; + +pub const OWNER_UPDATE_STORE_PAGE: TemplateFile = TemplateFile::new( + "identity.owner_update_store", + "identity/owner_update_store.html", +); + +pub fn register_templates(t: &mut tera::Tera) { + OWNER_UPDATE_STORE_PAGE + .register(t) + .expect(OWNER_UPDATE_STORE_PAGE.name); +} + +pub struct OwnerUpdateStorePage { + ctx: RefCell, +} + +impl OwnerUpdateStorePage { + pub fn new() -> Self { + let mut ctx = context(); + + //ctx.insert(PAYLOAD_KEY, p); + + let ctx = RefCell::new(ctx); + Self { ctx } + } + + pub fn render(&self) -> String { + TEMPLATES + .render(OWNER_UPDATE_STORE_PAGE.name, &self.ctx.borrow()) + .unwrap() + } + + pub fn page() -> String { + let p = Self::new(); + p.render() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn render_page_and_write() { + const FILE: &str = "./tmp/identity-owner_update_store.html"; + + let tw = TestWriterBuilder::default() + .file(FILE.into()) + .contents(OwnerUpdateStorePage::page()) + .build() + .unwrap(); + tw.write(); + } +} diff --git a/web_ui/src/utils.rs b/web_ui/src/utils.rs index f6674b6..a2412d5 100644 --- a/web_ui/src/utils.rs +++ b/web_ui/src/utils.rs @@ -117,7 +117,6 @@ impl<'a> Footer<'a> { } } - #[cfg(test)] pub mod tests { use super::*; From f62fb85fe6745b13f7f2eca8f47ae8491cf44e99 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Fri, 10 Jan 2025 16:20:20 +0530 Subject: [PATCH 37/63] feat: generate template page codegen script --- utils/generate_template_page.sh | 100 ++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100755 utils/generate_template_page.sh diff --git a/utils/generate_template_page.sh b/utils/generate_template_page.sh new file mode 100755 index 0000000..e52b764 --- /dev/null +++ b/utils/generate_template_page.sh @@ -0,0 +1,100 @@ +#!/bin/bash + +help() { + echo "Usage: web_test_generator.sh + + + + " +} + + + run() { + echo " +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later +use std::cell::RefCell; + +use derive_builder::*; +use derive_more::*; +use serde::*; +use tera::Context; +use url::Url; + +pub use super::*; +use crate::utils::*; + +pub const $2_PAGE: TemplateFile = TemplateFile::new( + \"$1.$3\", + \"$1/$3.html\", +); + +pub fn register_templates(t: &mut tera::Tera) { + $2_PAGE + .register(t) + .expect($2_PAGE.name); +} + +pub struct $4Page { + ctx: RefCell, +} + +impl $4Page { + pub fn new() -> Self { + let mut ctx = context(); + + //ctx.insert(PAYLOAD_KEY, p); + + let ctx = RefCell::new(ctx); + Self { ctx } + } + + pub fn render(&self) -> String { + TEMPLATES + .render($2_PAGE.name, &self.ctx.borrow()) + .unwrap() + } + + pub fn page() -> String { + let p = Self::new(); + p.render() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn render_page_and_write() { + const FILE: &str = \"./tmp/$1-$3.html\"; + + let tw = TestWriterBuilder::default() + .file(FILE.into()) + .contents($4Page::page()) + .build() + .unwrap(); + tw.write(); + } +} " +} + + + +if [ -z $1 ] +then + help +elif [ -z $2 ] +then + help +elif [ -z $3 ] +then + help +elif [ -z $4 ] +then + help +else + run $1 $2 $3 $4 | wl-copy + run $1 $2 $3 $4 +fi From 1f308c927073351a813ad2659d39b7a05109ce6d Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Fri, 10 Jan 2025 16:20:32 +0530 Subject: [PATCH 38/63] feat: use tera templates --- src/identity/adapters/input/web/employee.rs | 15 ++++--- .../input/web/employee_exit_organization.html | 14 ------ .../adapters/input/web/employee_login.html | 27 ------------ .../adapters/input/web/employee_register.html | 37 ---------------- src/identity/adapters/input/web/owner.rs | 42 ++++++++++++------ .../adapters/input/web/owner_add_store.html | 25 ----------- .../input/web/owner_change_password.html | 31 ------------- .../adapters/input/web/owner_delete_user.html | 20 --------- .../adapters/input/web/owner_login.html | 27 ------------ .../adapters/input/web/owner_register.html | 43 ------------------- .../input/web/owner_update_email.html | 25 ----------- .../input/web/owner_update_store.html | 25 ----------- .../input/web/owner_verify_email.html | 14 ------ 13 files changed, 37 insertions(+), 308 deletions(-) delete mode 100644 src/identity/adapters/input/web/employee_exit_organization.html delete mode 100644 src/identity/adapters/input/web/employee_login.html delete mode 100644 src/identity/adapters/input/web/employee_register.html delete mode 100644 src/identity/adapters/input/web/owner_add_store.html delete mode 100644 src/identity/adapters/input/web/owner_change_password.html delete mode 100644 src/identity/adapters/input/web/owner_delete_user.html delete mode 100644 src/identity/adapters/input/web/owner_login.html delete mode 100644 src/identity/adapters/input/web/owner_register.html delete mode 100644 src/identity/adapters/input/web/owner_update_email.html delete mode 100644 src/identity/adapters/input/web/owner_update_store.html delete mode 100644 src/identity/adapters/input/web/owner_verify_email.html diff --git a/src/identity/adapters/input/web/employee.rs b/src/identity/adapters/input/web/employee.rs index 3b78dcb..0278d45 100644 --- a/src/identity/adapters/input/web/employee.rs +++ b/src/identity/adapters/input/web/employee.rs @@ -32,11 +32,12 @@ pub fn services(cfg: &mut web::ServiceConfig) { #[get("/employee/login")] #[tracing::instrument(name = "login UI handler", skip())] async fn login_ui_handler() -> WebJsonRepsonse { - const LOGIN_PAGE: &str = include_str!("./employee_login.html"); + use web_ui::identity::employee_login::*; + let page = EmployeeLoginPage::page(); Ok(HttpResponse::Ok() .insert_header(ContentType::html()) - .body(LOGIN_PAGE)) + .body(page)) } #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)] @@ -84,11 +85,12 @@ async fn resend_login_otp_handler( #[get("/employee/register")] #[tracing::instrument(name = "register UI handler", skip())] async fn register_ui_handler() -> WebJsonRepsonse { - const REGISTER_PAGE: &str = include_str!("./employee_register.html"); + use web_ui::identity::employee_register::*; + let page = EmployeeRegisterPage::page(); Ok(HttpResponse::Ok() .insert_header(ContentType::html()) - .body(REGISTER_PAGE)) + .body(page)) } #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)] @@ -136,11 +138,12 @@ async fn resend_verification_otp_handler( #[get("/employee/organization/exit")] #[tracing::instrument(name = "Exit organizationUI handler", skip())] async fn exit_organization_ui_handler() -> WebJsonRepsonse { - const EXIT_ORGANIZATION: &str = include_str!("./employee_exit_organization.html"); + use web_ui::identity::employee_exit_organization::*; + let page = EmployeeExitOrganizationPage::page(); Ok(HttpResponse::Ok() .insert_header(ContentType::html()) - .body(EXIT_ORGANIZATION)) + .body(page)) } #[allow(clippy::too_many_arguments)] diff --git a/src/identity/adapters/input/web/employee_exit_organization.html b/src/identity/adapters/input/web/employee_exit_organization.html deleted file mode 100644 index 801785d..0000000 --- a/src/identity/adapters/input/web/employee_exit_organization.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - Exit organization | Vanikam - - - -
- -
- - diff --git a/src/identity/adapters/input/web/employee_login.html b/src/identity/adapters/input/web/employee_login.html deleted file mode 100644 index 56a8ac9..0000000 --- a/src/identity/adapters/input/web/employee_login.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - Login | Vanikam - - - -
- - - - - -
- -

New here? Click here to register!

- - - diff --git a/src/identity/adapters/input/web/employee_register.html b/src/identity/adapters/input/web/employee_register.html deleted file mode 100644 index 5184221..0000000 --- a/src/identity/adapters/input/web/employee_register.html +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - Register | Vanikam - - - -
- - - - - - - - - -
- -

Already have an account? Click here to log in!

- - - diff --git a/src/identity/adapters/input/web/owner.rs b/src/identity/adapters/input/web/owner.rs index 23f362f..30ca456 100644 --- a/src/identity/adapters/input/web/owner.rs +++ b/src/identity/adapters/input/web/owner.rs @@ -46,11 +46,13 @@ pub fn services(cfg: &mut web::ServiceConfig) { #[get("/owner/login")] #[tracing::instrument(name = "login UI handler", skip())] async fn login_ui_handler() -> WebJsonRepsonse { - const LOGIN_PAGE: &str = include_str!("./owner_login.html"); + use web_ui::identity::owner_login::*; + + let page = OwnerLoginPage::page(); Ok(HttpResponse::Ok() .insert_header(ContentType::html()) - .body(LOGIN_PAGE)) + .body(page)) } #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)] @@ -82,11 +84,13 @@ async fn login_form_submission_handler( #[get("/owner/register")] #[tracing::instrument(name = "register UI handler", skip())] async fn register_ui_handler() -> WebJsonRepsonse { - const REGISTER_PAGE: &str = include_str!("./owner_register.html"); + use web_ui::identity::owner_register::*; + + let page = OwnerRegisterPage::page(); Ok(HttpResponse::Ok() .insert_header(ContentType::html()) - .body(REGISTER_PAGE)) + .body(page)) } #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)] @@ -159,11 +163,13 @@ async fn update_email_form_submission_handler( #[get("/owner/user/password/change")] #[tracing::instrument(name = "Change password UI handler", skip())] async fn change_password_ui_handler() -> WebJsonRepsonse { - const CHANGE_PASSWORD: &str = include_str!("./owner_change_password.html"); + use web_ui::identity::owner_change_password::*; + + let page = OwnerChangePasswordPage::page(); Ok(HttpResponse::Ok() .insert_header(ContentType::html()) - .body(CHANGE_PASSWORD)) + .body(page)) } #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)] @@ -196,11 +202,13 @@ async fn change_password_form_submission_handler( #[get("/owner/user/delete")] #[tracing::instrument(name = "Delete user UI handler", skip())] async fn delete_user_ui_handler() -> WebJsonRepsonse { - const DELETE_USER: &str = include_str!("./owner_delete_user.html"); + use web_ui::identity::owner_delete_user::*; + + let page = OwnerDeleteUserPage::page(); Ok(HttpResponse::Ok() .insert_header(ContentType::html()) - .body(DELETE_USER)) + .body(page)) } #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)] @@ -231,11 +239,13 @@ async fn delete_user_form_submission_handler( #[get("/owner/user/email/verify")] #[tracing::instrument(name = "Verify email UI handler", skip())] async fn verify_email_ui_handler() -> WebJsonRepsonse { - const VERIFY_EMAIL: &str = include_str!("./owner_verify_email.html"); + use web_ui::identity::owner_verify_email::*; + + let page = OwnerVerifyEmailPage::page(); Ok(HttpResponse::Ok() .insert_header(ContentType::html()) - .body(VERIFY_EMAIL)) + .body(page)) } #[allow(clippy::too_many_arguments)] @@ -276,11 +286,13 @@ async fn resend_verification_email( #[get("/owner/store")] #[tracing::instrument(name = "Add store UI handler", skip())] async fn add_store_ui_handler() -> WebJsonRepsonse { - const ADD_STORE: &str = include_str!("./owner_add_store.html"); + use web_ui::identity::owner_add_store::*; + + let page = OwnerAddStorePage::page(); Ok(HttpResponse::Ok() .insert_header(ContentType::html()) - .body(ADD_STORE)) + .body(page)) } #[derive(Clone, Builder, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd)] @@ -312,11 +324,13 @@ async fn add_store_form_submission_handler( #[get("/owner/store/update")] #[tracing::instrument(name = "Add store UI handler", skip())] async fn update_store_ui_handler() -> WebJsonRepsonse { - const ADD_STORE: &str = include_str!("./owner_add_store.html"); + use web_ui::identity::owner_update_store::*; + + let page = OwnerUpdateStorePage::page(); Ok(HttpResponse::Ok() .insert_header(ContentType::html()) - .body(ADD_STORE)) + .body(page)) } #[derive(Clone, Builder, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd)] diff --git a/src/identity/adapters/input/web/owner_add_store.html b/src/identity/adapters/input/web/owner_add_store.html deleted file mode 100644 index e2e77bf..0000000 --- a/src/identity/adapters/input/web/owner_add_store.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - Create Store | Vanikam - - - -
- - - - - -
- - - diff --git a/src/identity/adapters/input/web/owner_change_password.html b/src/identity/adapters/input/web/owner_change_password.html deleted file mode 100644 index 242f9a6..0000000 --- a/src/identity/adapters/input/web/owner_change_password.html +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - Change Password | Vanikam - - - -
- - - - - - - - -
- - - diff --git a/src/identity/adapters/input/web/owner_delete_user.html b/src/identity/adapters/input/web/owner_delete_user.html deleted file mode 100644 index 13d6e67..0000000 --- a/src/identity/adapters/input/web/owner_delete_user.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - Delete account | Vanikam - - - -
- - - - -
- - diff --git a/src/identity/adapters/input/web/owner_login.html b/src/identity/adapters/input/web/owner_login.html deleted file mode 100644 index 095f9a7..0000000 --- a/src/identity/adapters/input/web/owner_login.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - Login | Vanikam - - - -
- - - - - -
- -

New here? Click here to register!

- - - diff --git a/src/identity/adapters/input/web/owner_register.html b/src/identity/adapters/input/web/owner_register.html deleted file mode 100644 index 94b5a43..0000000 --- a/src/identity/adapters/input/web/owner_register.html +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - Register | Vanikam - - - -
- - - - - - - - - - - - -
- -

Already have an account? Click here to log in!

- - - diff --git a/src/identity/adapters/input/web/owner_update_email.html b/src/identity/adapters/input/web/owner_update_email.html deleted file mode 100644 index c205d3c..0000000 --- a/src/identity/adapters/input/web/owner_update_email.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - Update Email | Vanikam - - - -
- - - - - -
- - - diff --git a/src/identity/adapters/input/web/owner_update_store.html b/src/identity/adapters/input/web/owner_update_store.html deleted file mode 100644 index 9c58a96..0000000 --- a/src/identity/adapters/input/web/owner_update_store.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - Update Store | Vanikam - - - -
- - - - - -
- - - diff --git a/src/identity/adapters/input/web/owner_verify_email.html b/src/identity/adapters/input/web/owner_verify_email.html deleted file mode 100644 index 3453545..0000000 --- a/src/identity/adapters/input/web/owner_verify_email.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - Verify email | Vanikam - - - -
- -
- - From ce37a68ac5e512c3b7ccc9100988bbf4a9a577d1 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Fri, 10 Jan 2025 16:22:50 +0530 Subject: [PATCH 39/63] feat: test twilio_client and web templates in makefile test cmd --- Makefile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Makefile b/Makefile index 619c1bd..d998763 100644 --- a/Makefile +++ b/Makefile @@ -96,6 +96,10 @@ test: ## Run tests --all-features \ --no-fail-fast -- --skip test_server_env_override \ --skip test_meili_env_override + cd ./twilio_client && cargo test --all-features \ + --no-fail-fast + cd ./web_ui && cargo test --all-features \ + --no-fail-fast cargo test --all-features \ --no-fail-fast -- test_server_env_override test_meili_env_override From 5f5903e31bcab5125b6d1e13a9b61558ac949908 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Fri, 10 Jan 2025 18:55:24 +0530 Subject: [PATCH 40/63] feat: codegen script to init cqrs framework --- utils/adapter_cqrs_init.sh | 44 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100755 utils/adapter_cqrs_init.sh diff --git a/utils/adapter_cqrs_init.sh b/utils/adapter_cqrs_init.sh new file mode 100755 index 0000000..5410d98 --- /dev/null +++ b/utils/adapter_cqrs_init.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +help() { + echo "Usage: adapter_type_gen.sh + + " +} + +run() { + for i in "${@:2}" + do + echo "let (${i}_cqrs_exec, ${i}_cqrs_query) = ${i}_view::init_cqrs(db.clone(), services.clone());" + done + + echo "let ${1,,}_cqrs_exec = types::Web$1CqrsExec::new(Arc::new(types::$1CqrsExecBuilder::default()" + for i in "${@:2}" + do + echo ".$i(${i}_cqrs_exec)" + done + echo ".build().unwrap(),));" + + echo " + let f = move |cfg: &mut web::ServiceConfig| { + cfg.configure(input::web::load_ctx());" + for i in "${@:2}" + do + echo "cfg.app_data(Data::new(${i}_cqrs_query.clone()));" + done + + echo "cfg.app_data(${1,,}_cqrs_exec.clone()); + }; + + Box::new(f)" + +} + + +if [ -z $1 ] +then + help +else + run "${@}" + run "${@}" | wl-copy +fi From c29645140115eedc8ddbb5c577bc7acc319022d0 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Fri, 10 Jan 2025 18:55:48 +0530 Subject: [PATCH 41/63] feat: codegen script to geenrate type aliases in adapters --- utils/adapter_type_gen.sh | 59 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100755 utils/adapter_type_gen.sh diff --git a/utils/adapter_type_gen.sh b/utils/adapter_type_gen.sh new file mode 100755 index 0000000..06c946b --- /dev/null +++ b/utils/adapter_type_gen.sh @@ -0,0 +1,59 @@ +#!/bin/bash + +help() { + echo "Usage: adapter_type_gen.sh + + " +} + + + run() { + echo " + +pub type $1$2CqrsExec = Arc>; +pub type $1$2CqrsView = Arc>; +pub type Web$1$2CqrsView = Data<$1$2CqrsView>; + +// struct + +${2,,}: $1$2CqrsExec, + +// trait impl + +self.${2,,}.execute(aggregate_id, command.clone()).await?; + + +// $2_view.rs DB stuff + +use crate::${1,,}::adapters::types::{$1$2CqrsExec, $1$2CqrsView}; +use crate::${1,,}::application::services::$1ServicesObj; + +pub fn init_cqrs( + db: $1DBPostgresAdapter, + services: $1ServicesObj, +) -> ($1$2CqrsExec, $1$2CqrsView) { + let queries: Vec>> = vec![Box::new(db.clone())]; + + let pool = db.pool.clone(); + + ( + std::sync::Arc::new(postgres_es::postgres_cqrs(pool.clone(), queries, services)), + std::sync::Arc::new(db.clone()), + ) +} +" + +} + + + +if [ -z $1 ] +then + help +elif [ -z $2 ] +then + help +else + run $1 $2 $3 $4 | wl-copy + run $1 $2 $3 $4 +fi From 5dcae64a04af97536fdaffedf0976e611479bc28 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Fri, 10 Jan 2025 18:56:23 +0530 Subject: [PATCH 42/63] feat: inventory: init cqrs framework for aggregates --- .../output/db/postgres/category_view.rs | 16 +++++++++++++ .../output/db/postgres/customization_view.rs | 23 ++++++++++++++++++- .../adapters/output/db/postgres/mod.rs | 8 +++---- .../output/db/postgres/product_view.rs | 16 +++++++++++++ .../adapters/output/db/postgres/store_view.rs | 16 +++++++++++++ 5 files changed, 74 insertions(+), 5 deletions(-) diff --git a/src/inventory/adapters/output/db/postgres/category_view.rs b/src/inventory/adapters/output/db/postgres/category_view.rs index ca05a38..fe1d304 100644 --- a/src/inventory/adapters/output/db/postgres/category_view.rs +++ b/src/inventory/adapters/output/db/postgres/category_view.rs @@ -10,6 +10,8 @@ use uuid::Uuid; use super::errors::*; use super::InventoryDBPostgresAdapter; +use crate::inventory::adapters::types::{InventoryCategoryCqrsExec, InventoryCategoryCqrsView}; +use crate::inventory::application::services::InventoryServicesObj; use crate::inventory::domain::category_aggregate::{Category, CategoryBuilder}; use crate::inventory::domain::events::InventoryEvent; use crate::utils::parse_aggregate_id::parse_aggregate_id; @@ -229,6 +231,20 @@ impl Query for InventoryDBPostgresAdapter { } } +pub fn init_cqrs( + db: InventoryDBPostgresAdapter, + services: InventoryServicesObj, +) -> (InventoryCategoryCqrsExec, InventoryCategoryCqrsView) { + let queries: Vec>> = vec![Box::new(db.clone())]; + + let pool = db.pool.clone(); + + ( + std::sync::Arc::new(postgres_es::postgres_cqrs(pool.clone(), queries, services)), + std::sync::Arc::new(db.clone()), + ) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/inventory/adapters/output/db/postgres/customization_view.rs b/src/inventory/adapters/output/db/postgres/customization_view.rs index 8ed83f2..d9d2705 100644 --- a/src/inventory/adapters/output/db/postgres/customization_view.rs +++ b/src/inventory/adapters/output/db/postgres/customization_view.rs @@ -12,6 +12,10 @@ use uuid::Uuid; use super::errors::*; use super::InventoryDBPostgresAdapter; +use crate::inventory::adapters::types::{ + InventoryCustomizationCqrsExec, InventoryCustomizationCqrsView, +}; +use crate::inventory::application::services::InventoryServicesObj; use crate::inventory::domain::{customization_aggregate::*, events::InventoryEvent}; use crate::utils::parse_aggregate_id::parse_aggregate_id; @@ -23,7 +27,7 @@ pub const NEW_CUSTOMIZATION_NON_UUID: &str = "new_customization_non_uuid-asdfa"; //} #[derive(Debug, Default, Serialize, Deserialize)] -struct CustomizationView { +pub struct CustomizationView { name: String, product_id: Uuid, customization_id: Uuid, @@ -224,6 +228,23 @@ impl Query for InventoryDBPostgresAdapter { } } +pub fn init_cqrs( + db: InventoryDBPostgresAdapter, + services: InventoryServicesObj, +) -> ( + InventoryCustomizationCqrsExec, + InventoryCustomizationCqrsView, +) { + let queries: Vec>> = vec![Box::new(db.clone())]; + + let pool = db.pool.clone(); + + ( + std::sync::Arc::new(postgres_es::postgres_cqrs(pool.clone(), queries, services)), + std::sync::Arc::new(db.clone()), + ) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/inventory/adapters/output/db/postgres/mod.rs b/src/inventory/adapters/output/db/postgres/mod.rs index 647268c..c0265b4 100644 --- a/src/inventory/adapters/output/db/postgres/mod.rs +++ b/src/inventory/adapters/output/db/postgres/mod.rs @@ -10,18 +10,18 @@ use crate::db::{migrate::RunMigrations, sqlx_postgres::Postgres}; mod category_id_exists; mod category_name_exists_for_store; -mod category_view; +pub mod category_view; mod customization_id_exists; mod customization_name_exists_for_product; -mod customization_view; +pub mod customization_view; mod errors; mod get_category; mod product_id_exists; mod product_name_exists_for_category; -mod product_view; +pub mod product_view; mod store_id_exists; mod store_name_exists; -mod store_view; +pub mod store_view; #[derive(Clone)] pub struct InventoryDBPostgresAdapter { diff --git a/src/inventory/adapters/output/db/postgres/product_view.rs b/src/inventory/adapters/output/db/postgres/product_view.rs index d3e3836..ba9e4ce 100644 --- a/src/inventory/adapters/output/db/postgres/product_view.rs +++ b/src/inventory/adapters/output/db/postgres/product_view.rs @@ -12,6 +12,8 @@ use uuid::Uuid; use super::errors::*; use super::InventoryDBPostgresAdapter; +use crate::inventory::adapters::types::{InventoryProductCqrsExec, InventoryProductCqrsView}; +use crate::inventory::application::services::InventoryServicesObj; use crate::inventory::domain::events::InventoryEvent; use crate::inventory::domain::product_aggregate::{Product, ProductBuilder}; use crate::types::currency::*; @@ -348,6 +350,20 @@ impl Query for InventoryDBPostgresAdapter { } } +pub fn init_cqrs( + db: InventoryDBPostgresAdapter, + services: InventoryServicesObj, +) -> (InventoryProductCqrsExec, InventoryProductCqrsView) { + let queries: Vec>> = vec![Box::new(db.clone())]; + + let pool = db.pool.clone(); + + ( + std::sync::Arc::new(postgres_es::postgres_cqrs(pool.clone(), queries, services)), + std::sync::Arc::new(db.clone()), + ) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/inventory/adapters/output/db/postgres/store_view.rs b/src/inventory/adapters/output/db/postgres/store_view.rs index bf35792..77d2600 100644 --- a/src/inventory/adapters/output/db/postgres/store_view.rs +++ b/src/inventory/adapters/output/db/postgres/store_view.rs @@ -10,6 +10,8 @@ use uuid::Uuid; use super::errors::*; use super::InventoryDBPostgresAdapter; +use crate::inventory::adapters::types::{InventoryStoreCqrsExec, InventoryStoreCqrsView}; +use crate::inventory::application::services::InventoryServicesObj; use crate::inventory::domain::events::InventoryEvent; use crate::inventory::domain::store_aggregate::{Store, StoreBuilder}; use crate::utils::parse_aggregate_id::parse_aggregate_id; @@ -225,6 +227,20 @@ impl Query for InventoryDBPostgresAdapter { } } +pub fn init_cqrs( + db: InventoryDBPostgresAdapter, + services: InventoryServicesObj, +) -> (InventoryStoreCqrsExec, InventoryStoreCqrsView) { + let queries: Vec>> = vec![Box::new(db.clone())]; + + let pool = db.pool.clone(); + + ( + std::sync::Arc::new(postgres_es::postgres_cqrs(pool.clone(), queries, services)), + std::sync::Arc::new(db.clone()), + ) +} + #[cfg(test)] mod tests { use super::*; From 4974f388d7023c6e6e363f8fbd81e3d91ba21532 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Fri, 10 Jan 2025 18:56:43 +0530 Subject: [PATCH 43/63] fix: export fts adapters --- src/inventory/adapters/output/full_text_search/mod.rs | 2 +- src/inventory/adapters/output/mod.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/inventory/adapters/output/full_text_search/mod.rs b/src/inventory/adapters/output/full_text_search/mod.rs index f7b764c..ecc2220 100644 --- a/src/inventory/adapters/output/full_text_search/mod.rs +++ b/src/inventory/adapters/output/full_text_search/mod.rs @@ -2,4 +2,4 @@ // // SPDX-License-Identifier: AGPL-3.0-or-later -mod meili; +pub mod meili; diff --git a/src/inventory/adapters/output/mod.rs b/src/inventory/adapters/output/mod.rs index d965892..8a85158 100644 --- a/src/inventory/adapters/output/mod.rs +++ b/src/inventory/adapters/output/mod.rs @@ -2,5 +2,5 @@ // // SPDX-License-Identifier: AGPL-3.0-or-later -mod db; -mod full_text_search; +pub mod db; +pub mod full_text_search; From 532c6410e15e0ef5ec44519d5af2e4d5a5971efd Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Fri, 10 Jan 2025 18:57:02 +0530 Subject: [PATCH 44/63] feat: define inventory adapter type aliases --- src/inventory/adapters/types.rs | 85 +++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 src/inventory/adapters/types.rs diff --git a/src/inventory/adapters/types.rs b/src/inventory/adapters/types.rs new file mode 100644 index 0000000..1463fcf --- /dev/null +++ b/src/inventory/adapters/types.rs @@ -0,0 +1,85 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later +#![allow(dead_code)] + +use std::sync::Arc; + +use actix_web::web::Data; +use async_trait::async_trait; +use cqrs_es::{persist::ViewRepository, AggregateError}; +use derive_builder::Builder; +use mockall::predicate::*; +use mockall::*; +use postgres_es::PostgresCqrs; + +use crate::inventory::{ + adapters::{ + // input::web::RoutesRepository, + output::db::postgres::{ + category_view::CategoryView, customization_view::CustomizationView, + product_view::ProductView, store_view::StoreView, InventoryDBPostgresAdapter, + }, + }, + application::services::{errors::InventoryError, InventoryServicesObj}, + domain::{ + category_aggregate::Category, commands::InventoryCommand, + customization_aggregate::Customization, product_aggregate::Product, store_aggregate::Store, + }, +}; + +//pub type WebInventoryRoutesRepository = Data>; + +pub type WebInventoryCqrsExec = Data>; + +pub type InventoryCustomizationCqrsExec = Arc>; +pub type InventoryCustomizationCqrsView = Arc>; +pub type WebInventoryCustomizationCqrsView = Data; + +pub type InventoryCategoryCqrsExec = Arc>; +pub type InventoryCategoryCqrsView = Arc>; +pub type WebInventoryCategoryCqrsView = Data; + +pub type InventoryStoreCqrsExec = Arc>; +pub type InventoryStoreCqrsView = Arc>; +pub type WebInventoryStoreCqrsView = Data; + +pub type InventoryProductCqrsExec = Arc>; +pub type InventoryProductCqrsView = Arc>; +pub type WebInventoryProductCqrsView = Data; + +#[automock] +#[async_trait] +pub trait InventoryCqrsExecutor { + async fn execute( + &self, + aggregate_id: &str, + command: InventoryCommand, + ) -> Result<(), AggregateError>; +} + +#[derive(Clone, Builder)] +pub struct InventoryCqrsExec { + category: InventoryCategoryCqrsExec, + customization: InventoryCustomizationCqrsExec, + store: InventoryStoreCqrsExec, + product: InventoryProductCqrsExec, +} + +#[async_trait] +impl InventoryCqrsExecutor for InventoryCqrsExec { + async fn execute( + &self, + aggregate_id: &str, + command: InventoryCommand, + ) -> Result<(), AggregateError> { + self.category.execute(aggregate_id, command.clone()).await?; + self.customization + .execute(aggregate_id, command.clone()) + .await?; + self.store.execute(aggregate_id, command.clone()).await?; + self.product.execute(aggregate_id, command.clone()).await?; + + Ok(()) + } +} From 711f9abb6bdcaa94025f8abb196db5bef6d9139c Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Fri, 10 Jan 2025 18:57:12 +0530 Subject: [PATCH 45/63] feat: load inventory adapters --- src/inventory/adapters/mod.rs | 88 ++++++++++++++++++++++++++++++++++- src/inventory/mod.rs | 2 +- src/utils/load_adapters.rs | 4 ++ 3 files changed, 92 insertions(+), 2 deletions(-) diff --git a/src/inventory/adapters/mod.rs b/src/inventory/adapters/mod.rs index 9b25f58..622ecf3 100644 --- a/src/inventory/adapters/mod.rs +++ b/src/inventory/adapters/mod.rs @@ -2,5 +2,91 @@ // // SPDX-License-Identifier: AGPL-3.0-or-later +use std::sync::Arc; + +use actix_web::web::{self, Data}; +use cqrs_es::{persist::ViewRepository, EventEnvelope, Query, View}; +use postgres_es::PostgresCqrs; +use sqlx::postgres::PgPool; + +use crate::inventory::{ + application::services::{InventoryServices, InventoryServicesObj}, + domain::{ + category_aggregate::Category, customization_aggregate::Customization, + product_aggregate::Product, store_aggregate::Store, + }, +}; +use crate::settings::Settings; +use output::{ + db::postgres::{ + category_view, customization_view, product_view, store_view, InventoryDBPostgresAdapter, + }, + full_text_search::meili::InventoryFTSMeili, +}; + mod input; -mod output; +pub mod output; +mod types; + +pub fn load_adapters(pool: PgPool, settings: Settings) -> impl FnOnce(&mut web::ServiceConfig) { + let db = InventoryDBPostgresAdapter::new(pool.clone()); + let fts = InventoryFTSMeili::new(&settings.meili.url, &settings.meili.api_key); + + let services: InventoryServicesObj = InventoryServices::new( + Arc::new(db.clone()), + Arc::new(db.clone()), + Arc::new(db.clone()), + Arc::new(db.clone()), + Arc::new(db.clone()), + Arc::new(db.clone()), + Arc::new(db.clone()), + Arc::new(db.clone()), + Arc::new(db.clone()), + Arc::new(fts.clone()), + ); + + let (category_cqrs_exec, category_cqrs_query) = + category_view::init_cqrs(db.clone(), services.clone()); + let (product_cqrs_exec, product_cqrs_query) = + product_view::init_cqrs(db.clone(), services.clone()); + let (customization_cqrs_exec, customization_cqrs_query) = + customization_view::init_cqrs(db.clone(), services.clone()); + let (store_cqrs_exec, store_cqrs_query) = store_view::init_cqrs(db.clone(), services.clone()); + let inventory_cqrs_exec = types::WebInventoryCqrsExec::new(Arc::new( + types::InventoryCqrsExecBuilder::default() + .category(category_cqrs_exec) + .product(product_cqrs_exec) + .customization(customization_cqrs_exec) + .store(store_cqrs_exec) + .build() + .unwrap(), + )); + + let f = move |cfg: &mut web::ServiceConfig| { + // cfg.configure(input::web::load_ctx()); + cfg.app_data(Data::new(category_cqrs_query.clone())); + cfg.app_data(Data::new(product_cqrs_query.clone())); + cfg.app_data(Data::new(customization_cqrs_query.clone())); + cfg.app_data(Data::new(store_cqrs_query.clone())); + cfg.app_data(inventory_cqrs_exec.clone()); + }; + + Box::new(f) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::db::migrate::*; + + #[actix_rt::test] + async fn inventory_load_adapters() { + let settings = crate::settings::tests::get_settings().await; + settings.create_db().await; + + let db = crate::db::sqlx_postgres::Postgres::init(&settings.database.url).await; + db.migrate().await; + + load_adapters(db.pool.clone(), settings.clone()); + } +} diff --git a/src/inventory/mod.rs b/src/inventory/mod.rs index 7272406..e311feb 100644 --- a/src/inventory/mod.rs +++ b/src/inventory/mod.rs @@ -2,6 +2,6 @@ // // SPDX-License-Identifier: AGPL-3.0-or-later -mod adapters; +pub mod adapters; mod application; mod domain; diff --git a/src/utils/load_adapters.rs b/src/utils/load_adapters.rs index 1fb3bd1..6d3d6c4 100644 --- a/src/utils/load_adapters.rs +++ b/src/utils/load_adapters.rs @@ -16,6 +16,10 @@ pub fn load_adapters(pool: PgPool, settings: Settings) -> impl FnOnce(&mut web:: pool.clone(), settings.clone(), )) + .configure(crate::inventory::adapters::load_adapters( + pool.clone(), + settings.clone(), + )) .configure(super::random_string::GenerateRandomString::inject()) .configure(super::uuid::GenerateUUID::inject()); }; From 7fd781b03dc07e785401b3a05356b983814c8043 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Fri, 10 Jan 2025 19:09:05 +0530 Subject: [PATCH 46/63] fix: index name is domain scoped --- src/inventory/adapters/output/meili/add_product_to_store.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/inventory/adapters/output/meili/add_product_to_store.rs b/src/inventory/adapters/output/meili/add_product_to_store.rs index 00dc96b..a7884ba 100644 --- a/src/inventory/adapters/output/meili/add_product_to_store.rs +++ b/src/inventory/adapters/output/meili/add_product_to_store.rs @@ -47,7 +47,7 @@ impl AddProductToStoreFTSPort for InventoryFTSMeili { product: &Product, category: &Category, ) -> InventoryFTSResult<()> { - let store_index = self.client.index(category.store_id().to_string()); + let store_index = self.client.index(format!("inventory.{}",category.store_id())); let meili_product = MeiliProduct::new(product, category); store_index .add_documents(&[meili_product], Some("product_id")) From d8d026b057d71995897814f3fe57f0572fcc8d57 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Fri, 10 Jan 2025 19:22:35 +0530 Subject: [PATCH 47/63] fix: delete redundant meili adapter --- .../output/meili/add_product_to_store.rs | 98 ------------------- src/inventory/adapters/output/meili/mod.rs | 19 ---- 2 files changed, 117 deletions(-) delete mode 100644 src/inventory/adapters/output/meili/add_product_to_store.rs delete mode 100644 src/inventory/adapters/output/meili/mod.rs diff --git a/src/inventory/adapters/output/meili/add_product_to_store.rs b/src/inventory/adapters/output/meili/add_product_to_store.rs deleted file mode 100644 index a7884ba..0000000 --- a/src/inventory/adapters/output/meili/add_product_to_store.rs +++ /dev/null @@ -1,98 +0,0 @@ -// 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::InventoryFTSMeili; -use crate::inventory::application::port::output::full_text_search::{ - add_product_to_store::*, errors::*, -}; -use crate::inventory::domain::{category_aggregate::*, product_aggregate::*}; -use crate::types::currency::*; -//use super::errors::*; - -#[derive( - Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder, -)] -pub struct MeiliProduct { - name: String, - description: Option, - image: Option, // string = file_name - price: Price, - category_name: String, - product_id: Uuid, -} - -impl MeiliProduct { - pub fn new(product: &Product, category: &Category) -> Self { - Self { - name: product.name().into(), - description: product.description().clone(), - image: product.image().clone(), - price: product.price().clone(), - category_name: category.name().into(), - product_id: *product.product_id(), - } - } -} - -#[async_trait::async_trait] -impl AddProductToStoreFTSPort for InventoryFTSMeili { - async fn add_product_to_store( - &self, - product: &Product, - category: &Category, - ) -> InventoryFTSResult<()> { - let store_index = self.client.index(format!("inventory.{}",category.store_id())); - let meili_product = MeiliProduct::new(product, category); - store_index - .add_documents(&[meili_product], Some("product_id")) - .await - .unwrap(); - - Ok(()) - } -} - -#[cfg(test)] -mod tests { - - use uuid::Uuid; - - use crate::types::quantity::Quantity; - - use super::*; - - #[actix_rt::test] - async fn test_meili() { - let settings = crate::settings::tests::get_settings().await; - let fts = InventoryFTSMeili::new(&settings.meili.url, &settings.meili.api_key); - - let category = Category::default(); - let product = ProductBuilder::default() - .name("test_meili_product".into()) - .description(Some("this is a test product".into())) - .image(None) - .price( - PriceBuilder::default() - .major(100) - .minor(0) - .currency(Currency::INR) - .build() - .unwrap(), - ) - .quantity(Quantity::default()) - .sku_able(false) - .deleted(false) - .category_id(*category.category_id()) - .product_id(Uuid::new_v4()) - .build() - .unwrap(); - - fts.add_product_to_store(&product, &category).await.unwrap(); - } -} diff --git a/src/inventory/adapters/output/meili/mod.rs b/src/inventory/adapters/output/meili/mod.rs deleted file mode 100644 index 009fbcd..0000000 --- a/src/inventory/adapters/output/meili/mod.rs +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Aravinth Manivannan -// -// SPDX-License-Identifier: AGPL-3.0-or-later - -use meilisearch_sdk::client::*; - -mod add_product_to_store; - -#[derive(Clone)] -pub struct InventoryFTSMeili { - client: Client, -} - -impl InventoryFTSMeili { - pub fn new(meili_url: &str, api_key: &str) -> Self { - let client = Client::new(meili_url, Some(api_key)).unwrap(); - Self { client } - } -} From 85f50124f86f6c8994d1fce4b2f99991245b7680 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Fri, 10 Jan 2025 19:22:55 +0530 Subject: [PATCH 48/63] feat: use domain-scoped indices in meili --- .../output/full_text_search/meili/add_product_to_store.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/inventory/adapters/output/full_text_search/meili/add_product_to_store.rs b/src/inventory/adapters/output/full_text_search/meili/add_product_to_store.rs index 00dc96b..4f83945 100644 --- a/src/inventory/adapters/output/full_text_search/meili/add_product_to_store.rs +++ b/src/inventory/adapters/output/full_text_search/meili/add_product_to_store.rs @@ -47,7 +47,7 @@ impl AddProductToStoreFTSPort for InventoryFTSMeili { product: &Product, category: &Category, ) -> InventoryFTSResult<()> { - let store_index = self.client.index(category.store_id().to_string()); + let store_index = self.client.index(format!("inventory-{}",category.store_id())); let meili_product = MeiliProduct::new(product, category); store_index .add_documents(&[meili_product], Some("product_id")) From 501c6a49688fae05650a8cab1f44f839ea7618cd Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Fri, 10 Jan 2025 19:23:09 +0530 Subject: [PATCH 49/63] feat: impl add_product_to_store FTS adapter for ordering --- .../meili/add_product_to_store.rs | 98 +++++++++++++++++++ .../output/full_text_search/meili/mod.rs | 19 ++++ .../adapters/output/full_text_search/mod.rs | 5 + src/ordering/adapters/output/mod.rs | 1 + 4 files changed, 123 insertions(+) create mode 100644 src/ordering/adapters/output/full_text_search/meili/add_product_to_store.rs create mode 100644 src/ordering/adapters/output/full_text_search/meili/mod.rs create mode 100644 src/ordering/adapters/output/full_text_search/mod.rs diff --git a/src/ordering/adapters/output/full_text_search/meili/add_product_to_store.rs b/src/ordering/adapters/output/full_text_search/meili/add_product_to_store.rs new file mode 100644 index 0000000..2928eb0 --- /dev/null +++ b/src/ordering/adapters/output/full_text_search/meili/add_product_to_store.rs @@ -0,0 +1,98 @@ +// 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::OrderingFTSMeili; +use crate::ordering::application::port::output::full_text_search::{ + add_product_to_store::*, errors::*, +}; +use crate::ordering::domain::{category_aggregate::*, product_aggregate::*}; +use crate::types::currency::*; +//use super::errors::*; + +#[derive( + Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder, +)] +pub struct MeiliProduct { + name: String, + description: Option, + image: Option, // string = file_name + price: Price, + category_name: String, + product_id: Uuid, +} + +impl MeiliProduct { + pub fn new(product: &Product, category: &Category) -> Self { + Self { + name: product.name().into(), + description: product.description().clone(), + image: product.image().clone(), + price: product.price().clone(), + category_name: category.name().into(), + product_id: *product.product_id(), + } + } +} + +#[async_trait::async_trait] +impl AddProductToStoreFTSPort for OrderingFTSMeili { + async fn add_product_to_store( + &self, + product: &Product, + category: &Category, + ) -> OrderingFTSResult<()> { + let store_index = self.client.index(format!("ordering-{}",category.store_id())); + let meili_product = MeiliProduct::new(product, category); + store_index + .add_documents(&[meili_product], Some("product_id")) + .await + .unwrap(); + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + + use uuid::Uuid; + + use crate::types::quantity::Quantity; + + use super::*; + + #[actix_rt::test] + async fn test_meili() { + let settings = crate::settings::tests::get_settings().await; + let fts = OrderingFTSMeili::new(&settings.meili.url, &settings.meili.api_key); + + let category = Category::default(); + let product = ProductBuilder::default() + .name("test_meili_product".into()) + .description(Some("this is a test product".into())) + .image(None) + .price( + PriceBuilder::default() + .major(100) + .minor(0) + .currency(Currency::INR) + .build() + .unwrap(), + ) + .quantity(Quantity::default()) + .sku_able(false) + .deleted(false) + .category_id(*category.category_id()) + .product_id(Uuid::new_v4()) + .build() + .unwrap(); + + fts.add_product_to_store(&product, &category).await.unwrap(); + } +} diff --git a/src/ordering/adapters/output/full_text_search/meili/mod.rs b/src/ordering/adapters/output/full_text_search/meili/mod.rs new file mode 100644 index 0000000..dc75c99 --- /dev/null +++ b/src/ordering/adapters/output/full_text_search/meili/mod.rs @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use meilisearch_sdk::client::*; + +mod add_product_to_store; + +#[derive(Clone)] +pub struct OrderingFTSMeili { + client: Client, +} + +impl OrderingFTSMeili { + pub fn new(meili_url: &str, api_key: &str) -> Self { + let client = Client::new(meili_url, Some(api_key)).unwrap(); + Self { client } + } +} diff --git a/src/ordering/adapters/output/full_text_search/mod.rs b/src/ordering/adapters/output/full_text_search/mod.rs new file mode 100644 index 0000000..ecc2220 --- /dev/null +++ b/src/ordering/adapters/output/full_text_search/mod.rs @@ -0,0 +1,5 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +pub mod meili; diff --git a/src/ordering/adapters/output/mod.rs b/src/ordering/adapters/output/mod.rs index 1589173..8a85158 100644 --- a/src/ordering/adapters/output/mod.rs +++ b/src/ordering/adapters/output/mod.rs @@ -3,3 +3,4 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pub mod db; +pub mod full_text_search; From afda7a6ec80434933a620720d678be87494d4bff Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Fri, 10 Jan 2025 20:11:17 +0530 Subject: [PATCH 50/63] feat: utils to init cqrs framework --- .../meili/add_product_to_store.rs | 4 +++- .../adapters/output/db/category_view.rs | 16 +++++++++++++++ .../adapters/output/db/customization_view.rs | 20 ++++++++++++++++++- src/ordering/adapters/output/db/kot_view.rs | 16 +++++++++++++++ .../adapters/output/db/line_item_view.rs | 16 +++++++++++++++ src/ordering/adapters/output/db/mod.rs | 14 ++++++------- src/ordering/adapters/output/db/order_view.rs | 16 +++++++++++++++ .../adapters/output/db/product_view.rs | 16 +++++++++++++++ src/ordering/adapters/output/db/store_view.rs | 16 +++++++++++++++ .../meili/add_product_to_store.rs | 4 +++- 10 files changed, 128 insertions(+), 10 deletions(-) diff --git a/src/inventory/adapters/output/full_text_search/meili/add_product_to_store.rs b/src/inventory/adapters/output/full_text_search/meili/add_product_to_store.rs index 4f83945..e56034b 100644 --- a/src/inventory/adapters/output/full_text_search/meili/add_product_to_store.rs +++ b/src/inventory/adapters/output/full_text_search/meili/add_product_to_store.rs @@ -47,7 +47,9 @@ impl AddProductToStoreFTSPort for InventoryFTSMeili { product: &Product, category: &Category, ) -> InventoryFTSResult<()> { - let store_index = self.client.index(format!("inventory-{}",category.store_id())); + let store_index = self + .client + .index(format!("inventory-{}", category.store_id())); let meili_product = MeiliProduct::new(product, category); store_index .add_documents(&[meili_product], Some("product_id")) diff --git a/src/ordering/adapters/output/db/category_view.rs b/src/ordering/adapters/output/db/category_view.rs index 247799d..503d9c8 100644 --- a/src/ordering/adapters/output/db/category_view.rs +++ b/src/ordering/adapters/output/db/category_view.rs @@ -10,6 +10,8 @@ use uuid::Uuid; use super::errors::*; use super::OrderingDBPostgresAdapter; +use crate::ordering::adapters::types::{OrderingCategoryCqrsExec, OrderingCategoryCqrsView}; +use crate::ordering::application::services::OrderingServicesObj; use crate::ordering::domain::category_aggregate::*; use crate::ordering::domain::events::OrderingEvent; use crate::utils::parse_aggregate_id::parse_aggregate_id; @@ -222,6 +224,20 @@ impl Query for OrderingDBPostgresAdapter { } } +pub fn init_cqrs( + db: OrderingDBPostgresAdapter, + services: OrderingServicesObj, +) -> (OrderingCategoryCqrsExec, OrderingCategoryCqrsView) { + let queries: Vec>> = vec![Box::new(db.clone())]; + + let pool = db.pool.clone(); + + ( + std::sync::Arc::new(postgres_es::postgres_cqrs(pool.clone(), queries, services)), + std::sync::Arc::new(db.clone()), + ) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/ordering/adapters/output/db/customization_view.rs b/src/ordering/adapters/output/db/customization_view.rs index c31a26b..b6fb775 100644 --- a/src/ordering/adapters/output/db/customization_view.rs +++ b/src/ordering/adapters/output/db/customization_view.rs @@ -12,13 +12,17 @@ use uuid::Uuid; use super::errors::*; use super::OrderingDBPostgresAdapter; +use crate::ordering::adapters::types::{ + OrderingCustomizationCqrsExec, OrderingCustomizationCqrsView, +}; +use crate::ordering::application::services::OrderingServicesObj; use crate::ordering::domain::{customization_aggregate::*, events::OrderingEvent}; use crate::utils::parse_aggregate_id::parse_aggregate_id; pub const NEW_CUSTOMIZATION_NON_UUID: &str = "ordering_new_customization_non_uuid-asdfa"; #[derive(Debug, Default, Serialize, Deserialize)] -struct CustomizationView { +pub struct CustomizationView { name: String, product_id: Uuid, customization_id: Uuid, @@ -219,6 +223,20 @@ impl Query for OrderingDBPostgresAdapter { } } +pub fn init_cqrs( + db: OrderingDBPostgresAdapter, + services: OrderingServicesObj, +) -> (OrderingCustomizationCqrsExec, OrderingCustomizationCqrsView) { + let queries: Vec>> = vec![Box::new(db.clone())]; + + let pool = db.pool.clone(); + + ( + std::sync::Arc::new(postgres_es::postgres_cqrs(pool.clone(), queries, services)), + std::sync::Arc::new(db.clone()), + ) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/ordering/adapters/output/db/kot_view.rs b/src/ordering/adapters/output/db/kot_view.rs index 2adccd8..58e67a2 100644 --- a/src/ordering/adapters/output/db/kot_view.rs +++ b/src/ordering/adapters/output/db/kot_view.rs @@ -13,6 +13,8 @@ use uuid::Uuid; use super::errors::*; use super::OrderingDBPostgresAdapter; +use crate::ordering::adapters::types::{OrderingKotCqrsExec, OrderingKotCqrsView}; +use crate::ordering::application::services::OrderingServicesObj; use crate::ordering::domain::events::OrderingEvent; use crate::ordering::domain::kot_aggregate::*; use crate::types::quantity::*; @@ -229,6 +231,20 @@ impl Query for OrderingDBPostgresAdapter { } } +pub fn init_cqrs( + db: OrderingDBPostgresAdapter, + services: OrderingServicesObj, +) -> (OrderingKotCqrsExec, OrderingKotCqrsView) { + let queries: Vec>> = vec![Box::new(db.clone())]; + + let pool = db.pool.clone(); + + ( + std::sync::Arc::new(postgres_es::postgres_cqrs(pool.clone(), queries, services)), + std::sync::Arc::new(db.clone()), + ) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/ordering/adapters/output/db/line_item_view.rs b/src/ordering/adapters/output/db/line_item_view.rs index 3e3fa33..ee61acc 100644 --- a/src/ordering/adapters/output/db/line_item_view.rs +++ b/src/ordering/adapters/output/db/line_item_view.rs @@ -13,6 +13,8 @@ use uuid::Uuid; use super::errors::*; use super::OrderingDBPostgresAdapter; +use crate::ordering::adapters::types::{OrderingLineItemCqrsExec, OrderingLineItemCqrsView}; +use crate::ordering::application::services::OrderingServicesObj; use crate::ordering::domain::events::OrderingEvent; use crate::ordering::domain::line_item_aggregate::*; use crate::types::quantity::*; @@ -319,6 +321,20 @@ impl Query for OrderingDBPostgresAdapter { } } +pub fn init_cqrs( + db: OrderingDBPostgresAdapter, + services: OrderingServicesObj, +) -> (OrderingLineItemCqrsExec, OrderingLineItemCqrsView) { + let queries: Vec>> = vec![Box::new(db.clone())]; + + let pool = db.pool.clone(); + + ( + std::sync::Arc::new(postgres_es::postgres_cqrs(pool.clone(), queries, services)), + std::sync::Arc::new(db.clone()), + ) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/ordering/adapters/output/db/mod.rs b/src/ordering/adapters/output/db/mod.rs index d02dffb..1e704ec 100644 --- a/src/ordering/adapters/output/db/mod.rs +++ b/src/ordering/adapters/output/db/mod.rs @@ -10,24 +10,24 @@ use crate::db::{migrate::RunMigrations, sqlx_postgres::Postgres}; mod category_id_exists; mod category_name_exists_for_store; -mod category_view; +pub mod category_view; mod customization_id_exists; mod customization_name_exists_for_product; -mod customization_view; +pub mod customization_view; mod errors; mod get_category; mod kot_id_exists; -mod kot_view; +pub mod kot_view; mod line_item_id_exists; -mod line_item_view; +pub mod line_item_view; mod order_id_exists; -mod order_view; +pub mod order_view; mod product_id_exists; mod product_name_exists_for_category; -mod product_view; +pub mod product_view; mod store_id_exists; mod store_name_exists; -mod store_view; +pub mod store_view; #[derive(Clone)] pub struct OrderingDBPostgresAdapter { diff --git a/src/ordering/adapters/output/db/order_view.rs b/src/ordering/adapters/output/db/order_view.rs index b075b57..00e4bb0 100644 --- a/src/ordering/adapters/output/db/order_view.rs +++ b/src/ordering/adapters/output/db/order_view.rs @@ -13,6 +13,8 @@ use uuid::Uuid; use super::errors::*; use super::OrderingDBPostgresAdapter; +use crate::ordering::adapters::types::{OrderingOrderCqrsExec, OrderingOrderCqrsView}; +use crate::ordering::application::services::OrderingServicesObj; use crate::ordering::domain::events::OrderingEvent; use crate::ordering::domain::order_aggregate::*; use crate::utils::parse_aggregate_id::parse_aggregate_id; @@ -237,6 +239,20 @@ impl Query for OrderingDBPostgresAdapter { } } +pub fn init_cqrs( + db: OrderingDBPostgresAdapter, + services: OrderingServicesObj, +) -> (OrderingOrderCqrsExec, OrderingOrderCqrsView) { + let queries: Vec>> = vec![Box::new(db.clone())]; + + let pool = db.pool.clone(); + + ( + std::sync::Arc::new(postgres_es::postgres_cqrs(pool.clone(), queries, services)), + std::sync::Arc::new(db.clone()), + ) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/ordering/adapters/output/db/product_view.rs b/src/ordering/adapters/output/db/product_view.rs index c192122..7e09910 100644 --- a/src/ordering/adapters/output/db/product_view.rs +++ b/src/ordering/adapters/output/db/product_view.rs @@ -12,6 +12,8 @@ use uuid::Uuid; use super::errors::*; use super::OrderingDBPostgresAdapter; +use crate::ordering::adapters::types::{OrderingProductCqrsExec, OrderingProductCqrsView}; +use crate::ordering::application::services::OrderingServicesObj; use crate::ordering::domain::events::OrderingEvent; use crate::ordering::domain::product_aggregate::{Product, ProductBuilder}; use crate::types::currency::*; @@ -348,6 +350,20 @@ impl Query for OrderingDBPostgresAdapter { } } +pub fn init_cqrs( + db: OrderingDBPostgresAdapter, + services: OrderingServicesObj, +) -> (OrderingProductCqrsExec, OrderingProductCqrsView) { + let queries: Vec>> = vec![Box::new(db.clone())]; + + let pool = db.pool.clone(); + + ( + std::sync::Arc::new(postgres_es::postgres_cqrs(pool.clone(), queries, services)), + std::sync::Arc::new(db.clone()), + ) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/ordering/adapters/output/db/store_view.rs b/src/ordering/adapters/output/db/store_view.rs index 7c3a29c..e99de17 100644 --- a/src/ordering/adapters/output/db/store_view.rs +++ b/src/ordering/adapters/output/db/store_view.rs @@ -10,6 +10,8 @@ use uuid::Uuid; use super::errors::*; use super::OrderingDBPostgresAdapter; +use crate::ordering::adapters::types::{OrderingStoreCqrsExec, OrderingStoreCqrsView}; +use crate::ordering::application::services::OrderingServicesObj; use crate::ordering::domain::events::OrderingEvent; use crate::ordering::domain::store_aggregate::*; use crate::utils::parse_aggregate_id::parse_aggregate_id; @@ -216,6 +218,20 @@ impl Query for OrderingDBPostgresAdapter { } } +pub fn init_cqrs( + db: OrderingDBPostgresAdapter, + services: OrderingServicesObj, +) -> (OrderingStoreCqrsExec, OrderingStoreCqrsView) { + let queries: Vec>> = vec![Box::new(db.clone())]; + + let pool = db.pool.clone(); + + ( + std::sync::Arc::new(postgres_es::postgres_cqrs(pool.clone(), queries, services)), + std::sync::Arc::new(db.clone()), + ) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/ordering/adapters/output/full_text_search/meili/add_product_to_store.rs b/src/ordering/adapters/output/full_text_search/meili/add_product_to_store.rs index 2928eb0..e12a6db 100644 --- a/src/ordering/adapters/output/full_text_search/meili/add_product_to_store.rs +++ b/src/ordering/adapters/output/full_text_search/meili/add_product_to_store.rs @@ -47,7 +47,9 @@ impl AddProductToStoreFTSPort for OrderingFTSMeili { product: &Product, category: &Category, ) -> OrderingFTSResult<()> { - let store_index = self.client.index(format!("ordering-{}",category.store_id())); + let store_index = self + .client + .index(format!("ordering-{}", category.store_id())); let meili_product = MeiliProduct::new(product, category); store_index .add_documents(&[meili_product], Some("product_id")) From 53da1568975860d2a25b62c4892a6e5d279a7e38 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Fri, 10 Jan 2025 20:11:47 +0530 Subject: [PATCH 51/63] feat: generate type aliases in loop --- utils/adapter_type_gen.sh | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/utils/adapter_type_gen.sh b/utils/adapter_type_gen.sh index 06c946b..a09d022 100755 --- a/utils/adapter_type_gen.sh +++ b/utils/adapter_type_gen.sh @@ -46,6 +46,13 @@ pub fn init_cqrs( } +run_loop() { + for i in "${@:2}" + do + run $1 $i + done +} + if [ -z $1 ] then @@ -54,6 +61,6 @@ elif [ -z $2 ] then help else - run $1 $2 $3 $4 | wl-copy - run $1 $2 $3 $4 + run_loop "$@" + run_loop "$@" | wl-copy fi From a17cc526337941acfdd3fee6da17a7cc7cf9d4aa Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Fri, 10 Jan 2025 20:12:11 +0530 Subject: [PATCH 52/63] feat: ordering adapter type aliases --- src/ordering/adapters/types.rs | 107 +++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 src/ordering/adapters/types.rs diff --git a/src/ordering/adapters/types.rs b/src/ordering/adapters/types.rs new file mode 100644 index 0000000..5b1c66b --- /dev/null +++ b/src/ordering/adapters/types.rs @@ -0,0 +1,107 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later +#![allow(dead_code)] + +use std::sync::Arc; + +use actix_web::web::Data; +use async_trait::async_trait; +use cqrs_es::{persist::ViewRepository, AggregateError}; +use derive_builder::Builder; +use mockall::predicate::*; +use mockall::*; +use postgres_es::PostgresCqrs; + +use crate::ordering::{ + adapters::{ + // input::web::RoutesRepository, + output::db::{ + category_view::CategoryView, customization_view::CustomizationView, kot_view::KotView, + line_item_view::LineItemView, order_view::OrderView, product_view::ProductView, + store_view::StoreView, OrderingDBPostgresAdapter, + }, + }, + application::services::{errors::OrderingError, OrderingServicesObj}, + domain::{ + category_aggregate::Category, commands::OrderingCommand, + customization_aggregate::Customization, kot_aggregate::Kot, line_item_aggregate::LineItem, + order_aggregate::Order, product_aggregate::Product, store_aggregate::Store, + }, +}; + +//pub type WebOrderingRoutesRepository = Data>; + +pub type OrderingOrderCqrsExec = Arc>; +pub type OrderingOrderCqrsView = Arc>; +pub type WebOrderingOrderCqrsView = Data; + +pub type OrderingCategoryCqrsExec = Arc>; +pub type OrderingCategoryCqrsView = Arc>; +pub type WebOrderingCategoryCqrsView = Data; + +pub type OrderingKotCqrsExec = Arc>; +pub type OrderingKotCqrsView = Arc>; +pub type WebOrderingKotCqrsView = Data; + +pub type OrderingStoreCqrsExec = Arc>; +pub type OrderingStoreCqrsView = Arc>; +pub type WebOrderingStoreCqrsView = Data; + +pub type OrderingCustomizationCqrsExec = Arc>; +pub type OrderingCustomizationCqrsView = Arc>; +pub type WebOrderingCustomizationCqrsView = Data; + +pub type OrderingProductCqrsExec = Arc>; +pub type OrderingProductCqrsView = Arc>; +pub type WebOrderingProductCqrsView = Data; + +pub type OrderingLineItemCqrsExec = Arc>; +pub type OrderingLineItemCqrsView = Arc>; +pub type WebOrderingLineItemCqrsView = Data; + +pub type WebOrderingCqrsExec = Data>; + +#[automock] +#[async_trait] +pub trait OrderingCqrsExecutor { + async fn execute( + &self, + aggregate_id: &str, + command: OrderingCommand, + ) -> Result<(), AggregateError>; +} + +#[derive(Clone, Builder)] +pub struct OrderingCqrsExec { + store: OrderingStoreCqrsExec, + product: OrderingProductCqrsExec, + line_item: OrderingLineItemCqrsExec, + customization: OrderingCustomizationCqrsExec, + kot: OrderingKotCqrsExec, + category: OrderingCategoryCqrsExec, + order: OrderingOrderCqrsExec, +} + +#[async_trait] +impl OrderingCqrsExecutor for OrderingCqrsExec { + async fn execute( + &self, + aggregate_id: &str, + command: OrderingCommand, + ) -> Result<(), AggregateError> { + self.kot.execute(aggregate_id, command.clone()).await?; + self.order.execute(aggregate_id, command.clone()).await?; + self.line_item + .execute(aggregate_id, command.clone()) + .await?; + self.product.execute(aggregate_id, command.clone()).await?; + self.store.execute(aggregate_id, command.clone()).await?; + self.customization + .execute(aggregate_id, command.clone()) + .await?; + self.category.execute(aggregate_id, command).await?; + + Ok(()) + } +} From d738fe54a7762ed627f69f171bd0c3332bae42a6 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Fri, 10 Jan 2025 20:12:31 +0530 Subject: [PATCH 53/63] fix: rename ordering service obj type alias --- src/ordering/application/services/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ordering/application/services/mod.rs b/src/ordering/application/services/mod.rs index a0ceac7..8cf6a10 100644 --- a/src/ordering/application/services/mod.rs +++ b/src/ordering/application/services/mod.rs @@ -159,7 +159,7 @@ impl OrderingServicesInterface for OrderingServices { } } -pub type OrderingServicesInterfaceObj = Arc; +pub type OrderingServicesObj = Arc; impl OrderingServices { pub fn new( @@ -179,7 +179,7 @@ impl OrderingServices { out_db_store_name_exists: StoreNameExistsDBPortObj, out_fts_add_product_to_store: AddProductToStoreFTSPortObj, - ) -> OrderingServicesInterfaceObj { + ) -> OrderingServicesObj { let add_line_item = Arc::new( AddLineItemServiceBuilder::default() .db_line_item_id_exists(out_db_line_item_id_exists.clone()) From 288453b3ea50f40c1e6e0c4e087ded595087a4d7 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Fri, 10 Jan 2025 20:12:42 +0530 Subject: [PATCH 54/63] feat: load ordering adapters --- src/ordering/adapters/mod.rs | 96 +++++++++++++++++++++++++++++++++++- src/ordering/mod.rs | 2 +- src/utils/load_adapters.rs | 4 ++ 3 files changed, 100 insertions(+), 2 deletions(-) diff --git a/src/ordering/adapters/mod.rs b/src/ordering/adapters/mod.rs index 9b25f58..c6bf8cf 100644 --- a/src/ordering/adapters/mod.rs +++ b/src/ordering/adapters/mod.rs @@ -2,5 +2,99 @@ // // SPDX-License-Identifier: AGPL-3.0-or-later +use std::sync::Arc; + +use actix_web::web::{self, Data}; +use cqrs_es::{persist::ViewRepository, EventEnvelope, Query, View}; +use postgres_es::PostgresCqrs; +use sqlx::postgres::PgPool; + +use crate::ordering::application::services::{OrderingServices, OrderingServicesObj}; +use crate::settings::Settings; +use output::{ + db::{ + category_view, customization_view, kot_view, line_item_view, order_view, product_view, + store_view, OrderingDBPostgresAdapter, + }, + full_text_search::meili::OrderingFTSMeili, +}; + mod input; -mod output; +pub mod output; +mod types; + +pub fn load_adapters(pool: PgPool, settings: Settings) -> impl FnOnce(&mut web::ServiceConfig) { + let db = OrderingDBPostgresAdapter::new(pool.clone()); + let fts = OrderingFTSMeili::new(&settings.meili.url, &settings.meili.api_key); + + let services: OrderingServicesObj = OrderingServices::new( + Arc::new(db.clone()), + Arc::new(db.clone()), + Arc::new(db.clone()), + Arc::new(db.clone()), + Arc::new(db.clone()), + Arc::new(db.clone()), + Arc::new(db.clone()), + Arc::new(db.clone()), + Arc::new(db.clone()), + Arc::new(db.clone()), + Arc::new(db.clone()), + Arc::new(db.clone()), + Arc::new(fts.clone()), + ); + + let (category_cqrs_exec, category_cqrs_query) = + category_view::init_cqrs(db.clone(), services.clone()); + let (product_cqrs_exec, product_cqrs_query) = + product_view::init_cqrs(db.clone(), services.clone()); + let (customization_cqrs_exec, customization_cqrs_query) = + customization_view::init_cqrs(db.clone(), services.clone()); + let (store_cqrs_exec, store_cqrs_query) = store_view::init_cqrs(db.clone(), services.clone()); + let (kot_cqrs_exec, kot_cqrs_query) = kot_view::init_cqrs(db.clone(), services.clone()); + let (line_item_cqrs_exec, line_item_cqrs_query) = + line_item_view::init_cqrs(db.clone(), services.clone()); + let (order_cqrs_exec, order_cqrs_query) = order_view::init_cqrs(db.clone(), services.clone()); + let ordering_cqrs_exec = types::WebOrderingCqrsExec::new(Arc::new( + types::OrderingCqrsExecBuilder::default() + .category(category_cqrs_exec) + .product(product_cqrs_exec) + .customization(customization_cqrs_exec) + .store(store_cqrs_exec) + .kot(kot_cqrs_exec) + .line_item(line_item_cqrs_exec) + .order(order_cqrs_exec) + .build() + .unwrap(), + )); + + let f = move |cfg: &mut web::ServiceConfig| { + //cfg.configure(input::web::load_ctx()); + cfg.app_data(Data::new(category_cqrs_query.clone())); + cfg.app_data(Data::new(product_cqrs_query.clone())); + cfg.app_data(Data::new(customization_cqrs_query.clone())); + cfg.app_data(Data::new(store_cqrs_query.clone())); + cfg.app_data(Data::new(kot_cqrs_query.clone())); + cfg.app_data(Data::new(line_item_cqrs_query.clone())); + cfg.app_data(Data::new(order_cqrs_query.clone())); + cfg.app_data(ordering_cqrs_exec.clone()); + }; + + Box::new(f) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::db::migrate::*; + + #[actix_rt::test] + async fn ordering_load_adapters() { + let settings = crate::settings::tests::get_settings().await; + settings.create_db().await; + + let db = crate::db::sqlx_postgres::Postgres::init(&settings.database.url).await; + db.migrate().await; + + load_adapters(db.pool.clone(), settings.clone()); + } +} diff --git a/src/ordering/mod.rs b/src/ordering/mod.rs index 7272406..e311feb 100644 --- a/src/ordering/mod.rs +++ b/src/ordering/mod.rs @@ -2,6 +2,6 @@ // // SPDX-License-Identifier: AGPL-3.0-or-later -mod adapters; +pub mod adapters; mod application; mod domain; diff --git a/src/utils/load_adapters.rs b/src/utils/load_adapters.rs index 6d3d6c4..03e1287 100644 --- a/src/utils/load_adapters.rs +++ b/src/utils/load_adapters.rs @@ -20,6 +20,10 @@ pub fn load_adapters(pool: PgPool, settings: Settings) -> impl FnOnce(&mut web:: pool.clone(), settings.clone(), )) + .configure(crate::ordering::adapters::load_adapters( + pool.clone(), + settings.clone(), + )) .configure(super::random_string::GenerateRandomString::inject()) .configure(super::uuid::GenerateUUID::inject()); }; From b6350f2225f240643b129ce86e8d05d4701ebc7a Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Fri, 10 Jan 2025 21:13:40 +0530 Subject: [PATCH 55/63] feat: bootstrap inventory templates --- web_ui/src/inventory/add_category.rs | 63 ++++++++++++++ web_ui/src/inventory/add_customization.rs | 67 +++++++++++++++ web_ui/src/inventory/add_product.rs | 63 ++++++++++++++ web_ui/src/inventory/mod.rs | 25 ++++++ web_ui/src/inventory/update_category.rs | 67 +++++++++++++++ web_ui/src/inventory/update_customization.rs | 67 +++++++++++++++ web_ui/src/inventory/update_product.rs | 65 +++++++++++++++ web_ui/src/lib.rs | 1 + web_ui/src/utils.rs | 1 + web_ui/templates/inventory/add_category.html | 25 ++++++ .../inventory/add_customization.html | 21 +++++ web_ui/templates/inventory/add_product.html | 83 +++++++++++++++++++ .../templates/inventory/update_category.html | 25 ++++++ .../inventory/update_customization.html | 21 +++++ .../templates/inventory/update_product.html | 83 +++++++++++++++++++ 15 files changed, 677 insertions(+) create mode 100644 web_ui/src/inventory/add_category.rs create mode 100644 web_ui/src/inventory/add_customization.rs create mode 100644 web_ui/src/inventory/add_product.rs create mode 100644 web_ui/src/inventory/mod.rs create mode 100644 web_ui/src/inventory/update_category.rs create mode 100644 web_ui/src/inventory/update_customization.rs create mode 100644 web_ui/src/inventory/update_product.rs create mode 100644 web_ui/templates/inventory/add_category.html create mode 100644 web_ui/templates/inventory/add_customization.html create mode 100644 web_ui/templates/inventory/add_product.html create mode 100644 web_ui/templates/inventory/update_category.html create mode 100644 web_ui/templates/inventory/update_customization.html create mode 100644 web_ui/templates/inventory/update_product.html diff --git a/web_ui/src/inventory/add_category.rs b/web_ui/src/inventory/add_category.rs new file mode 100644 index 0000000..ed6841e --- /dev/null +++ b/web_ui/src/inventory/add_category.rs @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later +use std::cell::RefCell; + +use derive_builder::*; +use derive_more::*; +use serde::*; +use tera::Context; +use url::Url; + +pub use super::*; +use crate::utils::*; + +pub const ADD_CATEGORY_PAGE: TemplateFile = + TemplateFile::new("inventory.add_category", "inventory/add_category.html"); + +pub fn register_templates(t: &mut tera::Tera) { + ADD_CATEGORY_PAGE.register(t).expect(ADD_CATEGORY_PAGE.name); +} + +pub struct AddCategoryPage { + ctx: RefCell, +} + +impl AddCategoryPage { + pub fn new() -> Self { + let mut ctx = context(); + + //ctx.insert(PAYLOAD_KEY, p); + + let ctx = RefCell::new(ctx); + Self { ctx } + } + + pub fn render(&self) -> String { + TEMPLATES + .render(ADD_CATEGORY_PAGE.name, &self.ctx.borrow()) + .unwrap() + } + + pub fn page() -> String { + let p = Self::new(); + p.render() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn render_page_and_write() { + const FILE: &str = "./tmp/inventory-add_category.html"; + + let tw = TestWriterBuilder::default() + .file(FILE.into()) + .contents(AddCategoryPage::page()) + .build() + .unwrap(); + tw.write(); + } +} diff --git a/web_ui/src/inventory/add_customization.rs b/web_ui/src/inventory/add_customization.rs new file mode 100644 index 0000000..97e8d7a --- /dev/null +++ b/web_ui/src/inventory/add_customization.rs @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later +use std::cell::RefCell; + +use derive_builder::*; +use derive_more::*; +use serde::*; +use tera::Context; +use url::Url; + +pub use super::*; +use crate::utils::*; + +pub const ADD_CUSTOMIZATION_PAGE: TemplateFile = TemplateFile::new( + "inventory.add_customization", + "inventory/add_customization.html", +); + +pub fn register_templates(t: &mut tera::Tera) { + ADD_CUSTOMIZATION_PAGE + .register(t) + .expect(ADD_CUSTOMIZATION_PAGE.name); +} + +pub struct AddCustomizationPage { + ctx: RefCell, +} + +impl AddCustomizationPage { + pub fn new() -> Self { + let mut ctx = context(); + + //ctx.insert(PAYLOAD_KEY, p); + + let ctx = RefCell::new(ctx); + Self { ctx } + } + + pub fn render(&self) -> String { + TEMPLATES + .render(ADD_CUSTOMIZATION_PAGE.name, &self.ctx.borrow()) + .unwrap() + } + + pub fn page() -> String { + let p = Self::new(); + p.render() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn render_page_and_write() { + const FILE: &str = "./tmp/inventory-add_customization.html"; + + let tw = TestWriterBuilder::default() + .file(FILE.into()) + .contents(AddCustomizationPage::page()) + .build() + .unwrap(); + tw.write(); + } +} diff --git a/web_ui/src/inventory/add_product.rs b/web_ui/src/inventory/add_product.rs new file mode 100644 index 0000000..b596f6c --- /dev/null +++ b/web_ui/src/inventory/add_product.rs @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later +use std::cell::RefCell; + +use derive_builder::*; +use derive_more::*; +use serde::*; +use tera::Context; +use url::Url; + +pub use super::*; +use crate::utils::*; + +pub const ADD_PRODUCT_PAGE: TemplateFile = + TemplateFile::new("inventory.add_product", "inventory/add_product.html"); + +pub fn register_templates(t: &mut tera::Tera) { + ADD_PRODUCT_PAGE.register(t).expect(ADD_PRODUCT_PAGE.name); +} + +pub struct AddProductPage { + ctx: RefCell, +} + +impl AddProductPage { + pub fn new() -> Self { + let mut ctx = context(); + + //ctx.insert(PAYLOAD_KEY, p); + + let ctx = RefCell::new(ctx); + Self { ctx } + } + + pub fn render(&self) -> String { + TEMPLATES + .render(ADD_PRODUCT_PAGE.name, &self.ctx.borrow()) + .unwrap() + } + + pub fn page() -> String { + let p = Self::new(); + p.render() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn render_page_and_write() { + const FILE: &str = "./tmp/inventory-add_product.html"; + + let tw = TestWriterBuilder::default() + .file(FILE.into()) + .contents(AddProductPage::page()) + .build() + .unwrap(); + tw.write(); + } +} diff --git a/web_ui/src/inventory/mod.rs b/web_ui/src/inventory/mod.rs new file mode 100644 index 0000000..67f0951 --- /dev/null +++ b/web_ui/src/inventory/mod.rs @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +pub mod add_category; +pub mod add_customization; +pub mod add_product; +pub mod update_category; +pub mod update_customization; +pub mod update_product; + +pub fn register_templates(t: &mut tera::Tera) { + for template in [ + add_category::ADD_CATEGORY_PAGE, + update_category::UPDATE_CATEGORY_PAGE, + add_product::ADD_PRODUCT_PAGE, + update_product::UPDATE_PRODUCT_PAGE, + add_customization::ADD_CUSTOMIZATION_PAGE, + update_customization::UPDATE_CUSTOMIZATION_PAGE, + ] + .iter() + { + template.register(t).expect(template.name); + } +} diff --git a/web_ui/src/inventory/update_category.rs b/web_ui/src/inventory/update_category.rs new file mode 100644 index 0000000..8fc78fa --- /dev/null +++ b/web_ui/src/inventory/update_category.rs @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later +use std::cell::RefCell; + +use derive_builder::*; +use derive_more::*; +use serde::*; +use tera::Context; +use url::Url; + +pub use super::*; +use crate::utils::*; + +pub const UPDATE_CATEGORY_PAGE: TemplateFile = TemplateFile::new( + "inventory.update_category", + "inventory/update_category.html", +); + +pub fn register_templates(t: &mut tera::Tera) { + UPDATE_CATEGORY_PAGE + .register(t) + .expect(UPDATE_CATEGORY_PAGE.name); +} + +pub struct UpdateCategoryPage { + ctx: RefCell, +} + +impl UpdateCategoryPage { + pub fn new() -> Self { + let mut ctx = context(); + + //ctx.insert(PAYLOAD_KEY, p); + + let ctx = RefCell::new(ctx); + Self { ctx } + } + + pub fn render(&self) -> String { + TEMPLATES + .render(UPDATE_CATEGORY_PAGE.name, &self.ctx.borrow()) + .unwrap() + } + + pub fn page() -> String { + let p = Self::new(); + p.render() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn render_page_and_write() { + const FILE: &str = "./tmp/inventory-update_category.html"; + + let tw = TestWriterBuilder::default() + .file(FILE.into()) + .contents(UpdateCategoryPage::page()) + .build() + .unwrap(); + tw.write(); + } +} diff --git a/web_ui/src/inventory/update_customization.rs b/web_ui/src/inventory/update_customization.rs new file mode 100644 index 0000000..2fce280 --- /dev/null +++ b/web_ui/src/inventory/update_customization.rs @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later +use std::cell::RefCell; + +use derive_builder::*; +use derive_more::*; +use serde::*; +use tera::Context; +use url::Url; + +pub use super::*; +use crate::utils::*; + +pub const UPDATE_CUSTOMIZATION_PAGE: TemplateFile = TemplateFile::new( + "inventory.update_customization", + "inventory/update_customization.html", +); + +pub fn register_templates(t: &mut tera::Tera) { + UPDATE_CUSTOMIZATION_PAGE + .register(t) + .expect(UPDATE_CUSTOMIZATION_PAGE.name); +} + +pub struct UpdateCustomizationPage { + ctx: RefCell, +} + +impl UpdateCustomizationPage { + pub fn new() -> Self { + let mut ctx = context(); + + //ctx.insert(PAYLOAD_KEY, p); + + let ctx = RefCell::new(ctx); + Self { ctx } + } + + pub fn render(&self) -> String { + TEMPLATES + .render(UPDATE_CUSTOMIZATION_PAGE.name, &self.ctx.borrow()) + .unwrap() + } + + pub fn page() -> String { + let p = Self::new(); + p.render() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn render_page_and_write() { + const FILE: &str = "./tmp/inventory-update_customization.html"; + + let tw = TestWriterBuilder::default() + .file(FILE.into()) + .contents(UpdateCustomizationPage::page()) + .build() + .unwrap(); + tw.write(); + } +} diff --git a/web_ui/src/inventory/update_product.rs b/web_ui/src/inventory/update_product.rs new file mode 100644 index 0000000..5c479ed --- /dev/null +++ b/web_ui/src/inventory/update_product.rs @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later +use std::cell::RefCell; + +use derive_builder::*; +use derive_more::*; +use serde::*; +use tera::Context; +use url::Url; + +pub use super::*; +use crate::utils::*; + +pub const UPDATE_PRODUCT_PAGE: TemplateFile = + TemplateFile::new("inventory.update_product", "inventory/update_product.html"); + +pub fn register_templates(t: &mut tera::Tera) { + UPDATE_PRODUCT_PAGE + .register(t) + .expect(UPDATE_PRODUCT_PAGE.name); +} + +pub struct UpdateProductPage { + ctx: RefCell, +} + +impl UpdateProductPage { + pub fn new() -> Self { + let mut ctx = context(); + + //ctx.insert(PAYLOAD_KEY, p); + + let ctx = RefCell::new(ctx); + Self { ctx } + } + + pub fn render(&self) -> String { + TEMPLATES + .render(UPDATE_PRODUCT_PAGE.name, &self.ctx.borrow()) + .unwrap() + } + + pub fn page() -> String { + let p = Self::new(); + p.render() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn render_page_and_write() { + const FILE: &str = "./tmp/inventory-update_product.html"; + + let tw = TestWriterBuilder::default() + .file(FILE.into()) + .contents(UpdateProductPage::page()) + .build() + .unwrap(); + tw.write(); + } +} diff --git a/web_ui/src/lib.rs b/web_ui/src/lib.rs index d02df30..1beb522 100644 --- a/web_ui/src/lib.rs +++ b/web_ui/src/lib.rs @@ -3,6 +3,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pub mod identity; +pub mod inventory; mod log; mod utils; diff --git a/web_ui/src/utils.rs b/web_ui/src/utils.rs index a2412d5..b183be3 100644 --- a/web_ui/src/utils.rs +++ b/web_ui/src/utils.rs @@ -47,6 +47,7 @@ lazy_static! { } tera.autoescape_on(vec![".html", ".sql"]); crate::identity::register_templates(&mut tera); + crate::inventory::register_templates(&mut tera); tera }; } diff --git a/web_ui/templates/inventory/add_category.html b/web_ui/templates/inventory/add_category.html new file mode 100644 index 0000000..83e9411 --- /dev/null +++ b/web_ui/templates/inventory/add_category.html @@ -0,0 +1,25 @@ + + + + + + Create Category | Vanikam + + + +
+ + + + + +
+ + + diff --git a/web_ui/templates/inventory/add_customization.html b/web_ui/templates/inventory/add_customization.html new file mode 100644 index 0000000..5f9a0d8 --- /dev/null +++ b/web_ui/templates/inventory/add_customization.html @@ -0,0 +1,21 @@ + + + + + + Add Customization | Vanikam + + + +
+ + + + +
+ + + diff --git a/web_ui/templates/inventory/add_product.html b/web_ui/templates/inventory/add_product.html new file mode 100644 index 0000000..a75765c --- /dev/null +++ b/web_ui/templates/inventory/add_product.html @@ -0,0 +1,83 @@ + + + + + + Add Product | Vanikam + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + diff --git a/web_ui/templates/inventory/update_category.html b/web_ui/templates/inventory/update_category.html new file mode 100644 index 0000000..1fbc54f --- /dev/null +++ b/web_ui/templates/inventory/update_category.html @@ -0,0 +1,25 @@ + + + + + + Update Category | Vanikam + + + +
+ + + + + +
+ + + diff --git a/web_ui/templates/inventory/update_customization.html b/web_ui/templates/inventory/update_customization.html new file mode 100644 index 0000000..248e25b --- /dev/null +++ b/web_ui/templates/inventory/update_customization.html @@ -0,0 +1,21 @@ + + + + + + Update Customization | Vanikam + + + +
+ + + + +
+ + + diff --git a/web_ui/templates/inventory/update_product.html b/web_ui/templates/inventory/update_product.html new file mode 100644 index 0000000..fde8c68 --- /dev/null +++ b/web_ui/templates/inventory/update_product.html @@ -0,0 +1,83 @@ + + + + + + Update Product | Vanikam + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + From 006c17e81ed487ddb5b0148aa67c9c9680fc4c49 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Fri, 10 Jan 2025 21:29:51 +0530 Subject: [PATCH 56/63] feat: bootstrap billing templates --- web_ui/src/billing/add_bill.rs | 69 ++++++++++++++ web_ui/src/billing/add_line_item.rs | 69 ++++++++++++++ web_ui/src/billing/delete_bill.rs | 68 ++++++++++++++ web_ui/src/billing/delete_line_item.rs | 67 ++++++++++++++ web_ui/src/billing/mod.rs | 25 +++++ web_ui/src/billing/update_bill.rs | 69 ++++++++++++++ web_ui/src/billing/update_line_item.rs | 69 ++++++++++++++ web_ui/src/lib.rs | 1 + web_ui/src/utils.rs | 1 + web_ui/templates/billing/add_bill.html | 17 ++++ web_ui/templates/billing/add_line_item.html | 91 +++++++++++++++++++ web_ui/templates/billing/delete_bill.html | 14 +++ .../templates/billing/delete_line_item.html | 15 +++ web_ui/templates/billing/update_bill.html | 39 ++++++++ .../templates/billing/update_line_item.html | 91 +++++++++++++++++++ 15 files changed, 705 insertions(+) create mode 100644 web_ui/src/billing/add_bill.rs create mode 100644 web_ui/src/billing/add_line_item.rs create mode 100644 web_ui/src/billing/delete_bill.rs create mode 100644 web_ui/src/billing/delete_line_item.rs create mode 100644 web_ui/src/billing/mod.rs create mode 100644 web_ui/src/billing/update_bill.rs create mode 100644 web_ui/src/billing/update_line_item.rs create mode 100644 web_ui/templates/billing/add_bill.html create mode 100644 web_ui/templates/billing/add_line_item.html create mode 100644 web_ui/templates/billing/delete_bill.html create mode 100644 web_ui/templates/billing/delete_line_item.html create mode 100644 web_ui/templates/billing/update_bill.html create mode 100644 web_ui/templates/billing/update_line_item.html diff --git a/web_ui/src/billing/add_bill.rs b/web_ui/src/billing/add_bill.rs new file mode 100644 index 0000000..800ab3c --- /dev/null +++ b/web_ui/src/billing/add_bill.rs @@ -0,0 +1,69 @@ + +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later +use std::cell::RefCell; + +use derive_builder::*; +use derive_more::*; +use serde::*; +use tera::Context; +use url::Url; + +pub use super::*; +use crate::utils::*; + +pub const ADD_BILL_PAGE: TemplateFile = TemplateFile::new( + "billing.add_bill", + "billing/add_bill.html", +); + +pub fn register_templates(t: &mut tera::Tera) { + ADD_BILL_PAGE + .register(t) + .expect(ADD_BILL_PAGE.name); +} + +pub struct AddBillPage { + ctx: RefCell, +} + +impl AddBillPage { + pub fn new() -> Self { + let mut ctx = context(); + + //ctx.insert(PAYLOAD_KEY, p); + + let ctx = RefCell::new(ctx); + Self { ctx } + } + + pub fn render(&self) -> String { + TEMPLATES + .render(ADD_BILL_PAGE.name, &self.ctx.borrow()) + .unwrap() + } + + pub fn page() -> String { + let p = Self::new(); + p.render() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn render_page_and_write() { + const FILE: &str = "./tmp/billing-add_bill.html"; + + let tw = TestWriterBuilder::default() + .file(FILE.into()) + .contents(AddBillPage::page()) + .build() + .unwrap(); + tw.write(); + } +} + diff --git a/web_ui/src/billing/add_line_item.rs b/web_ui/src/billing/add_line_item.rs new file mode 100644 index 0000000..31653e9 --- /dev/null +++ b/web_ui/src/billing/add_line_item.rs @@ -0,0 +1,69 @@ + +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later +use std::cell::RefCell; + +use derive_builder::*; +use derive_more::*; +use serde::*; +use tera::Context; +use url::Url; + +pub use super::*; +use crate::utils::*; + +pub const ADD_LINE_ITEM_PAGE: TemplateFile = TemplateFile::new( + "billing.add_line_item", + "billing/add_line_item.html", +); + +pub fn register_templates(t: &mut tera::Tera) { + ADD_LINE_ITEM_PAGE + .register(t) + .expect(ADD_LINE_ITEM_PAGE.name); +} + +pub struct AddLineItemPage { + ctx: RefCell, +} + +impl AddLineItemPage { + pub fn new() -> Self { + let mut ctx = context(); + + //ctx.insert(PAYLOAD_KEY, p); + + let ctx = RefCell::new(ctx); + Self { ctx } + } + + pub fn render(&self) -> String { + TEMPLATES + .render(ADD_LINE_ITEM_PAGE.name, &self.ctx.borrow()) + .unwrap() + } + + pub fn page() -> String { + let p = Self::new(); + p.render() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn render_page_and_write() { + const FILE: &str = "./tmp/billing-add_line_item.html"; + + let tw = TestWriterBuilder::default() + .file(FILE.into()) + .contents(AddLineItemPage::page()) + .build() + .unwrap(); + tw.write(); + } +} + diff --git a/web_ui/src/billing/delete_bill.rs b/web_ui/src/billing/delete_bill.rs new file mode 100644 index 0000000..af87b11 --- /dev/null +++ b/web_ui/src/billing/delete_bill.rs @@ -0,0 +1,68 @@ + +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later +use std::cell::RefCell; + +use derive_builder::*; +use derive_more::*; +use serde::*; +use tera::Context; +use url::Url; + +pub use super::*; +use crate::utils::*; + +pub const DELETE_BILL_PAGE: TemplateFile = TemplateFile::new( + "billing.delete_bill", + "billing/delete_bill.html", +); + +pub fn register_templates(t: &mut tera::Tera) { + DELETE_BILL_PAGE + .register(t) + .expect(DELETE_BILL_PAGE.name); +} + +pub struct DeleteBillPage { + ctx: RefCell, +} + +impl DeleteBillPage { + pub fn new() -> Self { + let mut ctx = context(); + + //ctx.insert(PAYLOAD_KEY, p); + + let ctx = RefCell::new(ctx); + Self { ctx } + } + + pub fn render(&self) -> String { + TEMPLATES + .render(DELETE_BILL_PAGE.name, &self.ctx.borrow()) + .unwrap() + } + + pub fn page() -> String { + let p = Self::new(); + p.render() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn render_page_and_write() { + const FILE: &str = "./tmp/billing-delete_bill.html"; + + let tw = TestWriterBuilder::default() + .file(FILE.into()) + .contents(DeleteBillPage::page()) + .build() + .unwrap(); + tw.write(); + } +} diff --git a/web_ui/src/billing/delete_line_item.rs b/web_ui/src/billing/delete_line_item.rs new file mode 100644 index 0000000..ae5b064 --- /dev/null +++ b/web_ui/src/billing/delete_line_item.rs @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later +use std::cell::RefCell; + +use derive_builder::*; +use derive_more::*; +use serde::*; +use tera::Context; +use url::Url; + +pub use super::*; +use crate::utils::*; + +pub const DELETE_LINE_ITEM_PAGE: TemplateFile = TemplateFile::new( + "billing.delete_line_item", + "billing/delete_line_item.html", +); + +pub fn register_templates(t: &mut tera::Tera) { + DELETE_LINE_ITEM_PAGE + .register(t) + .expect(DELETE_LINE_ITEM_PAGE.name); +} + +pub struct DeleteLineItemPage { + ctx: RefCell, +} + +impl DeleteLineItemPage { + pub fn new() -> Self { + let mut ctx = context(); + + //ctx.insert(PAYLOAD_KEY, p); + + let ctx = RefCell::new(ctx); + Self { ctx } + } + + pub fn render(&self) -> String { + TEMPLATES + .render(DELETE_LINE_ITEM_PAGE.name, &self.ctx.borrow()) + .unwrap() + } + + pub fn page() -> String { + let p = Self::new(); + p.render() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn render_page_and_write() { + const FILE: &str = "./tmp/billing-delete_line_item.html"; + + let tw = TestWriterBuilder::default() + .file(FILE.into()) + .contents(DeleteLineItemPage::page()) + .build() + .unwrap(); + tw.write(); + } +} diff --git a/web_ui/src/billing/mod.rs b/web_ui/src/billing/mod.rs new file mode 100644 index 0000000..400cefa --- /dev/null +++ b/web_ui/src/billing/mod.rs @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +pub mod add_bill; +pub mod update_bill; +pub mod delete_bill; +pub mod add_line_item; +pub mod update_line_item; +pub mod delete_line_item; + +pub fn register_templates(t: &mut tera::Tera) { + for template in [ + add_bill::ADD_BILL_PAGE, + update_bill::UPDATE_BILL_PAGE, + delete_bill::DELETE_BILL_PAGE, + add_line_item::ADD_LINE_ITEM_PAGE, + update_line_item::UPDATE_LINE_ITEM_PAGE, + delete_line_item::DELETE_LINE_ITEM_PAGE, + ] + .iter() + { + template.register(t).expect(template.name); + } +} diff --git a/web_ui/src/billing/update_bill.rs b/web_ui/src/billing/update_bill.rs new file mode 100644 index 0000000..de1d001 --- /dev/null +++ b/web_ui/src/billing/update_bill.rs @@ -0,0 +1,69 @@ + +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later +use std::cell::RefCell; + +use derive_builder::*; +use derive_more::*; +use serde::*; +use tera::Context; +use url::Url; + +pub use super::*; +use crate::utils::*; + +pub const UPDATE_BILL_PAGE: TemplateFile = TemplateFile::new( + "billing.update_bill", + "billing/update_bill.html", +); + +pub fn register_templates(t: &mut tera::Tera) { + UPDATE_BILL_PAGE + .register(t) + .expect(UPDATE_BILL_PAGE.name); +} + +pub struct UpdateBillPage { + ctx: RefCell, +} + +impl UpdateBillPage { + pub fn new() -> Self { + let mut ctx = context(); + + //ctx.insert(PAYLOAD_KEY, p); + + let ctx = RefCell::new(ctx); + Self { ctx } + } + + pub fn render(&self) -> String { + TEMPLATES + .render(UPDATE_BILL_PAGE.name, &self.ctx.borrow()) + .unwrap() + } + + pub fn page() -> String { + let p = Self::new(); + p.render() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn render_page_and_write() { + const FILE: &str = "./tmp/billing-update_bill.html"; + + let tw = TestWriterBuilder::default() + .file(FILE.into()) + .contents(UpdateBillPage::page()) + .build() + .unwrap(); + tw.write(); + } +} + diff --git a/web_ui/src/billing/update_line_item.rs b/web_ui/src/billing/update_line_item.rs new file mode 100644 index 0000000..d23f80b --- /dev/null +++ b/web_ui/src/billing/update_line_item.rs @@ -0,0 +1,69 @@ + +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later +use std::cell::RefCell; + +use derive_builder::*; +use derive_more::*; +use serde::*; +use tera::Context; +use url::Url; + +pub use super::*; +use crate::utils::*; + +pub const UPDATE_LINE_ITEM_PAGE: TemplateFile = TemplateFile::new( + "billing.update_line_item", + "billing/update_line_item.html", +); + +pub fn register_templates(t: &mut tera::Tera) { + UPDATE_LINE_ITEM_PAGE + .register(t) + .expect(UPDATE_LINE_ITEM_PAGE.name); +} + +pub struct UpdateLineItemPage { + ctx: RefCell, +} + +impl UpdateLineItemPage { + pub fn new() -> Self { + let mut ctx = context(); + + //ctx.insert(PAYLOAD_KEY, p); + + let ctx = RefCell::new(ctx); + Self { ctx } + } + + pub fn render(&self) -> String { + TEMPLATES + .render(UPDATE_LINE_ITEM_PAGE.name, &self.ctx.borrow()) + .unwrap() + } + + pub fn page() -> String { + let p = Self::new(); + p.render() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn render_page_and_write() { + const FILE: &str = "./tmp/billing-update_line_item.html"; + + let tw = TestWriterBuilder::default() + .file(FILE.into()) + .contents(UpdateLineItemPage::page()) + .build() + .unwrap(); + tw.write(); + } +} + diff --git a/web_ui/src/lib.rs b/web_ui/src/lib.rs index 1beb522..9e1b0e4 100644 --- a/web_ui/src/lib.rs +++ b/web_ui/src/lib.rs @@ -6,6 +6,7 @@ pub mod identity; pub mod inventory; mod log; mod utils; +pub mod billing; pub fn init() { lazy_static::initialize(&utils::TEMPLATES); diff --git a/web_ui/src/utils.rs b/web_ui/src/utils.rs index b183be3..5fd6108 100644 --- a/web_ui/src/utils.rs +++ b/web_ui/src/utils.rs @@ -48,6 +48,7 @@ lazy_static! { tera.autoescape_on(vec![".html", ".sql"]); crate::identity::register_templates(&mut tera); crate::inventory::register_templates(&mut tera); + crate::billing::register_templates(&mut tera); tera }; } diff --git a/web_ui/templates/billing/add_bill.html b/web_ui/templates/billing/add_bill.html new file mode 100644 index 0000000..86ae4b0 --- /dev/null +++ b/web_ui/templates/billing/add_bill.html @@ -0,0 +1,17 @@ + + + + + + Add Bill | Vanikam + + + Token Refreshed +
+ + +
+ + diff --git a/web_ui/templates/billing/add_line_item.html b/web_ui/templates/billing/add_line_item.html new file mode 100644 index 0000000..792a284 --- /dev/null +++ b/web_ui/templates/billing/add_line_item.html @@ -0,0 +1,91 @@ + + + + + + Add Line Item | Vanikam + + + Token Refreshed +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ + diff --git a/web_ui/templates/billing/delete_bill.html b/web_ui/templates/billing/delete_bill.html new file mode 100644 index 0000000..6868117 --- /dev/null +++ b/web_ui/templates/billing/delete_bill.html @@ -0,0 +1,14 @@ + + + + + + Delete Bill | Vanikam + + + Token Refreshed +
+ +
+ + diff --git a/web_ui/templates/billing/delete_line_item.html b/web_ui/templates/billing/delete_line_item.html new file mode 100644 index 0000000..432346f --- /dev/null +++ b/web_ui/templates/billing/delete_line_item.html @@ -0,0 +1,15 @@ + + + + + + Delete Line Item | Vanikam + + + Token Refreshed +
+ + +
+ + diff --git a/web_ui/templates/billing/update_bill.html b/web_ui/templates/billing/update_bill.html new file mode 100644 index 0000000..5feafcd --- /dev/null +++ b/web_ui/templates/billing/update_bill.html @@ -0,0 +1,39 @@ + + + + + + Update Bill | Vanikam + + + Token Refreshed +
+ + + + + + + + + +
+ + diff --git a/web_ui/templates/billing/update_line_item.html b/web_ui/templates/billing/update_line_item.html new file mode 100644 index 0000000..96f1e48 --- /dev/null +++ b/web_ui/templates/billing/update_line_item.html @@ -0,0 +1,91 @@ + + + + + + Update Line Item | Vanikam + + + Token Refreshed +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ + From 12c386ec8cc4ad37895b2fadbcb3e87fc1b5e8fc Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Fri, 10 Jan 2025 21:49:11 +0530 Subject: [PATCH 57/63] feat: bootstrap ordering templates --- web_ui/src/billing/add_bill.rs | 14 ++-- web_ui/src/billing/add_line_item.rs | 10 +-- web_ui/src/billing/delete_bill.rs | 13 ++-- web_ui/src/billing/delete_line_item.rs | 8 +-- web_ui/src/billing/mod.rs | 6 +- web_ui/src/billing/update_bill.rs | 14 ++-- web_ui/src/billing/update_line_item.rs | 10 +-- web_ui/src/lib.rs | 3 +- web_ui/src/ordering/add_category.rs | 63 +++++++++++++++++ web_ui/src/ordering/add_customization.rs | 67 +++++++++++++++++++ web_ui/src/ordering/add_kot.rs | 63 +++++++++++++++++ web_ui/src/ordering/add_line_item.rs | 65 ++++++++++++++++++ web_ui/src/ordering/add_order.rs | 63 +++++++++++++++++ web_ui/src/ordering/add_product.rs | 63 +++++++++++++++++ web_ui/src/ordering/delete_kot.rs | 63 +++++++++++++++++ web_ui/src/ordering/delete_line_item.rs | 67 +++++++++++++++++++ web_ui/src/ordering/delete_order.rs | 63 +++++++++++++++++ web_ui/src/ordering/mod.rs | 43 ++++++++++++ web_ui/src/ordering/update_category.rs | 65 ++++++++++++++++++ web_ui/src/ordering/update_customization.rs | 67 +++++++++++++++++++ web_ui/src/ordering/update_kot.rs | 63 +++++++++++++++++ web_ui/src/ordering/update_line_item.rs | 67 +++++++++++++++++++ web_ui/src/ordering/update_order.rs | 63 +++++++++++++++++ web_ui/src/ordering/update_product.rs | 65 ++++++++++++++++++ web_ui/src/utils.rs | 1 + web_ui/templates/billing/delete_bill.html | 1 - web_ui/templates/ordering/add_category.html | 0 .../templates/ordering/add_customization.html | 0 web_ui/templates/ordering/add_kot.html | 0 web_ui/templates/ordering/add_line_item.html | 0 web_ui/templates/ordering/add_order.html | 0 web_ui/templates/ordering/add_product.html | 0 web_ui/templates/ordering/delete_kot.html | 0 .../templates/ordering/delete_line_item.html | 0 web_ui/templates/ordering/delete_order.html | 0 .../templates/ordering/update_category.html | 0 .../ordering/update_customization.html | 0 web_ui/templates/ordering/update_kot.html | 0 .../templates/ordering/update_line_item.html | 0 web_ui/templates/ordering/update_order.html | 0 web_ui/templates/ordering/update_product.html | 0 41 files changed, 1037 insertions(+), 53 deletions(-) create mode 100644 web_ui/src/ordering/add_category.rs create mode 100644 web_ui/src/ordering/add_customization.rs create mode 100644 web_ui/src/ordering/add_kot.rs create mode 100644 web_ui/src/ordering/add_line_item.rs create mode 100644 web_ui/src/ordering/add_order.rs create mode 100644 web_ui/src/ordering/add_product.rs create mode 100644 web_ui/src/ordering/delete_kot.rs create mode 100644 web_ui/src/ordering/delete_line_item.rs create mode 100644 web_ui/src/ordering/delete_order.rs create mode 100644 web_ui/src/ordering/mod.rs create mode 100644 web_ui/src/ordering/update_category.rs create mode 100644 web_ui/src/ordering/update_customization.rs create mode 100644 web_ui/src/ordering/update_kot.rs create mode 100644 web_ui/src/ordering/update_line_item.rs create mode 100644 web_ui/src/ordering/update_order.rs create mode 100644 web_ui/src/ordering/update_product.rs create mode 100644 web_ui/templates/ordering/add_category.html create mode 100644 web_ui/templates/ordering/add_customization.html create mode 100644 web_ui/templates/ordering/add_kot.html create mode 100644 web_ui/templates/ordering/add_line_item.html create mode 100644 web_ui/templates/ordering/add_order.html create mode 100644 web_ui/templates/ordering/add_product.html create mode 100644 web_ui/templates/ordering/delete_kot.html create mode 100644 web_ui/templates/ordering/delete_line_item.html create mode 100644 web_ui/templates/ordering/delete_order.html create mode 100644 web_ui/templates/ordering/update_category.html create mode 100644 web_ui/templates/ordering/update_customization.html create mode 100644 web_ui/templates/ordering/update_kot.html create mode 100644 web_ui/templates/ordering/update_line_item.html create mode 100644 web_ui/templates/ordering/update_order.html create mode 100644 web_ui/templates/ordering/update_product.html diff --git a/web_ui/src/billing/add_bill.rs b/web_ui/src/billing/add_bill.rs index 800ab3c..a47c144 100644 --- a/web_ui/src/billing/add_bill.rs +++ b/web_ui/src/billing/add_bill.rs @@ -1,4 +1,3 @@ - // SPDX-FileCopyrightText: 2024 Aravinth Manivannan // // SPDX-License-Identifier: AGPL-3.0-or-later @@ -13,15 +12,11 @@ use url::Url; pub use super::*; use crate::utils::*; -pub const ADD_BILL_PAGE: TemplateFile = TemplateFile::new( - "billing.add_bill", - "billing/add_bill.html", -); +pub const ADD_BILL_PAGE: TemplateFile = + TemplateFile::new("billing.add_bill", "billing/add_bill.html"); pub fn register_templates(t: &mut tera::Tera) { - ADD_BILL_PAGE - .register(t) - .expect(ADD_BILL_PAGE.name); + ADD_BILL_PAGE.register(t).expect(ADD_BILL_PAGE.name); } pub struct AddBillPage { @@ -65,5 +60,4 @@ mod tests { .unwrap(); tw.write(); } -} - +} diff --git a/web_ui/src/billing/add_line_item.rs b/web_ui/src/billing/add_line_item.rs index 31653e9..f0afd67 100644 --- a/web_ui/src/billing/add_line_item.rs +++ b/web_ui/src/billing/add_line_item.rs @@ -1,4 +1,3 @@ - // SPDX-FileCopyrightText: 2024 Aravinth Manivannan // // SPDX-License-Identifier: AGPL-3.0-or-later @@ -13,10 +12,8 @@ use url::Url; pub use super::*; use crate::utils::*; -pub const ADD_LINE_ITEM_PAGE: TemplateFile = TemplateFile::new( - "billing.add_line_item", - "billing/add_line_item.html", -); +pub const ADD_LINE_ITEM_PAGE: TemplateFile = + TemplateFile::new("billing.add_line_item", "billing/add_line_item.html"); pub fn register_templates(t: &mut tera::Tera) { ADD_LINE_ITEM_PAGE @@ -65,5 +62,4 @@ mod tests { .unwrap(); tw.write(); } -} - +} diff --git a/web_ui/src/billing/delete_bill.rs b/web_ui/src/billing/delete_bill.rs index af87b11..9c75427 100644 --- a/web_ui/src/billing/delete_bill.rs +++ b/web_ui/src/billing/delete_bill.rs @@ -1,4 +1,3 @@ - // SPDX-FileCopyrightText: 2024 Aravinth Manivannan // // SPDX-License-Identifier: AGPL-3.0-or-later @@ -13,15 +12,11 @@ use url::Url; pub use super::*; use crate::utils::*; -pub const DELETE_BILL_PAGE: TemplateFile = TemplateFile::new( - "billing.delete_bill", - "billing/delete_bill.html", -); +pub const DELETE_BILL_PAGE: TemplateFile = + TemplateFile::new("billing.delete_bill", "billing/delete_bill.html"); pub fn register_templates(t: &mut tera::Tera) { - DELETE_BILL_PAGE - .register(t) - .expect(DELETE_BILL_PAGE.name); + DELETE_BILL_PAGE.register(t).expect(DELETE_BILL_PAGE.name); } pub struct DeleteBillPage { @@ -65,4 +60,4 @@ mod tests { .unwrap(); tw.write(); } -} +} diff --git a/web_ui/src/billing/delete_line_item.rs b/web_ui/src/billing/delete_line_item.rs index ae5b064..cdaad53 100644 --- a/web_ui/src/billing/delete_line_item.rs +++ b/web_ui/src/billing/delete_line_item.rs @@ -12,10 +12,8 @@ use url::Url; pub use super::*; use crate::utils::*; -pub const DELETE_LINE_ITEM_PAGE: TemplateFile = TemplateFile::new( - "billing.delete_line_item", - "billing/delete_line_item.html", -); +pub const DELETE_LINE_ITEM_PAGE: TemplateFile = + TemplateFile::new("billing.delete_line_item", "billing/delete_line_item.html"); pub fn register_templates(t: &mut tera::Tera) { DELETE_LINE_ITEM_PAGE @@ -64,4 +62,4 @@ mod tests { .unwrap(); tw.write(); } -} +} diff --git a/web_ui/src/billing/mod.rs b/web_ui/src/billing/mod.rs index 400cefa..bf2b70e 100644 --- a/web_ui/src/billing/mod.rs +++ b/web_ui/src/billing/mod.rs @@ -3,11 +3,11 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pub mod add_bill; -pub mod update_bill; -pub mod delete_bill; pub mod add_line_item; -pub mod update_line_item; +pub mod delete_bill; pub mod delete_line_item; +pub mod update_bill; +pub mod update_line_item; pub fn register_templates(t: &mut tera::Tera) { for template in [ diff --git a/web_ui/src/billing/update_bill.rs b/web_ui/src/billing/update_bill.rs index de1d001..aace18a 100644 --- a/web_ui/src/billing/update_bill.rs +++ b/web_ui/src/billing/update_bill.rs @@ -1,4 +1,3 @@ - // SPDX-FileCopyrightText: 2024 Aravinth Manivannan // // SPDX-License-Identifier: AGPL-3.0-or-later @@ -13,15 +12,11 @@ use url::Url; pub use super::*; use crate::utils::*; -pub const UPDATE_BILL_PAGE: TemplateFile = TemplateFile::new( - "billing.update_bill", - "billing/update_bill.html", -); +pub const UPDATE_BILL_PAGE: TemplateFile = + TemplateFile::new("billing.update_bill", "billing/update_bill.html"); pub fn register_templates(t: &mut tera::Tera) { - UPDATE_BILL_PAGE - .register(t) - .expect(UPDATE_BILL_PAGE.name); + UPDATE_BILL_PAGE.register(t).expect(UPDATE_BILL_PAGE.name); } pub struct UpdateBillPage { @@ -65,5 +60,4 @@ mod tests { .unwrap(); tw.write(); } -} - +} diff --git a/web_ui/src/billing/update_line_item.rs b/web_ui/src/billing/update_line_item.rs index d23f80b..d5dac6f 100644 --- a/web_ui/src/billing/update_line_item.rs +++ b/web_ui/src/billing/update_line_item.rs @@ -1,4 +1,3 @@ - // SPDX-FileCopyrightText: 2024 Aravinth Manivannan // // SPDX-License-Identifier: AGPL-3.0-or-later @@ -13,10 +12,8 @@ use url::Url; pub use super::*; use crate::utils::*; -pub const UPDATE_LINE_ITEM_PAGE: TemplateFile = TemplateFile::new( - "billing.update_line_item", - "billing/update_line_item.html", -); +pub const UPDATE_LINE_ITEM_PAGE: TemplateFile = + TemplateFile::new("billing.update_line_item", "billing/update_line_item.html"); pub fn register_templates(t: &mut tera::Tera) { UPDATE_LINE_ITEM_PAGE @@ -65,5 +62,4 @@ mod tests { .unwrap(); tw.write(); } -} - +} diff --git a/web_ui/src/lib.rs b/web_ui/src/lib.rs index 9e1b0e4..6f6c2e5 100644 --- a/web_ui/src/lib.rs +++ b/web_ui/src/lib.rs @@ -2,11 +2,12 @@ // // SPDX-License-Identifier: AGPL-3.0-or-later +pub mod billing; pub mod identity; pub mod inventory; mod log; +pub mod ordering; mod utils; -pub mod billing; pub fn init() { lazy_static::initialize(&utils::TEMPLATES); diff --git a/web_ui/src/ordering/add_category.rs b/web_ui/src/ordering/add_category.rs new file mode 100644 index 0000000..cccef16 --- /dev/null +++ b/web_ui/src/ordering/add_category.rs @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later +use std::cell::RefCell; + +use derive_builder::*; +use derive_more::*; +use serde::*; +use tera::Context; +use url::Url; + +pub use super::*; +use crate::utils::*; + +pub const ADD_CATEGORY_PAGE: TemplateFile = + TemplateFile::new("ordering.add_category", "ordering/add_category.html"); + +pub fn register_templates(t: &mut tera::Tera) { + ADD_CATEGORY_PAGE.register(t).expect(ADD_CATEGORY_PAGE.name); +} + +pub struct AddCategoryPage { + ctx: RefCell, +} + +impl AddCategoryPage { + pub fn new() -> Self { + let mut ctx = context(); + + //ctx.insert(PAYLOAD_KEY, p); + + let ctx = RefCell::new(ctx); + Self { ctx } + } + + pub fn render(&self) -> String { + TEMPLATES + .render(ADD_CATEGORY_PAGE.name, &self.ctx.borrow()) + .unwrap() + } + + pub fn page() -> String { + let p = Self::new(); + p.render() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn render_page_and_write() { + const FILE: &str = "./tmp/ordering-add_category.html"; + + let tw = TestWriterBuilder::default() + .file(FILE.into()) + .contents(AddCategoryPage::page()) + .build() + .unwrap(); + tw.write(); + } +} diff --git a/web_ui/src/ordering/add_customization.rs b/web_ui/src/ordering/add_customization.rs new file mode 100644 index 0000000..e70e987 --- /dev/null +++ b/web_ui/src/ordering/add_customization.rs @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later +use std::cell::RefCell; + +use derive_builder::*; +use derive_more::*; +use serde::*; +use tera::Context; +use url::Url; + +pub use super::*; +use crate::utils::*; + +pub const ADD_CUSTOMIZATION_PAGE: TemplateFile = TemplateFile::new( + "ordering.add_customization", + "ordering/add_customization.html", +); + +pub fn register_templates(t: &mut tera::Tera) { + ADD_CUSTOMIZATION_PAGE + .register(t) + .expect(ADD_CUSTOMIZATION_PAGE.name); +} + +pub struct AddCustomizationPage { + ctx: RefCell, +} + +impl AddCustomizationPage { + pub fn new() -> Self { + let mut ctx = context(); + + //ctx.insert(PAYLOAD_KEY, p); + + let ctx = RefCell::new(ctx); + Self { ctx } + } + + pub fn render(&self) -> String { + TEMPLATES + .render(ADD_CUSTOMIZATION_PAGE.name, &self.ctx.borrow()) + .unwrap() + } + + pub fn page() -> String { + let p = Self::new(); + p.render() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn render_page_and_write() { + const FILE: &str = "./tmp/ordering-add_customization.html"; + + let tw = TestWriterBuilder::default() + .file(FILE.into()) + .contents(AddCustomizationPage::page()) + .build() + .unwrap(); + tw.write(); + } +} diff --git a/web_ui/src/ordering/add_kot.rs b/web_ui/src/ordering/add_kot.rs new file mode 100644 index 0000000..2c92311 --- /dev/null +++ b/web_ui/src/ordering/add_kot.rs @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later +use std::cell::RefCell; + +use derive_builder::*; +use derive_more::*; +use serde::*; +use tera::Context; +use url::Url; + +pub use super::*; +use crate::utils::*; + +pub const ADD_KOT_PAGE: TemplateFile = + TemplateFile::new("ordering.add_kot", "ordering/add_kot.html"); + +pub fn register_templates(t: &mut tera::Tera) { + ADD_KOT_PAGE.register(t).expect(ADD_KOT_PAGE.name); +} + +pub struct AddKotPage { + ctx: RefCell, +} + +impl AddKotPage { + pub fn new() -> Self { + let mut ctx = context(); + + //ctx.insert(PAYLOAD_KEY, p); + + let ctx = RefCell::new(ctx); + Self { ctx } + } + + pub fn render(&self) -> String { + TEMPLATES + .render(ADD_KOT_PAGE.name, &self.ctx.borrow()) + .unwrap() + } + + pub fn page() -> String { + let p = Self::new(); + p.render() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn render_page_and_write() { + const FILE: &str = "./tmp/ordering-add_kot.html"; + + let tw = TestWriterBuilder::default() + .file(FILE.into()) + .contents(AddKotPage::page()) + .build() + .unwrap(); + tw.write(); + } +} diff --git a/web_ui/src/ordering/add_line_item.rs b/web_ui/src/ordering/add_line_item.rs new file mode 100644 index 0000000..ef46a41 --- /dev/null +++ b/web_ui/src/ordering/add_line_item.rs @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later +use std::cell::RefCell; + +use derive_builder::*; +use derive_more::*; +use serde::*; +use tera::Context; +use url::Url; + +pub use super::*; +use crate::utils::*; + +pub const ADD_LINE_ITEM_PAGE: TemplateFile = + TemplateFile::new("ordering.add_line_item", "ordering/add_line_item.html"); + +pub fn register_templates(t: &mut tera::Tera) { + ADD_LINE_ITEM_PAGE + .register(t) + .expect(ADD_LINE_ITEM_PAGE.name); +} + +pub struct AddLineItemPage { + ctx: RefCell, +} + +impl AddLineItemPage { + pub fn new() -> Self { + let mut ctx = context(); + + //ctx.insert(PAYLOAD_KEY, p); + + let ctx = RefCell::new(ctx); + Self { ctx } + } + + pub fn render(&self) -> String { + TEMPLATES + .render(ADD_LINE_ITEM_PAGE.name, &self.ctx.borrow()) + .unwrap() + } + + pub fn page() -> String { + let p = Self::new(); + p.render() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn render_page_and_write() { + const FILE: &str = "./tmp/ordering-add_line_item.html"; + + let tw = TestWriterBuilder::default() + .file(FILE.into()) + .contents(AddLineItemPage::page()) + .build() + .unwrap(); + tw.write(); + } +} diff --git a/web_ui/src/ordering/add_order.rs b/web_ui/src/ordering/add_order.rs new file mode 100644 index 0000000..9e11e4b --- /dev/null +++ b/web_ui/src/ordering/add_order.rs @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later +use std::cell::RefCell; + +use derive_builder::*; +use derive_more::*; +use serde::*; +use tera::Context; +use url::Url; + +pub use super::*; +use crate::utils::*; + +pub const ADD_ORDER_PAGE: TemplateFile = + TemplateFile::new("ordering.add_order", "ordering/add_order.html"); + +pub fn register_templates(t: &mut tera::Tera) { + ADD_ORDER_PAGE.register(t).expect(ADD_ORDER_PAGE.name); +} + +pub struct AddOrderPage { + ctx: RefCell, +} + +impl AddOrderPage { + pub fn new() -> Self { + let mut ctx = context(); + + //ctx.insert(PAYLOAD_KEY, p); + + let ctx = RefCell::new(ctx); + Self { ctx } + } + + pub fn render(&self) -> String { + TEMPLATES + .render(ADD_ORDER_PAGE.name, &self.ctx.borrow()) + .unwrap() + } + + pub fn page() -> String { + let p = Self::new(); + p.render() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn render_page_and_write() { + const FILE: &str = "./tmp/ordering-add_order.html"; + + let tw = TestWriterBuilder::default() + .file(FILE.into()) + .contents(AddOrderPage::page()) + .build() + .unwrap(); + tw.write(); + } +} diff --git a/web_ui/src/ordering/add_product.rs b/web_ui/src/ordering/add_product.rs new file mode 100644 index 0000000..95a1739 --- /dev/null +++ b/web_ui/src/ordering/add_product.rs @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later +use std::cell::RefCell; + +use derive_builder::*; +use derive_more::*; +use serde::*; +use tera::Context; +use url::Url; + +pub use super::*; +use crate::utils::*; + +pub const ADD_PRODUCT_PAGE: TemplateFile = + TemplateFile::new("ordering.add_product", "ordering/add_product.html"); + +pub fn register_templates(t: &mut tera::Tera) { + ADD_PRODUCT_PAGE.register(t).expect(ADD_PRODUCT_PAGE.name); +} + +pub struct AddProductPage { + ctx: RefCell, +} + +impl AddProductPage { + pub fn new() -> Self { + let mut ctx = context(); + + //ctx.insert(PAYLOAD_KEY, p); + + let ctx = RefCell::new(ctx); + Self { ctx } + } + + pub fn render(&self) -> String { + TEMPLATES + .render(ADD_PRODUCT_PAGE.name, &self.ctx.borrow()) + .unwrap() + } + + pub fn page() -> String { + let p = Self::new(); + p.render() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn render_page_and_write() { + const FILE: &str = "./tmp/ordering-add_product.html"; + + let tw = TestWriterBuilder::default() + .file(FILE.into()) + .contents(AddProductPage::page()) + .build() + .unwrap(); + tw.write(); + } +} diff --git a/web_ui/src/ordering/delete_kot.rs b/web_ui/src/ordering/delete_kot.rs new file mode 100644 index 0000000..c0ed0f2 --- /dev/null +++ b/web_ui/src/ordering/delete_kot.rs @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later +use std::cell::RefCell; + +use derive_builder::*; +use derive_more::*; +use serde::*; +use tera::Context; +use url::Url; + +pub use super::*; +use crate::utils::*; + +pub const DELETE_KOT_PAGE: TemplateFile = + TemplateFile::new("ordering.delete_kot", "ordering/delete_kot.html"); + +pub fn register_templates(t: &mut tera::Tera) { + DELETE_KOT_PAGE.register(t).expect(DELETE_KOT_PAGE.name); +} + +pub struct DeleteKotPage { + ctx: RefCell, +} + +impl DeleteKotPage { + pub fn new() -> Self { + let mut ctx = context(); + + //ctx.insert(PAYLOAD_KEY, p); + + let ctx = RefCell::new(ctx); + Self { ctx } + } + + pub fn render(&self) -> String { + TEMPLATES + .render(DELETE_KOT_PAGE.name, &self.ctx.borrow()) + .unwrap() + } + + pub fn page() -> String { + let p = Self::new(); + p.render() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn render_page_and_write() { + const FILE: &str = "./tmp/ordering-delete_kot.html"; + + let tw = TestWriterBuilder::default() + .file(FILE.into()) + .contents(DeleteKotPage::page()) + .build() + .unwrap(); + tw.write(); + } +} diff --git a/web_ui/src/ordering/delete_line_item.rs b/web_ui/src/ordering/delete_line_item.rs new file mode 100644 index 0000000..51d7bcf --- /dev/null +++ b/web_ui/src/ordering/delete_line_item.rs @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later +use std::cell::RefCell; + +use derive_builder::*; +use derive_more::*; +use serde::*; +use tera::Context; +use url::Url; + +pub use super::*; +use crate::utils::*; + +pub const DELETE_LINE_ITEM_PAGE: TemplateFile = TemplateFile::new( + "ordering.delete_line_item", + "ordering/delete_line_item.html", +); + +pub fn register_templates(t: &mut tera::Tera) { + DELETE_LINE_ITEM_PAGE + .register(t) + .expect(DELETE_LINE_ITEM_PAGE.name); +} + +pub struct DeleteLineItemPage { + ctx: RefCell, +} + +impl DeleteLineItemPage { + pub fn new() -> Self { + let mut ctx = context(); + + //ctx.insert(PAYLOAD_KEY, p); + + let ctx = RefCell::new(ctx); + Self { ctx } + } + + pub fn render(&self) -> String { + TEMPLATES + .render(DELETE_LINE_ITEM_PAGE.name, &self.ctx.borrow()) + .unwrap() + } + + pub fn page() -> String { + let p = Self::new(); + p.render() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn render_page_and_write() { + const FILE: &str = "./tmp/ordering-delete_line_item.html"; + + let tw = TestWriterBuilder::default() + .file(FILE.into()) + .contents(DeleteLineItemPage::page()) + .build() + .unwrap(); + tw.write(); + } +} diff --git a/web_ui/src/ordering/delete_order.rs b/web_ui/src/ordering/delete_order.rs new file mode 100644 index 0000000..ca714a4 --- /dev/null +++ b/web_ui/src/ordering/delete_order.rs @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later +use std::cell::RefCell; + +use derive_builder::*; +use derive_more::*; +use serde::*; +use tera::Context; +use url::Url; + +pub use super::*; +use crate::utils::*; + +pub const DELETE_ORDER_PAGE: TemplateFile = + TemplateFile::new("ordering.delete_order", "ordering/delete_order.html"); + +pub fn register_templates(t: &mut tera::Tera) { + DELETE_ORDER_PAGE.register(t).expect(DELETE_ORDER_PAGE.name); +} + +pub struct DeleteOrderPage { + ctx: RefCell, +} + +impl DeleteOrderPage { + pub fn new() -> Self { + let mut ctx = context(); + + //ctx.insert(PAYLOAD_KEY, p); + + let ctx = RefCell::new(ctx); + Self { ctx } + } + + pub fn render(&self) -> String { + TEMPLATES + .render(DELETE_ORDER_PAGE.name, &self.ctx.borrow()) + .unwrap() + } + + pub fn page() -> String { + let p = Self::new(); + p.render() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn render_page_and_write() { + const FILE: &str = "./tmp/ordering-delete_order.html"; + + let tw = TestWriterBuilder::default() + .file(FILE.into()) + .contents(DeleteOrderPage::page()) + .build() + .unwrap(); + tw.write(); + } +} diff --git a/web_ui/src/ordering/mod.rs b/web_ui/src/ordering/mod.rs new file mode 100644 index 0000000..0e6d015 --- /dev/null +++ b/web_ui/src/ordering/mod.rs @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +pub mod add_category; +pub mod add_customization; +pub mod add_kot; +pub mod add_line_item; +pub mod add_order; +pub mod add_product; +pub mod delete_kot; +pub mod delete_line_item; +pub mod delete_order; +pub mod update_category; +pub mod update_customization; +pub mod update_kot; +pub mod update_line_item; +pub mod update_order; +pub mod update_product; + +pub fn register_templates(t: &mut tera::Tera) { + for template in [ + add_category::ADD_CATEGORY_PAGE, + add_customization::ADD_CUSTOMIZATION_PAGE, + add_kot::ADD_KOT_PAGE, + add_line_item::ADD_LINE_ITEM_PAGE, + add_order::ADD_ORDER_PAGE, + add_product::ADD_PRODUCT_PAGE, + delete_kot::DELETE_KOT_PAGE, + delete_order::DELETE_ORDER_PAGE, + delete_line_item::DELETE_LINE_ITEM_PAGE, + update_category::UPDATE_CATEGORY_PAGE, + update_customization::UPDATE_CUSTOMIZATION_PAGE, + update_kot::UPDATE_KOT_PAGE, + update_line_item::UPDATE_LINE_ITEM_PAGE, + update_order::UPDATE_ORDER_PAGE, + update_product::UPDATE_PRODUCT_PAGE, + ] + .iter() + { + template.register(t).expect(template.name); + } +} diff --git a/web_ui/src/ordering/update_category.rs b/web_ui/src/ordering/update_category.rs new file mode 100644 index 0000000..ab7c0c0 --- /dev/null +++ b/web_ui/src/ordering/update_category.rs @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later +use std::cell::RefCell; + +use derive_builder::*; +use derive_more::*; +use serde::*; +use tera::Context; +use url::Url; + +pub use super::*; +use crate::utils::*; + +pub const UPDATE_CATEGORY_PAGE: TemplateFile = + TemplateFile::new("ordering.update_category", "ordering/update_category.html"); + +pub fn register_templates(t: &mut tera::Tera) { + UPDATE_CATEGORY_PAGE + .register(t) + .expect(UPDATE_CATEGORY_PAGE.name); +} + +pub struct UpdateCategoryPage { + ctx: RefCell, +} + +impl UpdateCategoryPage { + pub fn new() -> Self { + let mut ctx = context(); + + //ctx.insert(PAYLOAD_KEY, p); + + let ctx = RefCell::new(ctx); + Self { ctx } + } + + pub fn render(&self) -> String { + TEMPLATES + .render(UPDATE_CATEGORY_PAGE.name, &self.ctx.borrow()) + .unwrap() + } + + pub fn page() -> String { + let p = Self::new(); + p.render() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn render_page_and_write() { + const FILE: &str = "./tmp/ordering-update_category.html"; + + let tw = TestWriterBuilder::default() + .file(FILE.into()) + .contents(UpdateCategoryPage::page()) + .build() + .unwrap(); + tw.write(); + } +} diff --git a/web_ui/src/ordering/update_customization.rs b/web_ui/src/ordering/update_customization.rs new file mode 100644 index 0000000..f3a83dc --- /dev/null +++ b/web_ui/src/ordering/update_customization.rs @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later +use std::cell::RefCell; + +use derive_builder::*; +use derive_more::*; +use serde::*; +use tera::Context; +use url::Url; + +pub use super::*; +use crate::utils::*; + +pub const UPDATE_CUSTOMIZATION_PAGE: TemplateFile = TemplateFile::new( + "ordering.update_customization", + "ordering/update_customization.html", +); + +pub fn register_templates(t: &mut tera::Tera) { + UPDATE_CUSTOMIZATION_PAGE + .register(t) + .expect(UPDATE_CUSTOMIZATION_PAGE.name); +} + +pub struct UpdateCustomizationPage { + ctx: RefCell, +} + +impl UpdateCustomizationPage { + pub fn new() -> Self { + let mut ctx = context(); + + //ctx.insert(PAYLOAD_KEY, p); + + let ctx = RefCell::new(ctx); + Self { ctx } + } + + pub fn render(&self) -> String { + TEMPLATES + .render(UPDATE_CUSTOMIZATION_PAGE.name, &self.ctx.borrow()) + .unwrap() + } + + pub fn page() -> String { + let p = Self::new(); + p.render() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn render_page_and_write() { + const FILE: &str = "./tmp/ordering-update_customization.html"; + + let tw = TestWriterBuilder::default() + .file(FILE.into()) + .contents(UpdateCustomizationPage::page()) + .build() + .unwrap(); + tw.write(); + } +} diff --git a/web_ui/src/ordering/update_kot.rs b/web_ui/src/ordering/update_kot.rs new file mode 100644 index 0000000..ef743a7 --- /dev/null +++ b/web_ui/src/ordering/update_kot.rs @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later +use std::cell::RefCell; + +use derive_builder::*; +use derive_more::*; +use serde::*; +use tera::Context; +use url::Url; + +pub use super::*; +use crate::utils::*; + +pub const UPDATE_KOT_PAGE: TemplateFile = + TemplateFile::new("ordering.update_kot", "ordering/update_kot.html"); + +pub fn register_templates(t: &mut tera::Tera) { + UPDATE_KOT_PAGE.register(t).expect(UPDATE_KOT_PAGE.name); +} + +pub struct UpdateKotPage { + ctx: RefCell, +} + +impl UpdateKotPage { + pub fn new() -> Self { + let mut ctx = context(); + + //ctx.insert(PAYLOAD_KEY, p); + + let ctx = RefCell::new(ctx); + Self { ctx } + } + + pub fn render(&self) -> String { + TEMPLATES + .render(UPDATE_KOT_PAGE.name, &self.ctx.borrow()) + .unwrap() + } + + pub fn page() -> String { + let p = Self::new(); + p.render() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn render_page_and_write() { + const FILE: &str = "./tmp/ordering-update_kot.html"; + + let tw = TestWriterBuilder::default() + .file(FILE.into()) + .contents(UpdateKotPage::page()) + .build() + .unwrap(); + tw.write(); + } +} diff --git a/web_ui/src/ordering/update_line_item.rs b/web_ui/src/ordering/update_line_item.rs new file mode 100644 index 0000000..17aef27 --- /dev/null +++ b/web_ui/src/ordering/update_line_item.rs @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later +use std::cell::RefCell; + +use derive_builder::*; +use derive_more::*; +use serde::*; +use tera::Context; +use url::Url; + +pub use super::*; +use crate::utils::*; + +pub const UPDATE_LINE_ITEM_PAGE: TemplateFile = TemplateFile::new( + "ordering.update_line_item", + "ordering/update_line_item.html", +); + +pub fn register_templates(t: &mut tera::Tera) { + UPDATE_LINE_ITEM_PAGE + .register(t) + .expect(UPDATE_LINE_ITEM_PAGE.name); +} + +pub struct UpdateLineItemPage { + ctx: RefCell, +} + +impl UpdateLineItemPage { + pub fn new() -> Self { + let mut ctx = context(); + + //ctx.insert(PAYLOAD_KEY, p); + + let ctx = RefCell::new(ctx); + Self { ctx } + } + + pub fn render(&self) -> String { + TEMPLATES + .render(UPDATE_LINE_ITEM_PAGE.name, &self.ctx.borrow()) + .unwrap() + } + + pub fn page() -> String { + let p = Self::new(); + p.render() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn render_page_and_write() { + const FILE: &str = "./tmp/ordering-update_line_item.html"; + + let tw = TestWriterBuilder::default() + .file(FILE.into()) + .contents(UpdateLineItemPage::page()) + .build() + .unwrap(); + tw.write(); + } +} diff --git a/web_ui/src/ordering/update_order.rs b/web_ui/src/ordering/update_order.rs new file mode 100644 index 0000000..83072b3 --- /dev/null +++ b/web_ui/src/ordering/update_order.rs @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later +use std::cell::RefCell; + +use derive_builder::*; +use derive_more::*; +use serde::*; +use tera::Context; +use url::Url; + +pub use super::*; +use crate::utils::*; + +pub const UPDATE_ORDER_PAGE: TemplateFile = + TemplateFile::new("ordering.update_order", "ordering/update_order.html"); + +pub fn register_templates(t: &mut tera::Tera) { + UPDATE_ORDER_PAGE.register(t).expect(UPDATE_ORDER_PAGE.name); +} + +pub struct UpdateOrderPage { + ctx: RefCell, +} + +impl UpdateOrderPage { + pub fn new() -> Self { + let mut ctx = context(); + + //ctx.insert(PAYLOAD_KEY, p); + + let ctx = RefCell::new(ctx); + Self { ctx } + } + + pub fn render(&self) -> String { + TEMPLATES + .render(UPDATE_ORDER_PAGE.name, &self.ctx.borrow()) + .unwrap() + } + + pub fn page() -> String { + let p = Self::new(); + p.render() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn render_page_and_write() { + const FILE: &str = "./tmp/ordering-update_order.html"; + + let tw = TestWriterBuilder::default() + .file(FILE.into()) + .contents(UpdateOrderPage::page()) + .build() + .unwrap(); + tw.write(); + } +} diff --git a/web_ui/src/ordering/update_product.rs b/web_ui/src/ordering/update_product.rs new file mode 100644 index 0000000..d4f93a4 --- /dev/null +++ b/web_ui/src/ordering/update_product.rs @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later +use std::cell::RefCell; + +use derive_builder::*; +use derive_more::*; +use serde::*; +use tera::Context; +use url::Url; + +pub use super::*; +use crate::utils::*; + +pub const UPDATE_PRODUCT_PAGE: TemplateFile = + TemplateFile::new("ordering.update_product", "ordering/update_product.html"); + +pub fn register_templates(t: &mut tera::Tera) { + UPDATE_PRODUCT_PAGE + .register(t) + .expect(UPDATE_PRODUCT_PAGE.name); +} + +pub struct UpdateProductPage { + ctx: RefCell, +} + +impl UpdateProductPage { + pub fn new() -> Self { + let mut ctx = context(); + + //ctx.insert(PAYLOAD_KEY, p); + + let ctx = RefCell::new(ctx); + Self { ctx } + } + + pub fn render(&self) -> String { + TEMPLATES + .render(UPDATE_PRODUCT_PAGE.name, &self.ctx.borrow()) + .unwrap() + } + + pub fn page() -> String { + let p = Self::new(); + p.render() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn render_page_and_write() { + const FILE: &str = "./tmp/ordering-update_product.html"; + + let tw = TestWriterBuilder::default() + .file(FILE.into()) + .contents(UpdateProductPage::page()) + .build() + .unwrap(); + tw.write(); + } +} diff --git a/web_ui/src/utils.rs b/web_ui/src/utils.rs index 5fd6108..48d5ea9 100644 --- a/web_ui/src/utils.rs +++ b/web_ui/src/utils.rs @@ -49,6 +49,7 @@ lazy_static! { crate::identity::register_templates(&mut tera); crate::inventory::register_templates(&mut tera); crate::billing::register_templates(&mut tera); + crate::ordering::register_templates(&mut tera); tera }; } diff --git a/web_ui/templates/billing/delete_bill.html b/web_ui/templates/billing/delete_bill.html index 6868117..63c716d 100644 --- a/web_ui/templates/billing/delete_bill.html +++ b/web_ui/templates/billing/delete_bill.html @@ -6,7 +6,6 @@ Delete Bill | Vanikam - Token Refreshed
diff --git a/web_ui/templates/ordering/add_category.html b/web_ui/templates/ordering/add_category.html new file mode 100644 index 0000000..e69de29 diff --git a/web_ui/templates/ordering/add_customization.html b/web_ui/templates/ordering/add_customization.html new file mode 100644 index 0000000..e69de29 diff --git a/web_ui/templates/ordering/add_kot.html b/web_ui/templates/ordering/add_kot.html new file mode 100644 index 0000000..e69de29 diff --git a/web_ui/templates/ordering/add_line_item.html b/web_ui/templates/ordering/add_line_item.html new file mode 100644 index 0000000..e69de29 diff --git a/web_ui/templates/ordering/add_order.html b/web_ui/templates/ordering/add_order.html new file mode 100644 index 0000000..e69de29 diff --git a/web_ui/templates/ordering/add_product.html b/web_ui/templates/ordering/add_product.html new file mode 100644 index 0000000..e69de29 diff --git a/web_ui/templates/ordering/delete_kot.html b/web_ui/templates/ordering/delete_kot.html new file mode 100644 index 0000000..e69de29 diff --git a/web_ui/templates/ordering/delete_line_item.html b/web_ui/templates/ordering/delete_line_item.html new file mode 100644 index 0000000..e69de29 diff --git a/web_ui/templates/ordering/delete_order.html b/web_ui/templates/ordering/delete_order.html new file mode 100644 index 0000000..e69de29 diff --git a/web_ui/templates/ordering/update_category.html b/web_ui/templates/ordering/update_category.html new file mode 100644 index 0000000..e69de29 diff --git a/web_ui/templates/ordering/update_customization.html b/web_ui/templates/ordering/update_customization.html new file mode 100644 index 0000000..e69de29 diff --git a/web_ui/templates/ordering/update_kot.html b/web_ui/templates/ordering/update_kot.html new file mode 100644 index 0000000..e69de29 diff --git a/web_ui/templates/ordering/update_line_item.html b/web_ui/templates/ordering/update_line_item.html new file mode 100644 index 0000000..e69de29 diff --git a/web_ui/templates/ordering/update_order.html b/web_ui/templates/ordering/update_order.html new file mode 100644 index 0000000..e69de29 diff --git a/web_ui/templates/ordering/update_product.html b/web_ui/templates/ordering/update_product.html new file mode 100644 index 0000000..e69de29 From 8ad620476d69f9b68923df022f32f2f9a00a20d6 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Fri, 10 Jan 2025 21:54:05 +0530 Subject: [PATCH 58/63] feat: bootstrap inventory web adapter --- src/inventory/adapters/input/mod.rs | 1 + src/inventory/adapters/input/web/category.rs | 16 +++ src/inventory/adapters/input/web/errors.rs | 110 +++++++++++++++++++ src/inventory/adapters/input/web/mod.rs | 29 +++++ src/inventory/adapters/input/web/routes.rs | 34 ++++++ src/inventory/adapters/mod.rs | 2 +- src/inventory/adapters/types.rs | 4 +- 7 files changed, 193 insertions(+), 3 deletions(-) create mode 100644 src/inventory/adapters/input/web/category.rs create mode 100644 src/inventory/adapters/input/web/errors.rs create mode 100644 src/inventory/adapters/input/web/mod.rs create mode 100644 src/inventory/adapters/input/web/routes.rs diff --git a/src/inventory/adapters/input/mod.rs b/src/inventory/adapters/input/mod.rs index 56f60de..810dc4e 100644 --- a/src/inventory/adapters/input/mod.rs +++ b/src/inventory/adapters/input/mod.rs @@ -1,3 +1,4 @@ // SPDX-FileCopyrightText: 2024 Aravinth Manivannan // // SPDX-License-Identifier: AGPL-3.0-or-later +pub mod web; diff --git a/src/inventory/adapters/input/web/category.rs b/src/inventory/adapters/input/web/category.rs new file mode 100644 index 0000000..5aee4fd --- /dev/null +++ b/src/inventory/adapters/input/web/category.rs @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use actix_identity::Identity; +use actix_web::{get, http::header::ContentType, post, web, HttpRequest, HttpResponse, Responder}; +use derive_builder::Builder; +use serde::{Deserialize, Serialize}; +use url::Url; +use uuid::Uuid; + +use super::errors::*; +use super::types; +//use crate::utils::uuid::WebGetUUIDInterfaceObj; + +pub fn services(cfg: &mut web::ServiceConfig) {} diff --git a/src/inventory/adapters/input/web/errors.rs b/src/inventory/adapters/input/web/errors.rs new file mode 100644 index 0000000..7a5371f --- /dev/null +++ b/src/inventory/adapters/input/web/errors.rs @@ -0,0 +1,110 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use actix_web::http::StatusCode; +use actix_web::{HttpResponse, ResponseError}; +use derive_more::Display; +use serde::{Deserialize, Serialize}; + +use crate::inventory::application::services::errors::*; + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +struct ErrorResponse { + error: String, +} + +impl From for ErrorResponse { + fn from(value: WebError) -> Self { + ErrorResponse { + error: serde_json::to_string(&value).unwrap_or_else(|_| { + log::error!("Unable to serialize error"); + "Unable to serialize error".into() + }), + } + } +} + +#[derive(Debug, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +pub enum WebError { + InternalError, + BadRequest, + + DuplicateCategoryName, + DuplicateStoreName, + DuplicateProductName, + DuplicateCustomizationName, + DuplicateCustomizationID, + DuplicateStoreID, + DuplicateCategoryID, + DuplicateProductID, + ProductIDNotFound, + CategoryIDNotFound, + CustomizationIDNotFound, + StoreIDNotFound, +} + +impl From for WebError { + fn from(v: InventoryError) -> Self { + match v { + InventoryError::InternalError => Self::InternalError, + InventoryError::DuplicateCategoryName => Self::BadRequest, + InventoryError::DuplicateStoreName => Self::BadRequest, + InventoryError::DuplicateProductName => Self::BadRequest, + InventoryError::DuplicateCustomizationName => Self::BadRequest, + InventoryError::DuplicateCustomizationID => Self::InternalError, + InventoryError::DuplicateStoreID => Self::InternalError, + InventoryError::DuplicateCategoryID => Self::InternalError, + InventoryError::DuplicateProductID => Self::InternalError, + InventoryError::ProductIDNotFound => Self::BadRequest, + InventoryError::CategoryIDNotFound => Self::BadRequest, + InventoryError::CustomizationIDNotFound => Self::BadRequest, + InventoryError::StoreIDNotFound => Self::BadRequest, + } + } +} + +impl ResponseError for WebError { + fn status_code(&self) -> StatusCode { + match self { + Self::InternalError => StatusCode::INTERNAL_SERVER_ERROR, + Self::BadRequest => StatusCode::BAD_REQUEST, + + Self::DuplicateCategoryName => StatusCode::BAD_REQUEST, + Self::DuplicateStoreName => StatusCode::BAD_REQUEST, + Self::DuplicateProductName => StatusCode::BAD_REQUEST, + Self::DuplicateCustomizationName => StatusCode::BAD_REQUEST, + Self::DuplicateCustomizationID => StatusCode::INTERNAL_SERVER_ERROR, + Self::DuplicateStoreID => StatusCode::INTERNAL_SERVER_ERROR, + Self::DuplicateCategoryID => StatusCode::INTERNAL_SERVER_ERROR, + Self::DuplicateProductID => StatusCode::INTERNAL_SERVER_ERROR, + Self::ProductIDNotFound => StatusCode::BAD_REQUEST, + Self::CategoryIDNotFound => StatusCode::BAD_REQUEST, + Self::CustomizationIDNotFound => StatusCode::BAD_REQUEST, + Self::StoreIDNotFound => StatusCode::BAD_REQUEST, + } + } + + fn error_response(&self) -> actix_web::HttpResponse { + let e: ErrorResponse = self.clone().into(); + match self { + Self::InternalError => HttpResponse::InternalServerError().json(e), + Self::BadRequest => HttpResponse::BadRequest().json(e), + + Self::DuplicateCategoryName => HttpResponse::BadRequest().json(e), + Self::DuplicateStoreName => HttpResponse::BadRequest().json(e), + Self::DuplicateProductName => HttpResponse::BadRequest().json(e), + Self::DuplicateCustomizationName => HttpResponse::BadRequest().json(e), + Self::DuplicateCustomizationID => HttpResponse::InternalServerError().json(e), + Self::DuplicateStoreID => HttpResponse::InternalServerError().json(e), + Self::DuplicateCategoryID => HttpResponse::InternalServerError().json(e), + Self::DuplicateProductID => HttpResponse::InternalServerError().json(e), + Self::ProductIDNotFound => HttpResponse::BadRequest().json(e), + Self::CategoryIDNotFound => HttpResponse::BadRequest().json(e), + Self::CustomizationIDNotFound => HttpResponse::BadRequest().json(e), + Self::StoreIDNotFound => HttpResponse::BadRequest().json(e), + } + } +} + +pub type WebJsonRepsonse = Result; diff --git a/src/inventory/adapters/input/web/mod.rs b/src/inventory/adapters/input/web/mod.rs new file mode 100644 index 0000000..88a4b6e --- /dev/null +++ b/src/inventory/adapters/input/web/mod.rs @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use std::sync::Arc; + +use actix_web::web; + +use crate::inventory::adapters::types; + +//mod employee; +mod category; +mod errors; +mod routes; + +pub use errors::WebJsonRepsonse; + +pub use routes::RoutesRepository; + +pub fn load_ctx() -> impl FnOnce(&mut web::ServiceConfig) { + let routes = types::WebInventoryRoutesRepository::new(Arc::new(RoutesRepository::default())); + + let f = move |cfg: &mut web::ServiceConfig| { + cfg.app_data(routes); + cfg.configure(category::services); + }; + + Box::new(f) +} diff --git a/src/inventory/adapters/input/web/routes.rs b/src/inventory/adapters/input/web/routes.rs new file mode 100644 index 0000000..4bbce66 --- /dev/null +++ b/src/inventory/adapters/input/web/routes.rs @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use url::Url; +use uuid::Uuid; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct RoutesRepository { + add_category: String, + add_product: String, + add_customization: String, + update_product: String, + update_customization: String, + update_category: String, +} + +impl Default for RoutesRepository { + fn default() -> Self { + Self { + add_category: "/inventory/category/add".into(), + update_category: "/inventory/{category_uuid}/update".into(), + + add_product: "/inventory/{category_uuid}/product/add".into(), + update_product: "/inventory/{category_uuid}/update".into(), + + add_customization: "/inventory/{category_uuid}/{product_uuid}/customization/add".into(), + update_customization: + "/inventory/{category_uuid}/{product_uuid}/{customization_uuid}/update".into(), + } + } +} + +impl RoutesRepository {} diff --git a/src/inventory/adapters/mod.rs b/src/inventory/adapters/mod.rs index 622ecf3..01bdf7b 100644 --- a/src/inventory/adapters/mod.rs +++ b/src/inventory/adapters/mod.rs @@ -63,7 +63,7 @@ pub fn load_adapters(pool: PgPool, settings: Settings) -> impl FnOnce(&mut web:: )); let f = move |cfg: &mut web::ServiceConfig| { - // cfg.configure(input::web::load_ctx()); + cfg.configure(input::web::load_ctx()); cfg.app_data(Data::new(category_cqrs_query.clone())); cfg.app_data(Data::new(product_cqrs_query.clone())); cfg.app_data(Data::new(customization_cqrs_query.clone())); diff --git a/src/inventory/adapters/types.rs b/src/inventory/adapters/types.rs index 1463fcf..a2879c4 100644 --- a/src/inventory/adapters/types.rs +++ b/src/inventory/adapters/types.rs @@ -15,7 +15,7 @@ use postgres_es::PostgresCqrs; use crate::inventory::{ adapters::{ - // input::web::RoutesRepository, + input::web::RoutesRepository, output::db::postgres::{ category_view::CategoryView, customization_view::CustomizationView, product_view::ProductView, store_view::StoreView, InventoryDBPostgresAdapter, @@ -28,7 +28,7 @@ use crate::inventory::{ }, }; -//pub type WebInventoryRoutesRepository = Data>; +pub type WebInventoryRoutesRepository = Data>; pub type WebInventoryCqrsExec = Data>; From 354188d8b00afd8e6b6ffe972334e10ccd6afbf4 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Fri, 10 Jan 2025 21:54:40 +0530 Subject: [PATCH 59/63] feat: bootstrap ordering web adapter --- src/ordering/adapters/input/mod.rs | 1 + src/ordering/adapters/input/web/errors.rs | 114 ++++++++++++++++++++++ src/ordering/adapters/input/web/mod.rs | 26 +++++ src/ordering/adapters/input/web/routes.rs | 17 ++++ src/ordering/adapters/mod.rs | 2 +- src/ordering/adapters/types.rs | 4 +- 6 files changed, 161 insertions(+), 3 deletions(-) create mode 100644 src/ordering/adapters/input/web/errors.rs create mode 100644 src/ordering/adapters/input/web/mod.rs create mode 100644 src/ordering/adapters/input/web/routes.rs diff --git a/src/ordering/adapters/input/mod.rs b/src/ordering/adapters/input/mod.rs index 56f60de..810dc4e 100644 --- a/src/ordering/adapters/input/mod.rs +++ b/src/ordering/adapters/input/mod.rs @@ -1,3 +1,4 @@ // SPDX-FileCopyrightText: 2024 Aravinth Manivannan // // SPDX-License-Identifier: AGPL-3.0-or-later +pub mod web; diff --git a/src/ordering/adapters/input/web/errors.rs b/src/ordering/adapters/input/web/errors.rs new file mode 100644 index 0000000..a8b19fa --- /dev/null +++ b/src/ordering/adapters/input/web/errors.rs @@ -0,0 +1,114 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use actix_web::http::StatusCode; +use actix_web::{HttpResponse, ResponseError}; +use derive_more::Display; +use serde::{Deserialize, Serialize}; + +use crate::ordering::application::services::errors::*; + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +struct ErrorResponse { + error: String, +} + +impl From for ErrorResponse { + fn from(value: WebError) -> Self { + ErrorResponse { + error: serde_json::to_string(&value).unwrap_or_else(|_| { + log::error!("Unable to serialize error"); + "Unable to serialize error".into() + }), + } + } +} + +#[derive(Debug, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +pub enum WebError { + InternalError, + BadRequest, + + LineItemIDNotFound, + OrderIDNotFound, + KotIDNotFound, + DuplicateStoreName, + StoreIDNotFound, + CategoryIDNotFound, + DuplicateCategoryName, + DuplicateProductName, + ProductIDNotFound, + DuplicateCustomizationName, + CustomizationIDNotFound, +} + +impl From for WebError { + fn from(v: OrderingError) -> Self { + match v { + OrderingError::InternalError => Self::InternalError, + + OrderingError::LineItemIDNotFound => Self::LineItemIDNotFound, + OrderingError::OrderIDNotFound => Self::OrderIDNotFound, + OrderingError::KotIDNotFound => Self::KotIDNotFound, + OrderingError::DuplicateStoreName => Self::DuplicateStoreName, + OrderingError::StoreIDNotFound => Self::StoreIDNotFound, + OrderingError::CategoryIDNotFound => Self::CategoryIDNotFound, + OrderingError::DuplicateCategoryName => Self::DuplicateCategoryName, + OrderingError::DuplicateProductName => Self::DuplicateProductName, + OrderingError::ProductIDNotFound => Self::ProductIDNotFound, + OrderingError::DuplicateCustomizationName => Self::DuplicateCustomizationName, + OrderingError::CustomizationIDNotFound => Self::CustomizationIDNotFound, + OrderingError::DuplicateKotID + | OrderingError::DuplicateKotID + | OrderingError::DuplicateOrderID + | OrderingError::DuplicateStoreID + | OrderingError::DuplicateCustomizationID + | OrderingError::DuplicateProductID + | OrderingError::DuplicateCategoryID + | OrderingError::DuplicateLineItemID => Self::InternalError, + } + } +} + +impl ResponseError for WebError { + fn status_code(&self) -> StatusCode { + match self { + Self::InternalError => StatusCode::INTERNAL_SERVER_ERROR, + Self::BadRequest => StatusCode::BAD_REQUEST, + Self::LineItemIDNotFound => StatusCode::NOT_FOUND, + Self::OrderIDNotFound => StatusCode::NOT_FOUND, + Self::KotIDNotFound => StatusCode::NOT_FOUND, + Self::DuplicateStoreName => StatusCode::BAD_REQUEST, + Self::StoreIDNotFound => StatusCode::NOT_FOUND, + Self::CategoryIDNotFound => StatusCode::NOT_FOUND, + Self::DuplicateCategoryName => StatusCode::BAD_REQUEST, + Self::DuplicateProductName => StatusCode::BAD_REQUEST, + Self::ProductIDNotFound => StatusCode::NOT_FOUND, + Self::DuplicateCustomizationName => StatusCode::BAD_REQUEST, + Self::CustomizationIDNotFound => StatusCode::NOT_FOUND, + } + } + + fn error_response(&self) -> actix_web::HttpResponse { + let e: ErrorResponse = self.clone().into(); + match self { + Self::InternalError => HttpResponse::InternalServerError().json(e), + Self::BadRequest => HttpResponse::BadRequest().json(e), + + Self::LineItemIDNotFound => HttpResponse::NotFound().json(e), + Self::OrderIDNotFound => HttpResponse::NotFound().json(e), + Self::KotIDNotFound => HttpResponse::NotFound().json(e), + Self::DuplicateStoreName => HttpResponse::BadRequest().json(e), + Self::StoreIDNotFound => HttpResponse::NotFound().json(e), + Self::CategoryIDNotFound => HttpResponse::NotFound().json(e), + Self::DuplicateCategoryName => HttpResponse::BadRequest().json(e), + Self::DuplicateProductName => HttpResponse::BadRequest().json(e), + Self::ProductIDNotFound => HttpResponse::NotFound().json(e), + Self::DuplicateCustomizationName => HttpResponse::BadRequest().json(e), + Self::CustomizationIDNotFound => HttpResponse::NotFound().json(e), + } + } +} + +pub type WebJsonRepsonse = Result; diff --git a/src/ordering/adapters/input/web/mod.rs b/src/ordering/adapters/input/web/mod.rs new file mode 100644 index 0000000..9bf19d3 --- /dev/null +++ b/src/ordering/adapters/input/web/mod.rs @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use std::sync::Arc; + +use actix_web::web; + +use crate::ordering::adapters::types; + +mod errors; +mod routes; + +pub use errors::WebJsonRepsonse; + +pub use routes::RoutesRepository; + +pub fn load_ctx() -> impl FnOnce(&mut web::ServiceConfig) { + let routes = types::WebOrderingRoutesRepository::new(Arc::new(RoutesRepository::default())); + + let f = move |cfg: &mut web::ServiceConfig| { + cfg.app_data(routes); + }; + + Box::new(f) +} diff --git a/src/ordering/adapters/input/web/routes.rs b/src/ordering/adapters/input/web/routes.rs new file mode 100644 index 0000000..2c2d1bd --- /dev/null +++ b/src/ordering/adapters/input/web/routes.rs @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use url::Url; +use uuid::Uuid; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct RoutesRepository {} + +impl Default for RoutesRepository { + fn default() -> Self { + Self {} + } +} + +impl RoutesRepository {} diff --git a/src/ordering/adapters/mod.rs b/src/ordering/adapters/mod.rs index c6bf8cf..d301400 100644 --- a/src/ordering/adapters/mod.rs +++ b/src/ordering/adapters/mod.rs @@ -68,7 +68,7 @@ pub fn load_adapters(pool: PgPool, settings: Settings) -> impl FnOnce(&mut web:: )); let f = move |cfg: &mut web::ServiceConfig| { - //cfg.configure(input::web::load_ctx()); + cfg.configure(input::web::load_ctx()); cfg.app_data(Data::new(category_cqrs_query.clone())); cfg.app_data(Data::new(product_cqrs_query.clone())); cfg.app_data(Data::new(customization_cqrs_query.clone())); diff --git a/src/ordering/adapters/types.rs b/src/ordering/adapters/types.rs index 5b1c66b..b02e145 100644 --- a/src/ordering/adapters/types.rs +++ b/src/ordering/adapters/types.rs @@ -15,7 +15,7 @@ use postgres_es::PostgresCqrs; use crate::ordering::{ adapters::{ - // input::web::RoutesRepository, + input::web::RoutesRepository, output::db::{ category_view::CategoryView, customization_view::CustomizationView, kot_view::KotView, line_item_view::LineItemView, order_view::OrderView, product_view::ProductView, @@ -30,7 +30,7 @@ use crate::ordering::{ }, }; -//pub type WebOrderingRoutesRepository = Data>; +pub type WebOrderingRoutesRepository = Data>; pub type OrderingOrderCqrsExec = Arc>; pub type OrderingOrderCqrsView = Arc>; From 8899ef7ac1e97c522f2600b09cc79c6f54b703af Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Sat, 11 Jan 2025 14:04:53 +0530 Subject: [PATCH 60/63] feat: codegen script to generate handler stubs --- utils/generate_actix_handler.sh | 82 +++++++++++++++++++++++++++++ utils/web_handler_test_generator.sh | 4 +- 2 files changed, 84 insertions(+), 2 deletions(-) create mode 100755 utils/generate_actix_handler.sh diff --git a/utils/generate_actix_handler.sh b/utils/generate_actix_handler.sh new file mode 100755 index 0000000..32f12c2 --- /dev/null +++ b/utils/generate_actix_handler.sh @@ -0,0 +1,82 @@ +#!/bin/bash + +help() { + echo "Usage: generate_actix_handler.sh + + + +