From cddba71eb56e78e2c1051d7a1cbbba07bb2d1989 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Mon, 16 Sep 2024 14:54:23 +0530 Subject: [PATCH] feat: import inventory services&domain obj to implement pantry --- ...768dc19975d19c93adb222ee8f8666d28cef2.json | 23 ++ ...922537b577f2e7aeafd0708578bf62f8423db.json | 23 ++ ...833c7763ff3e1eb368456fd41e1208863b9b2.json | 19 + ...6c4bd40ee98d9d72f7a5e7fc8338d73468fe5.json | 22 ++ ...e274c0b064a370dda1c436c0e598ffd6ba599.json | 28 ++ ...60a8476780635aff1065d57097332a2dd8fd0.json | 22 ++ ...01889c7f2c4cc7eda9e6a7c0f501c103ab9ac.json | 46 +++ ...6da62e933651ffbe39ee27e5e3dd078d86e05.json | 28 ++ ...c6671f35d27bde39be34677a6b74228bf3e97.json | 19 + ...c6a258b3017a710f8c3e249d73a5d980e2115.json | 22 ++ ...33112bf6ea3eb662460776fcaa625046b7a0d.json | 22 ++ ...b976d0756b470f448394d4832866b5a209ecd.json | 28 ++ ...98d78af6a3fc78f860d0e5fc677622a1db9e3.json | 22 ++ ...10e60f44de01ccb095e63542c11e5b2386ee5.json | 46 +++ ...a75a065ad6a9a7a74432bcc3b0d6802af1eb2.json | 46 +++ ...0460dc3be59be1f8f5588bdc3d0f4489eb613.json | 19 + ...03226d1f03935d73dc3bfc1af490df4726c78.json | 40 +++ ...14f616f0f8987c81a0d1d3c8d923b580b2ae1.json | 18 + ...656d863de0c9140054de66eef32491ace9ddb.json | 19 + ...b2eb6ad8515e68d5520013634415109309e6e.json | 18 + ...4730963cb78e0d5954cb0eb4e0b5afb3f36d5.json | 23 ++ ...240914110303_cqrs_ordering_store_query.sql | 16 + ...914111900_cqrs_ordering_category_query.sql | 17 + ...1520_cqrs_ordering_customization_query.sql | 14 + ...0914121604_cqrs_ordering_product_query.sql | 29 ++ .../output/db/postgres/customization_view.rs | 20 +- .../output/db/postgres/product_view.rs | 2 +- src/inventory/domain/add_product_command.rs | 2 +- src/inventory/domain/product_added_event.rs | 2 +- src/inventory/domain/product_aggregate.rs | 2 +- .../domain/update_product_command.rs | 2 +- .../adapters/output/db/category_exists.rs | 78 ++++ .../adapters/output/db/category_id_exists.rs | 71 ++++ .../db/category_name_exists_for_store.rs | 102 ++++++ .../adapters/output/db/category_view.rs | 201 +++++++++++ .../output/db/customization_id_exists.rs | 99 ++++++ .../customization_name_exists_for_product.rs | 112 ++++++ .../adapters/output/db/customization_view.rs | 221 ++++++++++++ .../adapters/output/db/get_category.rs | 98 ++++++ src/ordering/adapters/output/db/mod.rs | 12 + .../adapters/output/db/product_id_exists.rs | 116 ++++++ .../db/product_name_exists_for_category.rs | 91 +++++ .../adapters/output/db/product_view.rs | 332 ++++++++++++++++++ .../adapters/output/db/store_id_exists.rs | 87 +++++ .../adapters/output/db/store_name_exists.rs | 81 +++++ src/ordering/adapters/output/db/store_view.rs | 302 ++++++++++++++++ .../port/output/db/category_id_exists.rs | 55 +++ .../db/category_name_exists_for_store.rs | 60 ++++ .../port/output/db/customization_id_exists.rs | 57 +++ .../customization_name_exists_for_product.rs | 65 ++++ .../application/port/output/db/errors.rs | 12 + .../port/output/db/get_category.rs | 44 +++ .../application/port/output/db/mod.rs | 9 + .../port/output/db/product_id_exists.rs | 53 +++ .../db/product_name_exists_for_category.rs | 61 ++++ .../port/output/db/store_id_exists.rs | 55 +++ .../port/output/db/store_name_exists.rs | 54 +++ .../full_text_search/add_product_to_store.rs | 46 +++ .../port/output/full_text_search/errors.rs | 11 + .../port/output/full_text_search/mod.rs | 7 + .../output/full_text_search/update_product.rs | 3 + src/ordering/application/port/output/mod.rs | 1 + .../services/add_category_service.rs | 211 +++++++++++ .../services/add_customization_service.rs | 191 ++++++++++ .../services/add_product_service.rs | 227 ++++++++++++ .../application/services/add_store_service.rs | 149 ++++++++ src/ordering/application/services/errors.rs | 44 ++- src/ordering/application/services/mod.rs | 48 +++ .../services/remove_category_service.rs | 3 + .../services/remove_product_service.rs | 3 + .../services/update_category_service.rs | 198 +++++++++++ .../services/update_customization_service.rs | 210 +++++++++++ .../services/update_product_service.rs | 206 +++++++++++ .../services/update_store_service.rs | 146 ++++++++ src/ordering/domain/add_category_command.rs | 90 +++++ .../domain/add_customization_command.rs | 91 +++++ src/ordering/domain/add_product_command.rs | 256 ++++++++++++++ src/ordering/domain/add_store_command.rs | 82 +++++ src/ordering/domain/category_added_event.rs | 19 + src/ordering/domain/category_aggregate.rs | 156 ++++++++ src/ordering/domain/category_updated_event.rs | 45 +++ src/ordering/domain/commands.rs | 18 +- .../domain/customization_added_event.rs | 43 +++ .../domain/customization_aggregate.rs | 143 ++++++++ .../domain/customization_updated_event.rs | 49 +++ src/ordering/domain/events.rs | 30 +- src/ordering/domain/mod.rs | 21 ++ src/ordering/domain/pantry_aggregate.rs | 3 + src/ordering/domain/product_added_event.rs | 51 +++ src/ordering/domain/product_aggregate.rs | 166 +++++++++ src/ordering/domain/product_updated_event.rs | 53 +++ src/ordering/domain/store_added_event.rs | 18 + src/ordering/domain/store_aggregate.rs | 146 ++++++++ src/ordering/domain/store_updated_event.rs | 43 +++ .../domain/update_category_command.rs | 117 ++++++ .../domain/update_customization_command.rs | 101 ++++++ src/ordering/domain/update_product_command.rs | 266 ++++++++++++++ src/ordering/domain/update_store_command.rs | 127 +++++++ src/types/mod.rs | 2 +- 99 files changed, 7003 insertions(+), 24 deletions(-) create mode 100644 .sqlx/query-0d2d5392e3cb1d5f7b164e93ccd768dc19975d19c93adb222ee8f8666d28cef2.json create mode 100644 .sqlx/query-2f5ec6062904e7124f56a237e80922537b577f2e7aeafd0708578bf62f8423db.json create mode 100644 .sqlx/query-37334f91e68e9d95bc9675d98be833c7763ff3e1eb368456fd41e1208863b9b2.json create mode 100644 .sqlx/query-4abd37d29572915fb167833c0496c4bd40ee98d9d72f7a5e7fc8338d73468fe5.json create mode 100644 .sqlx/query-5568e35d9a0b7acb1c0b1f65015e274c0b064a370dda1c436c0e598ffd6ba599.json create mode 100644 .sqlx/query-5ca085f34eea52fb8c78558149a60a8476780635aff1065d57097332a2dd8fd0.json create mode 100644 .sqlx/query-5d3972a89f5d64e0c9cbe3a086401889c7f2c4cc7eda9e6a7c0f501c103ab9ac.json create mode 100644 .sqlx/query-6f06c0f4b71f0458229dad370d46da62e933651ffbe39ee27e5e3dd078d86e05.json create mode 100644 .sqlx/query-7cb52847f00e985c9475485de63c6671f35d27bde39be34677a6b74228bf3e97.json create mode 100644 .sqlx/query-81de3abeb5dcbe7a87e20bab82dc6a258b3017a710f8c3e249d73a5d980e2115.json create mode 100644 .sqlx/query-89387ed4e97c8c957e576b2753533112bf6ea3eb662460776fcaa625046b7a0d.json create mode 100644 .sqlx/query-8f83767550a0efbb13020cff6b0b976d0756b470f448394d4832866b5a209ecd.json create mode 100644 .sqlx/query-9e05e649d2cc489f8870e888d7a98d78af6a3fc78f860d0e5fc677622a1db9e3.json create mode 100644 .sqlx/query-cae7149b31d542cc01d263d682510e60f44de01ccb095e63542c11e5b2386ee5.json create mode 100644 .sqlx/query-cfab77a90a7a7f3d74b739442d9a75a065ad6a9a7a74432bcc3b0d6802af1eb2.json create mode 100644 .sqlx/query-d0580ff6dc77150cb00186302f20460dc3be59be1f8f5588bdc3d0f4489eb613.json create mode 100644 .sqlx/query-d5eeb278addc02f44a2a799a0ba03226d1f03935d73dc3bfc1af490df4726c78.json create mode 100644 .sqlx/query-d873aaab136d804c0c0c1744d6914f616f0f8987c81a0d1d3c8d923b580b2ae1.json create mode 100644 .sqlx/query-d896f6ffb486efad5ed10a9c824656d863de0c9140054de66eef32491ace9ddb.json create mode 100644 .sqlx/query-d9c625876e7d398cb48c6278e69b2eb6ad8515e68d5520013634415109309e6e.json create mode 100644 .sqlx/query-f72e06dd5ed9f4b5943c53d84cc4730963cb78e0d5954cb0eb4e0b5afb3f36d5.json create mode 100644 migrations/20240914110303_cqrs_ordering_store_query.sql create mode 100644 migrations/20240914111900_cqrs_ordering_category_query.sql create mode 100644 migrations/20240914121520_cqrs_ordering_customization_query.sql create mode 100644 migrations/20240914121604_cqrs_ordering_product_query.sql create mode 100644 src/ordering/adapters/output/db/category_exists.rs create mode 100644 src/ordering/adapters/output/db/category_id_exists.rs create mode 100644 src/ordering/adapters/output/db/category_name_exists_for_store.rs create mode 100644 src/ordering/adapters/output/db/category_view.rs create mode 100644 src/ordering/adapters/output/db/customization_id_exists.rs create mode 100644 src/ordering/adapters/output/db/customization_name_exists_for_product.rs create mode 100644 src/ordering/adapters/output/db/customization_view.rs create mode 100644 src/ordering/adapters/output/db/get_category.rs create mode 100644 src/ordering/adapters/output/db/product_id_exists.rs create mode 100644 src/ordering/adapters/output/db/product_name_exists_for_category.rs create mode 100644 src/ordering/adapters/output/db/product_view.rs create mode 100644 src/ordering/adapters/output/db/store_id_exists.rs create mode 100644 src/ordering/adapters/output/db/store_name_exists.rs create mode 100644 src/ordering/adapters/output/db/store_view.rs create mode 100644 src/ordering/application/port/output/db/category_id_exists.rs create mode 100644 src/ordering/application/port/output/db/category_name_exists_for_store.rs create mode 100644 src/ordering/application/port/output/db/customization_id_exists.rs create mode 100644 src/ordering/application/port/output/db/customization_name_exists_for_product.rs create mode 100644 src/ordering/application/port/output/db/get_category.rs create mode 100644 src/ordering/application/port/output/db/product_id_exists.rs create mode 100644 src/ordering/application/port/output/db/product_name_exists_for_category.rs create mode 100644 src/ordering/application/port/output/db/store_id_exists.rs create mode 100644 src/ordering/application/port/output/db/store_name_exists.rs create mode 100644 src/ordering/application/port/output/full_text_search/add_product_to_store.rs create mode 100644 src/ordering/application/port/output/full_text_search/errors.rs create mode 100644 src/ordering/application/port/output/full_text_search/mod.rs create mode 100644 src/ordering/application/port/output/full_text_search/update_product.rs create mode 100644 src/ordering/application/services/add_category_service.rs create mode 100644 src/ordering/application/services/add_customization_service.rs create mode 100644 src/ordering/application/services/add_product_service.rs create mode 100644 src/ordering/application/services/add_store_service.rs create mode 100644 src/ordering/application/services/remove_category_service.rs create mode 100644 src/ordering/application/services/remove_product_service.rs create mode 100644 src/ordering/application/services/update_category_service.rs create mode 100644 src/ordering/application/services/update_customization_service.rs create mode 100644 src/ordering/application/services/update_product_service.rs create mode 100644 src/ordering/application/services/update_store_service.rs create mode 100644 src/ordering/domain/add_category_command.rs create mode 100644 src/ordering/domain/add_customization_command.rs create mode 100644 src/ordering/domain/add_product_command.rs create mode 100644 src/ordering/domain/add_store_command.rs create mode 100644 src/ordering/domain/category_added_event.rs create mode 100644 src/ordering/domain/category_aggregate.rs create mode 100644 src/ordering/domain/category_updated_event.rs create mode 100644 src/ordering/domain/customization_added_event.rs create mode 100644 src/ordering/domain/customization_aggregate.rs create mode 100644 src/ordering/domain/customization_updated_event.rs create mode 100644 src/ordering/domain/pantry_aggregate.rs create mode 100644 src/ordering/domain/product_added_event.rs create mode 100644 src/ordering/domain/product_aggregate.rs create mode 100644 src/ordering/domain/product_updated_event.rs create mode 100644 src/ordering/domain/store_added_event.rs create mode 100644 src/ordering/domain/store_aggregate.rs create mode 100644 src/ordering/domain/store_updated_event.rs create mode 100644 src/ordering/domain/update_category_command.rs create mode 100644 src/ordering/domain/update_customization_command.rs create mode 100644 src/ordering/domain/update_product_command.rs create mode 100644 src/ordering/domain/update_store_command.rs diff --git a/.sqlx/query-0d2d5392e3cb1d5f7b164e93ccd768dc19975d19c93adb222ee8f8666d28cef2.json b/.sqlx/query-0d2d5392e3cb1d5f7b164e93ccd768dc19975d19c93adb222ee8f8666d28cef2.json new file mode 100644 index 0000000..a8c37b2 --- /dev/null +++ b/.sqlx/query-0d2d5392e3cb1d5f7b164e93ccd768dc19975d19c93adb222ee8f8666d28cef2.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT EXISTS (\n SELECT 1\n FROM cqrs_ordering_category_query\n WHERE\n name = $1\n AND\n store_id = $2\n AND\n deleted = false\n );", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text", + "Uuid" + ] + }, + "nullable": [ + null + ] + }, + "hash": "0d2d5392e3cb1d5f7b164e93ccd768dc19975d19c93adb222ee8f8666d28cef2" +} diff --git a/.sqlx/query-2f5ec6062904e7124f56a237e80922537b577f2e7aeafd0708578bf62f8423db.json b/.sqlx/query-2f5ec6062904e7124f56a237e80922537b577f2e7aeafd0708578bf62f8423db.json new file mode 100644 index 0000000..ec0523a --- /dev/null +++ b/.sqlx/query-2f5ec6062904e7124f56a237e80922537b577f2e7aeafd0708578bf62f8423db.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT EXISTS (\n SELECT 1\n FROM cqrs_ordering_product_customizations_query\n WHERE\n name = $1\n AND\n product_id = $2\n AND\n deleted = false\n );", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text", + "Uuid" + ] + }, + "nullable": [ + null + ] + }, + "hash": "2f5ec6062904e7124f56a237e80922537b577f2e7aeafd0708578bf62f8423db" +} diff --git a/.sqlx/query-37334f91e68e9d95bc9675d98be833c7763ff3e1eb368456fd41e1208863b9b2.json b/.sqlx/query-37334f91e68e9d95bc9675d98be833c7763ff3e1eb368456fd41e1208863b9b2.json new file mode 100644 index 0000000..cbb567a --- /dev/null +++ b/.sqlx/query-37334f91e68e9d95bc9675d98be833c7763ff3e1eb368456fd41e1208863b9b2.json @@ -0,0 +1,19 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO cqrs_ordering_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": "37334f91e68e9d95bc9675d98be833c7763ff3e1eb368456fd41e1208863b9b2" +} diff --git a/.sqlx/query-4abd37d29572915fb167833c0496c4bd40ee98d9d72f7a5e7fc8338d73468fe5.json b/.sqlx/query-4abd37d29572915fb167833c0496c4bd40ee98d9d72f7a5e7fc8338d73468fe5.json new file mode 100644 index 0000000..351d651 --- /dev/null +++ b/.sqlx/query-4abd37d29572915fb167833c0496c4bd40ee98d9d72f7a5e7fc8338d73468fe5.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT EXISTS (\n SELECT 1\n FROM cqrs_ordering_store_query\n WHERE\n store_id = $1\n );", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + null + ] + }, + "hash": "4abd37d29572915fb167833c0496c4bd40ee98d9d72f7a5e7fc8338d73468fe5" +} diff --git a/.sqlx/query-5568e35d9a0b7acb1c0b1f65015e274c0b064a370dda1c436c0e598ffd6ba599.json b/.sqlx/query-5568e35d9a0b7acb1c0b1f65015e274c0b064a370dda1c436c0e598ffd6ba599.json new file mode 100644 index 0000000..637d01a --- /dev/null +++ b/.sqlx/query-5568e35d9a0b7acb1c0b1f65015e274c0b064a370dda1c436c0e598ffd6ba599.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT \n store_id, version\n FROM\n cqrs_ordering_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": "5568e35d9a0b7acb1c0b1f65015e274c0b064a370dda1c436c0e598ffd6ba599" +} diff --git a/.sqlx/query-5ca085f34eea52fb8c78558149a60a8476780635aff1065d57097332a2dd8fd0.json b/.sqlx/query-5ca085f34eea52fb8c78558149a60a8476780635aff1065d57097332a2dd8fd0.json new file mode 100644 index 0000000..6103c0e --- /dev/null +++ b/.sqlx/query-5ca085f34eea52fb8c78558149a60a8476780635aff1065d57097332a2dd8fd0.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT EXISTS (\n SELECT 1\n FROM cqrs_ordering_product_query\n WHERE\n product_id = $1\n );", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + null + ] + }, + "hash": "5ca085f34eea52fb8c78558149a60a8476780635aff1065d57097332a2dd8fd0" +} diff --git a/.sqlx/query-5d3972a89f5d64e0c9cbe3a086401889c7f2c4cc7eda9e6a7c0f501c103ab9ac.json b/.sqlx/query-5d3972a89f5d64e0c9cbe3a086401889c7f2c4cc7eda9e6a7c0f501c103ab9ac.json new file mode 100644 index 0000000..01beb70 --- /dev/null +++ b/.sqlx/query-5d3972a89f5d64e0c9cbe3a086401889c7f2c4cc7eda9e6a7c0f501c103ab9ac.json @@ -0,0 +1,46 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT \n name, description, category_id, store_id, deleted\n FROM\n cqrs_ordering_category_query\n WHERE\n category_id = $1;", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "description", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "category_id", + "type_info": "Uuid" + }, + { + "ordinal": 3, + "name": "store_id", + "type_info": "Uuid" + }, + { + "ordinal": 4, + "name": "deleted", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + true, + false, + false, + false + ] + }, + "hash": "5d3972a89f5d64e0c9cbe3a086401889c7f2c4cc7eda9e6a7c0f501c103ab9ac" +} diff --git a/.sqlx/query-6f06c0f4b71f0458229dad370d46da62e933651ffbe39ee27e5e3dd078d86e05.json b/.sqlx/query-6f06c0f4b71f0458229dad370d46da62e933651ffbe39ee27e5e3dd078d86e05.json new file mode 100644 index 0000000..d68a564 --- /dev/null +++ b/.sqlx/query-6f06c0f4b71f0458229dad370d46da62e933651ffbe39ee27e5e3dd078d86e05.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT \n customization_id, version\n FROM\n cqrs_ordering_product_customizations_query\n WHERE\n customization_id = $1;", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "customization_id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "version", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false + ] + }, + "hash": "6f06c0f4b71f0458229dad370d46da62e933651ffbe39ee27e5e3dd078d86e05" +} diff --git a/.sqlx/query-7cb52847f00e985c9475485de63c6671f35d27bde39be34677a6b74228bf3e97.json b/.sqlx/query-7cb52847f00e985c9475485de63c6671f35d27bde39be34677a6b74228bf3e97.json new file mode 100644 index 0000000..1a64627 --- /dev/null +++ b/.sqlx/query-7cb52847f00e985c9475485de63c6671f35d27bde39be34677a6b74228bf3e97.json @@ -0,0 +1,19 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO cqrs_ordering_category_query (\n version, name, description, category_id, store_id, deleted\n ) VALUES (\n $1, $2, $3, $4, $5, $6\n );", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Text", + "Text", + "Uuid", + "Uuid", + "Bool" + ] + }, + "nullable": [] + }, + "hash": "7cb52847f00e985c9475485de63c6671f35d27bde39be34677a6b74228bf3e97" +} diff --git a/.sqlx/query-81de3abeb5dcbe7a87e20bab82dc6a258b3017a710f8c3e249d73a5d980e2115.json b/.sqlx/query-81de3abeb5dcbe7a87e20bab82dc6a258b3017a710f8c3e249d73a5d980e2115.json new file mode 100644 index 0000000..a2a3466 --- /dev/null +++ b/.sqlx/query-81de3abeb5dcbe7a87e20bab82dc6a258b3017a710f8c3e249d73a5d980e2115.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT EXISTS (\n SELECT 1\n FROM cqrs_ordering_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": "81de3abeb5dcbe7a87e20bab82dc6a258b3017a710f8c3e249d73a5d980e2115" +} diff --git a/.sqlx/query-89387ed4e97c8c957e576b2753533112bf6ea3eb662460776fcaa625046b7a0d.json b/.sqlx/query-89387ed4e97c8c957e576b2753533112bf6ea3eb662460776fcaa625046b7a0d.json new file mode 100644 index 0000000..ddf6bbd --- /dev/null +++ b/.sqlx/query-89387ed4e97c8c957e576b2753533112bf6ea3eb662460776fcaa625046b7a0d.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT EXISTS (\n SELECT 1\n FROM cqrs_ordering_product_customizations_query\n WHERE\n customization_id = $1\n );", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + null + ] + }, + "hash": "89387ed4e97c8c957e576b2753533112bf6ea3eb662460776fcaa625046b7a0d" +} diff --git a/.sqlx/query-8f83767550a0efbb13020cff6b0b976d0756b470f448394d4832866b5a209ecd.json b/.sqlx/query-8f83767550a0efbb13020cff6b0b976d0756b470f448394d4832866b5a209ecd.json new file mode 100644 index 0000000..8eb704a --- /dev/null +++ b/.sqlx/query-8f83767550a0efbb13020cff6b0b976d0756b470f448394d4832866b5a209ecd.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT \n category_id, version\n FROM\n cqrs_ordering_category_query\n WHERE\n category_id = $1;", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "category_id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "version", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false + ] + }, + "hash": "8f83767550a0efbb13020cff6b0b976d0756b470f448394d4832866b5a209ecd" +} diff --git a/.sqlx/query-9e05e649d2cc489f8870e888d7a98d78af6a3fc78f860d0e5fc677622a1db9e3.json b/.sqlx/query-9e05e649d2cc489f8870e888d7a98d78af6a3fc78f860d0e5fc677622a1db9e3.json new file mode 100644 index 0000000..c0c2c14 --- /dev/null +++ b/.sqlx/query-9e05e649d2cc489f8870e888d7a98d78af6a3fc78f860d0e5fc677622a1db9e3.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT EXISTS (\n SELECT 1\n FROM cqrs_ordering_category_query\n WHERE\n category_id = $1\n );", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + null + ] + }, + "hash": "9e05e649d2cc489f8870e888d7a98d78af6a3fc78f860d0e5fc677622a1db9e3" +} diff --git a/.sqlx/query-cae7149b31d542cc01d263d682510e60f44de01ccb095e63542c11e5b2386ee5.json b/.sqlx/query-cae7149b31d542cc01d263d682510e60f44de01ccb095e63542c11e5b2386ee5.json new file mode 100644 index 0000000..f545da2 --- /dev/null +++ b/.sqlx/query-cae7149b31d542cc01d263d682510e60f44de01ccb095e63542c11e5b2386ee5.json @@ -0,0 +1,46 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT \n name, address, store_id, owner, deleted\n FROM\n cqrs_ordering_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": "cae7149b31d542cc01d263d682510e60f44de01ccb095e63542c11e5b2386ee5" +} diff --git a/.sqlx/query-cfab77a90a7a7f3d74b739442d9a75a065ad6a9a7a74432bcc3b0d6802af1eb2.json b/.sqlx/query-cfab77a90a7a7f3d74b739442d9a75a065ad6a9a7a74432bcc3b0d6802af1eb2.json new file mode 100644 index 0000000..ede59f6 --- /dev/null +++ b/.sqlx/query-cfab77a90a7a7f3d74b739442d9a75a065ad6a9a7a74432bcc3b0d6802af1eb2.json @@ -0,0 +1,46 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT\n name, description, store_id, category_id, deleted\n FROM\n cqrs_ordering_category_query\n WHERE\n category_id = $1;", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "description", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "store_id", + "type_info": "Uuid" + }, + { + "ordinal": 3, + "name": "category_id", + "type_info": "Uuid" + }, + { + "ordinal": 4, + "name": "deleted", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + true, + false, + false, + false + ] + }, + "hash": "cfab77a90a7a7f3d74b739442d9a75a065ad6a9a7a74432bcc3b0d6802af1eb2" +} diff --git a/.sqlx/query-d0580ff6dc77150cb00186302f20460dc3be59be1f8f5588bdc3d0f4489eb613.json b/.sqlx/query-d0580ff6dc77150cb00186302f20460dc3be59be1f8f5588bdc3d0f4489eb613.json new file mode 100644 index 0000000..7d2cb4d --- /dev/null +++ b/.sqlx/query-d0580ff6dc77150cb00186302f20460dc3be59be1f8f5588bdc3d0f4489eb613.json @@ -0,0 +1,19 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE\n cqrs_ordering_store_query\n SET\n version = $1,\n name = $2,\n address = $3,\n store_id = $4,\n owner = $5,\n deleted = $6;", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Text", + "Text", + "Uuid", + "Uuid", + "Bool" + ] + }, + "nullable": [] + }, + "hash": "d0580ff6dc77150cb00186302f20460dc3be59be1f8f5588bdc3d0f4489eb613" +} diff --git a/.sqlx/query-d5eeb278addc02f44a2a799a0ba03226d1f03935d73dc3bfc1af490df4726c78.json b/.sqlx/query-d5eeb278addc02f44a2a799a0ba03226d1f03935d73dc3bfc1af490df4726c78.json new file mode 100644 index 0000000..e4a56d1 --- /dev/null +++ b/.sqlx/query-d5eeb278addc02f44a2a799a0ba03226d1f03935d73dc3bfc1af490df4726c78.json @@ -0,0 +1,40 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT \n name,\n customization_id,\n product_id,\n deleted\n FROM\n cqrs_ordering_product_customizations_query\n WHERE\n customization_id = $1;", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "customization_id", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "product_id", + "type_info": "Uuid" + }, + { + "ordinal": 3, + "name": "deleted", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "d5eeb278addc02f44a2a799a0ba03226d1f03935d73dc3bfc1af490df4726c78" +} diff --git a/.sqlx/query-d873aaab136d804c0c0c1744d6914f616f0f8987c81a0d1d3c8d923b580b2ae1.json b/.sqlx/query-d873aaab136d804c0c0c1744d6914f616f0f8987c81a0d1d3c8d923b580b2ae1.json new file mode 100644 index 0000000..1d91ca4 --- /dev/null +++ b/.sqlx/query-d873aaab136d804c0c0c1744d6914f616f0f8987c81a0d1d3c8d923b580b2ae1.json @@ -0,0 +1,18 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO cqrs_ordering_product_customizations_query (\n version,\n name,\n customization_id,\n product_id,\n deleted\n ) VALUES (\n $1, $2, $3, $4, $5\n );", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Text", + "Uuid", + "Uuid", + "Bool" + ] + }, + "nullable": [] + }, + "hash": "d873aaab136d804c0c0c1744d6914f616f0f8987c81a0d1d3c8d923b580b2ae1" +} diff --git a/.sqlx/query-d896f6ffb486efad5ed10a9c824656d863de0c9140054de66eef32491ace9ddb.json b/.sqlx/query-d896f6ffb486efad5ed10a9c824656d863de0c9140054de66eef32491ace9ddb.json new file mode 100644 index 0000000..db85039 --- /dev/null +++ b/.sqlx/query-d896f6ffb486efad5ed10a9c824656d863de0c9140054de66eef32491ace9ddb.json @@ -0,0 +1,19 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE\n cqrs_ordering_category_query\n SET\n version = $1,\n name = $2,\n description = $3,\n category_id = $4,\n store_id = $5,\n deleted = $6;", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Text", + "Text", + "Uuid", + "Uuid", + "Bool" + ] + }, + "nullable": [] + }, + "hash": "d896f6ffb486efad5ed10a9c824656d863de0c9140054de66eef32491ace9ddb" +} diff --git a/.sqlx/query-d9c625876e7d398cb48c6278e69b2eb6ad8515e68d5520013634415109309e6e.json b/.sqlx/query-d9c625876e7d398cb48c6278e69b2eb6ad8515e68d5520013634415109309e6e.json new file mode 100644 index 0000000..1cff257 --- /dev/null +++ b/.sqlx/query-d9c625876e7d398cb48c6278e69b2eb6ad8515e68d5520013634415109309e6e.json @@ -0,0 +1,18 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE\n cqrs_ordering_product_customizations_query\n SET\n version = $1,\n name = $2,\n customization_id = $3,\n product_id = $4,\n deleted = $5;", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Text", + "Uuid", + "Uuid", + "Bool" + ] + }, + "nullable": [] + }, + "hash": "d9c625876e7d398cb48c6278e69b2eb6ad8515e68d5520013634415109309e6e" +} diff --git a/.sqlx/query-f72e06dd5ed9f4b5943c53d84cc4730963cb78e0d5954cb0eb4e0b5afb3f36d5.json b/.sqlx/query-f72e06dd5ed9f4b5943c53d84cc4730963cb78e0d5954cb0eb4e0b5afb3f36d5.json new file mode 100644 index 0000000..a1e9ae3 --- /dev/null +++ b/.sqlx/query-f72e06dd5ed9f4b5943c53d84cc4730963cb78e0d5954cb0eb4e0b5afb3f36d5.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT EXISTS (\n SELECT 1\n FROM cqrs_ordering_product_query\n WHERE\n name = $1\n AND\n category_id = $2\n AND\n deleted = false\n );", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text", + "Uuid" + ] + }, + "nullable": [ + null + ] + }, + "hash": "f72e06dd5ed9f4b5943c53d84cc4730963cb78e0d5954cb0eb4e0b5afb3f36d5" +} diff --git a/migrations/20240914110303_cqrs_ordering_store_query.sql b/migrations/20240914110303_cqrs_ordering_store_query.sql new file mode 100644 index 0000000..ea80241 --- /dev/null +++ b/migrations/20240914110303_cqrs_ordering_store_query.sql @@ -0,0 +1,16 @@ +--- SPDX-FileCopyrightText: 2024 Aravinth Manivannan +-- +-- SPDX-License-Identifier: AGPL-3.0-or-later + +CREATE TABLE IF NOT EXISTS cqrs_ordering_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/migrations/20240914111900_cqrs_ordering_category_query.sql b/migrations/20240914111900_cqrs_ordering_category_query.sql new file mode 100644 index 0000000..59ed38c --- /dev/null +++ b/migrations/20240914111900_cqrs_ordering_category_query.sql @@ -0,0 +1,17 @@ +-- SPDX-FileCopyrightText: 2024 Aravinth Manivannan +-- +-- SPDX-License-Identifier: AGPL-3.0-or-later + +CREATE TABLE IF NOT EXISTS cqrs_ordering_category_query +( + version bigint CHECK (version >= 0) NOT NULL, + + name TEXT NOT NULL, + description TEXT, + store_id UUID NOT NULL, + deleted BOOLEAN NOT NULL DEFAULT FALSE, + category_id UUID NOT NULL UNIQUE, + UNIQUE(store_id, name), + + PRIMARY KEY (category_id) +); diff --git a/migrations/20240914121520_cqrs_ordering_customization_query.sql b/migrations/20240914121520_cqrs_ordering_customization_query.sql new file mode 100644 index 0000000..02cb9bd --- /dev/null +++ b/migrations/20240914121520_cqrs_ordering_customization_query.sql @@ -0,0 +1,14 @@ +-- Add migration script here +CREATE TABLE IF NOT EXISTS cqrs_ordering_product_customizations_query +( + version bigint CHECK (version >= 0) NOT NULL, + + name TEXT NOT NULL, + customization_id UUID NOT NULL UNIQUE, + product_id UUID NOT NULL, + + deleted BOOLEAN NOT NULL DEFAULT FALSE, + UNIQUE(product_id, name), + + PRIMARY KEY (customization_id) +); diff --git a/migrations/20240914121604_cqrs_ordering_product_query.sql b/migrations/20240914121604_cqrs_ordering_product_query.sql new file mode 100644 index 0000000..8216cbb --- /dev/null +++ b/migrations/20240914121604_cqrs_ordering_product_query.sql @@ -0,0 +1,29 @@ +-- Add migration script here + +CREATE TABLE IF NOT EXISTS cqrs_ordering_product_query +( + version bigint CHECK (version >= 0) NOT NULL, + + name TEXT NOT NULL, + description TEXT, + image TEXT, + sku_able BOOLEAN NOT NULL DEFAULT FALSE, + product_id UUID NOT NULL UNIQUE, + + + price_minor INTEGER NOT NULL, + price_major INTEGER NOT NULL, + price_currency TEXT NOT NULL, + + quantity_major_number INTEGER NOT NULL, + quantity_minor_number INTEGER NOT NULL, + quantity_major_unit TEXT NOT NULL, + quantity_minor_unit TEXT NOT NULL, + + category_id UUID NOT NULL, + + deleted BOOLEAN NOT NULL DEFAULT FALSE, + UNIQUE(category_id, name), + + PRIMARY KEY (product_id) +) diff --git a/src/inventory/adapters/output/db/postgres/customization_view.rs b/src/inventory/adapters/output/db/postgres/customization_view.rs index e399f50..ffb3577 100644 --- a/src/inventory/adapters/output/db/postgres/customization_view.rs +++ b/src/inventory/adapters/output/db/postgres/customization_view.rs @@ -15,7 +15,7 @@ use super::InventoryDBPostgresAdapter; use crate::inventory::domain::{customization_aggregate::*, events::InventoryEvent}; use crate::utils::parse_aggregate_id::parse_aggregate_id; -pub const NEW_PRODUCT_NON_UUID: &str = "new_product_non_uuid-asdfa"; +pub const NEW_CUSTOMIZATION_NON_UUID: &str = "new_customization_non_uuid-asdfa"; //#[derive(Debug, Default, Serialize, Deserialize)] //struct Customizations { @@ -66,10 +66,11 @@ impl ViewRepository for InventoryDBPostgresAda &self, customization_id: &str, ) -> Result, PersistenceError> { - let customization_id = match parse_aggregate_id(customization_id, NEW_PRODUCT_NON_UUID)? { - Some((val, _)) => return Ok(Some(val)), - None => Uuid::parse_str(customization_id).unwrap(), - }; + let customization_id = + match parse_aggregate_id(customization_id, NEW_CUSTOMIZATION_NON_UUID)? { + Some((val, _)) => return Ok(Some(val)), + None => Uuid::parse_str(customization_id).unwrap(), + }; let res = sqlx::query_as!( CustomizationView, @@ -96,10 +97,11 @@ impl ViewRepository for InventoryDBPostgresAda &self, customization_id: &str, ) -> Result, PersistenceError> { - let customization_id = match parse_aggregate_id(customization_id, NEW_PRODUCT_NON_UUID)? { - Some(val) => return Ok(Some(val)), - None => Uuid::parse_str(customization_id).unwrap(), - }; + let customization_id = + match parse_aggregate_id(customization_id, NEW_CUSTOMIZATION_NON_UUID)? { + Some(val) => return Ok(Some(val)), + None => Uuid::parse_str(customization_id).unwrap(), + }; let res = sqlx::query_as!( CustomizationView, diff --git a/src/inventory/adapters/output/db/postgres/product_view.rs b/src/inventory/adapters/output/db/postgres/product_view.rs index 1bb81e7..fcdd9f0 100644 --- a/src/inventory/adapters/output/db/postgres/product_view.rs +++ b/src/inventory/adapters/output/db/postgres/product_view.rs @@ -14,8 +14,8 @@ use super::errors::*; use super::InventoryDBPostgresAdapter; use crate::inventory::domain::events::InventoryEvent; use crate::inventory::domain::product_aggregate::{Product, ProductBuilder}; -use crate::types::quantity::*; use crate::types::currency::*; +use crate::types::quantity::*; use crate::utils::parse_aggregate_id::parse_aggregate_id; pub const NEW_PRODUCT_NON_UUID: &str = "new_product_non_uuid-asdfa"; diff --git a/src/inventory/domain/add_product_command.rs b/src/inventory/domain/add_product_command.rs index f35cab4..f347838 100644 --- a/src/inventory/domain/add_product_command.rs +++ b/src/inventory/domain/add_product_command.rs @@ -8,8 +8,8 @@ use derive_more::{Display, Error}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -use crate::types::quantity::*; use crate::types::currency::*; +use crate::types::quantity::*; #[derive(Debug, Error, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] pub enum AddProductCommandError { diff --git a/src/inventory/domain/product_added_event.rs b/src/inventory/domain/product_added_event.rs index 3c2bb44..b0f23fb 100644 --- a/src/inventory/domain/product_added_event.rs +++ b/src/inventory/domain/product_added_event.rs @@ -7,8 +7,8 @@ use derive_getters::Getters; use serde::{Deserialize, Serialize}; use uuid::Uuid; -use crate::types::quantity::Quantity; use crate::types::currency::*; +use crate::types::quantity::Quantity; #[derive( Clone, Debug, Builder, Serialize, Deserialize, Getters, Eq, PartialEq, Ord, PartialOrd, diff --git a/src/inventory/domain/product_aggregate.rs b/src/inventory/domain/product_aggregate.rs index fc2350f..6a08d4a 100644 --- a/src/inventory/domain/product_aggregate.rs +++ b/src/inventory/domain/product_aggregate.rs @@ -12,8 +12,8 @@ use uuid::Uuid; use super::{commands::InventoryCommand, events::InventoryEvent}; use crate::inventory::application::services::errors::*; use crate::inventory::application::services::InventoryServicesInterface; -use crate::types::quantity::Quantity; use crate::types::currency::*; +use crate::types::quantity::Quantity; #[derive( Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder, diff --git a/src/inventory/domain/update_product_command.rs b/src/inventory/domain/update_product_command.rs index 77df803..f158f07 100644 --- a/src/inventory/domain/update_product_command.rs +++ b/src/inventory/domain/update_product_command.rs @@ -9,8 +9,8 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; use super::product_aggregate::Product; -use crate::types::quantity::Quantity; use crate::types::currency::*; +use crate::types::quantity::Quantity; #[derive(Debug, Error, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] pub enum UpdateProductCommandError { diff --git a/src/ordering/adapters/output/db/category_exists.rs b/src/ordering/adapters/output/db/category_exists.rs new file mode 100644 index 0000000..3724faf --- /dev/null +++ b/src/ordering/adapters/output/db/category_exists.rs @@ -0,0 +1,78 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use super::InventoryDBPostgresAdapter; +use crate::inventory::application::port::output::db::{ + errors::*, store_id_exists::*, +}; +use crate::inventory::domain::store_aggregate::*; + +#[async_trait::async_trait] +impl StoreIDExistsDBPort for InventoryDBPostgresAdapter { + async fn store_id_exists(&self, s: &Store) -> InventoryDBResult { + let res = sqlx::query!( + "SELECT EXISTS ( + SELECT 1 + FROM cqrs_inventory_store_query + WHERE + store_id = $1 + );", + s.store_id(), + ) + .fetch_one(&self.pool) + .await?; + if let Some(x) = res.exists { + Ok(x) + } else { + Ok(false) + } + } +} + +#[cfg(test)] +mod tests { + use uuid::Uuid; + + use super::*; + + #[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::InventoryDBPostgresAdapter::new( + sqlx::postgres::PgPool::connect(&settings.database.url) + .await + .unwrap(), + ); + + let store = StoreBuilder::default().name("store_name".into()).owner("store_owner".into()) + .address(Some("store_address".into())) + .store_id(store_id) + .build().unwrap(); + + // state doesn't exist + assert!(!db.store_id_exists(&store).await.unwrap()); + + sqlx::query!( + "INSERT INTO cqrs_inventory_store_query + (view_id, version, name, address, store_id, owner) + VALUES ($1, $2, $3, $4, $5, $6);", + "1", + 1, + store.name(), + store.address().as_ref().unwrap(), + store.store_id(), + store.owner(), + ) + .execute(&db.pool) + .await + .unwrap(); + + // state exists + assert!(db.store_id_exists(&store).await.unwrap()); + + settings.drop_db().await; + } +} diff --git a/src/ordering/adapters/output/db/category_id_exists.rs b/src/ordering/adapters/output/db/category_id_exists.rs new file mode 100644 index 0000000..5ccf4a9 --- /dev/null +++ b/src/ordering/adapters/output/db/category_id_exists.rs @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use uuid::Uuid; + +use super::OrderingDBPostgresAdapter; +use crate::ordering::application::port::output::db::{category_id_exists::*, errors::*}; +use crate::ordering::domain::category_aggregate::*; + +#[async_trait::async_trait] +impl CategoryIDExistsDBPort for OrderingDBPostgresAdapter { + async fn category_id_exists(&self, category_id: &Uuid) -> OrderingDBResult { + let res = sqlx::query!( + "SELECT EXISTS ( + SELECT 1 + FROM cqrs_ordering_category_query + WHERE + category_id = $1 + );", + category_id, + ) + .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::ordering::adapters::output::db::category_name_exists_for_store::tests::create_dummy_category_record; + + use super::*; + + #[actix_rt::test] + async fn test_postgres_category_exists() { + let category_id = Uuid::new_v4(); + let store_id = Uuid::new_v4(); + let settings = crate::settings::tests::get_settings().await; + settings.create_db().await; + let db = super::OrderingDBPostgresAdapter::new( + sqlx::postgres::PgPool::connect(&settings.database.url) + .await + .unwrap(), + ); + + let category = CategoryBuilder::default() + .name("category_name".into()) + .description(Some("category_description".into())) + .category_id(category_id) + .store_id(store_id) + .build() + .unwrap(); + + // state doesn't exist + assert!(!db.category_id_exists(category.category_id()).await.unwrap()); + + create_dummy_category_record(&category, &db).await; + + // state exists + assert!(db.category_id_exists(category.category_id()).await.unwrap()); + + settings.drop_db().await; + } +} diff --git a/src/ordering/adapters/output/db/category_name_exists_for_store.rs b/src/ordering/adapters/output/db/category_name_exists_for_store.rs new file mode 100644 index 0000000..e6e17f2 --- /dev/null +++ b/src/ordering/adapters/output/db/category_name_exists_for_store.rs @@ -0,0 +1,102 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use super::OrderingDBPostgresAdapter; +use crate::ordering::application::port::output::db::{ + category_name_exists_for_store::*, errors::*, +}; +use crate::ordering::domain::category_aggregate::*; + +#[async_trait::async_trait] +impl CategoryNameExistsForStoreDBPort for OrderingDBPostgresAdapter { + async fn category_name_exists_for_store(&self, s: &Category) -> OrderingDBResult { + let res = sqlx::query!( + "SELECT EXISTS ( + SELECT 1 + FROM cqrs_ordering_category_query + WHERE + name = $1 + AND + store_id = $2 + AND + deleted = false + );", + s.name(), + s.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 super::*; + + pub async fn create_dummy_category_record(c: &Category, db: &OrderingDBPostgresAdapter) { + sqlx::query!( + "INSERT INTO cqrs_ordering_category_query + (version, name, description, category_id, store_id, deleted) + VALUES ($1, $2, $3, $4, $5, $6);", + 1, + c.name(), + c.description().as_ref().unwrap(), + c.category_id(), + c.store_id(), + c.deleted().clone(), + ) + .execute(&db.pool) + .await + .unwrap(); + } + + #[actix_rt::test] + async fn test_postgres_category_exists() { + let category_id = Uuid::new_v4(); + let store_id = Uuid::new_v4(); + let settings = crate::settings::tests::get_settings().await; + settings.create_db().await; + let db = super::OrderingDBPostgresAdapter::new( + sqlx::postgres::PgPool::connect(&settings.database.url) + .await + .unwrap(), + ); + + let category = CategoryBuilder::default() + .name("category_name".into()) + .description(Some("category_description".into())) + .category_id(category_id) + .store_id(store_id) + .build() + .unwrap(); + + // state doesn't exist + assert!(!db.category_name_exists_for_store(&category).await.unwrap()); + + create_dummy_category_record(&category, &db).await; + + // state exists + assert!(db.category_name_exists_for_store(&category).await.unwrap()); + + // Set category.deleted = true; now db.category_name_exists_for_store must return false + sqlx::query!( + "UPDATE cqrs_ordering_category_query SET deleted = true WHERE category_id = $1;", + category.category_id(), + ) + .execute(&db.pool) + .await + .unwrap(); + assert!(!db.category_name_exists_for_store(&category).await.unwrap()); + + settings.drop_db().await; + } +} diff --git a/src/ordering/adapters/output/db/category_view.rs b/src/ordering/adapters/output/db/category_view.rs new file mode 100644 index 0000000..10b666f --- /dev/null +++ b/src/ordering/adapters/output/db/category_view.rs @@ -0,0 +1,201 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use async_trait::async_trait; +use cqrs_es::persist::{PersistenceError, ViewContext, ViewRepository}; +use cqrs_es::{EventEnvelope, Query, View}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use super::errors::*; +use super::OrderingDBPostgresAdapter; +use crate::ordering::domain::category_aggregate::Category; +use crate::ordering::domain::events::OrderingEvent; +use crate::utils::parse_aggregate_id::parse_aggregate_id; + +pub const NEW_CATEGORY_NON_UUID: &str = "ordering_new_category_non_uuid-asdfa"; + +// The view for a Category 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 CategoryView { + name: String, + description: Option, + category_id: Uuid, + store_id: Uuid, + deleted: bool, +} + +// 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 CategoryView { + fn update(&mut self, event: &EventEnvelope) { + if let OrderingEvent::CategoryAdded(val) = &event.payload { + self.name = val.name().into(); + self.description = val.description().clone(); + self.category_id = *val.category_id(); + self.store_id = *val.store_id(); + self.deleted = false; + } + } +} + +#[async_trait] +impl ViewRepository for OrderingDBPostgresAdapter { + async fn load(&self, category_id: &str) -> Result, PersistenceError> { + let category_id = match parse_aggregate_id(category_id, NEW_CATEGORY_NON_UUID)? { + Some((val, _)) => return Ok(Some(val)), + None => Uuid::parse_str(category_id).unwrap(), + }; + + let res = sqlx::query_as!( + CategoryView, + "SELECT + name, description, category_id, store_id, deleted + FROM + cqrs_ordering_category_query + WHERE + category_id = $1;", + category_id + ) + .fetch_one(&self.pool) + .await + .map_err(PostgresAggregateError::from)?; + Ok(Some(res)) + } + + async fn load_with_context( + &self, + category_id: &str, + ) -> Result, PersistenceError> { + let category_id = match parse_aggregate_id(category_id, NEW_CATEGORY_NON_UUID)? { + Some(val) => return Ok(Some(val)), + None => Uuid::parse_str(category_id).unwrap(), + }; + + let res = sqlx::query_as!( + CategoryView, + "SELECT + name, description, category_id, store_id, deleted + FROM + cqrs_ordering_category_query + WHERE + category_id = $1;", + category_id + ) + .fetch_one(&self.pool) + .await + .map_err(PostgresAggregateError::from)?; + + struct Context { + version: i64, + category_id: Uuid, + } + + let ctx = sqlx::query_as!( + Context, + "SELECT + category_id, version + FROM + cqrs_ordering_category_query + WHERE + category_id = $1;", + category_id + ) + .fetch_one(&self.pool) + .await + .map_err(PostgresAggregateError::from)?; + + let view_context = ViewContext::new(ctx.category_id.to_string(), ctx.version); + Ok(Some((res, view_context))) + } + + async fn update_view( + &self, + view: CategoryView, + context: ViewContext, + ) -> Result<(), PersistenceError> { + match context.version { + 0 => { + let version = context.version + 1; + sqlx::query!( + "INSERT INTO cqrs_ordering_category_query ( + version, name, description, category_id, store_id, deleted + ) VALUES ( + $1, $2, $3, $4, $5, $6 + );", + version, + view.name, + view.description, + view.category_id, + view.store_id, + view.deleted + ) + .execute(&self.pool) + .await + .map_err(PostgresAggregateError::from)?; + } + _ => { + let version = context.version + 1; + sqlx::query!( + "UPDATE + cqrs_ordering_category_query + SET + version = $1, + name = $2, + description = $3, + category_id = $4, + store_id = $5, + deleted = $6;", + version, + view.name, + view.description, + view.category_id, + view.store_id, + 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 OrderingDBPostgresAdapter { + async fn dispatch(&self, category_id: &str, events: &[EventEnvelope]) { + let res = self + .load_with_context(category_id) + .await + .unwrap_or_else(|_| { + Some(( + CategoryView::default(), + ViewContext::new(category_id.into(), 0), + )) + }); + let (mut view, view_context): (CategoryView, ViewContext) = res.unwrap(); + for event in events { + view.update(event); + } + self.update_view(view, view_context).await.unwrap(); + } +} diff --git a/src/ordering/adapters/output/db/customization_id_exists.rs b/src/ordering/adapters/output/db/customization_id_exists.rs new file mode 100644 index 0000000..ea4abbf --- /dev/null +++ b/src/ordering/adapters/output/db/customization_id_exists.rs @@ -0,0 +1,99 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use uuid::Uuid; + +use super::OrderingDBPostgresAdapter; +use crate::ordering::application::port::output::db::{customization_id_exists::*, errors::*}; + +#[async_trait::async_trait] +impl CustomizationIDExistsDBPort for OrderingDBPostgresAdapter { + async fn customization_id_exists(&self, customization_id: &Uuid) -> OrderingDBResult { + let res = sqlx::query!( + "SELECT EXISTS ( + SELECT 1 + FROM cqrs_ordering_product_customizations_query + WHERE + customization_id = $1 + );", + customization_id + ) + .fetch_one(&self.pool) + .await?; + if let Some(x) = res.exists { + Ok(x) + } else { + Ok(false) + } + } +} + +#[cfg(test)] +pub mod tests { + + use super::*; + + use crate::ordering::domain::customization_aggregate::*; + use crate::utils::uuid::tests::UUID; + + #[actix_rt::test] + async fn test_postgres_customization_exists() { + let settings = crate::settings::tests::get_settings().await; + settings.create_db().await; + let db = super::OrderingDBPostgresAdapter::new( + sqlx::postgres::PgPool::connect(&settings.database.url) + .await + .unwrap(), + ); + + let customization = CustomizationBuilder::default() + .name("customization_name".into()) + .customization_id(UUID) + .product_id(UUID) + .deleted(false) + .build() + .unwrap(); + + // state doesn't exist + assert!(!db + .customization_id_exists(customization.customization_id()) + .await + .unwrap()); + + create_dummy_customization_record(&customization, &db).await; + + // state exists + assert!(db + .customization_id_exists(customization.customization_id()) + .await + .unwrap()); + + settings.drop_db().await; + } + + pub async fn create_dummy_customization_record( + c: &Customization, + db: &OrderingDBPostgresAdapter, + ) { + sqlx::query!( + "INSERT INTO cqrs_ordering_product_customizations_query ( + version, + name, + customization_id, + product_id, + deleted + ) VALUES ( + $1, $2, $3, $4, $5 + );", + 1, + c.name(), + c.customization_id(), + UUID, + c.deleted().clone(), + ) + .execute(&db.pool) + .await + .unwrap(); + } +} diff --git a/src/ordering/adapters/output/db/customization_name_exists_for_product.rs b/src/ordering/adapters/output/db/customization_name_exists_for_product.rs new file mode 100644 index 0000000..dd8d151 --- /dev/null +++ b/src/ordering/adapters/output/db/customization_name_exists_for_product.rs @@ -0,0 +1,112 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use uuid::Uuid; + +use super::OrderingDBPostgresAdapter; +use crate::ordering::application::port::output::db::{ + customization_name_exists_for_product::*, errors::*, +}; +use crate::ordering::domain::customization_aggregate::*; + +#[async_trait::async_trait] +impl CustomizationNameExistsForProductDBPort for OrderingDBPostgresAdapter { + async fn customization_name_exists_for_product( + &self, + c: &Customization, + ) -> OrderingDBResult { + let res = sqlx::query!( + "SELECT EXISTS ( + SELECT 1 + FROM cqrs_ordering_product_customizations_query + WHERE + name = $1 + AND + product_id = $2 + AND + deleted = false + );", + c.name(), + c.product_id() + ) + .fetch_one(&self.pool) + .await?; + if let Some(x) = res.exists { + Ok(x) + } else { + Ok(false) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ordering::adapters::output::db::customization_id_exists::tests::create_dummy_customization_record; + use crate::utils::uuid::tests::UUID; + + #[actix_rt::test] + async fn test_postgres_customization_exists() { + let customization_name = "foo_customization"; + let product_id = UUID; + + let settings = crate::settings::tests::get_settings().await; + settings.create_db().await; + let db = super::OrderingDBPostgresAdapter::new( + sqlx::postgres::PgPool::connect(&settings.database.url) + .await + .unwrap(), + ); + + let customization = { + CustomizationBuilder::default() + .name(customization_name.into()) + .product_id(UUID) + .customization_id(UUID) + .deleted(false) + .build() + .unwrap() + }; + + // state doesn't exist + assert!(!db + .customization_name_exists_for_product(&customization) + .await + .unwrap()); + + create_dummy_customization_record(&customization, &db).await; + + // state exists + assert!(db + .customization_name_exists_for_product(&customization) + .await + .unwrap()); + + // Set customization.deleted = true; now db.customization_name_exists_for_product must return false + sqlx::query!( + "UPDATE + cqrs_ordering_product_customizations_query + SET + deleted = true + WHERE + customization_id = $1 + AND + product_id = $2 + AND + name = $3;", + customization.customization_id(), + &product_id, + customization.name() + ) + .execute(&db.pool) + .await + .unwrap(); + assert!(!db + .customization_name_exists_for_product(&customization) + .await + .unwrap()); + + settings.drop_db().await; + } +} diff --git a/src/ordering/adapters/output/db/customization_view.rs b/src/ordering/adapters/output/db/customization_view.rs new file mode 100644 index 0000000..f4c2821 --- /dev/null +++ b/src/ordering/adapters/output/db/customization_view.rs @@ -0,0 +1,221 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use std::str::FromStr; + +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::OrderingDBPostgresAdapter; +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 Customizations { +// customizations: Vec, +//} + +#[derive(Debug, Default, Serialize, Deserialize)] +struct CustomizationView { + name: String, + product_id: Uuid, + customization_id: Uuid, + deleted: bool, +} + +impl From for Customization { + fn from(v: CustomizationView) -> Self { + CustomizationBuilder::default() + .name(v.name) + .customization_id(v.customization_id) + .product_id(v.product_id) + .deleted(v.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 CustomizationView { + fn update(&mut self, event: &EventEnvelope) { + match &event.payload { + OrderingEvent::CustomizationAdded(val) => { + self.name = val.customization().name().into(); + self.product_id = *val.customization().product_id(); + self.customization_id = *val.customization().customization_id(); + + self.deleted = false; + } + _ => (), + } + } +} + +#[async_trait] +impl ViewRepository for OrderingDBPostgresAdapter { + async fn load( + &self, + customization_id: &str, + ) -> Result, PersistenceError> { + let customization_id = + match parse_aggregate_id(customization_id, NEW_CUSTOMIZATION_NON_UUID)? { + Some((val, _)) => return Ok(Some(val)), + None => Uuid::parse_str(customization_id).unwrap(), + }; + + let res = sqlx::query_as!( + CustomizationView, + "SELECT + name, + customization_id, + product_id, + deleted + FROM + cqrs_ordering_product_customizations_query + WHERE + customization_id = $1;", + customization_id + ) + .fetch_one(&self.pool) + .await + .map_err(PostgresAggregateError::from)?; + + // let customizations = res.get_customizations(&self).await?; + Ok(Some(res)) + } + + async fn load_with_context( + &self, + customization_id: &str, + ) -> Result, PersistenceError> { + let customization_id = + match parse_aggregate_id(customization_id, NEW_CUSTOMIZATION_NON_UUID)? { + Some(val) => return Ok(Some(val)), + None => Uuid::parse_str(customization_id).unwrap(), + }; + + let res = sqlx::query_as!( + CustomizationView, + "SELECT + name, + customization_id, + product_id, + deleted + FROM + cqrs_ordering_product_customizations_query + WHERE + customization_id = $1;", + customization_id + ) + .fetch_one(&self.pool) + .await + .map_err(PostgresAggregateError::from)?; + + // let customizations = res.get_customizations(&self).await?; + + struct Context { + version: i64, + customization_id: Uuid, + } + + let ctx = sqlx::query_as!( + Context, + "SELECT + customization_id, version + FROM + cqrs_ordering_product_customizations_query + WHERE + customization_id = $1;", + customization_id + ) + .fetch_one(&self.pool) + .await + .map_err(PostgresAggregateError::from)?; + + let view_context = ViewContext::new(ctx.customization_id.to_string(), ctx.version); + Ok(Some((res, view_context))) + } + + async fn update_view( + &self, + view: CustomizationView, + context: ViewContext, + ) -> Result<(), PersistenceError> { + match context.version { + 0 => { + let version = context.version + 1; + sqlx::query!( + "INSERT INTO cqrs_ordering_product_customizations_query ( + version, + name, + customization_id, + product_id, + deleted + ) VALUES ( + $1, $2, $3, $4, $5 + );", + version, + view.name, + view.customization_id, + view.product_id, + view.deleted, + ) + .execute(&self.pool) + .await + .map_err(PostgresAggregateError::from)?; + } + _ => { + let version = context.version + 1; + sqlx::query!( + "UPDATE + cqrs_ordering_product_customizations_query + SET + version = $1, + name = $2, + customization_id = $3, + product_id = $4, + deleted = $5;", + version, + view.name, + view.customization_id, + view.product_id, + view.deleted, + ) + .execute(&self.pool) + .await + .map_err(PostgresAggregateError::from)?; + } + } + + Ok(()) + } +} + +#[async_trait] +impl Query for OrderingDBPostgresAdapter { + async fn dispatch(&self, customization_id: &str, events: &[EventEnvelope]) { + let res = self + .load_with_context(&customization_id) + .await + .unwrap_or_else(|_| { + Some(( + CustomizationView::default(), + ViewContext::new(customization_id.into(), 0), + )) + }); + let (mut view, view_context): (CustomizationView, ViewContext) = res.unwrap(); + for event in events { + view.update(event); + } + self.update_view(view, view_context).await.unwrap(); + } +} diff --git a/src/ordering/adapters/output/db/get_category.rs b/src/ordering/adapters/output/db/get_category.rs new file mode 100644 index 0000000..5a85acf --- /dev/null +++ b/src/ordering/adapters/output/db/get_category.rs @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use uuid::Uuid; + +use super::errors::*; +use super::OrderingDBPostgresAdapter; +use crate::ordering::application::port::output::db::{errors::*, get_category::*}; +use crate::ordering::domain::category_aggregate::*; + +#[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd)] +pub struct InnerCategory { + name: String, + description: Option, + store_id: Uuid, + category_id: Uuid, + deleted: bool, +} + +impl From for Category { + fn from(v: InnerCategory) -> Self { + CategoryBuilder::default() + .name(v.name) + .description(v.description) + .store_id(v.store_id) + .category_id(v.category_id) + .deleted(v.deleted) + .build() + .unwrap() + } +} + +#[async_trait::async_trait] +impl GetCategoryDBPort for OrderingDBPostgresAdapter { + async fn get_category(&self, category_id: &Uuid) -> OrderingDBResult { + let res = sqlx::query_as!( + InnerCategory, + "SELECT + name, description, store_id, category_id, deleted + FROM + cqrs_ordering_category_query + WHERE + category_id = $1;", + category_id, + ) + .fetch_one(&self.pool) + .await + .map_err(|e| map_row_not_found_err(e, OrderingDBError::CategoryIDNotFound))?; + + Ok(res.into()) + } +} + +#[cfg(test)] +mod tests { + use uuid::Uuid; + + use crate::ordering::adapters::output::db::category_name_exists_for_store::tests::create_dummy_category_record; + + use super::*; + + #[actix_rt::test] + async fn test_postgres() { + let category_id = Uuid::new_v4(); + let store_id = Uuid::new_v4(); + let settings = crate::settings::tests::get_settings().await; + settings.create_db().await; + let db = super::OrderingDBPostgresAdapter::new( + sqlx::postgres::PgPool::connect(&settings.database.url) + .await + .unwrap(), + ); + + let category = CategoryBuilder::default() + .name("category_name".into()) + .description(Some("category_description".into())) + .category_id(category_id) + .store_id(store_id) + .build() + .unwrap(); + + // state doesn't exist + assert_eq!( + db.get_category(category.category_id()).await, + Err(OrderingDBError::CategoryIDNotFound) + ); + + create_dummy_category_record(&category, &db).await; + + // state exists + assert_eq!( + db.get_category(category.category_id()).await.unwrap(), + category + ); + settings.drop_db().await; + } +} diff --git a/src/ordering/adapters/output/db/mod.rs b/src/ordering/adapters/output/db/mod.rs index 2b87819..d6b82cf 100644 --- a/src/ordering/adapters/output/db/mod.rs +++ b/src/ordering/adapters/output/db/mod.rs @@ -8,13 +8,25 @@ use sqlx::postgres::PgPool; use crate::db::{migrate::RunMigrations, sqlx_postgres::Postgres}; +mod category_id_exists; +mod category_name_exists_for_store; +mod category_view; +mod customization_id_exists; +mod customization_name_exists_for_product; +mod customization_view; mod errors; +mod get_category; mod kot_id_exists; mod kot_view; mod line_item_id_exists; mod line_item_view; mod order_id_exists; mod order_view; +mod product_id_exists; +mod product_name_exists_for_category; +mod store_id_exists; +mod store_name_exists; +mod store_view; #[derive(Clone)] pub struct OrderingDBPostgresAdapter { diff --git a/src/ordering/adapters/output/db/product_id_exists.rs b/src/ordering/adapters/output/db/product_id_exists.rs new file mode 100644 index 0000000..7904a9a --- /dev/null +++ b/src/ordering/adapters/output/db/product_id_exists.rs @@ -0,0 +1,116 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use uuid::Uuid; + +use super::OrderingDBPostgresAdapter; +use crate::ordering::application::port::output::db::{errors::*, product_id_exists::*}; + +#[async_trait::async_trait] +impl ProductIDExistsDBPort for OrderingDBPostgresAdapter { + async fn product_id_exists(&self, product_id: &Uuid) -> OrderingDBResult { + let res = sqlx::query!( + "SELECT EXISTS ( + SELECT 1 + FROM cqrs_ordering_product_query + WHERE + product_id = $1 + );", + product_id + ) + .fetch_one(&self.pool) + .await?; + if let Some(x) = res.exists { + Ok(x) + } else { + Ok(false) + } + } +} + +#[cfg(test)] +pub mod tests { + + use super::*; + // use crate::ordering::domain::add_product_command::tests::get_customizations; + use crate::ordering::domain::{add_product_command::tests::get_command, product_aggregate::*}; + use crate::utils::uuid::tests::UUID; + + #[actix_rt::test] + async fn test_postgres_product_exists() { + let settings = crate::settings::tests::get_settings().await; + settings.create_db().await; + let db = super::OrderingDBPostgresAdapter::new( + sqlx::postgres::PgPool::connect(&settings.database.url) + .await + .unwrap(), + ); + + let cmd = get_command(); + + let product = ProductBuilder::default() + .name(cmd.name().into()) + .description(cmd.description().as_ref().map(|s| s.to_string())) + .image(cmd.image().as_ref().map(|s| s.to_string())) + .sku_able(*cmd.sku_able()) + .category_id(*cmd.category_id()) + .quantity(cmd.quantity().clone()) + .product_id(UUID.clone()) + .price(cmd.price().clone()) + .build() + .unwrap(); + + // state doesn't exist + assert!(!db.product_id_exists(product.product_id()).await.unwrap()); + + create_dummy_product_record(&product, &db).await; + + // state exists + assert!(db.product_id_exists(product.product_id()).await.unwrap()); + + settings.drop_db().await; + } + + pub async fn create_dummy_product_record(p: &Product, db: &OrderingDBPostgresAdapter) { + sqlx::query!( + "INSERT INTO cqrs_ordering_product_query ( + version, + name, + description, + image, + product_id, + category_id, + price_major, + price_minor, + price_currency, + sku_able, + quantity_minor_unit, + quantity_minor_number, + quantity_major_unit, + quantity_major_number, + deleted + ) VALUES ( + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15 + );", + 1, + p.name(), + p.description().as_ref().unwrap(), + p.image().as_ref().unwrap(), + p.product_id(), + p.category_id(), + p.price().major().clone() as i32, + p.price().minor().clone() as i32, + p.price().currency().to_string(), + p.sku_able().clone(), + p.quantity().major().unit().to_string(), + p.quantity().major().number().clone() as i32, + p.quantity().minor().unit().to_string(), + p.quantity().minor().number().clone() as i32, + p.deleted().clone(), + ) + .execute(&db.pool) + .await + .unwrap(); + } +} diff --git a/src/ordering/adapters/output/db/product_name_exists_for_category.rs b/src/ordering/adapters/output/db/product_name_exists_for_category.rs new file mode 100644 index 0000000..a7d3587 --- /dev/null +++ b/src/ordering/adapters/output/db/product_name_exists_for_category.rs @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use super::OrderingDBPostgresAdapter; +use crate::ordering::application::port::output::db::{ + errors::*, product_name_exists_for_category::*, +}; +use crate::ordering::domain::product_aggregate::*; + +#[async_trait::async_trait] +impl ProductNameExistsForCategoryDBPort for OrderingDBPostgresAdapter { + async fn product_name_exists_for_category(&self, s: &Product) -> OrderingDBResult { + let res = sqlx::query!( + "SELECT EXISTS ( + SELECT 1 + FROM cqrs_ordering_product_query + WHERE + name = $1 + AND + category_id = $2 + AND + deleted = false + );", + s.name(), + s.category_id(), + ) + .fetch_one(&self.pool) + .await?; + if let Some(x) = res.exists { + Ok(x) + } else { + Ok(false) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ordering::adapters::output::db::product_id_exists::tests::create_dummy_product_record; + use crate::ordering::domain::add_product_command::tests::get_command; + use crate::utils::uuid::tests::UUID; + + #[actix_rt::test] + async fn test_postgres_product_exists() { + let product_name = "foo_product"; + + let settings = crate::settings::tests::get_settings().await; + settings.create_db().await; + let db = super::OrderingDBPostgresAdapter::new( + sqlx::postgres::PgPool::connect(&settings.database.url) + .await + .unwrap(), + ); + + let cmd = get_command(); + + let product = ProductBuilder::default() + .name(product_name.into()) + .description(cmd.description().as_ref().map(|s| s.to_string())) + .image(cmd.image().as_ref().map(|s| s.to_string())) + .sku_able(*cmd.sku_able()) + .category_id(*cmd.category_id()) + .product_id(UUID) + .price(cmd.price().clone()) + .quantity(cmd.quantity().clone()) + .build() + .unwrap(); + + // state doesn't exist + assert!(!db.product_name_exists_for_category(&product).await.unwrap()); + + create_dummy_product_record(&product, &db).await; + + // state exists + assert!(db.product_name_exists_for_category(&product).await.unwrap()); + + // Set product.deleted = true; now db.product_name_exists_for_category must return false + sqlx::query!( + "UPDATE cqrs_ordering_product_query SET deleted = true WHERE product_id = $1;", + product.product_id(), + ) + .execute(&db.pool) + .await + .unwrap(); + assert!(!db.product_name_exists_for_category(&product).await.unwrap()); + + settings.drop_db().await; + } +} diff --git a/src/ordering/adapters/output/db/product_view.rs b/src/ordering/adapters/output/db/product_view.rs new file mode 100644 index 0000000..fcdd9f0 --- /dev/null +++ b/src/ordering/adapters/output/db/product_view.rs @@ -0,0 +1,332 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use std::str::FromStr; + +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::InventoryDBPostgresAdapter; +use crate::inventory::domain::events::InventoryEvent; +use crate::inventory::domain::product_aggregate::{Product, ProductBuilder}; +use crate::types::currency::*; +use crate::types::quantity::*; +use crate::utils::parse_aggregate_id::parse_aggregate_id; + +pub const NEW_PRODUCT_NON_UUID: &str = "new_product_non_uuid-asdfa"; + +// The view for a Product 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 ProductView { + name: String, + description: Option, + image: Option, // string = filename + product_id: Uuid, + sku_able: bool, + + price_minor: i32, + price_major: i32, + price_currency: String, + + quantity_major_number: i32, + quantity_minor_number: i32, + quantity_major_unit: String, + quantity_minor_unit: String, + + category_id: Uuid, + + deleted: bool, +} + +impl From for Product { + fn from(v: ProductView) -> Self { + let price = PriceBuilder::default() + .minor(v.price_minor as usize) + .major(v.price_major as usize) + .currency(Currency::from_str(&v.price_currency).unwrap()) + .build() + .unwrap(); + + let quantity = QuantityBuilder::default() + .minor( + QuantityPartBuilder::default() + .number(v.quantity_minor_number as usize) + .unit(QuantityUnit::from_str(&v.quantity_minor_unit).unwrap()) + .build() + .unwrap(), + ) + .major( + QuantityPartBuilder::default() + .number(v.quantity_major_number as usize) + .unit(QuantityUnit::from_str(&v.quantity_major_unit).unwrap()) + .build() + .unwrap(), + ) + .build() + .unwrap(); + + ProductBuilder::default() + .name(v.name) + .description(v.description) + .image(v.image) + .sku_able(v.sku_able) + .price(price) + .category_id(v.category_id) + .quantity(quantity) + .product_id(v.product_id) + .deleted(v.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 ProductView { + fn update(&mut self, event: &EventEnvelope) { + match &event.payload { + InventoryEvent::ProductAdded(val) => { + self.name = val.name().into(); + self.description = val.description().clone(); + self.image = val.image().clone(); + self.product_id = *val.product_id(); + self.category_id = *val.category_id(); + + self.sku_able = *val.sku_able(); + + self.price_minor = *val.price().minor() as i32; + self.price_major = *val.price().major() as i32; + self.price_currency = val.price().currency().to_string(); + + self.quantity_major_number = *val.quantity().major().number() as i32; + self.quantity_minor_number = *val.quantity().minor().number() as i32; + self.quantity_major_unit = val.quantity().major().unit().to_string(); + self.quantity_minor_unit = val.quantity().minor().unit().to_string(); + + self.deleted = false; + } + _ => (), + } + } +} + +#[async_trait] +impl ViewRepository for InventoryDBPostgresAdapter { + async fn load(&self, product_id: &str) -> Result, PersistenceError> { + let product_id = match parse_aggregate_id(product_id, NEW_PRODUCT_NON_UUID)? { + Some((val, _)) => return Ok(Some(val)), + None => Uuid::parse_str(product_id).unwrap(), + }; + + let res = sqlx::query_as!( + ProductView, + "SELECT + name, + description, + image, + product_id, + category_id, + price_major, + price_minor, + price_currency, + sku_able, + quantity_minor_unit, + quantity_minor_number, + quantity_major_unit, + quantity_major_number, + deleted + FROM + cqrs_inventory_product_query + WHERE + product_id = $1;", + product_id + ) + .fetch_one(&self.pool) + .await + .map_err(PostgresAggregateError::from)?; + + Ok(Some(res)) + } + + async fn load_with_context( + &self, + product_id: &str, + ) -> Result, PersistenceError> { + let product_id = match parse_aggregate_id(product_id, NEW_PRODUCT_NON_UUID)? { + Some(val) => return Ok(Some(val)), + None => Uuid::parse_str(product_id).unwrap(), + }; + + let res = sqlx::query_as!( + ProductView, + "SELECT + name, + description, + image, + product_id, + category_id, + price_major, + price_minor, + price_currency, + sku_able, + quantity_minor_unit, + quantity_minor_number, + quantity_major_unit, + quantity_major_number, + deleted + FROM + cqrs_inventory_product_query + WHERE + product_id = $1;", + product_id + ) + .fetch_one(&self.pool) + .await + .map_err(PostgresAggregateError::from)?; + + struct Context { + version: i64, + product_id: Uuid, + } + + let ctx = sqlx::query_as!( + Context, + "SELECT + product_id, version + FROM + cqrs_inventory_product_query + WHERE + product_id = $1;", + product_id + ) + .fetch_one(&self.pool) + .await + .map_err(PostgresAggregateError::from)?; + + let view_context = ViewContext::new(ctx.product_id.to_string(), ctx.version); + Ok(Some((res, view_context))) + } + + async fn update_view( + &self, + view: ProductView, + context: ViewContext, + ) -> Result<(), PersistenceError> { + match context.version { + 0 => { + let version = context.version + 1; + sqlx::query!( + "INSERT INTO cqrs_inventory_product_query ( + version, + name, + description, + image, + product_id, + category_id, + price_major, + price_minor, + price_currency, + sku_able, + quantity_minor_unit, + quantity_minor_number, + quantity_major_unit, + quantity_major_number, + + deleted + ) VALUES ( + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15 + );", + version, + view.name, + view.description, + view.image, + view.product_id, + view.category_id, + view.price_major, + view.price_minor, + view.price_currency, + view.sku_able, + view.quantity_minor_unit, + view.quantity_minor_number, + view.quantity_major_unit, + view.quantity_major_number, + view.deleted, + ) + .execute(&self.pool) + .await + .map_err(PostgresAggregateError::from)?; + } + + _ => { + let version = context.version + 1; + sqlx::query!( + "UPDATE + cqrs_inventory_product_query + SET + version = $1, + name = $2, + description = $3, + image = $4, + product_id = $5, + category_id = $6, + price_major = $7, + price_minor = $8, + price_currency = $9, + sku_able = $10, + quantity_minor_unit = $11, + quantity_minor_number = $12, + quantity_major_unit = $13, + quantity_major_number = $14, + deleted = $15;", + version, + view.name, + view.description, + view.image, + view.product_id, + view.category_id, + view.price_major, + view.price_minor, + view.price_currency, + view.sku_able, + view.quantity_minor_unit, + view.quantity_minor_number, + view.quantity_major_unit, + view.quantity_major_number, + view.deleted, + ) + .execute(&self.pool) + .await + .map_err(PostgresAggregateError::from)?; + } + } + + Ok(()) + } +} + +#[async_trait] +impl Query for InventoryDBPostgresAdapter { + async fn dispatch(&self, product_id: &str, events: &[EventEnvelope]) { + let res = self + .load_with_context(product_id) + .await + .unwrap_or_else(|_| { + Some(( + ProductView::default(), + ViewContext::new(product_id.into(), 0), + )) + }); + let (mut view, view_context): (ProductView, ViewContext) = res.unwrap(); + for event in events { + view.update(event); + } + self.update_view(view, view_context).await.unwrap(); + } +} diff --git a/src/ordering/adapters/output/db/store_id_exists.rs b/src/ordering/adapters/output/db/store_id_exists.rs new file mode 100644 index 0000000..c67fbe0 --- /dev/null +++ b/src/ordering/adapters/output/db/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::OrderingDBPostgresAdapter; +use crate::ordering::application::port::output::db::{errors::*, store_id_exists::*}; +use crate::ordering::domain::store_aggregate::*; + +#[async_trait::async_trait] +impl StoreIDExistsDBPort for OrderingDBPostgresAdapter { + async fn store_id_exists(&self, store_id: &Uuid) -> OrderingDBResult { + let res = sqlx::query!( + "SELECT EXISTS ( + SELECT 1 + FROM cqrs_ordering_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: &OrderingDBPostgresAdapter) { + sqlx::query!( + "INSERT INTO cqrs_ordering_store_query + (version, name, address, store_id, owner, deleted) + VALUES ($1, $2, $3, $4, $5 ,$6);", + 1, + s.name(), + s.address().as_ref().unwrap(), + 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::OrderingDBPostgresAdapter::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/ordering/adapters/output/db/store_name_exists.rs b/src/ordering/adapters/output/db/store_name_exists.rs new file mode 100644 index 0000000..7d3ec82 --- /dev/null +++ b/src/ordering/adapters/output/db/store_name_exists.rs @@ -0,0 +1,81 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use super::OrderingDBPostgresAdapter; +use crate::ordering::application::port::output::db::{errors::*, store_name_exists::*}; +use crate::ordering::domain::store_aggregate::*; + +#[async_trait::async_trait] +impl StoreNameExistsDBPort for OrderingDBPostgresAdapter { + async fn store_name_exists(&self, s: &Store) -> OrderingDBResult { + let res = sqlx::query!( + "SELECT EXISTS ( + SELECT 1 + FROM cqrs_ordering_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::ordering::adapters::output::db::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::OrderingDBPostgresAdapter::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_ordering_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/ordering/adapters/output/db/store_view.rs b/src/ordering/adapters/output/db/store_view.rs new file mode 100644 index 0000000..db91653 --- /dev/null +++ b/src/ordering/adapters/output/db/store_view.rs @@ -0,0 +1,302 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use async_trait::async_trait; +use cqrs_es::persist::{PersistenceError, ViewContext, ViewRepository}; +use cqrs_es::{EventEnvelope, Query, View}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use super::errors::*; +use super::OrderingDBPostgresAdapter; +use crate::ordering::domain::events::OrderingEvent; +use crate::ordering::domain::store_aggregate::Store; +use crate::utils::parse_aggregate_id::parse_aggregate_id; + +pub const NEW_STORE_NON_UUID: &str = "ordering_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, +} + +// 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) { + if let OrderingEvent::StoreAdded(val) = &event.payload { + self.name = val.name().into(); + self.address = val.address().clone(); + self.store_id = *val.store_id(); + self.owner = *val.owner(); + self.deleted = false; + } + } +} + +#[async_trait] +impl ViewRepository for OrderingDBPostgresAdapter { + 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_ordering_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_ordering_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_ordering_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_ordering_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_ordering_store_query + SET + version = $1, + name = $2, + address = $3, + store_id = $4, + owner = $5, + deleted = $6;", + version, + view.name, + view.address, + view.store_id, + 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 OrderingDBPostgresAdapter { + 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(); + } +} + +// 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 StoreQuery = GenericQuery; +//pub type StoreQuery = Query; + +//#[cfg(test)] +//mod tests { +// use super::*; +// +// use postgres_es::PostgresCqrs; +// +// use crate::{ +// db::migrate::*, +// ordering::{ +// application::services::{ +// add_category_service::tests::mock_add_category_service, add_customization_service::tests::mock_add_customization_service, add_line_item_service::tests::mock_add_line_item_service, add_product_service::tests::mock_add_product_service, add_store_service::AddStoreServiceBuilder, update_category_service::tests::mock_update_category_service, update_customization_service::tests::mock_update_customization_service, update_product_service::tests::mock_update_product_service, update_store_service::tests::mock_update_store_service, OrderingServicesBuilder +// }, +// domain::{ +// add_category_command::AddCategoryCommand, add_customization_command, +// add_product_command::tests::get_command, add_store_command::AddStoreCommand, +// commands::OrderingCommand, +// update_category_command::tests::get_update_category_command, +// update_customization_command::tests::get_update_customization_command, +// update_product_command, update_store_command::tests::get_update_store_cmd, +// }, +// }, +// tests::bdd::IS_NEVER_CALLED, +// utils::{random_string::GenerateRandomStringInterface, uuid::tests::UUID}, +// }; +// use std::sync::Arc; +// +// #[actix_rt::test] +// async fn pg_query() { +// let settings = crate::settings::tests::get_settings().await; +// //let settings = crate::settings::Settings::new().unwrap(); +// settings.create_db().await; +// +// let db = crate::db::sqlx_postgres::Postgres::init(&settings.database.url).await; +// db.migrate().await; +// let db = OrderingDBPostgresAdapter::new(db.pool.clone()); +// +// let simple_query = SimpleLoggingQuery {}; +// +// let queries: Vec>> = +// vec![Box::new(simple_query), Box::new(db.clone())]; +// +// let services = OrderingServicesBuilder::default() +// .add_store(Arc::new( +// AddStoreServiceBuilder::default() +// .db_store_id_exists(Arc::new(db.clone())) +// .db_store_name_exists(Arc::new(db.clone())) +// .get_uuid(Arc::new(crate::utils::uuid::GenerateUUID {})) +// .build() +// .unwrap(), +// )) +// .add_category(mock_add_category_service( +// IS_NEVER_CALLED, +// AddCategoryCommand::new("foo".into(), None, UUID, UUID).unwrap(), +// )) +// .add_product(mock_add_product_service(IS_NEVER_CALLED, get_command())) +// .add_customization(mock_add_customization_service( +// IS_NEVER_CALLED, +// add_customization_command::tests::get_command(), +// )) +// .update_product(mock_update_product_service( +// IS_NEVER_CALLED, +// update_product_command::tests::get_command(), +// )) +// .update_customization(mock_update_customization_service( +// IS_NEVER_CALLED, +// get_update_customization_command(), +// )) +// .update_category(mock_update_category_service( +// IS_NEVER_CALLED, +// get_update_category_command(), +// )) +// .update_store(mock_update_store_service( +// IS_NEVER_CALLED, +// get_update_store_cmd(), +// )) +// .build() +// .unwrap(); +// +// let (cqrs, _store_query): ( +// Arc>, +// Arc>, +// ) = ( +// Arc::new(postgres_es::postgres_cqrs( +// db.pool.clone(), +// queries, +// Arc::new(services), +// )), +// Arc::new(db.clone()), +// ); +// +// let rand = crate::utils::random_string::GenerateRandomString {}; +// let cmd = AddStoreCommand::new(rand.get_random(10), None, UUID).unwrap(); +// cqrs.execute("", OrderingCommand::AddStore(cmd.clone())) +// .await +// .unwrap(); +// +// settings.drop_db().await; +// } +//} diff --git a/src/ordering/application/port/output/db/category_id_exists.rs b/src/ordering/application/port/output/db/category_id_exists.rs new file mode 100644 index 0000000..de0d430 --- /dev/null +++ b/src/ordering/application/port/output/db/category_id_exists.rs @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use mockall::predicate::*; +use mockall::*; +use uuid::Uuid; + +use super::errors::*; +#[cfg(test)] +#[allow(unused_imports)] +pub use tests::*; + +#[automock] +#[async_trait::async_trait] +pub trait CategoryIDExistsDBPort: Send + Sync { + async fn category_id_exists(&self, category_id: &Uuid) -> OrderingDBResult; +} + +pub type CategoryIDExistsDBPortObj = std::sync::Arc; + +#[cfg(test)] +pub mod tests { + use super::*; + + use std::sync::Arc; + + pub fn mock_category_id_exists_db_port_false( + times: Option, + ) -> CategoryIDExistsDBPortObj { + let mut m = MockCategoryIDExistsDBPort::new(); + if let Some(times) = times { + m.expect_category_id_exists() + .times(times) + .returning(|_| Ok(false)); + } else { + m.expect_category_id_exists().returning(|_| Ok(false)); + } + + Arc::new(m) + } + + pub fn mock_category_id_exists_db_port_true(times: Option) -> CategoryIDExistsDBPortObj { + let mut m = MockCategoryIDExistsDBPort::new(); + if let Some(times) = times { + m.expect_category_id_exists() + .times(times) + .returning(|_| Ok(true)); + } else { + m.expect_category_id_exists().returning(|_| Ok(true)); + } + + Arc::new(m) + } +} diff --git a/src/ordering/application/port/output/db/category_name_exists_for_store.rs b/src/ordering/application/port/output/db/category_name_exists_for_store.rs new file mode 100644 index 0000000..391f255 --- /dev/null +++ b/src/ordering/application/port/output/db/category_name_exists_for_store.rs @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use mockall::predicate::*; +use mockall::*; + +use crate::ordering::domain::category_aggregate::Category; + +use super::errors::*; +#[cfg(test)] +#[allow(unused_imports)] +pub use tests::*; + +#[automock] +#[async_trait::async_trait] +pub trait CategoryNameExistsForStoreDBPort: Send + Sync { + async fn category_name_exists_for_store(&self, c: &Category) -> OrderingDBResult; +} + +pub type CategoryNameExistsForStoreDBPortObj = std::sync::Arc; + +#[cfg(test)] +pub mod tests { + use super::*; + + use std::sync::Arc; + + pub fn mock_category_name_exists_for_store_db_port_false( + times: Option, + ) -> CategoryNameExistsForStoreDBPortObj { + let mut m = MockCategoryNameExistsForStoreDBPort::new(); + if let Some(times) = times { + m.expect_category_name_exists_for_store() + .times(times) + .returning(|_| Ok(false)); + } else { + m.expect_category_name_exists_for_store() + .returning(|_| Ok(false)); + } + + Arc::new(m) + } + + pub fn mock_category_name_exists_for_store_db_port_true( + times: Option, + ) -> CategoryNameExistsForStoreDBPortObj { + let mut m = MockCategoryNameExistsForStoreDBPort::new(); + if let Some(times) = times { + m.expect_category_name_exists_for_store() + .times(times) + .returning(|_| Ok(true)); + } else { + m.expect_category_name_exists_for_store() + .returning(|_| Ok(true)); + } + + Arc::new(m) + } +} diff --git a/src/ordering/application/port/output/db/customization_id_exists.rs b/src/ordering/application/port/output/db/customization_id_exists.rs new file mode 100644 index 0000000..0b98387 --- /dev/null +++ b/src/ordering/application/port/output/db/customization_id_exists.rs @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use mockall::predicate::*; +use mockall::*; +use uuid::Uuid; + +use super::errors::*; +#[cfg(test)] +#[allow(unused_imports)] +pub use tests::*; + +#[automock] +#[async_trait::async_trait] +pub trait CustomizationIDExistsDBPort: Send + Sync { + async fn customization_id_exists(&self, c: &Uuid) -> OrderingDBResult; +} + +pub type CustomizationIDExistsDBPortObj = std::sync::Arc; + +#[cfg(test)] +pub mod tests { + use super::*; + + use std::sync::Arc; + + pub fn mock_customization_id_exists_db_port_false( + times: Option, + ) -> CustomizationIDExistsDBPortObj { + let mut m = MockCustomizationIDExistsDBPort::new(); + if let Some(times) = times { + m.expect_customization_id_exists() + .times(times) + .returning(|_| Ok(false)); + } else { + m.expect_customization_id_exists().returning(|_| Ok(false)); + } + + Arc::new(m) + } + + pub fn mock_customization_id_exists_db_port_true( + times: Option, + ) -> CustomizationIDExistsDBPortObj { + let mut m = MockCustomizationIDExistsDBPort::new(); + if let Some(times) = times { + m.expect_customization_id_exists() + .times(times) + .returning(|_| Ok(true)); + } else { + m.expect_customization_id_exists().returning(|_| Ok(true)); + } + + Arc::new(m) + } +} diff --git a/src/ordering/application/port/output/db/customization_name_exists_for_product.rs b/src/ordering/application/port/output/db/customization_name_exists_for_product.rs new file mode 100644 index 0000000..39f945d --- /dev/null +++ b/src/ordering/application/port/output/db/customization_name_exists_for_product.rs @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use mockall::predicate::*; +use mockall::*; +use uuid::Uuid; + +use crate::ordering::domain::customization_aggregate::Customization; + +use super::errors::*; +#[cfg(test)] +#[allow(unused_imports)] +pub use tests::*; + +#[automock] +#[async_trait::async_trait] +pub trait CustomizationNameExistsForProductDBPort: Send + Sync { + async fn customization_name_exists_for_product( + &self, + c: &Customization, + ) -> OrderingDBResult; +} + +pub type CustomizationNameExistsForProductDBPortObj = + std::sync::Arc; + +#[cfg(test)] +pub mod tests { + use super::*; + + use std::sync::Arc; + + pub fn mock_customization_name_exists_for_product_db_port_false( + times: Option, + ) -> CustomizationNameExistsForProductDBPortObj { + let mut m = MockCustomizationNameExistsForProductDBPort::new(); + if let Some(times) = times { + m.expect_customization_name_exists_for_product() + .times(times) + .returning(|_| Ok(false)); + } else { + m.expect_customization_name_exists_for_product() + .returning(|_| Ok(false)); + } + + Arc::new(m) + } + + pub fn mock_customization_name_exists_for_product_db_port_true( + times: Option, + ) -> CustomizationNameExistsForProductDBPortObj { + let mut m = MockCustomizationNameExistsForProductDBPort::new(); + if let Some(times) = times { + m.expect_customization_name_exists_for_product() + .times(times) + .returning(|_| Ok(true)); + } else { + m.expect_customization_name_exists_for_product() + .returning(|_| Ok(true)); + } + + Arc::new(m) + } +} diff --git a/src/ordering/application/port/output/db/errors.rs b/src/ordering/application/port/output/db/errors.rs index 3588c7c..fdb6ca3 100644 --- a/src/ordering/application/port/output/db/errors.rs +++ b/src/ordering/application/port/output/db/errors.rs @@ -16,4 +16,16 @@ pub enum OrderingDBError { OrderIDNotFound, DuplicateKotID, KotIDNotFound, + DuplicateStoreName, + DuplicateStoreID, + StoreIDNotFound, + DuplicateCategoryName, + DuplicateCategoryID, + CategoryIDNotFound, + DuplicateProductName, + DuplicateProductID, + ProductIDNotFound, + CustomizationIDNotFound, + DuplicateCustomizationID, + DuplicateCustomizationName, } diff --git a/src/ordering/application/port/output/db/get_category.rs b/src/ordering/application/port/output/db/get_category.rs new file mode 100644 index 0000000..0f00df5 --- /dev/null +++ b/src/ordering/application/port/output/db/get_category.rs @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use mockall::predicate::*; +use mockall::*; +use uuid::Uuid; + +use crate::ordering::domain::category_aggregate::Category; + +use super::errors::*; +#[cfg(test)] +#[allow(unused_imports)] +pub use tests::*; + +#[automock] +#[async_trait::async_trait] +pub trait GetCategoryDBPort: Send + Sync { + async fn get_category(&self, category_id: &Uuid) -> OrderingDBResult; +} + +pub type GetCategoryDBPortObj = std::sync::Arc; + +#[cfg(test)] +pub mod tests { + + use super::*; + + use std::sync::Arc; + + pub fn mock_get_category_db_port(times: Option) -> GetCategoryDBPortObj { + let mut m = MockGetCategoryDBPort::new(); + if let Some(times) = times { + m.expect_get_category() + .times(times) + .returning(|_| Ok(Category::default())); + } else { + m.expect_get_category() + .returning(|_| Ok(Category::default())); + } + + Arc::new(m) + } +} diff --git a/src/ordering/application/port/output/db/mod.rs b/src/ordering/application/port/output/db/mod.rs index 14fe72e..06c7496 100644 --- a/src/ordering/application/port/output/db/mod.rs +++ b/src/ordering/application/port/output/db/mod.rs @@ -2,7 +2,16 @@ // // SPDX-License-Identifier: AGPL-3.0-or-later +pub mod category_id_exists; +pub mod category_name_exists_for_store; +pub mod customization_id_exists; +pub mod customization_name_exists_for_product; pub mod errors; +pub mod get_category; pub mod kot_id_exists; pub mod line_item_id_exists; pub mod order_id_exists; +pub mod product_id_exists; +pub mod product_name_exists_for_category; +pub mod store_id_exists; +pub mod store_name_exists; diff --git a/src/ordering/application/port/output/db/product_id_exists.rs b/src/ordering/application/port/output/db/product_id_exists.rs new file mode 100644 index 0000000..680cd89 --- /dev/null +++ b/src/ordering/application/port/output/db/product_id_exists.rs @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use mockall::predicate::*; +use mockall::*; +use uuid::Uuid; + +use super::errors::*; +#[cfg(test)] +#[allow(unused_imports)] +pub use tests::*; + +#[automock] +#[async_trait::async_trait] +pub trait ProductIDExistsDBPort: Send + Sync { + async fn product_id_exists(&self, c: &Uuid) -> OrderingDBResult; +} + +pub type ProductIDExistsDBPortObj = std::sync::Arc; + +#[cfg(test)] +pub mod tests { + use super::*; + + use std::sync::Arc; + + pub fn mock_product_id_exists_db_port_false(times: Option) -> ProductIDExistsDBPortObj { + let mut m = MockProductIDExistsDBPort::new(); + if let Some(times) = times { + m.expect_product_id_exists() + .times(times) + .returning(|_| Ok(false)); + } else { + m.expect_product_id_exists().returning(|_| Ok(false)); + } + + Arc::new(m) + } + + pub fn mock_product_id_exists_db_port_true(times: Option) -> ProductIDExistsDBPortObj { + let mut m = MockProductIDExistsDBPort::new(); + if let Some(times) = times { + m.expect_product_id_exists() + .times(times) + .returning(|_| Ok(true)); + } else { + m.expect_product_id_exists().returning(|_| Ok(true)); + } + + Arc::new(m) + } +} diff --git a/src/ordering/application/port/output/db/product_name_exists_for_category.rs b/src/ordering/application/port/output/db/product_name_exists_for_category.rs new file mode 100644 index 0000000..abeca51 --- /dev/null +++ b/src/ordering/application/port/output/db/product_name_exists_for_category.rs @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use mockall::predicate::*; +use mockall::*; + +use crate::ordering::domain::product_aggregate::Product; + +use super::errors::*; +#[cfg(test)] +#[allow(unused_imports)] +pub use tests::*; + +#[automock] +#[async_trait::async_trait] +pub trait ProductNameExistsForCategoryDBPort: Send + Sync { + async fn product_name_exists_for_category(&self, c: &Product) -> OrderingDBResult; +} + +pub type ProductNameExistsForCategoryDBPortObj = + std::sync::Arc; + +#[cfg(test)] +pub mod tests { + use super::*; + + use std::sync::Arc; + + pub fn mock_product_name_exists_for_category_db_port_false( + times: Option, + ) -> ProductNameExistsForCategoryDBPortObj { + let mut m = MockProductNameExistsForCategoryDBPort::new(); + if let Some(times) = times { + m.expect_product_name_exists_for_category() + .times(times) + .returning(|_| Ok(false)); + } else { + m.expect_product_name_exists_for_category() + .returning(|_| Ok(false)); + } + + Arc::new(m) + } + + pub fn mock_product_name_exists_for_category_db_port_true( + times: Option, + ) -> ProductNameExistsForCategoryDBPortObj { + let mut m = MockProductNameExistsForCategoryDBPort::new(); + if let Some(times) = times { + m.expect_product_name_exists_for_category() + .times(times) + .returning(|_| Ok(true)); + } else { + m.expect_product_name_exists_for_category() + .returning(|_| Ok(true)); + } + + Arc::new(m) + } +} diff --git a/src/ordering/application/port/output/db/store_id_exists.rs b/src/ordering/application/port/output/db/store_id_exists.rs new file mode 100644 index 0000000..3d5b5ca --- /dev/null +++ b/src/ordering/application/port/output/db/store_id_exists.rs @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use mockall::predicate::*; +use mockall::*; +use uuid::Uuid; + +use crate::ordering::domain::store_aggregate::Store; + +use super::errors::*; +#[cfg(test)] +#[allow(unused_imports)] +pub use tests::*; + +#[automock] +#[async_trait::async_trait] +pub trait StoreIDExistsDBPort: Send + Sync { + async fn store_id_exists(&self, store_id: &Uuid) -> OrderingDBResult; +} + +pub type StoreIDExistsDBPortObj = std::sync::Arc; + +#[cfg(test)] +pub mod tests { + use super::*; + + use std::sync::Arc; + + pub fn mock_store_id_exists_db_port_false(times: Option) -> StoreIDExistsDBPortObj { + let mut m = MockStoreIDExistsDBPort::new(); + if let Some(times) = times { + m.expect_store_id_exists() + .times(times) + .returning(|_| Ok(false)); + } else { + m.expect_store_id_exists().returning(|_| Ok(false)); + } + + Arc::new(m) + } + + pub fn mock_store_id_exists_db_port_true(times: Option) -> StoreIDExistsDBPortObj { + let mut m = MockStoreIDExistsDBPort::new(); + if let Some(times) = times { + m.expect_store_id_exists() + .times(times) + .returning(|_| Ok(true)); + } else { + m.expect_store_id_exists().returning(|_| Ok(true)); + } + + Arc::new(m) + } +} diff --git a/src/ordering/application/port/output/db/store_name_exists.rs b/src/ordering/application/port/output/db/store_name_exists.rs new file mode 100644 index 0000000..d7d01cb --- /dev/null +++ b/src/ordering/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::ordering::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) -> OrderingDBResult; +} + +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) + } +} diff --git a/src/ordering/application/port/output/full_text_search/add_product_to_store.rs b/src/ordering/application/port/output/full_text_search/add_product_to_store.rs new file mode 100644 index 0000000..8e68995 --- /dev/null +++ b/src/ordering/application/port/output/full_text_search/add_product_to_store.rs @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use mockall::predicate::*; +use mockall::*; + +use super::errors::*; + +#[cfg(test)] +#[allow(unused_imports)] +pub use tests::*; + +use crate::ordering::domain::{category_aggregate::*, product_aggregate::*}; + +#[automock] +#[async_trait::async_trait] +pub trait AddProductToStoreFTSPort: Send + Sync { + async fn add_product_to_store( + &self, + product: &Product, + cateogry: &Category, + ) -> OrderingFTSResult<()>; +} + +pub type AddProductToStoreFTSPortObj = std::sync::Arc; + +#[cfg(test)] +pub mod tests { + use super::*; + + use std::sync::Arc; + + pub fn mock_add_product_to_store_fts_port(times: Option) -> AddProductToStoreFTSPortObj { + let mut m = MockAddProductToStoreFTSPort::new(); + if let Some(times) = times { + m.expect_add_product_to_store() + .times(times) + .returning(|_, _| Ok(())); + } else { + m.expect_add_product_to_store().returning(|_, _| Ok(())); + } + + Arc::new(m) + } +} diff --git a/src/ordering/application/port/output/full_text_search/errors.rs b/src/ordering/application/port/output/full_text_search/errors.rs new file mode 100644 index 0000000..89de2d3 --- /dev/null +++ b/src/ordering/application/port/output/full_text_search/errors.rs @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use derive_more::Display; +use serde::{Deserialize, Serialize}; + +pub type OrderingFTSResult = Result; + +#[derive(Debug, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +pub enum OrderingFTSError {} diff --git a/src/ordering/application/port/output/full_text_search/mod.rs b/src/ordering/application/port/output/full_text_search/mod.rs new file mode 100644 index 0000000..422bcd3 --- /dev/null +++ b/src/ordering/application/port/output/full_text_search/mod.rs @@ -0,0 +1,7 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +pub mod add_product_to_store; +pub mod errors; +pub mod update_product; diff --git a/src/ordering/application/port/output/full_text_search/update_product.rs b/src/ordering/application/port/output/full_text_search/update_product.rs new file mode 100644 index 0000000..56f60de --- /dev/null +++ b/src/ordering/application/port/output/full_text_search/update_product.rs @@ -0,0 +1,3 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/src/ordering/application/port/output/mod.rs b/src/ordering/application/port/output/mod.rs index 1589173..8a85158 100644 --- a/src/ordering/application/port/output/mod.rs +++ b/src/ordering/application/port/output/mod.rs @@ -3,3 +3,4 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pub mod db; +pub mod full_text_search; diff --git a/src/ordering/application/services/add_category_service.rs b/src/ordering/application/services/add_category_service.rs new file mode 100644 index 0000000..61e04ba --- /dev/null +++ b/src/ordering/application/services/add_category_service.rs @@ -0,0 +1,211 @@ +// 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::ordering::{ + application::port::output::db::{ + category_id_exists::*, category_name_exists_for_store::*, store_id_exists::*, + }, + domain::{ + add_category_command::AddCategoryCommand, + category_added_event::{CategoryAddedEvent, CategoryAddedEventBuilder}, + category_aggregate::*, + }, +}; +use crate::utils::uuid::*; + +#[automock] +#[async_trait::async_trait] +pub trait AddCategoryUseCase: Send + Sync { + async fn add_category(&self, cmd: AddCategoryCommand) -> OrderingResult; +} + +pub type AddCategoryServiceObj = Arc; + +#[derive(Clone, Builder)] +pub struct AddCategoryService { + db_store_id_exists: StoreIDExistsDBPortObj, + db_category_name_exists_for_store: CategoryNameExistsForStoreDBPortObj, + db_category_id_exists: CategoryIDExistsDBPortObj, + get_uuid: GetUUIDInterfaceObj, +} + +#[async_trait::async_trait] +impl AddCategoryUseCase for AddCategoryService { + async fn add_category(&self, cmd: AddCategoryCommand) -> OrderingResult { + if !self + .db_store_id_exists + .store_id_exists(cmd.store_id()) + .await? + { + return Err(OrderingError::StoreIDNotFound); + } + + let mut category_id = self.get_uuid.get_uuid(); + + loop { + if self + .db_category_id_exists + .category_id_exists(&category_id) + .await? + { + category_id = self.get_uuid.get_uuid(); + + continue; + } else { + break; + } + } + + let category = CategoryBuilder::default() + .name(cmd.name().into()) + .description(cmd.description().as_ref().map(|s| s.to_string())) + .store_id(*cmd.store_id()) + .category_id(category_id) + .build() + .unwrap(); + + if self + .db_category_name_exists_for_store + .category_name_exists_for_store(&category) + .await? + { + return Err(OrderingError::DuplicateCategoryName); + } + + Ok(CategoryAddedEventBuilder::default() + .name(category.name().into()) + .description(category.description().as_ref().map(|s| s.to_string())) + .added_by_user(*cmd.adding_by()) + .store_id(*category.store_id()) + .category_id(*category.category_id()) + .build() + .unwrap()) + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + + use uuid::Uuid; + + use crate::utils::uuid::tests::UUID; + use crate::{tests::bdd::*, utils::uuid::tests::mock_get_uuid}; + + pub fn mock_add_category_service( + times: Option, + cmd: AddCategoryCommand, + ) -> AddCategoryServiceObj { + let mut m = MockAddCategoryUseCase::new(); + + let res = CategoryAddedEventBuilder::default() + .name(cmd.name().into()) + .description(cmd.description().as_ref().map(|s| s.to_string())) + .added_by_user(*cmd.adding_by()) + .store_id(*cmd.store_id()) + .category_id(UUID) + .build() + .unwrap(); + + if let Some(times) = times { + m.expect_add_category() + .times(times) + .returning(move |_| Ok(res.clone())); + } else { + m.expect_add_category().returning(move |_| Ok(res.clone())); + } + + Arc::new(m) + } + + #[actix_rt::test] + async fn test_service_category_doesnt_exist() { + let name = "foo"; + let description = "bar"; + let user_id = UUID; + let store_id = Uuid::new_v4(); + + // description = None + let cmd = AddCategoryCommand::new(name.into(), Some(description.into()), store_id, user_id) + .unwrap(); + + let s = AddCategoryServiceBuilder::default() + .db_store_id_exists(mock_store_id_exists_db_port_true(IS_CALLED_ONLY_ONCE)) + .db_category_name_exists_for_store(mock_category_name_exists_for_store_db_port_false( + IS_CALLED_ONLY_ONCE, + )) + .db_category_id_exists(mock_category_id_exists_db_port_false(IS_CALLED_ONLY_ONCE)) + .get_uuid(mock_get_uuid(IS_CALLED_ONLY_ONCE)) + .build() + .unwrap(); + + let res = s.add_category(cmd.clone()).await.unwrap(); + assert_eq!(res.name(), cmd.name()); + assert_eq!(res.description(), cmd.description()); + assert_eq!(res.added_by_user(), cmd.adding_by()); + assert_eq!(res.store_id(), cmd.store_id()); + assert_eq!(res.category_id(), &UUID); + } + + #[actix_rt::test] + async fn test_service_category_name_exists_for_store() { + let name = "foo"; + let description = "bar"; + let user_id = UUID; + let store_id = Uuid::new_v4(); + + // description = None + let cmd = AddCategoryCommand::new(name.into(), Some(description.into()), store_id, user_id) + .unwrap(); + + let s = AddCategoryServiceBuilder::default() + .db_store_id_exists(mock_store_id_exists_db_port_true(IS_CALLED_ONLY_ONCE)) + .db_category_name_exists_for_store(mock_category_name_exists_for_store_db_port_true( + IS_CALLED_ONLY_ONCE, + )) + .db_category_id_exists(mock_category_id_exists_db_port_false(IS_CALLED_ONLY_ONCE)) + .get_uuid(mock_get_uuid(IS_CALLED_ONLY_ONCE)) + .build() + .unwrap(); + + assert_eq!( + s.add_category(cmd.clone()).await, + Err(OrderingError::DuplicateCategoryName) + ) + } + + #[actix_rt::test] + async fn test_service_store_doesnt_exist() { + let name = "foo"; + let description = "bar"; + let user_id = UUID; + let store_id = Uuid::new_v4(); + + // description = None + let cmd = AddCategoryCommand::new(name.into(), Some(description.into()), store_id, user_id) + .unwrap(); + + let s = AddCategoryServiceBuilder::default() + .db_store_id_exists(mock_store_id_exists_db_port_false(IS_CALLED_ONLY_ONCE)) + .db_category_name_exists_for_store(mock_category_name_exists_for_store_db_port_false( + IS_NEVER_CALLED, + )) + .db_category_id_exists(mock_category_id_exists_db_port_false(IS_NEVER_CALLED)) + .get_uuid(mock_get_uuid(IS_NEVER_CALLED)) + .build() + .unwrap(); + + assert_eq!( + s.add_category(cmd.clone()).await, + Err(OrderingError::StoreIDNotFound) + ) + } +} diff --git a/src/ordering/application/services/add_customization_service.rs b/src/ordering/application/services/add_customization_service.rs new file mode 100644 index 0000000..7e5146c --- /dev/null +++ b/src/ordering/application/services/add_customization_service.rs @@ -0,0 +1,191 @@ +// 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::ordering::{ + application::port::output::db::{ + customization_id_exists::{self, *}, + customization_name_exists_for_product::*, + product_id_exists::{self, *}, + product_name_exists_for_category::*, + }, + domain::{ + add_customization_command::AddCustomizationCommand, + customization_added_event::{self, *}, + customization_aggregate::*, + product_added_event::{self, ProductAddedEvent, ProductAddedEventBuilder}, + product_aggregate::*, + }, +}; +use crate::utils::uuid::*; + +#[automock] +#[async_trait::async_trait] +pub trait AddCustomizationUseCase: Send + Sync { + async fn add_customization( + &self, + cmd: AddCustomizationCommand, + ) -> OrderingResult; +} + +pub type AddCustomizationServiceObj = Arc; + +#[derive(Clone, Builder)] +pub struct AddCustomizationService { + db_product_id_exists: ProductIDExistsDBPortObj, + db_customization_id_exists: CustomizationIDExistsDBPortObj, + db_customization_name_exists_for_product: CustomizationNameExistsForProductDBPortObj, + get_uuid: GetUUIDInterfaceObj, +} + +#[async_trait::async_trait] +impl AddCustomizationUseCase for AddCustomizationService { + async fn add_customization( + &self, + cmd: AddCustomizationCommand, + ) -> OrderingResult { + if !self + .db_product_id_exists + .product_id_exists(cmd.product_id()) + .await? + { + return Err(OrderingError::ProductIDNotFound); + } + + let mut customization_id = self.get_uuid.get_uuid(); + loop { + if self + .db_customization_id_exists + .customization_id_exists(&customization_id) + .await? + { + customization_id = self.get_uuid.get_uuid(); + continue; + } else { + break; + } + } + + let customization = CustomizationBuilder::default() + .name(cmd.name().into()) + .deleted(false) + .product_id(*cmd.product_id()) + .customization_id(customization_id) + .build() + .unwrap(); + + if self + .db_customization_name_exists_for_product + .customization_name_exists_for_product(&customization) + .await? + { + return Err(OrderingError::DuplicateCustomizationName); + } + + Ok(CustomizationAddedEventBuilder::default() + .customization(customization) + .build() + .unwrap()) + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + + use customization_added_event::tests::get_customization_added_event_from_cmd; + use uuid::Uuid; + + use crate::ordering::domain::add_customization_command::tests::get_command; + use crate::utils::uuid::tests::UUID; + use crate::{tests::bdd::*, utils::uuid::tests::mock_get_uuid}; + + pub fn mock_add_customization_service( + times: Option, + cmd: AddCustomizationCommand, + ) -> AddCustomizationServiceObj { + let mut m = MockAddCustomizationUseCase::new(); + + let res = get_customization_added_event_from_cmd(&cmd); + if let Some(times) = times { + m.expect_add_customization() + .times(times) + .returning(move |_| Ok(res.clone())); + } else { + m.expect_add_customization() + .returning(move |_| Ok(res.clone())); + } + + Arc::new(m) + } + + #[actix_rt::test] + async fn test_service_product_doesnt_exist() { + let cmd = get_command(); + + let s = AddCustomizationServiceBuilder::default() + .db_product_id_exists(mock_product_id_exists_db_port_true(IS_CALLED_ONLY_ONCE)) + .db_customization_id_exists(mock_customization_id_exists_db_port_false( + IS_CALLED_ONLY_ONCE, + )) + .db_customization_name_exists_for_product( + mock_customization_name_exists_for_product_db_port_false(IS_CALLED_ONLY_ONCE), + ) + .get_uuid(mock_get_uuid(IS_CALLED_ONLY_ONCE)) + .build() + .unwrap(); + + let res = s.add_customization(cmd.clone()).await.unwrap(); + assert_eq!(res.customization().name(), cmd.name()); + // assert_eq!(customization_added_events.len(), cmd.customizations().len()); + } + + #[actix_rt::test] + async fn test_service_product_name_exists_for_store() { + let cmd = get_command(); + + let s = AddCustomizationServiceBuilder::default() + .db_product_id_exists(mock_product_id_exists_db_port_true(IS_CALLED_ONLY_ONCE)) + .db_customization_id_exists(mock_customization_id_exists_db_port_false( + IS_CALLED_ONLY_ONCE, + )) + .db_customization_name_exists_for_product( + mock_customization_name_exists_for_product_db_port_true(IS_CALLED_ONLY_ONCE), + ) + .get_uuid(mock_get_uuid(IS_CALLED_ONLY_ONCE)) + .build() + .unwrap(); + + assert_eq!( + s.add_customization(cmd.clone()).await, + Err(OrderingError::DuplicateCustomizationName) + ) + } + + #[actix_rt::test] + async fn test_service_product_id_not_found() { + let cmd = get_command(); + + let s = AddCustomizationServiceBuilder::default() + .db_product_id_exists(mock_product_id_exists_db_port_false(IS_CALLED_ONLY_ONCE)) + .db_customization_id_exists(mock_customization_id_exists_db_port_true(IS_NEVER_CALLED)) + .db_customization_name_exists_for_product( + mock_customization_name_exists_for_product_db_port_false(IS_NEVER_CALLED), + ) + .get_uuid(mock_get_uuid(IS_NEVER_CALLED)) + .build() + .unwrap(); + + assert_eq!( + s.add_customization(cmd.clone()).await, + Err(OrderingError::ProductIDNotFound) + ) + } +} diff --git a/src/ordering/application/services/add_product_service.rs b/src/ordering/application/services/add_product_service.rs new file mode 100644 index 0000000..3eb4e7e --- /dev/null +++ b/src/ordering/application/services/add_product_service.rs @@ -0,0 +1,227 @@ +// 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::ordering::{ + application::port::output::{ + db::{ + category_id_exists::*, get_category::*, product_id_exists::*, + product_name_exists_for_category::*, + }, + full_text_search::add_product_to_store::*, + }, + domain::{ + add_product_command::AddProductCommand, + product_added_event::{ProductAddedEvent, ProductAddedEventBuilder}, + product_aggregate::*, + }, +}; +use crate::utils::uuid::*; + +#[automock] +#[async_trait::async_trait] +pub trait AddProductUseCase: Send + Sync { + async fn add_product(&self, cmd: AddProductCommand) -> OrderingResult; +} + +pub type AddProductServiceObj = Arc; + +#[derive(Clone, Builder)] +pub struct AddProductService { + db_category_id_exists: CategoryIDExistsDBPortObj, + db_product_name_exists_for_category: ProductNameExistsForCategoryDBPortObj, + db_product_id_exists: ProductIDExistsDBPortObj, + db_get_category: GetCategoryDBPortObj, + fts_add_product: AddProductToStoreFTSPortObj, + get_uuid: GetUUIDInterfaceObj, +} + +#[async_trait::async_trait] +impl AddProductUseCase for AddProductService { + async fn add_product(&self, cmd: AddProductCommand) -> OrderingResult { + if !self + .db_category_id_exists + .category_id_exists(cmd.category_id()) + .await? + { + return Err(OrderingError::CategoryIDNotFound); + } + + let mut product_id = self.get_uuid.get_uuid(); + + loop { + if self + .db_product_id_exists + .product_id_exists(&product_id) + .await? + { + product_id = self.get_uuid.get_uuid(); + continue; + } else { + break; + } + } + + let product = ProductBuilder::default() + .name(cmd.name().into()) + .description(cmd.description().as_ref().map(|s| s.to_string())) + .image(cmd.image().clone()) + .sku_able(*cmd.sku_able()) + .price(cmd.price().clone()) + .category_id(*cmd.category_id()) + .quantity(cmd.quantity().clone()) + .product_id(product_id) + .build() + .unwrap(); + + if self + .db_product_name_exists_for_category + .product_name_exists_for_category(&product) + .await? + { + return Err(OrderingError::DuplicateProductName); + } + + let category = self + .db_get_category + .get_category(product.category_id()) + .await?; + self.fts_add_product + .add_product_to_store(&product, &category) + .await?; + + Ok(ProductAddedEventBuilder::default() + .added_by_user(*cmd.adding_by()) + .name(product.name().into()) + .description(product.description().as_ref().map(|s| s.to_string())) + .image(product.image().clone()) + .sku_able(*product.sku_able()) + .price(product.price().clone()) + .category_id(*product.category_id()) + .product_id(*product.product_id()) + .category_id(product.category_id().clone()) + .product_id(product.product_id().clone()) + .quantity(product.quantity().clone()) + .build() + .unwrap()) + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + + use crate::ordering::domain::add_product_command::tests::get_command; + use crate::utils::uuid::tests::UUID; + use crate::{tests::bdd::*, utils::uuid::tests::mock_get_uuid}; + + pub fn mock_add_product_service( + times: Option, + cmd: AddProductCommand, + ) -> AddProductServiceObj { + let mut m = MockAddProductUseCase::new(); + + let res = //( + ProductAddedEventBuilder::default() + .name(cmd.name().into()) + .description(cmd.description().as_ref().map(|s| s.to_string())) + .image(cmd.image().as_ref().map(|s| s.to_string())) + .sku_able(cmd.sku_able().clone()) + .category_id(cmd.category_id().clone()) + .product_id(UUID.clone()) + .price(cmd.price().clone()) + .quantity(cmd.quantity().clone()) + .added_by_user(cmd.adding_by().clone()) + .build() + .unwrap(); + + if let Some(times) = times { + m.expect_add_product() + .times(times) + .returning(move |_| Ok(res.clone())); + } else { + m.expect_add_product().returning(move |_| Ok(res.clone())); + } + + Arc::new(m) + } + + #[actix_rt::test] + async fn test_service_product_doesnt_exist() { + let cmd = get_command(); + + let s = AddProductServiceBuilder::default() + .db_product_name_exists_for_category( + mock_product_name_exists_for_category_db_port_false(IS_CALLED_ONLY_ONCE), + ) + .db_product_id_exists(mock_product_id_exists_db_port_false(IS_CALLED_ONLY_ONCE)) + .db_get_category(mock_get_category_db_port(IS_CALLED_ONLY_ONCE)) + .db_category_id_exists(mock_category_id_exists_db_port_true(IS_CALLED_ONLY_ONCE)) + .fts_add_product(mock_add_product_to_store_fts_port(IS_CALLED_ONLY_ONCE)) + .get_uuid(mock_get_uuid(IS_CALLED_ONLY_ONCE)) + .build() + .unwrap(); + + let res = s.add_product(cmd.clone()).await.unwrap(); + assert_eq!(res.name(), cmd.name()); + assert_eq!(res.description(), cmd.description()); + assert_eq!(res.image(), cmd.image()); + assert_eq!(res.sku_able(), cmd.sku_able()); + assert_eq!(res.price(), cmd.price()); + assert_eq!(res.added_by_user(), cmd.adding_by()); + assert_eq!(res.category_id(), cmd.category_id()); + assert_eq!(res.product_id(), &UUID); + assert_eq!(res.quantity(), cmd.quantity()); + } + + #[actix_rt::test] + async fn test_service_product_name_exists_for_store() { + let cmd = get_command(); + + let s = AddProductServiceBuilder::default() + .db_product_name_exists_for_category( + mock_product_name_exists_for_category_db_port_true(IS_CALLED_ONLY_ONCE), + ) + .db_category_id_exists(mock_category_id_exists_db_port_true(IS_CALLED_ONLY_ONCE)) + .get_uuid(mock_get_uuid(IS_CALLED_ONLY_ONCE)) + .db_product_id_exists(mock_product_id_exists_db_port_false(IS_CALLED_ONLY_ONCE)) + .db_get_category(mock_get_category_db_port(IS_NEVER_CALLED)) + .fts_add_product(mock_add_product_to_store_fts_port(IS_NEVER_CALLED)) + .build() + .unwrap(); + + assert_eq!( + s.add_product(cmd.clone()).await, + Err(OrderingError::DuplicateProductName) + ) + } + + #[actix_rt::test] + async fn test_service_category_id_doesnt_exist() { + let cmd = get_command(); + + let s = AddProductServiceBuilder::default() + .db_product_name_exists_for_category( + mock_product_name_exists_for_category_db_port_false(IS_NEVER_CALLED), + ) + .db_product_id_exists(mock_product_id_exists_db_port_false(IS_NEVER_CALLED)) + .db_category_id_exists(mock_category_id_exists_db_port_false(IS_CALLED_ONLY_ONCE)) + .db_get_category(mock_get_category_db_port(IS_NEVER_CALLED)) + .fts_add_product(mock_add_product_to_store_fts_port(IS_NEVER_CALLED)) + .get_uuid(mock_get_uuid(IS_NEVER_CALLED)) + .build() + .unwrap(); + + assert_eq!( + s.add_product(cmd.clone()).await, + Err(OrderingError::CategoryIDNotFound) + ) + } +} diff --git a/src/ordering/application/services/add_store_service.rs b/src/ordering/application/services/add_store_service.rs new file mode 100644 index 0000000..041ead3 --- /dev/null +++ b/src/ordering/application/services/add_store_service.rs @@ -0,0 +1,149 @@ +// 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::ordering::{ + application::port::output::db::{store_id_exists::*, store_name_exists::*}, + domain::{ + add_store_command::AddStoreCommand, + store_added_event::{StoreAddedEvent, StoreAddedEventBuilder}, + store_aggregate::*, + }, +}; +use crate::utils::uuid::*; + +#[automock] +#[async_trait::async_trait] +pub trait AddStoreUseCase: Send + Sync { + async fn add_store(&self, cmd: AddStoreCommand) -> OrderingResult; +} + +pub type AddStoreServiceObj = Arc; + +#[derive(Clone, Builder)] +pub struct AddStoreService { + db_store_id_exists: StoreIDExistsDBPortObj, + db_store_name_exists: StoreNameExistsDBPortObj, + get_uuid: GetUUIDInterfaceObj, +} + +#[async_trait::async_trait] +impl AddStoreUseCase for AddStoreService { + async fn add_store(&self, cmd: AddStoreCommand) -> OrderingResult { + let mut store_id = self.get_uuid.get_uuid(); + + loop { + if self.db_store_id_exists.store_id_exists(&store_id).await? { + store_id = self.get_uuid.get_uuid(); + continue; + } else { + break; + } + } + + let store = StoreBuilder::default() + .name(cmd.name().into()) + .address(cmd.address().as_ref().map(|s| s.to_string())) + .owner(*cmd.owner()) + .store_id(store_id) + .build() + .unwrap(); + + if self.db_store_name_exists.store_name_exists(&store).await? { + return Err(OrderingError::DuplicateStoreName); + } + + Ok(StoreAddedEventBuilder::default() + .name(store.name().into()) + .address(store.address().as_ref().map(|s| s.to_string())) + .owner(*cmd.owner()) + .store_id(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(UUID) + .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; + + // address = None + let cmd = AddStoreCommand::new(name.into(), Some(address.into()), owner).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)) + .get_uuid(mock_get_uuid(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(), &UUID); + } + + #[actix_rt::test] + async fn test_service_store_name_exists() { + let name = "foo"; + let address = "bar"; + let owner = UUID; + + // address = None + let cmd = AddStoreCommand::new(name.into(), Some(address.into()), owner).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)) + .get_uuid(mock_get_uuid(IS_CALLED_ONLY_ONCE)) + .build() + .unwrap(); + + assert_eq!( + s.add_store(cmd.clone()).await, + Err(OrderingError::DuplicateStoreName) + ); + } +} diff --git a/src/ordering/application/services/errors.rs b/src/ordering/application/services/errors.rs index f27978a..b8e598f 100644 --- a/src/ordering/application/services/errors.rs +++ b/src/ordering/application/services/errors.rs @@ -6,7 +6,9 @@ use derive_more::{Display, Error}; use log::error; use serde::{Deserialize, Serialize}; -use crate::ordering::application::port::output::db::errors::OrderingDBError; +use crate::ordering::application::port::output::{ + db::errors::OrderingDBError, full_text_search::errors::OrderingFTSError, +}; pub type OrderingResult = Result; @@ -16,6 +18,14 @@ pub enum OrderingError { InternalError, OrderIDNotFound, KotIDNotFound, + DuplicateStoreName, + StoreIDNotFound, + CategoryIDNotFound, + DuplicateCategoryName, + DuplicateProductName, + ProductIDNotFound, + DuplicateCustomizationName, + CustomizationIDNotFound, } // impl From for OrderingError { @@ -30,6 +40,13 @@ impl From for OrderingError { error!("DuplicateOrderID"); Self::InternalError } + OrderingDBError::DuplicateStoreName => Self::DuplicateStoreName, + OrderingDBError::DuplicateStoreID => { + error!("DuplicateStoreID"); + Self::InternalError + } + OrderingDBError::StoreIDNotFound => OrderingError::StoreIDNotFound, + OrderingDBError::OrderIDNotFound => OrderingError::OrderIDNotFound, OrderingDBError::DuplicateKotID => { @@ -38,6 +55,31 @@ impl From for OrderingError { } OrderingDBError::KotIDNotFound => OrderingError::KotIDNotFound, OrderingDBError::InternalError => Self::InternalError, + OrderingDBError::DuplicateCategoryName => Self::DuplicateCategoryName, + OrderingDBError::DuplicateCategoryID => { + error!("DuplicateCategoryID"); + Self::InternalError + } + OrderingDBError::CategoryIDNotFound => OrderingError::CategoryIDNotFound, + OrderingDBError::DuplicateProductName => Self::DuplicateProductName, + OrderingDBError::DuplicateProductID => { + error!("DuplicateProductID"); + Self::InternalError + } + OrderingDBError::ProductIDNotFound => OrderingError::ProductIDNotFound, + OrderingDBError::DuplicateCustomizationName => Self::DuplicateCustomizationName, + OrderingDBError::DuplicateCustomizationID => { + error!("DuplicateCustomizationID"); + Self::InternalError + } + OrderingDBError::CustomizationIDNotFound => OrderingError::CustomizationIDNotFound, } } } + +impl From for OrderingError { + fn from(value: OrderingFTSError) -> Self { + error!("{}", value); + OrderingError::InternalError + } +} diff --git a/src/ordering/application/services/mod.rs b/src/ordering/application/services/mod.rs index 2d7b9cf..d56fc69 100644 --- a/src/ordering/application/services/mod.rs +++ b/src/ordering/application/services/mod.rs @@ -9,15 +9,23 @@ use mockall::*; pub mod errors; //services +pub mod add_category_service; +pub mod add_customization_service; pub mod add_kot_service; pub mod add_line_item_service; pub mod add_order_service; +pub mod add_product_service; +pub mod add_store_service; pub mod delete_kot_service; pub mod delete_line_item_service; pub mod delete_order_service; +pub mod update_category_service; +pub mod update_customization_service; pub mod update_kot_service; pub mod update_line_item_service; pub mod update_order_service; +pub mod update_product_service; +pub mod update_store_service; #[automock] pub trait OrderingServicesInterface: Send + Sync { @@ -30,6 +38,14 @@ pub trait OrderingServicesInterface: Send + Sync { 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; } #[derive(Clone, Builder)] @@ -43,6 +59,14 @@ pub struct OrderingServices { 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, } impl OrderingServicesInterface for OrderingServices { @@ -80,4 +104,28 @@ impl OrderingServicesInterface for OrderingServices { fn delete_kot(&self) -> delete_kot_service::DeleteKotServiceObj { self.delete_kot.clone() } + fn add_store(&self) -> add_store_service::AddStoreServiceObj { + self.add_store.clone() + } + fn update_store(&self) -> update_store_service::UpdateStoreServiceObj { + self.update_store.clone() + } + fn add_category(&self) -> add_category_service::AddCategoryServiceObj { + self.add_category.clone() + } + fn update_category(&self) -> update_category_service::UpdateCategoryServiceObj { + self.update_category.clone() + } + fn add_product(&self) -> add_product_service::AddProductServiceObj { + self.add_product.clone() + } + fn update_product(&self) -> update_product_service::UpdateProductServiceObj { + self.update_product.clone() + } + fn add_customization(&self) -> add_customization_service::AddCustomizationServiceObj { + self.add_customization.clone() + } + fn update_customization(&self) -> update_customization_service::UpdateCustomizationServiceObj { + self.update_customization.clone() + } } diff --git a/src/ordering/application/services/remove_category_service.rs b/src/ordering/application/services/remove_category_service.rs new file mode 100644 index 0000000..56f60de --- /dev/null +++ b/src/ordering/application/services/remove_category_service.rs @@ -0,0 +1,3 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/src/ordering/application/services/remove_product_service.rs b/src/ordering/application/services/remove_product_service.rs new file mode 100644 index 0000000..56f60de --- /dev/null +++ b/src/ordering/application/services/remove_product_service.rs @@ -0,0 +1,3 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/src/ordering/application/services/update_category_service.rs b/src/ordering/application/services/update_category_service.rs new file mode 100644 index 0000000..ffb087f --- /dev/null +++ b/src/ordering/application/services/update_category_service.rs @@ -0,0 +1,198 @@ +// 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::ordering::{ + application::port::output::db::{ + category_id_exists::*, category_name_exists_for_store::*, store_id_exists::*, + }, + domain::{category_aggregate::*, category_updated_event::*, update_category_command::*}, +}; +use crate::utils::uuid::*; + +#[automock] +#[async_trait::async_trait] +pub trait UpdateCategoryUseCase: Send + Sync { + async fn update_category( + &self, + cmd: UpdateCategoryCommand, + ) -> OrderingResult; +} + +pub type UpdateCategoryServiceObj = Arc; + +#[derive(Clone, Builder)] +pub struct UpdateCategoryService { + // TODO: check if store ID exists + db_category_name_exists_for_store: CategoryNameExistsForStoreDBPortObj, + db_category_id_exists: CategoryIDExistsDBPortObj, + db_store_id_exists: StoreIDExistsDBPortObj, +} + +#[async_trait::async_trait] +impl UpdateCategoryUseCase for UpdateCategoryService { + async fn update_category( + &self, + cmd: UpdateCategoryCommand, + ) -> OrderingResult { + if !self + .db_category_id_exists + .category_id_exists(cmd.old_category().category_id()) + .await? + { + return Err(OrderingError::CategoryIDNotFound); + } + + if !self + .db_store_id_exists + .store_id_exists(cmd.old_category().store_id()) + .await? + { + return Err(OrderingError::StoreIDNotFound); + } + + let updated_category = CategoryBuilder::default() + .name(cmd.name().into()) + .description(cmd.description().as_ref().map(|s| s.to_string())) + .category_id(*cmd.old_category().category_id()) + .store_id(*cmd.old_category().store_id()) + .build() + .unwrap(); + + if updated_category.name() != cmd.old_category().name() { + if self + .db_category_name_exists_for_store + .category_name_exists_for_store(&updated_category) + .await? + { + return Err(OrderingError::DuplicateCategoryName); + } + } + + Ok(CategoryUpdatedEventBuilder::default() + .added_by_user(*cmd.adding_by()) + .old_category(cmd.old_category().clone()) + .new_category(updated_category) + .build() + .unwrap()) + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + + use crate::ordering::domain::category_updated_event; + use crate::ordering::domain::update_category_command::tests::get_update_category_command; + use crate::utils::uuid::tests::UUID; + use crate::{tests::bdd::*, utils::uuid::tests::mock_get_uuid}; + + pub fn mock_update_category_service( + times: Option, + cmd: UpdateCategoryCommand, + ) -> UpdateCategoryServiceObj { + let mut m = MockUpdateCategoryUseCase::new(); + + let res = category_updated_event::tests::get_category_updated_event_from_command(&cmd); + + if let Some(times) = times { + m.expect_update_category() + .times(times) + .returning(move |_| Ok(res.clone())); + } else { + m.expect_update_category() + .returning(move |_| Ok(res.clone())); + } + + Arc::new(m) + } + + #[actix_rt::test] + async fn test_service() { + let cmd = get_update_category_command(); + + let s = UpdateCategoryServiceBuilder::default() + .db_store_id_exists(mock_store_id_exists_db_port_true(IS_CALLED_ONLY_ONCE)) + .db_category_name_exists_for_store(mock_category_name_exists_for_store_db_port_false( + IS_CALLED_ONLY_ONCE, + )) + .db_category_id_exists(mock_category_id_exists_db_port_true(IS_CALLED_ONLY_ONCE)) + .build() + .unwrap(); + + let res = s.update_category(cmd.clone()).await.unwrap(); + assert_eq!(res.new_category().name(), cmd.name()); + assert_eq!(res.new_category().description(), cmd.description()); + assert_eq!(res.new_category().store_id(), cmd.old_category().store_id()); + assert_eq!( + res.new_category().category_id(), + cmd.old_category().category_id() + ); + assert_eq!(res.old_category(), cmd.old_category()); + assert_eq!(res.added_by_user(), cmd.adding_by()); + } + + #[actix_rt::test] + async fn test_service_store_doesnt_exist() { + let cmd = get_update_category_command(); + + let s = UpdateCategoryServiceBuilder::default() + .db_store_id_exists(mock_store_id_exists_db_port_false(IS_CALLED_ONLY_ONCE)) + .db_category_name_exists_for_store(mock_category_name_exists_for_store_db_port_false( + IS_NEVER_CALLED, + )) + .db_category_id_exists(mock_category_id_exists_db_port_true(IS_CALLED_ONLY_ONCE)) + .build() + .unwrap(); + + assert_eq!( + s.update_category(cmd.clone()).await, + Err(OrderingError::StoreIDNotFound) + ); + } + + #[actix_rt::test] + async fn test_category_id_not_found() { + let cmd = get_update_category_command(); + + let s = UpdateCategoryServiceBuilder::default() + .db_store_id_exists(mock_store_id_exists_db_port_true(IS_NEVER_CALLED)) + .db_category_name_exists_for_store(mock_category_name_exists_for_store_db_port_false( + IS_NEVER_CALLED, + )) + .db_category_id_exists(mock_category_id_exists_db_port_false(IS_CALLED_ONLY_ONCE)) + .build() + .unwrap(); + + assert_eq!( + s.update_category(cmd.clone()).await, + Err(OrderingError::CategoryIDNotFound) + ); + } + + #[actix_rt::test] + async fn test_duplicate_new_name() { + let cmd = get_update_category_command(); + + let s = UpdateCategoryServiceBuilder::default() + .db_store_id_exists(mock_store_id_exists_db_port_true(IS_CALLED_ONLY_ONCE)) + .db_category_name_exists_for_store(mock_category_name_exists_for_store_db_port_true( + IS_CALLED_ONLY_ONCE, + )) + .db_category_id_exists(mock_category_id_exists_db_port_true(IS_CALLED_ONLY_ONCE)) + .build() + .unwrap(); + + assert_eq!( + s.update_category(cmd.clone()).await, + Err(OrderingError::DuplicateCategoryName) + ); + } +} diff --git a/src/ordering/application/services/update_customization_service.rs b/src/ordering/application/services/update_customization_service.rs new file mode 100644 index 0000000..00961b3 --- /dev/null +++ b/src/ordering/application/services/update_customization_service.rs @@ -0,0 +1,210 @@ +// 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::ordering::{ + application::port::output::db::{ + customization_id_exists::*, customization_name_exists_for_product::*, product_id_exists::*, + }, + domain::{ + customization_aggregate::*, customization_updated_event::*, update_customization_command::*, + }, +}; +use crate::utils::uuid::*; + +#[automock] +#[async_trait::async_trait] +pub trait UpdateCustomizationUseCase: Send + Sync { + async fn update_customization( + &self, + cmd: UpdateCustomizationCommand, + ) -> OrderingResult; +} + +pub type UpdateCustomizationServiceObj = Arc; + +#[derive(Clone, Builder)] +pub struct UpdateCustomizationService { + // TODO: check if product ID exists + db_customization_name_exists_for_product: CustomizationNameExistsForProductDBPortObj, + db_customization_id_exists: CustomizationIDExistsDBPortObj, + db_product_id_exists: ProductIDExistsDBPortObj, +} + +#[async_trait::async_trait] +impl UpdateCustomizationUseCase for UpdateCustomizationService { + async fn update_customization( + &self, + cmd: UpdateCustomizationCommand, + ) -> OrderingResult { + if !self + .db_customization_id_exists + .customization_id_exists(cmd.old_customization().customization_id()) + .await? + { + return Err(OrderingError::CustomizationIDNotFound); + } + + if !self + .db_product_id_exists + .product_id_exists(cmd.old_customization().product_id()) + .await? + { + return Err(OrderingError::ProductIDNotFound); + } + + let updated_customization = CustomizationBuilder::default() + .name(cmd.name().into()) + .product_id(*cmd.old_customization().product_id()) + .customization_id(*cmd.old_customization().customization_id()) + .deleted(*cmd.old_customization().deleted()) + .build() + .unwrap(); + + if updated_customization.name() != cmd.old_customization().name() { + if self + .db_customization_name_exists_for_product + .customization_name_exists_for_product(&updated_customization) + .await? + { + return Err(OrderingError::DuplicateCustomizationName); + } + } + + Ok(CustomizationUpdatedEventBuilder::default() + .added_by_user(*cmd.adding_by()) + .old_customization(cmd.old_customization().clone()) + .new_customization(updated_customization) + .build() + .unwrap()) + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + + use crate::ordering::domain::customization_updated_event; + use crate::ordering::domain::update_customization_command::tests::get_update_customization_command; + use crate::tests::bdd::*; + + pub fn mock_update_customization_service( + times: Option, + cmd: UpdateCustomizationCommand, + ) -> UpdateCustomizationServiceObj { + let mut m = MockUpdateCustomizationUseCase::new(); + + let res = + customization_updated_event::tests::get_customization_updated_event_from_command(&cmd); + + if let Some(times) = times { + m.expect_update_customization() + .times(times) + .returning(move |_| Ok(res.clone())); + } else { + m.expect_update_customization() + .returning(move |_| Ok(res.clone())); + } + + Arc::new(m) + } + + #[actix_rt::test] + async fn test_service() { + let cmd = get_update_customization_command(); + + let s = UpdateCustomizationServiceBuilder::default() + .db_product_id_exists(mock_product_id_exists_db_port_true(IS_CALLED_ONLY_ONCE)) + .db_customization_name_exists_for_product( + mock_customization_name_exists_for_product_db_port_false(IS_CALLED_ONLY_ONCE), + ) + .db_customization_id_exists(mock_customization_id_exists_db_port_true( + IS_CALLED_ONLY_ONCE, + )) + .build() + .unwrap(); + + let res = s.update_customization(cmd.clone()).await.unwrap(); + assert_eq!(res.new_customization().name(), cmd.name()); + assert_eq!( + res.new_customization().product_id(), + cmd.old_customization().product_id() + ); + assert_eq!( + res.new_customization().customization_id(), + cmd.old_customization().customization_id() + ); + assert_eq!(res.old_customization(), cmd.old_customization()); + assert_eq!(res.added_by_user(), cmd.adding_by()); + } + + #[actix_rt::test] + async fn test_service_product_doesnt_exist() { + let cmd = get_update_customization_command(); + + let s = UpdateCustomizationServiceBuilder::default() + .db_product_id_exists(mock_product_id_exists_db_port_false(IS_CALLED_ONLY_ONCE)) + .db_customization_name_exists_for_product( + mock_customization_name_exists_for_product_db_port_false(IS_NEVER_CALLED), + ) + .db_customization_id_exists(mock_customization_id_exists_db_port_true( + IS_CALLED_ONLY_ONCE, + )) + .build() + .unwrap(); + + assert_eq!( + s.update_customization(cmd.clone()).await, + Err(OrderingError::ProductIDNotFound) + ); + } + + #[actix_rt::test] + async fn test_customization_id_not_found() { + let cmd = get_update_customization_command(); + + let s = UpdateCustomizationServiceBuilder::default() + .db_product_id_exists(mock_product_id_exists_db_port_true(IS_NEVER_CALLED)) + .db_customization_name_exists_for_product( + mock_customization_name_exists_for_product_db_port_false(IS_NEVER_CALLED), + ) + .db_customization_id_exists(mock_customization_id_exists_db_port_false( + IS_CALLED_ONLY_ONCE, + )) + .build() + .unwrap(); + + assert_eq!( + s.update_customization(cmd.clone()).await, + Err(OrderingError::CustomizationIDNotFound) + ); + } + + #[actix_rt::test] + async fn test_duplicate_new_name() { + let cmd = get_update_customization_command(); + + let s = UpdateCustomizationServiceBuilder::default() + .db_product_id_exists(mock_product_id_exists_db_port_true(IS_CALLED_ONLY_ONCE)) + .db_customization_name_exists_for_product( + mock_customization_name_exists_for_product_db_port_true(IS_CALLED_ONLY_ONCE), + ) + .db_customization_id_exists(mock_customization_id_exists_db_port_true( + IS_CALLED_ONLY_ONCE, + )) + .build() + .unwrap(); + + assert_eq!( + s.update_customization(cmd.clone()).await, + Err(OrderingError::DuplicateCustomizationName) + ); + } +} diff --git a/src/ordering/application/services/update_product_service.rs b/src/ordering/application/services/update_product_service.rs new file mode 100644 index 0000000..6616e59 --- /dev/null +++ b/src/ordering/application/services/update_product_service.rs @@ -0,0 +1,206 @@ +// 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::ordering::{ + application::port::output::db::{ + category_id_exists::*, product_id_exists::*, product_name_exists_for_category::*, + }, + domain::{product_aggregate::*, product_updated_event::*, update_product_command::*}, +}; +use crate::utils::uuid::*; + +#[automock] +#[async_trait::async_trait] +pub trait UpdateProductUseCase: Send + Sync { + async fn update_product( + &self, + cmd: UpdateProductCommand, + ) -> OrderingResult; +} + +pub type UpdateProductServiceObj = Arc; + +#[derive(Clone, Builder)] +pub struct UpdateProductService { + // TODO: check if category ID exists + db_product_name_exists_for_category: ProductNameExistsForCategoryDBPortObj, + db_product_id_exists: ProductIDExistsDBPortObj, + db_category_id_exists: CategoryIDExistsDBPortObj, +} + +#[async_trait::async_trait] +impl UpdateProductUseCase for UpdateProductService { + async fn update_product( + &self, + cmd: UpdateProductCommand, + ) -> OrderingResult { + if !self + .db_product_id_exists + .product_id_exists(cmd.old_product().product_id()) + .await? + { + return Err(OrderingError::ProductIDNotFound); + } + + if !self + .db_category_id_exists + .category_id_exists(cmd.category_id()) + .await? + { + return Err(OrderingError::CategoryIDNotFound); + } + + let updated_product = ProductBuilder::default() + .name(cmd.name().into()) + .description(cmd.description().as_ref().map(|s| s.to_string())) + .image(cmd.image().clone()) + .sku_able(*cmd.sku_able()) + .price(cmd.price().clone()) + .category_id(*cmd.category_id()) + .quantity(cmd.quantity().clone()) + .product_id(cmd.old_product().product_id().clone()) + .build() + .unwrap(); + + if updated_product.name() != cmd.old_product().name() { + if self + .db_product_name_exists_for_category + .product_name_exists_for_category(&updated_product) + .await? + { + return Err(OrderingError::DuplicateProductName); + } + } + + Ok(ProductUpdatedEventBuilder::default() + .added_by_user(*cmd.adding_by()) + .old_product(cmd.old_product().clone()) + .new_product(updated_product) + .build() + .unwrap()) + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + + use crate::ordering::domain::product_updated_event; + use crate::ordering::domain::update_product_command::tests::get_command; + use crate::utils::uuid::tests::UUID; + use crate::{tests::bdd::*, utils::uuid::tests::mock_get_uuid}; + + pub fn mock_update_product_service( + times: Option, + cmd: UpdateProductCommand, + ) -> UpdateProductServiceObj { + let mut m = MockUpdateProductUseCase::new(); + + let res = product_updated_event::tests::get_event_from_command(&cmd); + + if let Some(times) = times { + m.expect_update_product() + .times(times) + .returning(move |_| Ok(res.clone())); + } else { + m.expect_update_product() + .returning(move |_| Ok(res.clone())); + } + + Arc::new(m) + } + + #[actix_rt::test] + async fn test_service() { + let cmd = get_command(); + + let s = UpdateProductServiceBuilder::default() + .db_category_id_exists(mock_category_id_exists_db_port_true(IS_CALLED_ONLY_ONCE)) + .db_product_name_exists_for_category( + mock_product_name_exists_for_category_db_port_false(IS_CALLED_ONLY_ONCE), + ) + .db_product_id_exists(mock_product_id_exists_db_port_true(IS_CALLED_ONLY_ONCE)) + .build() + .unwrap(); + + let res = s.update_product(cmd.clone()).await.unwrap(); + assert_eq!(res.new_product().name(), cmd.name()); + assert_eq!(res.new_product().description(), cmd.description()); + assert_eq!(res.new_product().image(), cmd.image()); + assert_eq!(res.new_product().sku_able(), cmd.sku_able()); + assert_eq!(res.new_product().price(), cmd.price()); + assert_eq!(res.new_product().category_id(), cmd.category_id()); + assert_eq!( + res.new_product().product_id(), + cmd.old_product().product_id() + ); + assert_eq!(res.new_product().quantity(), cmd.quantity()); + assert_eq!(res.old_product(), cmd.old_product()); + assert_eq!(res.added_by_user(), cmd.adding_by()); + } + + #[actix_rt::test] + async fn test_service_category_doesnt_exist() { + let cmd = get_command(); + + let s = UpdateProductServiceBuilder::default() + .db_category_id_exists(mock_category_id_exists_db_port_false(IS_CALLED_ONLY_ONCE)) + .db_product_name_exists_for_category( + mock_product_name_exists_for_category_db_port_false(IS_NEVER_CALLED), + ) + .db_product_id_exists(mock_product_id_exists_db_port_true(IS_CALLED_ONLY_ONCE)) + .build() + .unwrap(); + + assert_eq!( + s.update_product(cmd.clone()).await, + Err(OrderingError::CategoryIDNotFound) + ); + } + + #[actix_rt::test] + async fn test_product_id_not_found() { + let cmd = get_command(); + + let s = UpdateProductServiceBuilder::default() + .db_category_id_exists(mock_category_id_exists_db_port_true(IS_NEVER_CALLED)) + .db_product_name_exists_for_category( + mock_product_name_exists_for_category_db_port_false(IS_NEVER_CALLED), + ) + .db_product_id_exists(mock_product_id_exists_db_port_false(IS_CALLED_ONLY_ONCE)) + .build() + .unwrap(); + + assert_eq!( + s.update_product(cmd.clone()).await, + Err(OrderingError::ProductIDNotFound) + ); + } + + #[actix_rt::test] + async fn test_duplicate_new_name() { + let cmd = get_command(); + + let s = UpdateProductServiceBuilder::default() + .db_category_id_exists(mock_category_id_exists_db_port_true(IS_CALLED_ONLY_ONCE)) + .db_product_name_exists_for_category( + mock_product_name_exists_for_category_db_port_true(IS_CALLED_ONLY_ONCE), + ) + .db_product_id_exists(mock_product_id_exists_db_port_true(IS_CALLED_ONLY_ONCE)) + .build() + .unwrap(); + + assert_eq!( + s.update_product(cmd.clone()).await, + Err(OrderingError::DuplicateProductName) + ); + } +} diff --git a/src/ordering/application/services/update_store_service.rs b/src/ordering/application/services/update_store_service.rs new file mode 100644 index 0000000..67039fa --- /dev/null +++ b/src/ordering/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::ordering::{ + 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) -> OrderingResult; +} + +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) -> OrderingResult { + if !self + .db_store_id_exists + .store_id_exists(cmd.old_store().store_id()) + .await? + { + return Err(OrderingError::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(OrderingError::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::ordering::domain::store_updated_event::tests::get_store_updated_event_from_command; + use crate::ordering::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(OrderingError::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(OrderingError::StoreIDNotFound) + ); + } +} diff --git a/src/ordering/domain/add_category_command.rs b/src/ordering/domain/add_category_command.rs new file mode 100644 index 0000000..9c8d0eb --- /dev/null +++ b/src/ordering/domain/add_category_command.rs @@ -0,0 +1,90 @@ +// 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; + +#[derive(Debug, Error, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +pub enum AddCategoryCommandError { + NameIsEmpty, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)] +pub struct AddCategoryCommand { + name: String, + description: Option, + store_id: Uuid, + adding_by: Uuid, +} + +impl AddCategoryCommand { + pub fn new( + name: String, + description: Option, + store_id: Uuid, + adding_by: Uuid, + ) -> Result { + let description: Option = if let Some(description) = description { + let description = description.trim(); + if description.is_empty() { + None + } else { + Some(description.to_owned()) + } + } else { + None + }; + + let name = name.trim().to_owned(); + if name.is_empty() { + return Err(AddCategoryCommandError::NameIsEmpty); + } + + Ok(Self { + name, + store_id, + description, + adding_by, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::utils::uuid::tests::UUID; + + #[test] + fn test_cmd() { + let name = "foo"; + let description = "bar"; + let adding_by = UUID; + let store_id = Uuid::new_v4(); + + // description = None + let cmd = AddCategoryCommand::new(name.into(), None, store_id, adding_by).unwrap(); + assert_eq!(cmd.name(), name); + assert_eq!(cmd.description(), &None); + assert_eq!(cmd.adding_by(), &adding_by); + assert_eq!(cmd.store_id(), &store_id); + + // description = Some + let cmd = + AddCategoryCommand::new(name.into(), Some(description.into()), store_id, adding_by) + .unwrap(); + assert_eq!(cmd.name(), name); + assert_eq!(cmd.description(), &Some(description.to_owned())); + assert_eq!(cmd.adding_by(), &adding_by); + assert_eq!(cmd.store_id(), &store_id); + + // AddCategoryCommandError::NameIsEmpty + assert_eq!( + AddCategoryCommand::new("".into(), Some(description.into()), store_id, adding_by,), + Err(AddCategoryCommandError::NameIsEmpty) + ) + } +} diff --git a/src/ordering/domain/add_customization_command.rs b/src/ordering/domain/add_customization_command.rs new file mode 100644 index 0000000..c79a64e --- /dev/null +++ b/src/ordering/domain/add_customization_command.rs @@ -0,0 +1,91 @@ +// 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 AddCustomizationCommandError { + NameIsEmpty, +} + +#[derive( + Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder, +)] +pub struct UnvalidatedAddCustomizationCommand { + name: String, + product_id: Uuid, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)] +pub struct AddCustomizationCommand { + name: String, + product_id: Uuid, +} + +impl UnvalidatedAddCustomizationCommand { + pub fn validate(self) -> Result { + let name = self.name.trim().to_owned(); + if name.is_empty() { + return Err(AddCustomizationCommandError::NameIsEmpty); + } + + Ok(AddCustomizationCommand { + name, + product_id: self.product_id, + }) + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + + use crate::utils::uuid::tests::UUID; + + pub fn get_command() -> AddCustomizationCommand { + UnvalidatedAddCustomizationCommandBuilder::default() + .name("foo".into()) + .product_id(UUID.clone()) + .build() + .unwrap() + .validate() + .unwrap() + } + + #[test] + fn test_cmd() { + let name = "foo"; + let product_id = UUID; + + let cmd = UnvalidatedAddCustomizationCommandBuilder::default() + .name(name.into()) + .product_id(product_id.clone()) + .build() + .unwrap() + .validate() + .unwrap(); + + assert_eq!(cmd.name(), name); + assert_eq!(cmd.product_id(), &product_id); + } + + #[test] + fn test_cmd_name_is_empty() { + let product_id = UUID; + + assert_eq!( + UnvalidatedAddCustomizationCommandBuilder::default() + .name("".into()) + .product_id(product_id.clone()) + .build() + .unwrap() + .validate(), + Err(AddCustomizationCommandError::NameIsEmpty) + ); + } +} diff --git a/src/ordering/domain/add_product_command.rs b/src/ordering/domain/add_product_command.rs new file mode 100644 index 0000000..f347838 --- /dev/null +++ b/src/ordering/domain/add_product_command.rs @@ -0,0 +1,256 @@ +// 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; + +use crate::types::currency::*; +use crate::types::quantity::*; + +#[derive(Debug, Error, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +pub enum AddProductCommandError { + NameIsEmpty, +} + +#[derive( + Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder, +)] +pub struct UnvalidatedAddProductCommand { + name: String, + description: Option, + image: Option, + category_id: Uuid, + sku_able: bool, + quantity: Quantity, + price: Price, + adding_by: Uuid, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)] +pub struct AddProductCommand { + name: String, + description: Option, + image: Option, + category_id: Uuid, + sku_able: bool, + price: Price, + quantity: Quantity, + adding_by: Uuid, +} + +impl UnvalidatedAddProductCommand { + pub fn validate(self) -> Result { + let description: Option = if let Some(description) = self.description { + let description = description.trim(); + if description.is_empty() { + None + } else { + Some(description.to_owned()) + } + } else { + None + }; + + let image: Option = if let Some(image) = self.image { + let image = image.trim(); + if image.is_empty() { + None + } else { + Some(image.to_owned()) + } + } else { + None + }; + + let name = self.name.trim().to_owned(); + if name.is_empty() { + return Err(AddProductCommandError::NameIsEmpty); + } + + Ok(AddProductCommand { + name, + description, + image, + category_id: self.category_id, + sku_able: self.sku_able, + price: self.price, + quantity: self.quantity, + adding_by: self.adding_by, + }) + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + + use crate::utils::uuid::tests::UUID; + + pub fn get_command() -> AddProductCommand { + let name = "foo"; + let adding_by = UUID; + let category_id = Uuid::new_v4(); + let sku_able = false; + let image = Some("image".to_string()); + let description = Some("description".to_string()); + + let price = PriceBuilder::default() + .minor(0) + .major(100) + .currency(Currency::INR) + .build() + .unwrap(); + + let quantity = QuantityBuilder::default() + .minor( + QuantityPartBuilder::default() + .number(0) + .unit(QuantityUnit::DiscreteNumber) + .build() + .unwrap(), + ) + .major( + QuantityPartBuilder::default() + .number(1) + .unit(QuantityUnit::DiscreteNumber) + .build() + .unwrap(), + ) + .build() + .unwrap(); + + let cmd = UnvalidatedAddProductCommandBuilder::default() + .name(name.into()) + .description(description.clone()) + .image(image.clone()) + .category_id(category_id) + .adding_by(adding_by) + .quantity(quantity) + .sku_able(sku_able) + .price(price.clone()) + .build() + .unwrap(); + + cmd.validate().unwrap() + } + + #[test] + fn test_description_and_image_none() { + let name = "foo"; + let adding_by = UUID; + let category_id = Uuid::new_v4(); + let sku_able = false; + + let price = PriceBuilder::default() + .minor(0) + .major(100) + .currency(Currency::INR) + .build() + .unwrap(); + + let quantity = Quantity::default(); + + // description = None + let cmd = UnvalidatedAddProductCommandBuilder::default() + .name(name.into()) + .description(None) + .image(None) + .category_id(category_id) + .adding_by(adding_by) + .quantity(quantity.clone()) + .sku_able(sku_able) + .price(price.clone()) + .build() + .unwrap(); + + let cmd = cmd.validate().unwrap(); + + assert_eq!(cmd.name(), name); + assert_eq!(cmd.description(), &None); + assert_eq!(cmd.adding_by(), &adding_by); + assert_eq!(cmd.category_id(), &category_id); + assert_eq!(cmd.image(), &None); + assert_eq!(cmd.sku_able(), &sku_able); + assert_eq!(cmd.price(), &price); + assert_eq!(cmd.quantity(), &quantity); + } + #[test] + fn test_description_some() { + let name = "foo"; + let adding_by = UUID; + let category_id = Uuid::new_v4(); + let sku_able = false; + let image = Some("image".to_string()); + let description = Some("description".to_string()); + + let price = PriceBuilder::default() + .minor(0) + .major(100) + .currency(Currency::INR) + .build() + .unwrap(); + + let quantity = Quantity::default(); + + let cmd = UnvalidatedAddProductCommandBuilder::default() + .name(name.into()) + .description(description.clone()) + .image(image.clone()) + .category_id(category_id) + .quantity(quantity.clone()) + .adding_by(adding_by) + .sku_able(sku_able) + .price(price.clone()) + // .customizations(customizations.clone()) + .build() + .unwrap(); + + let cmd = cmd.validate().unwrap(); + + assert_eq!(cmd.name(), name); + assert_eq!(cmd.description(), &description); + assert_eq!(cmd.adding_by(), &adding_by); + assert_eq!(cmd.category_id(), &category_id); + assert_eq!(cmd.image(), &image); + assert_eq!(cmd.sku_able(), &sku_able); + assert_eq!(cmd.price(), &price); + assert_eq!(cmd.quantity(), &quantity); + } + + #[test] + fn test_name_is_empty() { + let adding_by = UUID; + let category_id = Uuid::new_v4(); + let sku_able = false; + let image = Some("image".to_string()); + let description = Some("description".to_string()); + + let price = PriceBuilder::default() + .minor(0) + .major(100) + .currency(Currency::INR) + .build() + .unwrap(); + + let quantity = Quantity::default(); + + let cmd = UnvalidatedAddProductCommandBuilder::default() + .name("".into()) + .description(description.clone()) + .image(image.clone()) + .category_id(category_id) + .adding_by(adding_by) + .quantity(quantity) + .sku_able(sku_able) + .price(price.clone()) + .build() + .unwrap(); + + // AddProductCommandError::NameIsEmpty + assert_eq!(cmd.validate(), Err(AddProductCommandError::NameIsEmpty)) + } +} diff --git a/src/ordering/domain/add_store_command.rs b/src/ordering/domain/add_store_command.rs new file mode 100644 index 0000000..3f11af8 --- /dev/null +++ b/src/ordering/domain/add_store_command.rs @@ -0,0 +1,82 @@ +// 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; + +#[derive(Debug, Error, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +pub enum AddStoreCommandError { + NameIsEmpty, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)] +pub struct AddStoreCommand { + name: String, + address: Option, + owner: Uuid, +} + +impl AddStoreCommand { + pub fn new( + name: String, + address: Option, + owner: 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(AddStoreCommandError::NameIsEmpty); + } + + Ok(Self { + name, + address, + owner, + }) + } +} + +#[cfg(test)] +mod tests { + use crate::utils::uuid::tests::UUID; + + use super::*; + + #[test] + fn test_cmd() { + let name = "foo"; + let address = "bar"; + let owner = UUID; + + // address = None + let cmd = AddStoreCommand::new(name.into(), None, owner).unwrap(); + assert_eq!(cmd.name(), name); + assert_eq!(cmd.address(), &None); + assert_eq!(cmd.owner(), &owner); + + // address = Some + let cmd = AddStoreCommand::new(name.into(), Some(address.into()), owner).unwrap(); + assert_eq!(cmd.name(), name); + assert_eq!(cmd.address(), &Some(address.to_owned())); + assert_eq!(cmd.owner(), &owner); + + // AddStoreCommandError::NameIsEmpty + assert_eq!( + AddStoreCommand::new("".into(), Some(address.into()), owner), + Err(AddStoreCommandError::NameIsEmpty) + ) + } +} diff --git a/src/ordering/domain/category_added_event.rs b/src/ordering/domain/category_added_event.rs new file mode 100644 index 0000000..32923f0 --- /dev/null +++ b/src/ordering/domain/category_added_event.rs @@ -0,0 +1,19 @@ +// 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 CategoryAddedEvent { + name: String, + description: Option, + added_by_user: Uuid, + category_id: Uuid, + store_id: Uuid, +} diff --git a/src/ordering/domain/category_aggregate.rs b/src/ordering/domain/category_aggregate.rs new file mode 100644 index 0000000..c14130f --- /dev/null +++ b/src/ordering/domain/category_aggregate.rs @@ -0,0 +1,156 @@ +// 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::ordering::application::services::errors::*; +use crate::ordering::application::services::OrderingServicesInterface; + +use super::{commands::OrderingCommand, events::OrderingEvent}; + +#[derive( + Clone, Debug, Serialize, Default, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder, +)] +pub struct Category { + name: String, + description: Option, + store_id: Uuid, + category_id: Uuid, + #[builder(default = "false")] + deleted: bool, +} + +#[async_trait] +impl Aggregate for Category { + type Command = OrderingCommand; + type Event = OrderingEvent; + type Error = OrderingError; + type Services = std::sync::Arc; + + // This identifier should be unique to the system. + fn aggregate_type() -> String { + "ordering.category".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 { + OrderingCommand::AddCategory(cmd) => { + let res = services.add_category().add_category(cmd).await?; + Ok(vec![OrderingEvent::CategoryAdded(res)]) + } + OrderingCommand::UpdateCategory(cmd) => { + let res = services.update_category().update_category(cmd).await?; + Ok(vec![OrderingEvent::CategoryUpdated(res)]) + } + _ => Ok(Vec::default()), + } + } + + fn apply(&mut self, event: Self::Event) { + match event { + OrderingEvent::CategoryAdded(e) => { + *self = CategoryBuilder::default() + .name(e.name().into()) + .category_id(*e.category_id()) + .description(e.description().clone()) + .store_id(*e.store_id()) + .build() + .unwrap() + } + + OrderingEvent::CategoryUpdated(e) => *self = e.new_category().clone(), + _ => (), + } + } +} + +#[cfg(test)] +mod aggregate_tests { + use std::sync::Arc; + + use cqrs_es::test::TestFramework; + use update_category_service::tests::mock_update_category_service; + use uuid::Uuid; + + use super::*; + use crate::ordering::{ + application::services::{add_category_service::tests::*, *}, + domain::{ + add_category_command::*, category_added_event::*, + category_updated_event::tests::get_category_updated_event_from_command, + commands::OrderingCommand, events::OrderingEvent, + update_category_command::tests::get_update_category_command, + }, + }; + use crate::tests::bdd::*; + use crate::utils::uuid::tests::*; + + type CategoryTestFramework = TestFramework; + + #[test] + fn test_create_category() { + let name = "category_name"; + let description = Some("category_description".to_string()); + let adding_by = UUID; + let store_id = Uuid::new_v4(); + let category_id = UUID; + + let cmd = + AddCategoryCommand::new(name.into(), description.clone(), store_id, adding_by).unwrap(); + + let expected = CategoryAddedEventBuilder::default() + .name(cmd.name().into()) + .description(cmd.description().as_ref().map(|s| s.to_string())) + .added_by_user(*cmd.adding_by()) + .store_id(*cmd.store_id()) + .category_id(category_id) + .build() + .unwrap(); + let expected = OrderingEvent::CategoryAdded(expected); + + let mut services = MockOrderingServicesInterface::new(); + services + .expect_add_category() + .times(IS_CALLED_ONLY_ONCE.unwrap()) + .return_const(mock_add_category_service(IS_CALLED_ONLY_ONCE, cmd.clone())); + + CategoryTestFramework::with(Arc::new(services)) + .given_no_previous_events() + .when(OrderingCommand::AddCategory(cmd)) + .then_expect_events(vec![expected]); + } + + #[test] + fn test_update_category() { + let cmd = get_update_category_command(); + + let expected = get_category_updated_event_from_command(&cmd); + let expected = OrderingEvent::CategoryUpdated(expected); + + let mut services = MockOrderingServicesInterface::new(); + services + .expect_update_category() + .times(IS_CALLED_ONLY_ONCE.unwrap()) + .return_const(mock_update_category_service( + IS_CALLED_ONLY_ONCE, + cmd.clone(), + )); + + CategoryTestFramework::with(Arc::new(services)) + .given_no_previous_events() + .when(OrderingCommand::UpdateCategory(cmd)) + .then_expect_events(vec![expected]); + } +} diff --git a/src/ordering/domain/category_updated_event.rs b/src/ordering/domain/category_updated_event.rs new file mode 100644 index 0000000..b14bb20 --- /dev/null +++ b/src/ordering/domain/category_updated_event.rs @@ -0,0 +1,45 @@ +// 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::category_aggregate::*; + +#[derive( + Clone, Debug, Builder, Serialize, Deserialize, Getters, Eq, PartialEq, Ord, PartialOrd, +)] +pub struct CategoryUpdatedEvent { + added_by_user: Uuid, + new_category: Category, + old_category: Category, +} + +#[cfg(test)] +pub mod tests { + use crate::ordering::domain::update_category_command::UpdateCategoryCommand; + + use super::*; + + pub fn get_category_updated_event_from_command( + cmd: &UpdateCategoryCommand, + ) -> CategoryUpdatedEvent { + let category = CategoryBuilder::default() + .name(cmd.name().into()) + .description(cmd.description().as_ref().map(|s| s.to_string())) + .category_id(*cmd.old_category().category_id()) + .store_id(*cmd.old_category().store_id()) + .build() + .unwrap(); + + CategoryUpdatedEventBuilder::default() + .new_category(category) + .old_category(cmd.old_category().clone()) + .added_by_user(*cmd.adding_by()) + .build() + .unwrap() + } +} diff --git a/src/ordering/domain/commands.rs b/src/ordering/domain/commands.rs index 193ddeb..935d179 100644 --- a/src/ordering/domain/commands.rs +++ b/src/ordering/domain/commands.rs @@ -6,11 +6,15 @@ use mockall::predicate::*; use serde::{Deserialize, Serialize}; use super::{ + add_category_command::AddCategoryCommand, add_customization_command::AddCustomizationCommand, add_kot_command::AddKotCommand, add_line_item_command::AddLineItemCommand, - add_order_command::AddOrderCommand, delete_kot_command::DeleteKotCommand, + add_order_command::AddOrderCommand, add_product_command::AddProductCommand, + add_store_command::AddStoreCommand, delete_kot_command::DeleteKotCommand, delete_line_item_command::DeleteLineItemCommand, delete_order_command::DeleteOrderCommand, - update_kot_command::UpdateKotCommand, update_line_item_command::UpdateLineItemCommand, - update_order_command::UpdateOrderCommand, + update_category_command::UpdateCategoryCommand, + update_customization_command::UpdateCustomizationCommand, update_kot_command::UpdateKotCommand, + update_line_item_command::UpdateLineItemCommand, update_order_command::UpdateOrderCommand, + update_product_command::UpdateProductCommand, update_store_command::UpdateStoreCommand, }; #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd)] @@ -24,4 +28,12 @@ pub enum OrderingCommand { AddKot(AddKotCommand), UpdateKot(UpdateKotCommand), DeleteKot(DeleteKotCommand), + AddProduct(AddProductCommand), + UpdateProduct(UpdateProductCommand), + AddStore(AddStoreCommand), + UpdateStore(UpdateStoreCommand), + AddCategory(AddCategoryCommand), + UpdateCategory(UpdateCategoryCommand), + AddCustomization(AddCustomizationCommand), + UpdateCustomization(UpdateCustomizationCommand), } diff --git a/src/ordering/domain/customization_added_event.rs b/src/ordering/domain/customization_added_event.rs new file mode 100644 index 0000000..bc54e66 --- /dev/null +++ b/src/ordering/domain/customization_added_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::customization_aggregate::*; + +#[derive( + Clone, Debug, Builder, Serialize, Deserialize, Getters, Eq, PartialEq, Ord, PartialOrd, +)] +pub struct CustomizationAddedEvent { + customization: Customization, +} + +#[cfg(test)] +pub mod tests { + use crate::ordering::domain::add_customization_command::AddCustomizationCommand; + + use super::*; + + use crate::utils::uuid::tests::UUID; + + pub fn get_customization_added_event_from_cmd( + cmd: &AddCustomizationCommand, + ) -> CustomizationAddedEvent { + let customization = CustomizationBuilder::default() + .name(cmd.name().into()) + .deleted(false) + .product_id(*cmd.product_id()) + .customization_id(UUID.clone()) + .build() + .unwrap(); + + CustomizationAddedEventBuilder::default() + .customization(customization) + .build() + .unwrap() + } +} diff --git a/src/ordering/domain/customization_aggregate.rs b/src/ordering/domain/customization_aggregate.rs new file mode 100644 index 0000000..345aedc --- /dev/null +++ b/src/ordering/domain/customization_aggregate.rs @@ -0,0 +1,143 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use std::str::FromStr; + +use async_trait::async_trait; +use config::builder; +use cqrs_es::Aggregate; +use derive_builder::Builder; +use derive_getters::Getters; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use super::{commands::OrderingCommand, events::OrderingEvent}; +use crate::ordering::application::services::errors::*; +use crate::ordering::application::services::OrderingServicesInterface; + +#[derive( + Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder, +)] +pub struct Customization { + name: String, + customization_id: Uuid, + product_id: Uuid, + #[builder(default = "false")] + deleted: bool, +} + +#[async_trait] +impl Aggregate for Customization { + type Command = OrderingCommand; + type Event = OrderingEvent; + type Error = OrderingError; + type Services = std::sync::Arc; + + // This identifier should be unique to the system. + fn aggregate_type() -> String { + "ordering.product".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 { + OrderingCommand::AddCustomization(cmd) => { + let res = services.add_customization().add_customization(cmd).await?; + Ok(vec![OrderingEvent::CustomizationAdded(res)]) + } + OrderingCommand::UpdateCustomization(cmd) => { + let res = services + .update_customization() + .update_customization(cmd) + .await?; + Ok(vec![OrderingEvent::CustomizationUpdated(res)]) + } + + _ => Ok(Vec::default()), + } + } + + fn apply(&mut self, event: Self::Event) { + match event { + OrderingEvent::CustomizationAdded(e) => { + *self = e.customization().clone(); + } + OrderingEvent::CustomizationUpdated(e) => { + *self = e.new_customization().clone(); + } + _ => (), + } + } +} + +#[cfg(test)] +mod aggregate_tests { + use std::sync::Arc; + + use cqrs_es::test::TestFramework; + + use super::*; + use crate::ordering::{ + application::services::{ + add_customization_service::tests::*, update_customization_service::tests::*, *, + }, + domain::{ + add_customization_command, commands::OrderingCommand, + customization_added_event::tests::get_customization_added_event_from_cmd, + customization_updated_event::tests::get_customization_updated_event_from_command, + events::OrderingEvent, + update_customization_command::tests::get_update_customization_command, + }, + }; + use crate::tests::bdd::*; + + type CustomizationTestFramework = TestFramework; + + #[test] + fn test_create_customization() { + let cmd = add_customization_command::tests::get_command(); + let expected = get_customization_added_event_from_cmd(&cmd); + let expected = OrderingEvent::CustomizationAdded(expected); + + let mut services = MockOrderingServicesInterface::new(); + services + .expect_add_customization() + .times(IS_CALLED_ONLY_ONCE.unwrap()) + .return_const(mock_add_customization_service( + IS_CALLED_ONLY_ONCE, + cmd.clone(), + )); + + CustomizationTestFramework::with(Arc::new(services)) + .given_no_previous_events() + .when(OrderingCommand::AddCustomization(cmd)) + .then_expect_events(vec![expected]); + } + + #[test] + fn test_update_customization() { + let cmd = get_update_customization_command(); + let expected = get_customization_updated_event_from_command(&cmd); + let expected = OrderingEvent::CustomizationUpdated(expected); + + let mut services = MockOrderingServicesInterface::new(); + services + .expect_update_customization() + .times(IS_CALLED_ONLY_ONCE.unwrap()) + .return_const(mock_update_customization_service( + IS_CALLED_ONLY_ONCE, + cmd.clone(), + )); + + CustomizationTestFramework::with(Arc::new(services)) + .given_no_previous_events() + .when(OrderingCommand::UpdateCustomization(cmd)) + .then_expect_events(vec![expected]); + } +} diff --git a/src/ordering/domain/customization_updated_event.rs b/src/ordering/domain/customization_updated_event.rs new file mode 100644 index 0000000..4cf255e --- /dev/null +++ b/src/ordering/domain/customization_updated_event.rs @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use derive_builder::Builder; +use derive_getters::Getters; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use super::customization_aggregate::Customization; + +#[derive( + Clone, Debug, Builder, Serialize, Deserialize, Getters, Eq, PartialEq, Ord, PartialOrd, +)] +pub struct CustomizationUpdatedEvent { + added_by_user: Uuid, + + old_customization: Customization, + new_customization: Customization, +} + +#[cfg(test)] +pub mod tests { + use crate::ordering::domain::{ + customization_aggregate::*, update_customization_command::UpdateCustomizationCommand, + }; + use crate::utils::uuid::tests::UUID; + + use super::*; + + pub fn get_customization_updated_event_from_command( + cmd: &UpdateCustomizationCommand, + ) -> CustomizationUpdatedEvent { + let customization = CustomizationBuilder::default() + .name(cmd.name().into()) + .deleted(false) + .customization_id(UUID.clone()) + .product_id(UUID.clone()) + .build() + .unwrap(); + + CustomizationUpdatedEventBuilder::default() + .added_by_user(cmd.adding_by().clone()) + .new_customization(customization) + .old_customization(cmd.old_customization().clone()) + .build() + .unwrap() + } +} diff --git a/src/ordering/domain/events.rs b/src/ordering/domain/events.rs index 1049e70..0410d6b 100644 --- a/src/ordering/domain/events.rs +++ b/src/ordering/domain/events.rs @@ -6,11 +6,15 @@ use cqrs_es::DomainEvent; use serde::{Deserialize, Serialize}; use super::{ - kot_added_event::KotAddedEvent, kot_deleted_event::KotDeletedEvent, - kot_updated_event::KotUpdatedEvent, line_item_added_event::LineItemAddedEvent, - line_item_deleted_event::LineItemDeletedEvent, line_item_updated_event::LineItemUpdatedEvent, - order_added_event::OrderAddedEvent, order_deleted_event::OrderDeletedEvent, - order_updated_event::OrderUpdatedEvent, + category_added_event::CategoryAddedEvent, category_updated_event::CategoryUpdatedEvent, + customization_added_event::CustomizationAddedEvent, + customization_updated_event::CustomizationUpdatedEvent, kot_added_event::KotAddedEvent, + kot_deleted_event::KotDeletedEvent, kot_updated_event::KotUpdatedEvent, + line_item_added_event::LineItemAddedEvent, line_item_deleted_event::LineItemDeletedEvent, + line_item_updated_event::LineItemUpdatedEvent, order_added_event::OrderAddedEvent, + order_deleted_event::OrderDeletedEvent, order_updated_event::OrderUpdatedEvent, + product_added_event::ProductAddedEvent, product_updated_event::ProductUpdatedEvent, + store_added_event::StoreAddedEvent, store_updated_event::StoreUpdatedEvent, }; #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd)] @@ -24,6 +28,14 @@ pub enum OrderingEvent { KotAdded(KotAddedEvent), KotUpdated(KotUpdatedEvent), KotDeleted(KotDeletedEvent), + ProductAdded(ProductAddedEvent), + ProductUpdated(ProductUpdatedEvent), + StoreAdded(StoreAddedEvent), + StoreUpdated(StoreUpdatedEvent), + CategoryAdded(CategoryAddedEvent), + CategoryUpdated(CategoryUpdatedEvent), + CustomizationAdded(CustomizationAddedEvent), + CustomizationUpdated(CustomizationUpdatedEvent), } impl DomainEvent for OrderingEvent { @@ -42,6 +54,14 @@ impl DomainEvent for OrderingEvent { OrderingEvent::KotAdded { .. } => "OrderingKotAdded", OrderingEvent::KotUpdated { .. } => "OrderingKotUpdated", OrderingEvent::KotDeleted { .. } => "OrderingKotDeleted", + OrderingEvent::ProductAdded { .. } => "OrderingProductAdded", + OrderingEvent::ProductUpdated { .. } => "OrderingProductUpdated", + OrderingEvent::StoreAdded { .. } => "OrderingStoreAdded", + OrderingEvent::StoreUpdated { .. } => "OrderingStoreUpdated", + OrderingEvent::CategoryAdded { .. } => "OrderingCategoryAdded", + OrderingEvent::CategoryUpdated { .. } => "OrderingCategoryUpdated", + OrderingEvent::CustomizationAdded { .. } => "OrderingCustomizationAdded", + OrderingEvent::CustomizationUpdated { .. } => "OrderingCategoryUpdated", }; e.to_string() diff --git a/src/ordering/domain/mod.rs b/src/ordering/domain/mod.rs index 5901453..b8ad093 100644 --- a/src/ordering/domain/mod.rs +++ b/src/ordering/domain/mod.rs @@ -3,23 +3,40 @@ // SPDX-License-Identifier: AGPL-3.0-or-later // aggregates +pub mod category_aggregate; +pub mod customization_aggregate; pub mod kot_aggregate; pub mod line_item_aggregate; pub mod order_aggregate; +pub mod pantry_aggregate; +pub mod product_aggregate; +pub mod store_aggregate; // commands +pub mod add_category_command; +pub mod add_customization_command; pub mod add_kot_command; pub mod add_line_item_command; pub mod add_order_command; +pub mod add_product_command; +pub mod add_store_command; pub mod commands; pub mod delete_kot_command; pub mod delete_line_item_command; pub mod delete_order_command; +pub mod update_category_command; +pub mod update_customization_command; pub mod update_kot_command; pub mod update_line_item_command; pub mod update_order_command; +pub mod update_product_command; +pub mod update_store_command; // events +pub mod category_added_event; +pub mod category_updated_event; +pub mod customization_added_event; +pub mod customization_updated_event; pub mod events; pub mod kot_added_event; pub mod kot_deleted_event; @@ -30,3 +47,7 @@ pub mod line_item_updated_event; pub mod order_added_event; pub mod order_deleted_event; pub mod order_updated_event; +pub mod product_added_event; +pub mod product_updated_event; +pub mod store_added_event; +pub mod store_updated_event; diff --git a/src/ordering/domain/pantry_aggregate.rs b/src/ordering/domain/pantry_aggregate.rs new file mode 100644 index 0000000..56f60de --- /dev/null +++ b/src/ordering/domain/pantry_aggregate.rs @@ -0,0 +1,3 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/src/ordering/domain/product_added_event.rs b/src/ordering/domain/product_added_event.rs new file mode 100644 index 0000000..7396a16 --- /dev/null +++ b/src/ordering/domain/product_added_event.rs @@ -0,0 +1,51 @@ +// 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 crate::types::currency::*; +use crate::types::quantity::Quantity; + +#[derive( + Clone, Debug, Builder, Serialize, Deserialize, Getters, Eq, PartialEq, Ord, PartialOrd, +)] +pub struct ProductAddedEvent { + added_by_user: Uuid, + + name: String, + description: Option, + image: Option, // string = file_name + price: Price, + quantity: Quantity, + category_id: Uuid, + sku_able: bool, + product_id: Uuid, +} + +#[cfg(test)] +pub mod tests { + use crate::ordering::domain::add_product_command::AddProductCommand; + + use super::*; + + use crate::utils::uuid::tests::UUID; + + pub fn get_event_from_command(cmd: &AddProductCommand) -> ProductAddedEvent { + ProductAddedEventBuilder::default() + .name(cmd.name().into()) + .description(cmd.description().as_ref().map(|s| s.to_string())) + .image(cmd.image().as_ref().map(|s| s.to_string())) + .sku_able(*cmd.sku_able()) + .category_id(*cmd.category_id()) + .product_id(UUID) + .price(cmd.price().clone()) + .quantity(cmd.quantity().clone()) + .added_by_user(*cmd.adding_by()) + .build() + .unwrap() + } +} diff --git a/src/ordering/domain/product_aggregate.rs b/src/ordering/domain/product_aggregate.rs new file mode 100644 index 0000000..70baee7 --- /dev/null +++ b/src/ordering/domain/product_aggregate.rs @@ -0,0 +1,166 @@ +// 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 super::{commands::OrderingCommand, events::OrderingEvent}; +use crate::ordering::application::services::errors::*; +use crate::ordering::application::services::OrderingServicesInterface; +use crate::types::currency::*; +use crate::types::quantity::Quantity; + +#[derive( + Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder, +)] +pub struct Product { + name: String, + description: Option, + image: Option, // string = file_name + price: Price, + // stock = Σ (not sold SKU), if SKU is relevant. Where irrelevant; it exists independent of SKU. + // relevancy is determined Product.sku_able + quantity: Quantity, + category_id: Uuid, + sku_able: bool, + product_id: Uuid, + #[builder(default = "false")] + deleted: bool, +} + +#[async_trait] +impl Aggregate for Product { + type Command = OrderingCommand; + type Event = OrderingEvent; + type Error = OrderingError; + type Services = std::sync::Arc; + + // This identifier should be unique to the system. + fn aggregate_type() -> String { + "ordering.product".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 { + OrderingCommand::AddProduct(cmd) => { + let res = services.add_product().add_product(cmd).await?; + Ok(vec![OrderingEvent::ProductAdded(res)]) + } + OrderingCommand::UpdateProduct(cmd) => { + let res = services.update_product().update_product(cmd).await?; + Ok(vec![OrderingEvent::ProductUpdated(res)]) + } + _ => Ok(Vec::default()), + } + } + + fn apply(&mut self, event: Self::Event) { + match event { + OrderingEvent::ProductAdded(e) => { + *self = ProductBuilder::default() + .name(e.name().into()) + .description(e.description().clone()) + .image(e.image().clone()) + .price(e.price().clone()) + .category_id(e.category_id().clone()) + .sku_able(e.sku_able().clone()) + .product_id(e.product_id().clone()) + .quantity(e.quantity().clone()) + .deleted(false) + .build() + .unwrap(); + } + OrderingEvent::ProductUpdated(e) => { + *self = e.new_product().clone(); + } + _ => (), + } + } +} + +#[cfg(test)] +mod aggregate_tests { + use std::str::FromStr; + use std::sync::Arc; + + use cqrs_es::test::TestFramework; + use update_product_service::tests::mock_update_product_service; + + use super::*; + use crate::ordering::{ + application::services::{add_product_service::tests::*, *}, + domain::{ + add_product_command::tests::get_command, commands::OrderingCommand, + events::OrderingEvent, product_added_event::tests::get_event_from_command, + product_updated_event, update_product_command, + }, + }; + use crate::tests::bdd::*; + + type ProductTestFramework = TestFramework; + + #[test] + fn test_create_product() { + let cmd = get_command(); + let expected = get_event_from_command(&cmd); + let expected = OrderingEvent::ProductAdded(expected); + + let mut services = MockOrderingServicesInterface::new(); + services + .expect_add_product() + .times(IS_CALLED_ONLY_ONCE.unwrap()) + .return_const(mock_add_product_service(IS_CALLED_ONLY_ONCE, cmd.clone())); + + ProductTestFramework::with(Arc::new(services)) + .given_no_previous_events() + .when(OrderingCommand::AddProduct(cmd)) + .then_expect_events(vec![expected]); + } + + #[test] + fn test_update_product() { + let cmd = update_product_command::tests::get_command(); + let expected = product_updated_event::tests::get_event_from_command(&cmd); + let expected = OrderingEvent::ProductUpdated(expected); + + let mut services = MockOrderingServicesInterface::new(); + services + .expect_update_product() + .times(IS_CALLED_ONLY_ONCE.unwrap()) + .return_const(mock_update_product_service( + IS_CALLED_ONLY_ONCE, + cmd.clone(), + )); + + ProductTestFramework::with(Arc::new(services)) + .given_no_previous_events() + .when(OrderingCommand::UpdateProduct(cmd)) + .then_expect_events(vec![expected]); + } + + fn test_helper(t: T, str_value: &str) -> bool + where + T: ToString + FromStr + std::fmt::Debug + PartialEq, + ::Err: std::fmt::Debug, + { + println!("Testing type: {:?} against value {str_value}", t); + assert_eq!(t.to_string(), str_value.to_string()); + + assert_eq!(T::from_str(str_value).unwrap(), t); + + assert_eq!(T::from_str(t.to_string().as_str()).unwrap(), t,); + + true + } +} diff --git a/src/ordering/domain/product_updated_event.rs b/src/ordering/domain/product_updated_event.rs new file mode 100644 index 0000000..3bcf13c --- /dev/null +++ b/src/ordering/domain/product_updated_event.rs @@ -0,0 +1,53 @@ +// 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::product_aggregate::Product; + +#[derive( + Clone, Debug, Builder, Serialize, Deserialize, Getters, Eq, PartialEq, Ord, PartialOrd, +)] +pub struct ProductUpdatedEvent { + added_by_user: Uuid, + + old_product: Product, + new_product: Product, +} + +#[cfg(test)] +pub mod tests { + use crate::ordering::domain::{ + product_aggregate::*, update_product_command::UpdateProductCommand, + }; + + use super::*; + + #[test] + fn test_name() {} + + pub fn get_event_from_command(cmd: &UpdateProductCommand) -> ProductUpdatedEvent { + let updated_product = ProductBuilder::default() + .name(cmd.name().into()) + .description(cmd.description().as_ref().map(|s| s.to_string())) + .image(cmd.image().clone()) + .sku_able(*cmd.sku_able()) + .price(cmd.price().clone()) + .category_id(*cmd.category_id()) + .quantity(cmd.quantity().clone()) + .product_id(cmd.old_product().product_id().clone()) + .build() + .unwrap(); + + ProductUpdatedEventBuilder::default() + .added_by_user(cmd.adding_by().clone()) + .new_product(updated_product) + .old_product(cmd.old_product().clone()) + .build() + .unwrap() + } +} diff --git a/src/ordering/domain/store_added_event.rs b/src/ordering/domain/store_added_event.rs new file mode 100644 index 0000000..6336da0 --- /dev/null +++ b/src/ordering/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/ordering/domain/store_aggregate.rs b/src/ordering/domain/store_aggregate.rs new file mode 100644 index 0000000..9ac4c6e --- /dev/null +++ b/src/ordering/domain/store_aggregate.rs @@ -0,0 +1,146 @@ +// 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::ordering::application::services::errors::*; +use crate::ordering::application::services::OrderingServicesInterface; + +use super::{commands::OrderingCommand, events::OrderingEvent}; + +#[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 = OrderingCommand; + type Event = OrderingEvent; + type Error = OrderingError; + type Services = std::sync::Arc; + + // This identifier should be unique to the system. + fn aggregate_type() -> String { + "ordering.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 { + OrderingCommand::AddStore(cmd) => { + let res = services.add_store().add_store(cmd).await?; + Ok(vec![OrderingEvent::StoreAdded(res)]) + } + OrderingCommand::UpdateStore(cmd) => { + let res = services.update_store().update_store(cmd).await?; + Ok(vec![OrderingEvent::StoreUpdated(res)]) + } + + _ => Ok(Vec::default()), + } + } + + fn apply(&mut self, event: Self::Event) { + match event { + OrderingEvent::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; + } + // OrderingEvent::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::ordering::{ + application::services::{add_store_service::tests::*, *}, + domain::{ + add_store_command::*, commands::OrderingCommand, events::OrderingEvent, + 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 = OrderingEvent::StoreAdded(expected); + + let cmd = AddStoreCommand::new(name.into(), address.clone(), owner).unwrap(); + + let mut services = MockOrderingServicesInterface::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(OrderingCommand::AddStore(cmd)) + .then_expect_events(vec![expected]); + } + + #[test] + fn test_update_store() { + let cmd = get_update_store_cmd(); + let expected = OrderingEvent::StoreUpdated(get_store_updated_event_from_command(&cmd)); + + let mut services = MockOrderingServicesInterface::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(OrderingCommand::UpdateStore(cmd)) + .then_expect_events(vec![expected]); + } +} diff --git a/src/ordering/domain/store_updated_event.rs b/src/ordering/domain/store_updated_event.rs new file mode 100644 index 0000000..f3df2c9 --- /dev/null +++ b/src/ordering/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::ordering::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/ordering/domain/update_category_command.rs b/src/ordering/domain/update_category_command.rs new file mode 100644 index 0000000..b5e9785 --- /dev/null +++ b/src/ordering/domain/update_category_command.rs @@ -0,0 +1,117 @@ +// 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::category_aggregate::*; + +#[derive(Debug, Error, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +pub enum UpdateCategoryCommandError { + NameIsEmpty, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)] +pub struct UpdateCategoryCommand { + name: String, + description: Option, + adding_by: Uuid, + + old_category: Category, +} + +impl UpdateCategoryCommand { + pub fn new( + name: String, + description: Option, + old_category: Category, + adding_by: Uuid, + ) -> Result { + let description: Option = if let Some(description) = description { + let description = description.trim(); + if description.is_empty() { + None + } else { + Some(description.to_owned()) + } + } else { + None + }; + + let name = name.trim().to_owned(); + if name.is_empty() { + return Err(UpdateCategoryCommandError::NameIsEmpty); + } + + Ok(Self { + name, + description, + old_category, + adding_by, + }) + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + + use crate::utils::uuid::tests::UUID; + + pub fn get_update_category_command() -> UpdateCategoryCommand { + let name = "foo"; + let description = "bar"; + let adding_by = UUID; + let old_category = Category::default(); + UpdateCategoryCommand::new( + name.into(), + Some(description.into()), + old_category.clone(), + adding_by, + ) + .unwrap() + } + + #[test] + fn test_cmd() { + let name = "foo"; + let description = "bar"; + let adding_by = UUID; + let old_category = Category::default(); + + // description = None + let cmd = + UpdateCategoryCommand::new(name.into(), None, old_category.clone(), adding_by).unwrap(); + assert_eq!(cmd.name(), name); + assert_eq!(cmd.description(), &None); + assert_eq!(cmd.adding_by(), &adding_by); + assert_eq!(cmd.old_category(), &old_category); + + // description = Some + let cmd = UpdateCategoryCommand::new( + name.into(), + Some(description.into()), + old_category.clone(), + adding_by, + ) + .unwrap(); + assert_eq!(cmd.name(), name); + assert_eq!(cmd.description(), &Some(description.to_owned())); + assert_eq!(cmd.adding_by(), &adding_by); + assert_eq!(cmd.old_category(), &old_category); + + // UpdateCategoryCommandError::NameIsEmpty + assert_eq!( + UpdateCategoryCommand::new( + "".into(), + Some(description.into()), + old_category, + adding_by, + ), + Err(UpdateCategoryCommandError::NameIsEmpty) + ) + } +} diff --git a/src/ordering/domain/update_customization_command.rs b/src/ordering/domain/update_customization_command.rs new file mode 100644 index 0000000..1dc2d7a --- /dev/null +++ b/src/ordering/domain/update_customization_command.rs @@ -0,0 +1,101 @@ +// 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; + +use super::customization_aggregate::Customization; + +#[derive(Debug, Error, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +pub enum UpdateCustomizationCommandError { + NameIsEmpty, +} + +#[derive( + Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder, +)] +pub struct UnvalidatedUpdateCustomizationCommand { + name: String, + adding_by: Uuid, + + old_customization: Customization, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)] +pub struct UpdateCustomizationCommand { + name: String, + old_customization: Customization, + adding_by: Uuid, +} + +impl UnvalidatedUpdateCustomizationCommand { + pub fn validate(self) -> Result { + let name = self.name.trim().to_owned(); + if name.is_empty() { + return Err(UpdateCustomizationCommandError::NameIsEmpty); + } + + Ok(UpdateCustomizationCommand { + name, + old_customization: self.old_customization, + adding_by: self.adding_by, + }) + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + + use crate::{ + ordering::domain::customization_aggregate::Customization, utils::uuid::tests::UUID, + }; + + pub fn get_update_customization_command() -> UpdateCustomizationCommand { + let customozation = Customization::default(); + UnvalidatedUpdateCustomizationCommandBuilder::default() + .name("foo".into()) + .old_customization(customozation) + .adding_by(UUID.clone()) + .build() + .unwrap() + .validate() + .unwrap() + } + + #[test] + fn test_cmd() { + let name = "foo"; + + let cmd = UnvalidatedUpdateCustomizationCommandBuilder::default() + .name(name.into()) + .old_customization(Customization::default()) + .adding_by(UUID.clone()) + .build() + .unwrap() + .validate() + .unwrap(); + + assert_eq!(cmd.name(), name); + assert_eq!(cmd.adding_by(), &UUID); + assert_eq!(cmd.old_customization(), &Customization::default()); + } + + #[test] + fn test_cmd_name_is_empty() { + assert_eq!( + UnvalidatedUpdateCustomizationCommandBuilder::default() + .name("".into()) + .adding_by(UUID.clone()) + .old_customization(Customization::default()) + .build() + .unwrap() + .validate(), + Err(UpdateCustomizationCommandError::NameIsEmpty) + ); + } +} diff --git a/src/ordering/domain/update_product_command.rs b/src/ordering/domain/update_product_command.rs new file mode 100644 index 0000000..f158f07 --- /dev/null +++ b/src/ordering/domain/update_product_command.rs @@ -0,0 +1,266 @@ +// 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; + +use super::product_aggregate::Product; +use crate::types::currency::*; +use crate::types::quantity::Quantity; + +#[derive(Debug, Error, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +pub enum UpdateProductCommandError { + NameIsEmpty, +} + +#[derive( + Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder, +)] +pub struct UnvalidatedUpdateProductCommand { + name: String, + description: Option, + image: Option, + category_id: Uuid, + sku_able: bool, + quantity: Quantity, + price: Price, + adding_by: Uuid, + old_product: Product, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)] +pub struct UpdateProductCommand { + name: String, + description: Option, + image: Option, + category_id: Uuid, + sku_able: bool, + price: Price, + quantity: Quantity, + adding_by: Uuid, + old_product: Product, +} + +impl UnvalidatedUpdateProductCommand { + pub fn validate(self) -> Result { + let description: Option = if let Some(description) = self.description { + let description = description.trim(); + if description.is_empty() { + None + } else { + Some(description.to_owned()) + } + } else { + None + }; + + let image: Option = if let Some(image) = self.image { + let image = image.trim(); + if image.is_empty() { + None + } else { + Some(image.to_owned()) + } + } else { + None + }; + + let name = self.name.trim().to_owned(); + if name.is_empty() { + return Err(UpdateProductCommandError::NameIsEmpty); + } + + Ok(UpdateProductCommand { + name, + description, + image, + category_id: self.category_id, + sku_able: self.sku_able, + price: self.price, + quantity: self.quantity, + adding_by: self.adding_by, + old_product: self.old_product, + }) + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + + use crate::types::quantity::*; + use crate::utils::uuid::tests::UUID; + + pub fn get_command() -> UpdateProductCommand { + let name = "foo"; + let adding_by = UUID; + let category_id = Uuid::new_v4(); + let sku_able = false; + let image = Some("image".to_string()); + let description = Some("description".to_string()); + + let price = PriceBuilder::default() + .minor(0) + .major(100) + .currency(Currency::INR) + .build() + .unwrap(); + + let quantity = QuantityBuilder::default() + .minor( + QuantityPartBuilder::default() + .number(0) + .unit(QuantityUnit::DiscreteNumber) + .build() + .unwrap(), + ) + .major( + QuantityPartBuilder::default() + .number(1) + .unit(QuantityUnit::DiscreteNumber) + .build() + .unwrap(), + ) + .build() + .unwrap(); + + let cmd = UnvalidatedUpdateProductCommandBuilder::default() + .name(name.into()) + .description(description.clone()) + .image(image.clone()) + .category_id(category_id.clone()) + .adding_by(adding_by.clone()) + .quantity(quantity) + .sku_able(sku_able) + .price(price.clone()) + .old_product(Product::default()) + .build() + .unwrap(); + + cmd.validate().unwrap() + } + + #[test] + fn test_description_and_image_none() { + let name = "foo"; + let adding_by = UUID; + let category_id = Uuid::new_v4(); + let sku_able = false; + + let price = PriceBuilder::default() + .minor(0) + .major(100) + .currency(Currency::INR) + .build() + .unwrap(); + + let quantity = Quantity::default(); + + // description = None + let cmd = UnvalidatedUpdateProductCommandBuilder::default() + .name(name.into()) + .description(None) + .image(None) + .category_id(category_id.clone()) + .adding_by(adding_by.clone()) + .quantity(quantity.clone()) + .sku_able(sku_able) + .price(price.clone()) + .old_product(Product::default()) + .build() + .unwrap(); + + let cmd = cmd.validate().unwrap(); + + assert_eq!(cmd.name(), name); + assert_eq!(cmd.description(), &None); + assert_eq!(cmd.adding_by(), &adding_by); + assert_eq!(cmd.category_id(), &category_id); + assert_eq!(cmd.image(), &None); + assert_eq!(cmd.sku_able(), &sku_able); + assert_eq!(cmd.old_product(), &Product::default()); + assert_eq!(cmd.price(), &price); + assert_eq!(cmd.quantity(), &quantity); + } + #[test] + fn test_description_some() { + let name = "foo"; + let adding_by = UUID; + let category_id = Uuid::new_v4(); + let sku_able = false; + let image = Some("image".to_string()); + let description = Some("description".to_string()); + + let price = PriceBuilder::default() + .minor(0) + .major(100) + .currency(Currency::INR) + .build() + .unwrap(); + + let quantity = Quantity::default(); + + let cmd = UnvalidatedUpdateProductCommandBuilder::default() + .name(name.into()) + .description(description.clone()) + .image(image.clone()) + .category_id(category_id.clone()) + .quantity(quantity.clone()) + .adding_by(adding_by.clone()) + .sku_able(sku_able) + .price(price.clone()) + .old_product(Product::default()) + .build() + .unwrap(); + + let cmd = cmd.validate().unwrap(); + + assert_eq!(cmd.name(), name); + assert_eq!(cmd.description(), &description); + assert_eq!(cmd.adding_by(), &adding_by); + assert_eq!(cmd.category_id(), &category_id); + assert_eq!(cmd.image(), &image); + assert_eq!(cmd.sku_able(), &sku_able); + assert_eq!(cmd.price(), &price); + assert_eq!(cmd.quantity(), &quantity); + assert_eq!(cmd.old_product(), &Product::default()); + } + + #[test] + fn test_name_is_empty() { + let adding_by = UUID; + let category_id = Uuid::new_v4(); + let sku_able = false; + let image = Some("image".to_string()); + let description = Some("description".to_string()); + + let price = PriceBuilder::default() + .minor(0) + .major(100) + .currency(Currency::INR) + .build() + .unwrap(); + + let quantity = Quantity::default(); + + let cmd = UnvalidatedUpdateProductCommandBuilder::default() + .name("".into()) + .description(description.clone()) + .image(image.clone()) + .category_id(category_id.clone()) + .adding_by(adding_by.clone()) + .quantity(quantity) + .old_product(Product::default()) + .sku_able(sku_able) + .price(price.clone()) + .build() + .unwrap(); + + // UpdateProductCommandError::NameIsEmpty + assert_eq!(cmd.validate(), Err(UpdateProductCommandError::NameIsEmpty)) + } +} diff --git a/src/ordering/domain/update_store_command.rs b/src/ordering/domain/update_store_command.rs new file mode 100644 index 0000000..53c2e2c --- /dev/null +++ b/src/ordering/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) + ) + } +} diff --git a/src/types/mod.rs b/src/types/mod.rs index 57406e6..c695f54 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -2,5 +2,5 @@ // // SPDX-License-Identifier: AGPL-3.0-or-later -pub mod quantity; pub mod currency; +pub mod quantity;