Merge pull request 'feat: import inventory services&domain obj to implement pantry' (#104) from pantry-aggregate into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #104
This commit is contained in:
commit
83f0726bae
104 changed files with 7155 additions and 76 deletions
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
16
migrations/20240914110303_cqrs_ordering_store_query.sql
Normal file
16
migrations/20240914110303_cqrs_ordering_store_query.sql
Normal file
|
@ -0,0 +1,16 @@
|
|||
--- SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
--
|
||||
-- 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)
|
||||
);
|
17
migrations/20240914111900_cqrs_ordering_category_query.sql
Normal file
17
migrations/20240914111900_cqrs_ordering_category_query.sql
Normal file
|
@ -0,0 +1,17 @@
|
|||
-- SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
--
|
||||
-- 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)
|
||||
);
|
|
@ -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)
|
||||
);
|
29
migrations/20240914121604_cqrs_ordering_product_query.sql
Normal file
29
migrations/20240914121604_cqrs_ordering_product_query.sql
Normal file
|
@ -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)
|
||||
)
|
|
@ -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<CustomizationView, Customization> for InventoryDBPostgresAda
|
|||
&self,
|
||||
customization_id: &str,
|
||||
) -> Result<Option<CustomizationView>, 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<CustomizationView, Customization> for InventoryDBPostgresAda
|
|||
&self,
|
||||
customization_id: &str,
|
||||
) -> Result<Option<(CustomizationView, ViewContext)>, 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,
|
||||
|
|
|
@ -13,9 +13,8 @@ use uuid::Uuid;
|
|||
use super::errors::*;
|
||||
use super::InventoryDBPostgresAdapter;
|
||||
use crate::inventory::domain::events::InventoryEvent;
|
||||
use crate::inventory::domain::product_aggregate::{
|
||||
Currency, PriceBuilder, Product, ProductBuilder,
|
||||
};
|
||||
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;
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ use crate::inventory::application::port::output::full_text_search::{
|
|||
add_product_to_store::*, errors::*,
|
||||
};
|
||||
use crate::inventory::domain::{category_aggregate::*, product_aggregate::*};
|
||||
use crate::types::currency::*;
|
||||
//use super::errors::*;
|
||||
|
||||
#[derive(
|
||||
|
|
|
@ -12,6 +12,7 @@ use crate::inventory::application::port::output::full_text_search::{
|
|||
add_product_to_store::*, errors::*,
|
||||
};
|
||||
use crate::inventory::domain::{category_aggregate::*, product_aggregate::*};
|
||||
use crate::types::currency::*;
|
||||
//use super::errors::*;
|
||||
|
||||
#[derive(
|
||||
|
|
|
@ -8,7 +8,7 @@ use derive_more::{Display, Error};
|
|||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::product_aggregate::Price;
|
||||
use crate::types::currency::*;
|
||||
use crate::types::quantity::*;
|
||||
|
||||
#[derive(Debug, Error, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
|
@ -88,10 +88,7 @@ impl UnvalidatedAddProductCommand {
|
|||
pub mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::{
|
||||
inventory::domain::product_aggregate::{Currency, PriceBuilder},
|
||||
utils::uuid::tests::UUID,
|
||||
};
|
||||
use crate::utils::uuid::tests::UUID;
|
||||
|
||||
pub fn get_command() -> AddProductCommand {
|
||||
let name = "foo";
|
||||
|
|
|
@ -7,7 +7,7 @@ use derive_getters::Getters;
|
|||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::product_aggregate::Price;
|
||||
use crate::types::currency::*;
|
||||
use crate::types::quantity::Quantity;
|
||||
|
||||
#[derive(
|
||||
|
|
|
@ -2,55 +2,19 @@
|
|||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use cqrs_es::Aggregate;
|
||||
use derive_builder::Builder;
|
||||
use derive_getters::Getters;
|
||||
use derive_more::Display;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::{commands::InventoryCommand, events::InventoryEvent};
|
||||
use crate::inventory::application::services::errors::*;
|
||||
use crate::inventory::application::services::InventoryServicesInterface;
|
||||
use crate::types::currency::*;
|
||||
use crate::types::quantity::Quantity;
|
||||
|
||||
#[derive(Clone, Display, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd)]
|
||||
pub enum Currency {
|
||||
#[display(fmt = "{}", INR)]
|
||||
INR,
|
||||
}
|
||||
|
||||
const INR: &str = "INR";
|
||||
|
||||
impl FromStr for Currency {
|
||||
type Err = String;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let s = s.trim();
|
||||
match s {
|
||||
INR => Ok(Self::INR),
|
||||
_ => Err("Currency unsupported".into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Currency {
|
||||
fn default() -> Self {
|
||||
Self::INR
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder,
|
||||
)]
|
||||
pub struct Price {
|
||||
major: usize,
|
||||
minor: usize,
|
||||
currency: Currency,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder,
|
||||
)]
|
||||
|
@ -127,6 +91,7 @@ impl Aggregate for Product {
|
|||
|
||||
#[cfg(test)]
|
||||
mod aggregate_tests {
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use cqrs_es::test::TestFramework;
|
||||
|
@ -198,9 +163,4 @@ mod aggregate_tests {
|
|||
|
||||
true
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn currency_to_string_from_str() {
|
||||
assert!(test_helper(Currency::INR, INR));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,15 +2,14 @@
|
|||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use derive_builder::Builder;
|
||||
use derive_getters::Getters;
|
||||
use derive_more::{Display, Error};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::product_aggregate::{Price, Product};
|
||||
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)]
|
||||
|
@ -94,10 +93,7 @@ pub mod tests {
|
|||
use super::*;
|
||||
|
||||
use crate::types::quantity::*;
|
||||
use crate::{
|
||||
inventory::domain::product_aggregate::{Currency, PriceBuilder},
|
||||
utils::uuid::tests::UUID,
|
||||
};
|
||||
use crate::utils::uuid::tests::UUID;
|
||||
|
||||
pub fn get_command() -> UpdateProductCommand {
|
||||
let name = "foo";
|
||||
|
|
78
src/ordering/adapters/output/db/category_exists.rs
Normal file
78
src/ordering/adapters/output/db/category_exists.rs
Normal file
|
@ -0,0 +1,78 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// 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<bool> {
|
||||
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;
|
||||
}
|
||||
}
|
71
src/ordering/adapters/output/db/category_id_exists.rs
Normal file
71
src/ordering/adapters/output/db/category_id_exists.rs
Normal file
|
@ -0,0 +1,71 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// 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<bool> {
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// 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<bool> {
|
||||
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;
|
||||
}
|
||||
}
|
201
src/ordering/adapters/output/db/category_view.rs
Normal file
201
src/ordering/adapters/output/db/category_view.rs
Normal file
|
@ -0,0 +1,201 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// 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<String>,
|
||||
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<Category> for CategoryView {
|
||||
fn update(&mut self, event: &EventEnvelope<Category>) {
|
||||
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<CategoryView, Category> for OrderingDBPostgresAdapter {
|
||||
async fn load(&self, category_id: &str) -> Result<Option<CategoryView>, 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<Option<(CategoryView, ViewContext)>, 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<Category> for SimpleLoggingQuery {
|
||||
async fn dispatch(&self, aggregate_id: &str, events: &[EventEnvelope<Category>]) {
|
||||
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<Category> for OrderingDBPostgresAdapter {
|
||||
async fn dispatch(&self, category_id: &str, events: &[EventEnvelope<Category>]) {
|
||||
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();
|
||||
}
|
||||
}
|
99
src/ordering/adapters/output/db/customization_id_exists.rs
Normal file
99
src/ordering/adapters/output/db/customization_id_exists.rs
Normal file
|
@ -0,0 +1,99 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// 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<bool> {
|
||||
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();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// 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<bool> {
|
||||
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;
|
||||
}
|
||||
}
|
221
src/ordering/adapters/output/db/customization_view.rs
Normal file
221
src/ordering/adapters/output/db/customization_view.rs
Normal file
|
@ -0,0 +1,221 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// 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<CustomizationView>,
|
||||
//}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||
struct CustomizationView {
|
||||
name: String,
|
||||
product_id: Uuid,
|
||||
customization_id: Uuid,
|
||||
deleted: bool,
|
||||
}
|
||||
|
||||
impl From<CustomizationView> 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<Customization> for CustomizationView {
|
||||
fn update(&mut self, event: &EventEnvelope<Customization>) {
|
||||
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<CustomizationView, Customization> for OrderingDBPostgresAdapter {
|
||||
async fn load(
|
||||
&self,
|
||||
customization_id: &str,
|
||||
) -> Result<Option<CustomizationView>, 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<Option<(CustomizationView, ViewContext)>, 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<Customization> for OrderingDBPostgresAdapter {
|
||||
async fn dispatch(&self, customization_id: &str, events: &[EventEnvelope<Customization>]) {
|
||||
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();
|
||||
}
|
||||
}
|
98
src/ordering/adapters/output/db/get_category.rs
Normal file
98
src/ordering/adapters/output/db/get_category.rs
Normal file
|
@ -0,0 +1,98 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// 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<String>,
|
||||
store_id: Uuid,
|
||||
category_id: Uuid,
|
||||
deleted: bool,
|
||||
}
|
||||
|
||||
impl From<InnerCategory> 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<Category> {
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
116
src/ordering/adapters/output/db/product_id_exists.rs
Normal file
116
src/ordering/adapters/output/db/product_id_exists.rs
Normal file
|
@ -0,0 +1,116 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// 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<bool> {
|
||||
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();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// 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<bool> {
|
||||
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;
|
||||
}
|
||||
}
|
332
src/ordering/adapters/output/db/product_view.rs
Normal file
332
src/ordering/adapters/output/db/product_view.rs
Normal file
|
@ -0,0 +1,332 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// 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<String>,
|
||||
image: Option<String>, // 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<ProductView> 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<Product> for ProductView {
|
||||
fn update(&mut self, event: &EventEnvelope<Product>) {
|
||||
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<ProductView, Product> for InventoryDBPostgresAdapter {
|
||||
async fn load(&self, product_id: &str) -> Result<Option<ProductView>, 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<Option<(ProductView, ViewContext)>, 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<Product> for InventoryDBPostgresAdapter {
|
||||
async fn dispatch(&self, product_id: &str, events: &[EventEnvelope<Product>]) {
|
||||
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();
|
||||
}
|
||||
}
|
87
src/ordering/adapters/output/db/store_id_exists.rs
Normal file
87
src/ordering/adapters/output/db/store_id_exists.rs
Normal file
|
@ -0,0 +1,87 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// 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<bool> {
|
||||
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;
|
||||
}
|
||||
}
|
81
src/ordering/adapters/output/db/store_name_exists.rs
Normal file
81
src/ordering/adapters/output/db/store_name_exists.rs
Normal file
|
@ -0,0 +1,81 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// 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<bool> {
|
||||
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;
|
||||
}
|
||||
}
|
302
src/ordering/adapters/output/db/store_view.rs
Normal file
302
src/ordering/adapters/output/db/store_view.rs
Normal file
|
@ -0,0 +1,302 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// 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<String>,
|
||||
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<Store> for StoreView {
|
||||
fn update(&mut self, event: &EventEnvelope<Store>) {
|
||||
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<StoreView, Store> for OrderingDBPostgresAdapter {
|
||||
async fn load(&self, store_id: &str) -> Result<Option<StoreView>, 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<Option<(StoreView, ViewContext)>, 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<Store> for SimpleLoggingQuery {
|
||||
async fn dispatch(&self, aggregate_id: &str, events: &[EventEnvelope<Store>]) {
|
||||
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<Store> for OrderingDBPostgresAdapter {
|
||||
async fn dispatch(&self, store_id: &str, events: &[EventEnvelope<Store>]) {
|
||||
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<OrderingDBPostgresAdapter, StoreView, Store>;
|
||||
//pub type StoreQuery = Query<dyn OrderingDBPostgresAdapter, StoreView, Store>;
|
||||
|
||||
//#[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<Box<dyn Query<Store>>> =
|
||||
// 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<PostgresCqrs<Store>>,
|
||||
// Arc<dyn ViewRepository<StoreView, Store>>,
|
||||
// ) = (
|
||||
// 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;
|
||||
// }
|
||||
//}
|
|
@ -0,0 +1,55 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use mockall::predicate::*;
|
||||
use mockall::*;
|
||||
use uuid::Uuid;
|
||||
|
||||
use 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<bool>;
|
||||
}
|
||||
|
||||
pub type CategoryIDExistsDBPortObj = std::sync::Arc<dyn CategoryIDExistsDBPort>;
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn mock_category_id_exists_db_port_false(
|
||||
times: Option<usize>,
|
||||
) -> 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<usize>) -> 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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// 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<bool>;
|
||||
}
|
||||
|
||||
pub type CategoryNameExistsForStoreDBPortObj = std::sync::Arc<dyn CategoryNameExistsForStoreDBPort>;
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn mock_category_name_exists_for_store_db_port_false(
|
||||
times: Option<usize>,
|
||||
) -> 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<usize>,
|
||||
) -> 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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use mockall::predicate::*;
|
||||
use mockall::*;
|
||||
use uuid::Uuid;
|
||||
|
||||
use 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<bool>;
|
||||
}
|
||||
|
||||
pub type CustomizationIDExistsDBPortObj = std::sync::Arc<dyn CustomizationIDExistsDBPort>;
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn mock_customization_id_exists_db_port_false(
|
||||
times: Option<usize>,
|
||||
) -> 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<usize>,
|
||||
) -> 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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use mockall::predicate::*;
|
||||
use mockall::*;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::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<bool>;
|
||||
}
|
||||
|
||||
pub type CustomizationNameExistsForProductDBPortObj =
|
||||
std::sync::Arc<dyn CustomizationNameExistsForProductDBPort>;
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn mock_customization_name_exists_for_product_db_port_false(
|
||||
times: Option<usize>,
|
||||
) -> 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<usize>,
|
||||
) -> 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)
|
||||
}
|
||||
}
|
|
@ -16,4 +16,16 @@ pub enum OrderingDBError {
|
|||
OrderIDNotFound,
|
||||
DuplicateKotID,
|
||||
KotIDNotFound,
|
||||
DuplicateStoreName,
|
||||
DuplicateStoreID,
|
||||
StoreIDNotFound,
|
||||
DuplicateCategoryName,
|
||||
DuplicateCategoryID,
|
||||
CategoryIDNotFound,
|
||||
DuplicateProductName,
|
||||
DuplicateProductID,
|
||||
ProductIDNotFound,
|
||||
CustomizationIDNotFound,
|
||||
DuplicateCustomizationID,
|
||||
DuplicateCustomizationName,
|
||||
}
|
||||
|
|
44
src/ordering/application/port/output/db/get_category.rs
Normal file
44
src/ordering/application/port/output/db/get_category.rs
Normal file
|
@ -0,0 +1,44 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use mockall::predicate::*;
|
||||
use mockall::*;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::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<Category>;
|
||||
}
|
||||
|
||||
pub type GetCategoryDBPortObj = std::sync::Arc<dyn GetCategoryDBPort>;
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn mock_get_category_db_port(times: Option<usize>) -> 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)
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
53
src/ordering/application/port/output/db/product_id_exists.rs
Normal file
53
src/ordering/application/port/output/db/product_id_exists.rs
Normal file
|
@ -0,0 +1,53 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use mockall::predicate::*;
|
||||
use mockall::*;
|
||||
use uuid::Uuid;
|
||||
|
||||
use 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<bool>;
|
||||
}
|
||||
|
||||
pub type ProductIDExistsDBPortObj = std::sync::Arc<dyn ProductIDExistsDBPort>;
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn mock_product_id_exists_db_port_false(times: Option<usize>) -> 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<usize>) -> 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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use mockall::predicate::*;
|
||||
use mockall::*;
|
||||
|
||||
use 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<bool>;
|
||||
}
|
||||
|
||||
pub type ProductNameExistsForCategoryDBPortObj =
|
||||
std::sync::Arc<dyn ProductNameExistsForCategoryDBPort>;
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn mock_product_name_exists_for_category_db_port_false(
|
||||
times: Option<usize>,
|
||||
) -> 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<usize>,
|
||||
) -> 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)
|
||||
}
|
||||
}
|
55
src/ordering/application/port/output/db/store_id_exists.rs
Normal file
55
src/ordering/application/port/output/db/store_id_exists.rs
Normal file
|
@ -0,0 +1,55 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use mockall::predicate::*;
|
||||
use mockall::*;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::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<bool>;
|
||||
}
|
||||
|
||||
pub type StoreIDExistsDBPortObj = std::sync::Arc<dyn StoreIDExistsDBPort>;
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn mock_store_id_exists_db_port_false(times: Option<usize>) -> 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<usize>) -> 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)
|
||||
}
|
||||
}
|
54
src/ordering/application/port/output/db/store_name_exists.rs
Normal file
54
src/ordering/application/port/output/db/store_name_exists.rs
Normal file
|
@ -0,0 +1,54 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// 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<bool>;
|
||||
}
|
||||
|
||||
pub type StoreNameExistsDBPortObj = std::sync::Arc<dyn StoreNameExistsDBPort>;
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn mock_store_name_exists_db_port_false(times: Option<usize>) -> 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<usize>) -> 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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// 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<dyn AddProductToStoreFTSPort>;
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn mock_add_product_to_store_fts_port(times: Option<usize>) -> 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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use derive_more::Display;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub type OrderingFTSResult<V> = Result<V, OrderingFTSError>;
|
||||
|
||||
#[derive(Debug, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum OrderingFTSError {}
|
|
@ -0,0 +1,7 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
pub mod add_product_to_store;
|
||||
pub mod errors;
|
||||
pub mod update_product;
|
|
@ -0,0 +1,3 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
@ -3,3 +3,4 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
pub mod db;
|
||||
pub mod full_text_search;
|
||||
|
|
211
src/ordering/application/services/add_category_service.rs
Normal file
211
src/ordering/application/services/add_category_service.rs
Normal file
|
@ -0,0 +1,211 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use derive_builder::Builder;
|
||||
use mockall::predicate::*;
|
||||
use mockall::*;
|
||||
|
||||
use super::errors::*;
|
||||
use crate::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<CategoryAddedEvent>;
|
||||
}
|
||||
|
||||
pub type AddCategoryServiceObj = Arc<dyn AddCategoryUseCase>;
|
||||
|
||||
#[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<CategoryAddedEvent> {
|
||||
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<usize>,
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
191
src/ordering/application/services/add_customization_service.rs
Normal file
191
src/ordering/application/services/add_customization_service.rs
Normal file
|
@ -0,0 +1,191 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use derive_builder::Builder;
|
||||
use mockall::predicate::*;
|
||||
use mockall::*;
|
||||
|
||||
use super::errors::*;
|
||||
use crate::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<CustomizationAddedEvent>;
|
||||
}
|
||||
|
||||
pub type AddCustomizationServiceObj = Arc<dyn AddCustomizationUseCase>;
|
||||
|
||||
#[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<CustomizationAddedEvent> {
|
||||
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<usize>,
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
227
src/ordering/application/services/add_product_service.rs
Normal file
227
src/ordering/application/services/add_product_service.rs
Normal file
|
@ -0,0 +1,227 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use derive_builder::Builder;
|
||||
use mockall::predicate::*;
|
||||
use mockall::*;
|
||||
|
||||
use super::errors::*;
|
||||
use crate::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<ProductAddedEvent>;
|
||||
}
|
||||
|
||||
pub type AddProductServiceObj = Arc<dyn AddProductUseCase>;
|
||||
|
||||
#[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<ProductAddedEvent> {
|
||||
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<usize>,
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
149
src/ordering/application/services/add_store_service.rs
Normal file
149
src/ordering/application/services/add_store_service.rs
Normal file
|
@ -0,0 +1,149 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use derive_builder::Builder;
|
||||
use mockall::predicate::*;
|
||||
use mockall::*;
|
||||
|
||||
use super::errors::*;
|
||||
use crate::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<StoreAddedEvent>;
|
||||
}
|
||||
|
||||
pub type AddStoreServiceObj = Arc<dyn AddStoreUseCase>;
|
||||
|
||||
#[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<StoreAddedEvent> {
|
||||
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<usize>,
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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<V> = Result<V, OrderingError>;
|
||||
|
||||
|
@ -16,6 +18,14 @@ pub enum OrderingError {
|
|||
InternalError,
|
||||
OrderIDNotFound,
|
||||
KotIDNotFound,
|
||||
DuplicateStoreName,
|
||||
StoreIDNotFound,
|
||||
CategoryIDNotFound,
|
||||
DuplicateCategoryName,
|
||||
DuplicateProductName,
|
||||
ProductIDNotFound,
|
||||
DuplicateCustomizationName,
|
||||
CustomizationIDNotFound,
|
||||
}
|
||||
//
|
||||
impl From<OrderingDBError> for OrderingError {
|
||||
|
@ -30,6 +40,13 @@ impl From<OrderingDBError> 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<OrderingDBError> 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<OrderingFTSError> for OrderingError {
|
||||
fn from(value: OrderingFTSError) -> Self {
|
||||
error!("{}", value);
|
||||
OrderingError::InternalError
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
@ -0,0 +1,3 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
198
src/ordering/application/services/update_category_service.rs
Normal file
198
src/ordering/application/services/update_category_service.rs
Normal file
|
@ -0,0 +1,198 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use derive_builder::Builder;
|
||||
use mockall::predicate::*;
|
||||
use mockall::*;
|
||||
|
||||
use super::errors::*;
|
||||
use crate::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<CategoryUpdatedEvent>;
|
||||
}
|
||||
|
||||
pub type UpdateCategoryServiceObj = Arc<dyn UpdateCategoryUseCase>;
|
||||
|
||||
#[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<CategoryUpdatedEvent> {
|
||||
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<usize>,
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,210 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use derive_builder::Builder;
|
||||
use mockall::predicate::*;
|
||||
use mockall::*;
|
||||
|
||||
use super::errors::*;
|
||||
use crate::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<CustomizationUpdatedEvent>;
|
||||
}
|
||||
|
||||
pub type UpdateCustomizationServiceObj = Arc<dyn UpdateCustomizationUseCase>;
|
||||
|
||||
#[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<CustomizationUpdatedEvent> {
|
||||
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<usize>,
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
206
src/ordering/application/services/update_product_service.rs
Normal file
206
src/ordering/application/services/update_product_service.rs
Normal file
|
@ -0,0 +1,206 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use derive_builder::Builder;
|
||||
use mockall::predicate::*;
|
||||
use mockall::*;
|
||||
|
||||
use super::errors::*;
|
||||
use crate::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<ProductUpdatedEvent>;
|
||||
}
|
||||
|
||||
pub type UpdateProductServiceObj = Arc<dyn UpdateProductUseCase>;
|
||||
|
||||
#[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<ProductUpdatedEvent> {
|
||||
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<usize>,
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
146
src/ordering/application/services/update_store_service.rs
Normal file
146
src/ordering/application/services/update_store_service.rs
Normal file
|
@ -0,0 +1,146 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use derive_builder::Builder;
|
||||
use mockall::predicate::*;
|
||||
use mockall::*;
|
||||
|
||||
use super::errors::*;
|
||||
use crate::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<StoreUpdatedEvent>;
|
||||
}
|
||||
|
||||
pub type UpdateStoreServiceObj = Arc<dyn UpdateStoreUseCase>;
|
||||
|
||||
#[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<StoreUpdatedEvent> {
|
||||
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<usize>,
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
90
src/ordering/domain/add_category_command.rs
Normal file
90
src/ordering/domain/add_category_command.rs
Normal file
|
@ -0,0 +1,90 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// 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<String>,
|
||||
store_id: Uuid,
|
||||
adding_by: Uuid,
|
||||
}
|
||||
|
||||
impl AddCategoryCommand {
|
||||
pub fn new(
|
||||
name: String,
|
||||
description: Option<String>,
|
||||
store_id: Uuid,
|
||||
adding_by: Uuid,
|
||||
) -> Result<Self, AddCategoryCommandError> {
|
||||
let description: Option<String> = 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)
|
||||
)
|
||||
}
|
||||
}
|
91
src/ordering/domain/add_customization_command.rs
Normal file
91
src/ordering/domain/add_customization_command.rs
Normal file
|
@ -0,0 +1,91 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use derive_builder::Builder;
|
||||
use derive_getters::Getters;
|
||||
use 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<AddCustomizationCommand, AddCustomizationCommandError> {
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
256
src/ordering/domain/add_product_command.rs
Normal file
256
src/ordering/domain/add_product_command.rs
Normal file
|
@ -0,0 +1,256 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use derive_builder::Builder;
|
||||
use derive_getters::Getters;
|
||||
use 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<String>,
|
||||
image: Option<String>,
|
||||
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<String>,
|
||||
image: Option<String>,
|
||||
category_id: Uuid,
|
||||
sku_able: bool,
|
||||
price: Price,
|
||||
quantity: Quantity,
|
||||
adding_by: Uuid,
|
||||
}
|
||||
|
||||
impl UnvalidatedAddProductCommand {
|
||||
pub fn validate(self) -> Result<AddProductCommand, AddProductCommandError> {
|
||||
let description: Option<String> = 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<String> = 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))
|
||||
}
|
||||
}
|
82
src/ordering/domain/add_store_command.rs
Normal file
82
src/ordering/domain/add_store_command.rs
Normal file
|
@ -0,0 +1,82 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// 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<String>,
|
||||
owner: Uuid,
|
||||
}
|
||||
|
||||
impl AddStoreCommand {
|
||||
pub fn new(
|
||||
name: String,
|
||||
address: Option<String>,
|
||||
owner: Uuid,
|
||||
) -> Result<Self, AddStoreCommandError> {
|
||||
let address: Option<String> = 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)
|
||||
)
|
||||
}
|
||||
}
|
19
src/ordering/domain/category_added_event.rs
Normal file
19
src/ordering/domain/category_added_event.rs
Normal file
|
@ -0,0 +1,19 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use derive_builder::Builder;
|
||||
use derive_getters::Getters;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(
|
||||
Clone, Debug, Builder, Serialize, Deserialize, Getters, Eq, PartialEq, Ord, PartialOrd,
|
||||
)]
|
||||
pub struct CategoryAddedEvent {
|
||||
name: String,
|
||||
description: Option<String>,
|
||||
added_by_user: Uuid,
|
||||
category_id: Uuid,
|
||||
store_id: Uuid,
|
||||
}
|
156
src/ordering/domain/category_aggregate.rs
Normal file
156
src/ordering/domain/category_aggregate.rs
Normal file
|
@ -0,0 +1,156 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// 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<String>,
|
||||
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<dyn OrderingServicesInterface>;
|
||||
|
||||
// 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<Vec<Self::Event>, 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<Category>;
|
||||
|
||||
#[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]);
|
||||
}
|
||||
}
|
45
src/ordering/domain/category_updated_event.rs
Normal file
45
src/ordering/domain/category_updated_event.rs
Normal file
|
@ -0,0 +1,45 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use derive_builder::Builder;
|
||||
use derive_getters::Getters;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::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()
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
}
|
||||
|
|
43
src/ordering/domain/customization_added_event.rs
Normal file
43
src/ordering/domain/customization_added_event.rs
Normal file
|
@ -0,0 +1,43 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use derive_builder::Builder;
|
||||
use derive_getters::Getters;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::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()
|
||||
}
|
||||
}
|
143
src/ordering/domain/customization_aggregate.rs
Normal file
143
src/ordering/domain/customization_aggregate.rs
Normal file
|
@ -0,0 +1,143 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// 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<dyn OrderingServicesInterface>;
|
||||
|
||||
// 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<Vec<Self::Event>, 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<Customization>;
|
||||
|
||||
#[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]);
|
||||
}
|
||||
}
|
49
src/ordering/domain/customization_updated_event.rs
Normal file
49
src/ordering/domain/customization_updated_event.rs
Normal file
|
@ -0,0 +1,49 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use derive_builder::Builder;
|
||||
use derive_getters::Getters;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::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()
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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;
|
||||
|
|
3
src/ordering/domain/pantry_aggregate.rs
Normal file
3
src/ordering/domain/pantry_aggregate.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
51
src/ordering/domain/product_added_event.rs
Normal file
51
src/ordering/domain/product_added_event.rs
Normal file
|
@ -0,0 +1,51 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use derive_builder::Builder;
|
||||
use derive_getters::Getters;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use 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<String>,
|
||||
image: Option<String>, // 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()
|
||||
}
|
||||
}
|
166
src/ordering/domain/product_aggregate.rs
Normal file
166
src/ordering/domain/product_aggregate.rs
Normal file
|
@ -0,0 +1,166 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// 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<String>,
|
||||
image: Option<String>, // 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<dyn OrderingServicesInterface>;
|
||||
|
||||
// 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<Vec<Self::Event>, 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<Product>;
|
||||
|
||||
#[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: T, str_value: &str) -> bool
|
||||
where
|
||||
T: ToString + FromStr + std::fmt::Debug + PartialEq,
|
||||
<T as FromStr>::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
|
||||
}
|
||||
}
|
53
src/ordering/domain/product_updated_event.rs
Normal file
53
src/ordering/domain/product_updated_event.rs
Normal file
|
@ -0,0 +1,53 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use derive_builder::Builder;
|
||||
use derive_getters::Getters;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::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()
|
||||
}
|
||||
}
|
18
src/ordering/domain/store_added_event.rs
Normal file
18
src/ordering/domain/store_added_event.rs
Normal file
|
@ -0,0 +1,18 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use derive_builder::Builder;
|
||||
use derive_getters::Getters;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(
|
||||
Clone, Debug, Builder, Serialize, Deserialize, Getters, Eq, PartialEq, Ord, PartialOrd,
|
||||
)]
|
||||
pub struct StoreAddedEvent {
|
||||
name: String,
|
||||
address: Option<String>,
|
||||
owner: Uuid,
|
||||
store_id: Uuid,
|
||||
}
|
146
src/ordering/domain/store_aggregate.rs
Normal file
146
src/ordering/domain/store_aggregate.rs
Normal file
|
@ -0,0 +1,146 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// 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<String>,
|
||||
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<dyn OrderingServicesInterface>;
|
||||
|
||||
// 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<Vec<Self::Event>, 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<Store>;
|
||||
|
||||
#[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]);
|
||||
}
|
||||
}
|
43
src/ordering/domain/store_updated_event.rs
Normal file
43
src/ordering/domain/store_updated_event.rs
Normal file
|
@ -0,0 +1,43 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use derive_builder::Builder;
|
||||
use derive_getters::Getters;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::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()
|
||||
}
|
||||
}
|
117
src/ordering/domain/update_category_command.rs
Normal file
117
src/ordering/domain/update_category_command.rs
Normal file
|
@ -0,0 +1,117 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// 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<String>,
|
||||
adding_by: Uuid,
|
||||
|
||||
old_category: Category,
|
||||
}
|
||||
|
||||
impl UpdateCategoryCommand {
|
||||
pub fn new(
|
||||
name: String,
|
||||
description: Option<String>,
|
||||
old_category: Category,
|
||||
adding_by: Uuid,
|
||||
) -> Result<Self, UpdateCategoryCommandError> {
|
||||
let description: Option<String> = 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)
|
||||
)
|
||||
}
|
||||
}
|
101
src/ordering/domain/update_customization_command.rs
Normal file
101
src/ordering/domain/update_customization_command.rs
Normal file
|
@ -0,0 +1,101 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use derive_builder::Builder;
|
||||
use derive_getters::Getters;
|
||||
use 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<UpdateCustomizationCommand, UpdateCustomizationCommandError> {
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
266
src/ordering/domain/update_product_command.rs
Normal file
266
src/ordering/domain/update_product_command.rs
Normal file
|
@ -0,0 +1,266 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use derive_builder::Builder;
|
||||
use derive_getters::Getters;
|
||||
use 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<String>,
|
||||
image: Option<String>,
|
||||
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<String>,
|
||||
image: Option<String>,
|
||||
category_id: Uuid,
|
||||
sku_able: bool,
|
||||
price: Price,
|
||||
quantity: Quantity,
|
||||
adding_by: Uuid,
|
||||
old_product: Product,
|
||||
}
|
||||
|
||||
impl UnvalidatedUpdateProductCommand {
|
||||
pub fn validate(self) -> Result<UpdateProductCommand, UpdateProductCommandError> {
|
||||
let description: Option<String> = 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<String> = 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))
|
||||
}
|
||||
}
|
127
src/ordering/domain/update_store_command.rs
Normal file
127
src/ordering/domain/update_store_command.rs
Normal file
|
@ -0,0 +1,127 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// 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<String>,
|
||||
owner: Uuid,
|
||||
old_store: Store,
|
||||
adding_by: Uuid,
|
||||
}
|
||||
|
||||
impl UpdateStoreCommand {
|
||||
pub fn new(
|
||||
name: String,
|
||||
address: Option<String>,
|
||||
owner: Uuid,
|
||||
old_store: Store,
|
||||
adding_by: Uuid,
|
||||
) -> Result<Self, UpdateStoreCommandError> {
|
||||
let address: Option<String> = 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)
|
||||
)
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue