@reventlessdev/online-shop-hybrid-ordering 1.0.0-alpha.210 → 1.0.0-alpha.211
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/package.json +8 -8
- package/src/CatalogProduct/StateChangeSlice/SyncCatalogProduct.res +18 -0
- package/src/CatalogProduct/StateChangeSlice/SyncCatalogProduct.res.mjs +21 -1
- package/src/CatalogProduct/StateChangeSlice/SyncCatalogProduct_Behavior.res +26 -4
- package/src/CatalogProduct/StateChangeSlice/SyncCatalogProduct_Behavior.res.mjs +99 -39
- package/src/CatalogProduct/StateViewSliceStream/AvailableProducts.res +2 -0
- package/src/CatalogProduct/StateViewSliceStream/AvailableProducts.res.mjs +13 -2
- package/src/CatalogProduct/StateViewSliceStream/AvailableProducts_Projection.res +11 -0
- package/src/CatalogProduct/StateViewSliceStream/AvailableProducts_Projection.res.mjs +28 -19
- package/src/Customer/Aggregate/Customer.res +14 -1
- package/src/Customer/Aggregate/Customer.res.mjs +16 -3
- package/src/Customer/Aggregate/Customer_Behavior.res +29 -10
- package/src/Customer/Aggregate/Customer_Behavior.res.mjs +79 -36
- package/src/Customer/ReadModelStream/Customers.res +29 -4
- package/src/Customer/ReadModelStream/Customers.res.mjs +15 -3
- package/src/Customer/ReadModelStream/Customers_Projections.res +8 -5
- package/src/Customer/ReadModelStream/Customers_Projections.res.mjs +24 -12
- package/src/Extension/Products_Extension.res +9 -0
- package/src/Extension/Products_Extension.res.mjs +36 -19
- package/src/Order/StateViewSliceStream/Orders.res +4 -2
- package/src/Order/StateViewSliceStream/Orders.res.mjs +4 -26
- package/src/Order/StateViewSliceStream/Orders_Projection.res +3 -3
- package/src/Order/StateViewSliceStream/Orders_Projection.res.mjs +3 -3
- package/tests/CatalogProduct/StateChangeSlice/SyncCatalogProduct_GWT.res +45 -0
- package/tests/CatalogProduct/StateChangeSlice/SyncCatalogProduct_GWT.res.mjs +70 -0
- package/tests/CatalogProduct/StateViewSliceStream/AvailableProducts_GWT.res +19 -0
- package/tests/CatalogProduct/StateViewSliceStream/AvailableProducts_GWT.res.mjs +32 -2
- package/tests/Customer/ReadModelStream/Customers_GWT.res +7 -7
- package/tests/Customer/ReadModelStream/Customers_GWT.res.mjs +7 -7
- package/tests/Flow/OrderingFlow_GWT.res +1 -1
- package/tests/Flow/OrderingFlow_GWT.res.mjs +1 -1
- package/tests/Order/StateViewSliceStream/Orders_GWT.res +5 -5
- package/tests/Order/StateViewSliceStream/Orders_GWT.res.mjs +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,16 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# 1.0.0-alpha.211 (2026-08-16)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **core:** let [@retired](https://github.com/retired) name a lifecycle state, not only a boolean ([6bb346b](https://github.com/ReventlessDev/reventless-core/commit/6bb346b4f6a5f33826fc24537953482a76067177))
|
|
11
|
+
* **core:** mark the state that retires a row, and allow more than one ([cb1461f](https://github.com/ReventlessDev/reventless-core/commit/cb1461f024d3ca3b53fd9c8b010a054e3fcc4555))
|
|
12
|
+
* **example:** give the hybrid shop's archive a way back ([524d374](https://github.com/ReventlessDev/reventless-core/commit/524d3748e455c70924b7861cbd4748744186de41))
|
|
13
|
+
* **spec:** [@retired](https://github.com/retired) state-field annotation and its schema emission ([2d8234b](https://github.com/ReventlessDev/reventless-core/commit/2d8234b6b3dd8f479031a67eb5b4b47b5c0c2ff9))
|
|
14
|
+
|
|
15
|
+
|
|
6
16
|
# 1.0.0-alpha.210 (2026-08-15)
|
|
7
17
|
|
|
8
18
|
**Note:** Version bump only for package @reventlessdev/online-shop-hybrid-ordering
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/online-shop-hybrid-ordering",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.211",
|
|
4
4
|
"description": "Online Shop Hybrid example — Ordering plugin (Customer aggregate + Order/CatalogProduct DCB)",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"jest": {
|
|
@@ -21,18 +21,18 @@
|
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"sury": "^11.0.0-alpha.4",
|
|
24
|
-
"@reventlessdev/online-shop-hybrid-
|
|
24
|
+
"@reventlessdev/online-shop-hybrid-catalog-spec": "1.0.0-alpha.111",
|
|
25
25
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
|
|
26
|
-
"@reventlessdev/
|
|
27
|
-
"@reventlessdev/
|
|
28
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.
|
|
29
|
-
"@reventlessdev/reventless-
|
|
30
|
-
"@reventlessdev/reventless-
|
|
26
|
+
"@reventlessdev/reventless-gwt": "1.0.0-alpha.184",
|
|
27
|
+
"@reventlessdev/online-shop-hybrid-ordering-spec": "1.0.0-alpha.111",
|
|
28
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.143",
|
|
29
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.115",
|
|
30
|
+
"@reventlessdev/reventless-local": "3.0.0-alpha.225"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"rescript": "12.3.0",
|
|
34
34
|
"sury-ppx": "11.0.0-alpha.2",
|
|
35
|
-
"@reventlessdev/reventless-ppx": "1.0.0-alpha.
|
|
35
|
+
"@reventlessdev/reventless-ppx": "1.0.0-alpha.67"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"rescript": "12.3.0"
|
|
@@ -7,11 +7,19 @@
|
|
|
7
7
|
type consumedEvent =
|
|
8
8
|
| CatalogProductSynced({name: string, price: Reventless.Money.t})
|
|
9
9
|
| CatalogProductPriceChanged({price: Reventless.Money.t})
|
|
10
|
+
| CatalogProductWithdrawn
|
|
11
|
+
| CatalogProductRelisted
|
|
10
12
|
|
|
11
13
|
@schema
|
|
12
14
|
type command =
|
|
13
15
|
| SyncNewProduct({productId: string, name: string, price: Reventless.Money.t})
|
|
14
16
|
| ChangeSyncedPrice({productId: string, price: Reventless.Money.t})
|
|
17
|
+
// The withdrawal and the way back. Both carry only the id: the shadow this
|
|
18
|
+
// slice maintains already holds the name and price, and it survives the read
|
|
19
|
+
// model's delete — which is what lets a relist restore availability from state
|
|
20
|
+
// Ordering owns rather than asking Catalog to re-send facts it already has.
|
|
21
|
+
| WithdrawSyncedProduct({productId: string})
|
|
22
|
+
| RelistSyncedProduct({productId: string})
|
|
15
23
|
|
|
16
24
|
@schema
|
|
17
25
|
type error = unit // always succeeds — sync is idempotent
|
|
@@ -27,3 +35,13 @@ type event =
|
|
|
27
35
|
productId: string,
|
|
28
36
|
price: Reventless.Money.t,
|
|
29
37
|
})
|
|
38
|
+
| CatalogProductWithdrawn({productId: string})
|
|
39
|
+
// Carries the shadow's name and price so the projection can restore the row
|
|
40
|
+
// without a second read: the fold has them in hand at exactly the moment the
|
|
41
|
+
// decision is made, and an event that states what it caused is what makes the
|
|
42
|
+
// projection a pure mapping.
|
|
43
|
+
| CatalogProductRelisted({
|
|
44
|
+
productId: string,
|
|
45
|
+
name: string,
|
|
46
|
+
price: Reventless.Money.t,
|
|
47
|
+
})
|
|
@@ -13,7 +13,9 @@ let consumedEventSchema = S.union([
|
|
|
13
13
|
S.schema(s => ({
|
|
14
14
|
TAG: "CatalogProductPriceChanged",
|
|
15
15
|
price: s.m(Money$Reventless.schema)
|
|
16
|
-
}))
|
|
16
|
+
})),
|
|
17
|
+
S.literal("CatalogProductWithdrawn"),
|
|
18
|
+
S.literal("CatalogProductRelisted")
|
|
17
19
|
]);
|
|
18
20
|
|
|
19
21
|
let commandSchema = S.union([
|
|
@@ -27,6 +29,14 @@ let commandSchema = S.union([
|
|
|
27
29
|
TAG: "ChangeSyncedPrice",
|
|
28
30
|
productId: s.m(DcbTag$Reventless.string),
|
|
29
31
|
price: s.m(Money$Reventless.schema)
|
|
32
|
+
})),
|
|
33
|
+
S.schema(s => ({
|
|
34
|
+
TAG: "WithdrawSyncedProduct",
|
|
35
|
+
productId: s.m(DcbTag$Reventless.string)
|
|
36
|
+
})),
|
|
37
|
+
S.schema(s => ({
|
|
38
|
+
TAG: "RelistSyncedProduct",
|
|
39
|
+
productId: s.m(DcbTag$Reventless.string)
|
|
30
40
|
}))
|
|
31
41
|
]);
|
|
32
42
|
|
|
@@ -41,6 +51,16 @@ let eventSchema = S.union([
|
|
|
41
51
|
TAG: "CatalogProductPriceChanged",
|
|
42
52
|
productId: s.m(DcbTag$Reventless.string),
|
|
43
53
|
price: s.m(Money$Reventless.schema)
|
|
54
|
+
})),
|
|
55
|
+
S.schema(s => ({
|
|
56
|
+
TAG: "CatalogProductWithdrawn",
|
|
57
|
+
productId: s.m(DcbTag$Reventless.string)
|
|
58
|
+
})),
|
|
59
|
+
S.schema(s => ({
|
|
60
|
+
TAG: "CatalogProductRelisted",
|
|
61
|
+
productId: s.m(DcbTag$Reventless.string),
|
|
62
|
+
name: s.m(S.string),
|
|
63
|
+
price: s.m(Money$Reventless.schema)
|
|
44
64
|
}))
|
|
45
65
|
]);
|
|
46
66
|
|
|
@@ -2,20 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
// No price until one has been synced — see ChangeProductPrice_Behavior for why
|
|
4
4
|
// a money field's "empty" is an absence rather than a zero.
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
//
|
|
6
|
+
// `withdrawn` sits beside the shadow rather than replacing it: a withdrawn
|
|
7
|
+
// product's name and price are exactly what a relist needs, and dropping them
|
|
8
|
+
// here would force Catalog to re-send facts Ordering already holds.
|
|
9
|
+
type state = {name: string, price: option<Reventless.Money.t>, withdrawn: bool}
|
|
10
|
+
let initialState = {name: "", price: None, withdrawn: false}
|
|
7
11
|
|
|
8
12
|
let evolve = (state, event) =>
|
|
9
13
|
switch event {
|
|
10
|
-
| CatalogProductSynced({name, price}) => {name, price: Some(price)}
|
|
14
|
+
| CatalogProductSynced({name, price}) => {name, price: Some(price), withdrawn: false}
|
|
11
15
|
| CatalogProductPriceChanged({price}) => {...state, price: Some(price)}
|
|
16
|
+
| CatalogProductWithdrawn => {...state, withdrawn: true}
|
|
17
|
+
| CatalogProductRelisted => {...state, withdrawn: false}
|
|
12
18
|
}
|
|
13
19
|
|
|
14
20
|
let decide = (state, command) =>
|
|
15
21
|
switch command {
|
|
16
22
|
| SyncNewProduct({productId, name, price}) =>
|
|
17
23
|
// Idempotent: re-syncing identical data emits nothing (at-least-once delivery).
|
|
18
|
-
if state.name == name && state.price == Some(price) {
|
|
24
|
+
if state.name == name && state.price == Some(price) && !state.withdrawn {
|
|
19
25
|
Ok([])
|
|
20
26
|
} else {
|
|
21
27
|
Ok([CatalogProductSynced({productId, name, price})])
|
|
@@ -26,4 +32,20 @@ let decide = (state, command) =>
|
|
|
26
32
|
} else {
|
|
27
33
|
Ok([CatalogProductPriceChanged({productId, price})])
|
|
28
34
|
}
|
|
35
|
+
| WithdrawSyncedProduct({productId: theId}) =>
|
|
36
|
+
if state.withdrawn {
|
|
37
|
+
Ok([])
|
|
38
|
+
} else {
|
|
39
|
+
Ok([CatalogProductWithdrawn({productId: theId})])
|
|
40
|
+
}
|
|
41
|
+
| RelistSyncedProduct({productId: theId}) =>
|
|
42
|
+
switch (state.withdrawn, state.price) {
|
|
43
|
+
| (false, _) => Ok([]) // idempotent — already orderable
|
|
44
|
+
// A relist of a product never synced has nothing to restore. Emitting an
|
|
45
|
+
// event with an invented name and a zero price would put a row in the
|
|
46
|
+
// shopper's catalog that no Catalog product backs.
|
|
47
|
+
| (true, None) => Ok([])
|
|
48
|
+
| (true, Some(price)) =>
|
|
49
|
+
Ok([CatalogProductRelisted({productId: theId, name: state.name, price})])
|
|
50
|
+
}
|
|
29
51
|
}
|
|
@@ -3,63 +3,123 @@
|
|
|
3
3
|
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
4
4
|
|
|
5
5
|
function evolve(state, event) {
|
|
6
|
-
if (event
|
|
7
|
-
|
|
8
|
-
name: event.name,
|
|
9
|
-
price: event.price
|
|
10
|
-
};
|
|
11
|
-
} else {
|
|
12
|
-
return {
|
|
13
|
-
name: state.name,
|
|
14
|
-
price: event.price
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function decide(state, command) {
|
|
20
|
-
if (command.TAG === "SyncNewProduct") {
|
|
21
|
-
let price = command.price;
|
|
22
|
-
let name = command.name;
|
|
23
|
-
if (state.name === name && Primitive_object.equal(state.price, price)) {
|
|
6
|
+
if (typeof event !== "object") {
|
|
7
|
+
if (event === "CatalogProductWithdrawn") {
|
|
24
8
|
return {
|
|
25
|
-
|
|
26
|
-
|
|
9
|
+
name: state.name,
|
|
10
|
+
price: state.price,
|
|
11
|
+
withdrawn: true
|
|
27
12
|
};
|
|
28
13
|
} else {
|
|
29
14
|
return {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
productId: command.productId,
|
|
34
|
-
name: name,
|
|
35
|
-
price: price
|
|
36
|
-
}]
|
|
15
|
+
name: state.name,
|
|
16
|
+
price: state.price,
|
|
17
|
+
withdrawn: false
|
|
37
18
|
};
|
|
38
19
|
}
|
|
39
|
-
}
|
|
40
|
-
let price$1 = command.price;
|
|
41
|
-
if (Primitive_object.equal(state.price, price$1)) {
|
|
20
|
+
} else if (event.TAG === "CatalogProductSynced") {
|
|
42
21
|
return {
|
|
43
|
-
|
|
44
|
-
|
|
22
|
+
name: event.name,
|
|
23
|
+
price: event.price,
|
|
24
|
+
withdrawn: false
|
|
45
25
|
};
|
|
46
26
|
} else {
|
|
47
27
|
return {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
productId: command.productId,
|
|
52
|
-
price: price$1
|
|
53
|
-
}]
|
|
28
|
+
name: state.name,
|
|
29
|
+
price: event.price,
|
|
30
|
+
withdrawn: state.withdrawn
|
|
54
31
|
};
|
|
55
32
|
}
|
|
56
33
|
}
|
|
57
34
|
|
|
35
|
+
function decide(state, command) {
|
|
36
|
+
switch (command.TAG) {
|
|
37
|
+
case "SyncNewProduct" :
|
|
38
|
+
let price = command.price;
|
|
39
|
+
let name = command.name;
|
|
40
|
+
if (state.name === name && Primitive_object.equal(state.price, price) && !state.withdrawn) {
|
|
41
|
+
return {
|
|
42
|
+
TAG: "Ok",
|
|
43
|
+
_0: []
|
|
44
|
+
};
|
|
45
|
+
} else {
|
|
46
|
+
return {
|
|
47
|
+
TAG: "Ok",
|
|
48
|
+
_0: [{
|
|
49
|
+
TAG: "CatalogProductSynced",
|
|
50
|
+
productId: command.productId,
|
|
51
|
+
name: name,
|
|
52
|
+
price: price
|
|
53
|
+
}]
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
case "ChangeSyncedPrice" :
|
|
57
|
+
let price$1 = command.price;
|
|
58
|
+
if (Primitive_object.equal(state.price, price$1)) {
|
|
59
|
+
return {
|
|
60
|
+
TAG: "Ok",
|
|
61
|
+
_0: []
|
|
62
|
+
};
|
|
63
|
+
} else {
|
|
64
|
+
return {
|
|
65
|
+
TAG: "Ok",
|
|
66
|
+
_0: [{
|
|
67
|
+
TAG: "CatalogProductPriceChanged",
|
|
68
|
+
productId: command.productId,
|
|
69
|
+
price: price$1
|
|
70
|
+
}]
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
case "WithdrawSyncedProduct" :
|
|
74
|
+
if (state.withdrawn) {
|
|
75
|
+
return {
|
|
76
|
+
TAG: "Ok",
|
|
77
|
+
_0: []
|
|
78
|
+
};
|
|
79
|
+
} else {
|
|
80
|
+
return {
|
|
81
|
+
TAG: "Ok",
|
|
82
|
+
_0: [{
|
|
83
|
+
TAG: "CatalogProductWithdrawn",
|
|
84
|
+
productId: command.productId
|
|
85
|
+
}]
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
case "RelistSyncedProduct" :
|
|
89
|
+
let match = state.withdrawn;
|
|
90
|
+
let match$1 = state.price;
|
|
91
|
+
if (match) {
|
|
92
|
+
if (match$1 !== undefined) {
|
|
93
|
+
return {
|
|
94
|
+
TAG: "Ok",
|
|
95
|
+
_0: [{
|
|
96
|
+
TAG: "CatalogProductRelisted",
|
|
97
|
+
productId: command.productId,
|
|
98
|
+
name: state.name,
|
|
99
|
+
price: match$1
|
|
100
|
+
}]
|
|
101
|
+
};
|
|
102
|
+
} else {
|
|
103
|
+
return {
|
|
104
|
+
TAG: "Ok",
|
|
105
|
+
_0: []
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
return {
|
|
110
|
+
TAG: "Ok",
|
|
111
|
+
_0: []
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
58
117
|
let Spec;
|
|
59
118
|
|
|
60
119
|
let initialState = {
|
|
61
120
|
name: "",
|
|
62
|
-
price: undefined
|
|
121
|
+
price: undefined,
|
|
122
|
+
withdrawn: false
|
|
63
123
|
};
|
|
64
124
|
|
|
65
125
|
let moduleUrl = "@reventlessdev/online-shop-hybrid-ordering/src/CatalogProduct/StateChangeSlice/SyncCatalogProduct_Behavior.res.mjs";
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
type consumedEvent =
|
|
11
11
|
| CatalogProductSynced({productId: string, name: string, price: Reventless.Money.t})
|
|
12
12
|
| CatalogProductPriceChanged({productId: string, price: Reventless.Money.t})
|
|
13
|
+
| CatalogProductWithdrawn({productId: string})
|
|
14
|
+
| CatalogProductRelisted({productId: string, name: string, price: Reventless.Money.t})
|
|
13
15
|
|
|
14
16
|
@schema
|
|
15
17
|
type state = {productId: string, name: string, price: Reventless.Money.t}
|
|
@@ -16,6 +16,16 @@ let consumedEventSchema = S.union([
|
|
|
16
16
|
TAG: "CatalogProductPriceChanged",
|
|
17
17
|
productId: s.m(S.string),
|
|
18
18
|
price: s.m(Money$Reventless.schema)
|
|
19
|
+
})),
|
|
20
|
+
S.schema(s => ({
|
|
21
|
+
TAG: "CatalogProductWithdrawn",
|
|
22
|
+
productId: s.m(S.string)
|
|
23
|
+
})),
|
|
24
|
+
S.schema(s => ({
|
|
25
|
+
TAG: "CatalogProductRelisted",
|
|
26
|
+
productId: s.m(S.string),
|
|
27
|
+
name: s.m(S.string),
|
|
28
|
+
price: s.m(Money$Reventless.schema)
|
|
19
29
|
}))
|
|
20
30
|
]);
|
|
21
31
|
|
|
@@ -42,10 +52,11 @@ let stateSchema$1 = S.Metadata.set(stateSchema, StateAnnotations$Reventless.stat
|
|
|
42
52
|
scanSort: [],
|
|
43
53
|
semantic: [],
|
|
44
54
|
metric: [],
|
|
45
|
-
|
|
55
|
+
lifecycle: undefined,
|
|
46
56
|
groupBy: undefined,
|
|
47
57
|
visibility: "Internal",
|
|
48
|
-
live: undefined
|
|
58
|
+
live: undefined,
|
|
59
|
+
retired: undefined
|
|
49
60
|
});
|
|
50
61
|
|
|
51
62
|
let name = "AvailableProducts";
|
|
@@ -5,4 +5,15 @@ let project = ({event}) =>
|
|
|
5
5
|
| CatalogProductSynced({productId, name, price}) => [Set(productId, {productId, name, price})]
|
|
6
6
|
| CatalogProductPriceChanged({productId, price}) =>
|
|
7
7
|
[Update(productId, p => {...p, price})]
|
|
8
|
+
// `Delete`, not a flag. This view answers "what can I order", so a withdrawn
|
|
9
|
+
// product leaves it. Marking it instead — `@retired` on the shopper view —
|
|
10
|
+
// would look tidy and be wrong twice over: a shopper is not elevated, so the
|
|
11
|
+
// narrowing would hide it correctly, but an operator IS, and would find
|
|
12
|
+
// discontinued stock in the shopper's catalog. It would also make Ordering
|
|
13
|
+
// restate a lifecycle Catalog owns.
|
|
14
|
+
| CatalogProductWithdrawn({productId}) => [Delete(productId)]
|
|
15
|
+
// Set again, from the relist event's own payload — which the aggregate filled
|
|
16
|
+
// from the shadow it kept through the withdrawal. Catalog is never asked to
|
|
17
|
+
// re-send what Ordering already has.
|
|
18
|
+
| CatalogProductRelisted({productId, name, price}) => [Set(productId, {productId, name, price})]
|
|
8
19
|
}
|
|
@@ -3,27 +3,36 @@
|
|
|
3
3
|
|
|
4
4
|
function project(param) {
|
|
5
5
|
let event = param.event;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
6
|
+
switch (event.TAG) {
|
|
7
|
+
case "CatalogProductPriceChanged" :
|
|
8
|
+
let price = event.price;
|
|
9
|
+
return [{
|
|
10
|
+
TAG: "Update",
|
|
11
|
+
_0: event.productId,
|
|
12
|
+
_1: p => ({
|
|
13
|
+
productId: p.productId,
|
|
14
|
+
name: p.name,
|
|
15
|
+
price: price
|
|
16
|
+
})
|
|
17
|
+
}];
|
|
18
|
+
case "CatalogProductWithdrawn" :
|
|
19
|
+
return [{
|
|
20
|
+
TAG: "Delete",
|
|
21
|
+
_0: event.productId
|
|
22
|
+
}];
|
|
23
|
+
case "CatalogProductSynced" :
|
|
24
|
+
case "CatalogProductRelisted" :
|
|
25
|
+
break;
|
|
17
26
|
}
|
|
18
|
-
let
|
|
27
|
+
let productId = event.productId;
|
|
19
28
|
return [{
|
|
20
|
-
TAG: "
|
|
21
|
-
_0:
|
|
22
|
-
_1:
|
|
23
|
-
productId:
|
|
24
|
-
name:
|
|
25
|
-
price: price
|
|
26
|
-
}
|
|
29
|
+
TAG: "Set",
|
|
30
|
+
_0: productId,
|
|
31
|
+
_1: {
|
|
32
|
+
productId: productId,
|
|
33
|
+
name: event.name,
|
|
34
|
+
price: event.price
|
|
35
|
+
}
|
|
27
36
|
}];
|
|
28
37
|
}
|
|
29
38
|
|
|
@@ -37,7 +37,19 @@ type command =
|
|
|
37
37
|
// address that has since changed is dropped rather than applied.
|
|
38
38
|
| @noApi SetLocation({location: Reventless.GeoPoint.t, resolvedFrom: string})
|
|
39
39
|
| @noApi MarkAddressUnresolvable({address: string, reason: string})
|
|
40
|
-
| Deactivate
|
|
40
|
+
| @allowedStates([Customers.Active]) Deactivate
|
|
41
|
+
// The way back. Deactivation withdraws a customer from ordinary use; it does
|
|
42
|
+
// not erase them, so restoring one needs no payload — the profile is still in
|
|
43
|
+
// the state, and asking a caller to re-supply an email they never changed
|
|
44
|
+
// would be a second chance to get it wrong.
|
|
45
|
+
//
|
|
46
|
+
// `@allowedStates([Deactivated])` is the whole of "offer this on a deactivated
|
|
47
|
+
// customer and nowhere else". It works because the view's retirement IS a state
|
|
48
|
+
// of its lifecycle rather than a boolean beside one, so the states a command
|
|
49
|
+
// names and the states a row can be in are the same vocabulary. The refusal
|
|
50
|
+
// still lives in `decide`; this only stops a menu offering what the write side
|
|
51
|
+
// would reject.
|
|
52
|
+
| @allowedStates([Customers.Deactivated]) Reactivate
|
|
41
53
|
|
|
42
54
|
@schema
|
|
43
55
|
type event =
|
|
@@ -60,6 +72,7 @@ type event =
|
|
|
60
72
|
// two things is one nobody can act on.
|
|
61
73
|
| AddressUnresolvable({address: string, reason: string})
|
|
62
74
|
| Deactivated
|
|
75
|
+
| Reactivated
|
|
63
76
|
|
|
64
77
|
@schema
|
|
65
78
|
type error =
|
|
@@ -33,7 +33,8 @@ let commandSchema = S.union([
|
|
|
33
33
|
address: s.m(S.string),
|
|
34
34
|
reason: s.m(S.string)
|
|
35
35
|
})),
|
|
36
|
-
S.literal("Deactivate")
|
|
36
|
+
S.literal("Deactivate"),
|
|
37
|
+
S.literal("Reactivate")
|
|
37
38
|
]);
|
|
38
39
|
|
|
39
40
|
let eventSchema = S.union([
|
|
@@ -65,7 +66,8 @@ let eventSchema = S.union([
|
|
|
65
66
|
address: s.m(S.string),
|
|
66
67
|
reason: s.m(S.string)
|
|
67
68
|
})),
|
|
68
|
-
S.literal("Deactivated")
|
|
69
|
+
S.literal("Deactivated"),
|
|
70
|
+
S.literal("Reactivated")
|
|
69
71
|
]);
|
|
70
72
|
|
|
71
73
|
let errorSchema = S.union([
|
|
@@ -79,6 +81,17 @@ let commandSchema$1 = Api$ReventlessInfra.markNoApiVariants(commandSchema, [
|
|
|
79
81
|
"MarkAddressUnresolvable"
|
|
80
82
|
]);
|
|
81
83
|
|
|
84
|
+
let commandSchema$2 = Api$ReventlessInfra.markAllowedStates(commandSchema$1, [
|
|
85
|
+
[
|
|
86
|
+
"Deactivate",
|
|
87
|
+
["Active"]
|
|
88
|
+
],
|
|
89
|
+
[
|
|
90
|
+
"Reactivate",
|
|
91
|
+
["Deactivated"]
|
|
92
|
+
]
|
|
93
|
+
]);
|
|
94
|
+
|
|
82
95
|
function commandAuthorization(param) {
|
|
83
96
|
return "AllowAuthenticated";
|
|
84
97
|
}
|
|
@@ -94,7 +107,7 @@ export {
|
|
|
94
107
|
Id,
|
|
95
108
|
eventSchema,
|
|
96
109
|
errorSchema,
|
|
97
|
-
commandSchema$
|
|
110
|
+
commandSchema$2 as commandSchema,
|
|
98
111
|
moduleUrl,
|
|
99
112
|
commandAuthorization,
|
|
100
113
|
}
|
|
@@ -20,7 +20,17 @@ type state =
|
|
|
20
20
|
location: option<Reventless.GeoPoint.t>,
|
|
21
21
|
locationResolvedFrom: option<string>,
|
|
22
22
|
})
|
|
23
|
-
|
|
23
|
+
// Carries the profile it was holding when it was withdrawn. Deactivation is
|
|
24
|
+
// not deletion — the orders still name this customer — so the state that comes
|
|
25
|
+
// back on reactivation has to be the state that went in. A payloadless
|
|
26
|
+
// `Deactivated` would make `Reactivate` either impossible or a second
|
|
27
|
+
// registration, and a second registration is a different fact.
|
|
28
|
+
| Deactivated({
|
|
29
|
+
email: string,
|
|
30
|
+
address: string,
|
|
31
|
+
location: option<Reventless.GeoPoint.t>,
|
|
32
|
+
locationResolvedFrom: option<string>,
|
|
33
|
+
})
|
|
24
34
|
|
|
25
35
|
let initialState = NotCreated
|
|
26
36
|
|
|
@@ -44,8 +54,12 @@ let evolve = (state, event) =>
|
|
|
44
54
|
// handing it back for another round.
|
|
45
55
|
| (Active(s), AddressUnresolvable({address})) =>
|
|
46
56
|
Active({...s, location: None, locationResolvedFrom: Some(address)})
|
|
47
|
-
| (Active(
|
|
48
|
-
|
|
57
|
+
| (Active({email, address, location, locationResolvedFrom}), Customer.Deactivated) =>
|
|
58
|
+
Deactivated({email, address, location, locationResolvedFrom})
|
|
59
|
+
| (Deactivated({email, address, location, locationResolvedFrom}), Reactivated) =>
|
|
60
|
+
Active({email, address, location, locationResolvedFrom})
|
|
61
|
+
| (Deactivated(_), _) => state
|
|
62
|
+
| (Active(_), Reactivated) => state
|
|
49
63
|
| (NotCreated, _) => state
|
|
50
64
|
}
|
|
51
65
|
|
|
@@ -58,6 +72,7 @@ let decide = (state, command) =>
|
|
|
58
72
|
| (NotCreated, SetLocation(_)) => Error(CustomerNotFound)
|
|
59
73
|
| (NotCreated, MarkAddressUnresolvable(_)) => Error(CustomerNotFound)
|
|
60
74
|
| (NotCreated, Deactivate) => Error(CustomerNotFound)
|
|
75
|
+
| (NotCreated, Reactivate) => Error(CustomerNotFound)
|
|
61
76
|
|
|
62
77
|
| (Active(_), Register(_)) => Error(CustomerAlreadyRegistered)
|
|
63
78
|
| (Active(s), UpdateEmail({email})) if email == s.email => Ok([])
|
|
@@ -94,15 +109,19 @@ let decide = (state, command) =>
|
|
|
94
109
|
Ok([AddressUnresolvable({address, reason})])
|
|
95
110
|
|
|
96
111
|
| (Active(_), Deactivate) => Ok([Customer.Deactivated])
|
|
112
|
+
// Already where the caller is asking it to be.
|
|
113
|
+
| (Active(_), Reactivate) => Ok([])
|
|
97
114
|
|
|
98
|
-
| (Deactivated, Register(_)) => Error(CustomerAlreadyDeactivated)
|
|
99
|
-
| (Deactivated, UpdateEmail(_)) => Error(CustomerAlreadyDeactivated)
|
|
100
|
-
| (Deactivated, UpdateAddress(_)) => Error(CustomerAlreadyDeactivated)
|
|
101
|
-
| (Deactivated, SetAddressLocation(_)) => Error(CustomerAlreadyDeactivated)
|
|
115
|
+
| (Deactivated(_), Register(_)) => Error(CustomerAlreadyDeactivated)
|
|
116
|
+
| (Deactivated(_), UpdateEmail(_)) => Error(CustomerAlreadyDeactivated)
|
|
117
|
+
| (Deactivated(_), UpdateAddress(_)) => Error(CustomerAlreadyDeactivated)
|
|
118
|
+
| (Deactivated(_), SetAddressLocation(_)) => Error(CustomerAlreadyDeactivated)
|
|
102
119
|
// A deactivated customer has no location work owing — swallow rather than
|
|
103
120
|
// error, so an in-flight geocode landing after deactivation does not park a
|
|
104
121
|
// TODO row in Failed forever.
|
|
105
|
-
| (Deactivated, SetLocation(_)) => Ok([])
|
|
106
|
-
| (Deactivated, MarkAddressUnresolvable(_)) => Ok([])
|
|
107
|
-
| (Deactivated, Deactivate) => Ok([]) // idempotent
|
|
122
|
+
| (Deactivated(_), SetLocation(_)) => Ok([])
|
|
123
|
+
| (Deactivated(_), MarkAddressUnresolvable(_)) => Ok([])
|
|
124
|
+
| (Deactivated(_), Deactivate) => Ok([]) // idempotent
|
|
125
|
+
// The one command this state exists to accept.
|
|
126
|
+
| (Deactivated(_), Reactivate) => Ok([Reactivated])
|
|
108
127
|
}
|