@reventlessdev/online-shop-hybrid-ordering 1.0.0-alpha.232 → 1.0.0-alpha.233
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 +24 -0
- package/package.json +10 -10
- package/src/CatalogProduct/StateChangeSlice/SyncCatalogProduct.res +13 -0
- package/src/CatalogProduct/StateChangeSlice/SyncCatalogProduct.res.mjs +17 -2
- package/src/CatalogProduct/StateChangeSlice/SyncCatalogProduct_Behavior.res +23 -3
- package/src/CatalogProduct/StateChangeSlice/SyncCatalogProduct_Behavior.res.mjs +46 -15
- package/src/Customer/Aggregate/Customer.res +4 -3
- package/src/Customer/Aggregate/Customer.res.mjs +4 -0
- package/src/Extension/Products_Extension.res +5 -0
- package/src/Extension/Products_Extension.res.mjs +13 -0
- package/src/ExtensionPoint/Orders_ExtensionPointMapping.res.mjs +1 -1
- package/src/LifecycleModel.res +20 -0
- package/src/LifecycleModel.res.mjs +80 -0
- package/src/Notification/AutomationSlice/NotificationIntake.res.mjs +1 -1
- package/src/Notification/StateChangeSlice/NotificationPreferences.res.mjs +1 -1
- package/src/Notification/StateChangeSlice/NotificationSourceClaims.res.mjs +1 -1
- package/src/Order/AutomationSlice/AutoShipOrder.res.mjs +1 -1
- package/src/Order/StateChangeSlice/PlaceOrder.res +75 -3
- package/src/Order/StateChangeSlice/PlaceOrder.res.mjs +50 -5
- package/src/Order/StateChangeSlice/PlaceOrder_Behavior.res +140 -10
- package/src/Order/StateChangeSlice/PlaceOrder_Behavior.res.mjs +224 -19
- package/src/Order/StateViewSliceStream/Orders.res +57 -2
- package/src/Order/StateViewSliceStream/Orders.res.mjs +61 -4
- package/src/Order/StateViewSliceStream/Orders_Projection.res +21 -1
- package/src/Order/StateViewSliceStream/Orders_Projection.res.mjs +24 -31
- package/src/Plugin.res +1 -0
- package/src/Plugin.res.mjs +2 -1
- package/src/capabilities.json +21 -0
- package/tests/Customer/Aggregate/Customer_GWT.res +22 -0
- package/tests/Customer/Aggregate/Customer_GWT.res.mjs +14 -0
- package/tests/Customer/ReadModelStream/Customers_GWT.res +19 -0
- package/tests/Customer/ReadModelStream/Customers_GWT.res.mjs +18 -0
- package/tests/Flow/OrderingFlow_GWT.res +31 -2
- package/tests/Flow/OrderingFlow_GWT.res.mjs +30 -4
- package/tests/Notification/NotificationConformance_GWT.res +25 -24
- package/tests/Notification/NotificationConformance_GWT.res.mjs +27 -28
- package/tests/Order/StateChangeSlice/PlaceOrder_GWT.res +467 -29
- package/tests/Order/StateChangeSlice/PlaceOrder_GWT.res.mjs +652 -192
- package/tests/Order/StateViewSliceStream/Orders_GWT.res +75 -0
- package/tests/Order/StateViewSliceStream/Orders_GWT.res.mjs +104 -12
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,30 @@
|
|
|
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.233 (2026-09-07)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **example:** ordering names catalog's image store instead of minting its own ([c155060](https://github.com/ReventlessDev/reventless-core/commit/c1550609ed861367107327d1edf84f40018f4773))
|
|
11
|
+
* feat(dcb)!: a record inside an array is still a reference and a key ([bcb9b13](https://github.com/ReventlessDev/reventless-core/commit/bcb9b13958323d8319d7fcf25c055800e84150f3))
|
|
12
|
+
* refactor(traits)!: consumed facts are grouped, not suffixed ([a844b7a](https://github.com/ReventlessDev/reventless-core/commit/a844b7a99349ca20633d68cf32b95cb7677d02a4))
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* **catalog:** the set says which member now stands, and orders freeze it ([79d882d](https://github.com/ReventlessDev/reventless-core/commit/79d882d094c11b35a1cb42958a89fc63561e43ab))
|
|
16
|
+
* **core:** a command's lifecycle edge comes from its scenarios, not its claim ([1879d05](https://github.com/ReventlessDev/reventless-core/commit/1879d0570a981e7fae5bcd3808f8a36e898bd0fd))
|
|
17
|
+
|
|
18
|
+
### BREAKING CHANGES
|
|
19
|
+
|
|
20
|
+
* an `int` field is now `Int` in the generated SDL and
|
|
21
|
+
`"integer"` in JSON Schema, where it was `Float`/`"number"`. A client
|
|
22
|
+
declaring `$retryCount: Float!` against one of these fields must move to
|
|
23
|
+
`Int!`.
|
|
24
|
+
* a graft's `Binding` must expose `Consumed` instead of the
|
|
25
|
+
`*C` bindings. The scaffolds emit the new shape, so a regenerated graft is
|
|
26
|
+
already correct; a hand-written one moves its consumed builders into a module.
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
6
30
|
# 1.0.0-alpha.232 (2026-09-04)
|
|
7
31
|
|
|
8
32
|
**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.233",
|
|
4
4
|
"description": "Online Shop Hybrid example — Ordering plugin (Customer aggregate + Order/CatalogProduct DCB)",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"jest": {
|
|
@@ -21,20 +21,20 @@
|
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"sury": "11.0.0-rc.2",
|
|
24
|
-
"@reventlessdev/online-shop-hybrid-catalog-spec": "1.0.0-alpha.
|
|
24
|
+
"@reventlessdev/online-shop-hybrid-catalog-spec": "1.0.0-alpha.126",
|
|
25
25
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
|
|
26
|
-
"@reventlessdev/
|
|
27
|
-
"@reventlessdev/reventless-gwt": "1.0.0-alpha.
|
|
28
|
-
"@reventlessdev/
|
|
29
|
-
"@reventlessdev/reventless-local": "3.0.0-alpha.
|
|
30
|
-
"@reventlessdev/reventless-spec": "3.0.0-alpha.
|
|
31
|
-
"@reventlessdev/trait-
|
|
32
|
-
"@reventlessdev/trait-
|
|
26
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.158",
|
|
27
|
+
"@reventlessdev/reventless-gwt": "1.0.0-alpha.204",
|
|
28
|
+
"@reventlessdev/online-shop-hybrid-ordering-spec": "1.0.0-alpha.126",
|
|
29
|
+
"@reventlessdev/reventless-local": "3.0.0-alpha.246",
|
|
30
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.130",
|
|
31
|
+
"@reventlessdev/trait-address-geocoding": "1.0.0-alpha.6",
|
|
32
|
+
"@reventlessdev/trait-notification": "1.0.0-alpha.6"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"rescript": "12.3.0",
|
|
36
36
|
"sury-ppx": "11.0.0-rc.2",
|
|
37
|
-
"@reventlessdev/reventless-ppx": "1.0.0-alpha.
|
|
37
|
+
"@reventlessdev/reventless-ppx": "1.0.0-alpha.78"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
40
|
"rescript": "12.3.0"
|
|
@@ -9,6 +9,7 @@ type consumedEvent =
|
|
|
9
9
|
| CatalogProductPriceChanged({price: Reventless.Money.t})
|
|
10
10
|
| CatalogProductWithdrawn
|
|
11
11
|
| CatalogProductRelisted
|
|
12
|
+
| CatalogProductImageChanged({productImage?: Reventless.UploadableImage.t})
|
|
12
13
|
|
|
13
14
|
@schema
|
|
14
15
|
type command =
|
|
@@ -20,6 +21,14 @@ type command =
|
|
|
20
21
|
// Ordering owns rather than asking Catalog to re-send facts it already has.
|
|
21
22
|
| WithdrawSyncedProduct({productId: string})
|
|
22
23
|
| RelistSyncedProduct({productId: string})
|
|
24
|
+
// The picture Catalog says to show. Carried as the ref Catalog's store minted,
|
|
25
|
+
// so Ordering stores a path it can hand to a reader without knowing where the
|
|
26
|
+
// bytes live or being able to write there — which is why the store is named
|
|
27
|
+
// rather than derived: the field's own name would declare an Ordering store.
|
|
28
|
+
| ChangeSyncedProductImage({
|
|
29
|
+
productId: string,
|
|
30
|
+
@storageRef("Catalog.productImages") productImage?: Reventless.UploadableImage.t,
|
|
31
|
+
})
|
|
23
32
|
|
|
24
33
|
@schema
|
|
25
34
|
type error = unit // always succeeds — sync is idempotent
|
|
@@ -45,3 +54,7 @@ type event =
|
|
|
45
54
|
name: string,
|
|
46
55
|
price: Reventless.Money.t,
|
|
47
56
|
})
|
|
57
|
+
| CatalogProductImageChanged({
|
|
58
|
+
productId: string,
|
|
59
|
+
@storageRef("Catalog.productImages") productImage?: Reventless.UploadableImage.t,
|
|
60
|
+
})
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import * as Sury from "sury";
|
|
4
4
|
import * as Money$Reventless from "@reventlessdev/reventless-spec/src/semantic/Money.res.mjs";
|
|
5
5
|
import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
|
|
6
|
+
import * as UploadableImage$Reventless from "@reventlessdev/reventless-spec/src/semantic/UploadableImage.res.mjs";
|
|
6
7
|
|
|
7
8
|
let consumedEventSchema = Sury.union([
|
|
8
9
|
Sury.$schema(s => ({
|
|
@@ -15,7 +16,11 @@ let consumedEventSchema = Sury.union([
|
|
|
15
16
|
price: s.m(Money$Reventless.schema)
|
|
16
17
|
})),
|
|
17
18
|
Sury.literal("CatalogProductWithdrawn"),
|
|
18
|
-
Sury.literal("CatalogProductRelisted")
|
|
19
|
+
Sury.literal("CatalogProductRelisted"),
|
|
20
|
+
Sury.$schema(s => ({
|
|
21
|
+
TAG: "CatalogProductImageChanged",
|
|
22
|
+
productImage: s.m(Sury.$option(UploadableImage$Reventless.forField(undefined, "productImages")))
|
|
23
|
+
}))
|
|
19
24
|
]);
|
|
20
25
|
|
|
21
26
|
let commandSchema = Sury.union([
|
|
@@ -37,6 +42,11 @@ let commandSchema = Sury.union([
|
|
|
37
42
|
Sury.$schema(s => ({
|
|
38
43
|
TAG: "RelistSyncedProduct",
|
|
39
44
|
productId: s.m(DcbTag$Reventless.string)
|
|
45
|
+
})),
|
|
46
|
+
Sury.$schema(s => ({
|
|
47
|
+
TAG: "ChangeSyncedProductImage",
|
|
48
|
+
productId: s.m(DcbTag$Reventless.string),
|
|
49
|
+
productImage: s.m(Sury.$option(UploadableImage$Reventless.forField("Catalog", "productImages")))
|
|
40
50
|
}))
|
|
41
51
|
]);
|
|
42
52
|
|
|
@@ -63,6 +73,11 @@ let eventSchema = Sury.union([
|
|
|
63
73
|
productId: s.m(DcbTag$Reventless.string),
|
|
64
74
|
name: s.m(Sury.string),
|
|
65
75
|
price: s.m(Money$Reventless.schema)
|
|
76
|
+
})),
|
|
77
|
+
Sury.$schema(s => ({
|
|
78
|
+
TAG: "CatalogProductImageChanged",
|
|
79
|
+
productId: s.m(DcbTag$Reventless.string),
|
|
80
|
+
productImage: s.m(Sury.$option(UploadableImage$Reventless.forField("Catalog", "productImages")))
|
|
66
81
|
}))
|
|
67
82
|
]);
|
|
68
83
|
|
|
@@ -71,7 +86,7 @@ function commandAuthorization(param) {
|
|
|
71
86
|
}
|
|
72
87
|
|
|
73
88
|
function commandTransition(param) {
|
|
74
|
-
return "
|
|
89
|
+
return "Undeclared";
|
|
75
90
|
}
|
|
76
91
|
|
|
77
92
|
let traits = [];
|
|
@@ -6,15 +6,25 @@
|
|
|
6
6
|
// `withdrawn` sits beside the shadow rather than replacing it: a withdrawn
|
|
7
7
|
// product's name and price are exactly what a relist needs, and dropping them
|
|
8
8
|
// here would force Catalog to re-send facts Ordering already holds.
|
|
9
|
-
type state = {
|
|
10
|
-
|
|
9
|
+
type state = {
|
|
10
|
+
name: string,
|
|
11
|
+
price: option<Reventless.Money.t>,
|
|
12
|
+
withdrawn: bool,
|
|
13
|
+
// The picture last announced, kept for the same reason `name` is: so a repeat
|
|
14
|
+
// of the same announcement can be recognised and dropped.
|
|
15
|
+
productImage: option<Reventless.UploadableImage.t>,
|
|
16
|
+
}
|
|
17
|
+
let initialState = {name: "", price: None, withdrawn: false, productImage: None}
|
|
11
18
|
|
|
12
19
|
let evolve = (state, event) =>
|
|
13
20
|
switch event {
|
|
14
|
-
|
|
21
|
+
// Spread rather than rebuilt: the picture is announced on its own event, so a
|
|
22
|
+
// re-sync of name and price must not quietly drop it.
|
|
23
|
+
| CatalogProductSynced({name, price}) => {...state, name, price: Some(price), withdrawn: false}
|
|
15
24
|
| CatalogProductPriceChanged({price}) => {...state, price: Some(price)}
|
|
16
25
|
| CatalogProductWithdrawn => {...state, withdrawn: true}
|
|
17
26
|
| CatalogProductRelisted => {...state, withdrawn: false}
|
|
27
|
+
| CatalogProductImageChanged({productImage: ?productImage}) => {...state, productImage}
|
|
18
28
|
}
|
|
19
29
|
|
|
20
30
|
let decide = (state, command) =>
|
|
@@ -48,4 +58,14 @@ let decide = (state, command) =>
|
|
|
48
58
|
| (true, Some(price)) =>
|
|
49
59
|
Ok([CatalogProductRelisted({productId: theId, name: state.name, price})])
|
|
50
60
|
}
|
|
61
|
+
// Idempotent like the rest: Catalog re-announcing the same picture is the
|
|
62
|
+
// ordinary shape of at-least-once delivery, not a change.
|
|
63
|
+
// Compared as options, so "the picture is gone" is recognised as a change and
|
|
64
|
+
// re-announcing the same absence is not.
|
|
65
|
+
| ChangeSyncedProductImage({productId, productImage: ?productImage}) =>
|
|
66
|
+
if state.productImage == productImage {
|
|
67
|
+
Ok([])
|
|
68
|
+
} else {
|
|
69
|
+
Ok([CatalogProductImageChanged({productId, productImage: ?productImage})])
|
|
70
|
+
}
|
|
51
71
|
}
|
|
@@ -8,27 +8,40 @@ function evolve(state, event) {
|
|
|
8
8
|
return {
|
|
9
9
|
name: state.name,
|
|
10
10
|
price: state.price,
|
|
11
|
-
withdrawn: true
|
|
11
|
+
withdrawn: true,
|
|
12
|
+
productImage: state.productImage
|
|
12
13
|
};
|
|
13
14
|
} else {
|
|
14
15
|
return {
|
|
15
16
|
name: state.name,
|
|
16
17
|
price: state.price,
|
|
17
|
-
withdrawn: false
|
|
18
|
+
withdrawn: false,
|
|
19
|
+
productImage: state.productImage
|
|
18
20
|
};
|
|
19
21
|
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
22
|
+
}
|
|
23
|
+
switch (event.TAG) {
|
|
24
|
+
case "CatalogProductSynced" :
|
|
25
|
+
return {
|
|
26
|
+
name: event.name,
|
|
27
|
+
price: event.price,
|
|
28
|
+
withdrawn: false,
|
|
29
|
+
productImage: state.productImage
|
|
30
|
+
};
|
|
31
|
+
case "CatalogProductPriceChanged" :
|
|
32
|
+
return {
|
|
33
|
+
name: state.name,
|
|
34
|
+
price: event.price,
|
|
35
|
+
withdrawn: state.withdrawn,
|
|
36
|
+
productImage: state.productImage
|
|
37
|
+
};
|
|
38
|
+
case "CatalogProductImageChanged" :
|
|
39
|
+
return {
|
|
40
|
+
name: state.name,
|
|
41
|
+
price: state.price,
|
|
42
|
+
withdrawn: state.withdrawn,
|
|
43
|
+
productImage: event.productImage
|
|
44
|
+
};
|
|
32
45
|
}
|
|
33
46
|
}
|
|
34
47
|
|
|
@@ -111,6 +124,23 @@ function decide(state, command) {
|
|
|
111
124
|
_0: []
|
|
112
125
|
};
|
|
113
126
|
}
|
|
127
|
+
case "ChangeSyncedProductImage" :
|
|
128
|
+
let productImage = command.productImage;
|
|
129
|
+
if (Primitive_object.equal(state.productImage, productImage)) {
|
|
130
|
+
return {
|
|
131
|
+
TAG: "Ok",
|
|
132
|
+
_0: []
|
|
133
|
+
};
|
|
134
|
+
} else {
|
|
135
|
+
return {
|
|
136
|
+
TAG: "Ok",
|
|
137
|
+
_0: [{
|
|
138
|
+
TAG: "CatalogProductImageChanged",
|
|
139
|
+
productId: command.productId,
|
|
140
|
+
productImage: productImage
|
|
141
|
+
}]
|
|
142
|
+
};
|
|
143
|
+
}
|
|
114
144
|
}
|
|
115
145
|
}
|
|
116
146
|
|
|
@@ -119,7 +149,8 @@ let Spec;
|
|
|
119
149
|
let initialState = {
|
|
120
150
|
name: "",
|
|
121
151
|
price: undefined,
|
|
122
|
-
withdrawn: false
|
|
152
|
+
withdrawn: false,
|
|
153
|
+
productImage: undefined
|
|
123
154
|
};
|
|
124
155
|
|
|
125
156
|
let moduleUrl = "@reventlessdev/online-shop-hybrid-ordering/src/CatalogProduct/StateChangeSlice/SyncCatalogProduct_Behavior.res.mjs";
|
|
@@ -93,9 +93,10 @@ let commandTransition = (command: command): Reventless.Transition.t<lifecycleSta
|
|
|
93
93
|
open Reventless.Transition
|
|
94
94
|
switch command {
|
|
95
95
|
// Registration brings the row into existence, so there is no state it could
|
|
96
|
-
// come from
|
|
97
|
-
//
|
|
98
|
-
|
|
96
|
+
// come from. `Creates` rather than `Unrestricted`, which would claim it is
|
|
97
|
+
// legal on an already-registered customer — and the scenarios show it inert
|
|
98
|
+
// there. The target is the state the view puts a fresh row in.
|
|
99
|
+
| Register(_) => Creates(Customers.Active)
|
|
99
100
|
// A from-set and no target: legal on an active customer, and it does not move
|
|
100
101
|
// them. Each is `Error(CustomerAlreadyDeactivated)` on a deactivated one, so
|
|
101
102
|
// this says exactly what `decide` already enforces — it only stops a menu
|
|
@@ -25,6 +25,11 @@ module Mapping = {
|
|
|
25
25
|
| ProductRelisted({productId: theId}) => [
|
|
26
26
|
PublishStateChangeSliceCommand(RelistSyncedProduct({productId: theId})),
|
|
27
27
|
]
|
|
28
|
+
| ProductImageChanged({productId, productImage: ?productImage}) => [
|
|
29
|
+
PublishStateChangeSliceCommand(
|
|
30
|
+
ChangeSyncedProductImage({productId, productImage: ?productImage}),
|
|
31
|
+
),
|
|
32
|
+
]
|
|
28
33
|
}
|
|
29
34
|
|
|
30
35
|
let mapOutgoingEvent = None
|
|
@@ -39,6 +39,15 @@ function mapIncomingEvent(_id, event, _meta, _pluginDef, _queryEngine) {
|
|
|
39
39
|
productId: event.productId
|
|
40
40
|
}
|
|
41
41
|
}];
|
|
42
|
+
case "ProductImageChanged" :
|
|
43
|
+
return [{
|
|
44
|
+
TAG: "PublishStateChangeSliceCommand",
|
|
45
|
+
_0: {
|
|
46
|
+
TAG: "ChangeSyncedProductImage",
|
|
47
|
+
productId: event.productId,
|
|
48
|
+
productImage: event.productImage
|
|
49
|
+
}
|
|
50
|
+
}];
|
|
42
51
|
}
|
|
43
52
|
}
|
|
44
53
|
|
|
@@ -58,6 +67,10 @@ let handledEvents = [
|
|
|
58
67
|
{
|
|
59
68
|
name: "ProductRelisted",
|
|
60
69
|
toCommandTypes: ["RelistSyncedProduct"]
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: "ProductImageChanged",
|
|
73
|
+
toCommandTypes: ["ChangeSyncedProductImage"]
|
|
61
74
|
}
|
|
62
75
|
];
|
|
63
76
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// AUTO-GENERATED — do not edit. Run `pnpm run check:lifecycle:update` to update.
|
|
2
|
+
//
|
|
3
|
+
// What ordering's own given/when/then scenarios say about each command: the
|
|
4
|
+
// states one shows it taking effect from, the states those land in, and whether
|
|
5
|
+
// it brings a row into existence. `Plugin_Structure` prefers this to the
|
|
6
|
+
// `@transition` annotation where it says anything, and falls back to the
|
|
7
|
+
// annotation where it is silent.
|
|
8
|
+
|
|
9
|
+
let model: array<Reventless.Plugin.derivedEdge> = [
|
|
10
|
+
{component: "CancelOrder", command: "CancelOrder", level: Reventless.Plugin.Instance, allowedStates: ["Placed"], targets: ["Cancelled"]},
|
|
11
|
+
{component: "CancelOrder", command: "ReopenOrder", level: Reventless.Plugin.Instance, allowedStates: ["Cancelled"], targets: ["Placed"]},
|
|
12
|
+
{component: "Customer", command: "Deactivate", level: Reventless.Plugin.Instance, allowedStates: ["Active"], targets: ["Deactivated"]},
|
|
13
|
+
{component: "Customer", command: "Reactivate", level: Reventless.Plugin.Instance, allowedStates: ["Deactivated"], targets: ["Active"]},
|
|
14
|
+
{component: "Customer", command: "Register", level: Reventless.Plugin.Collection, allowedStates: [], targets: ["Active"]},
|
|
15
|
+
{component: "Customer", command: "SetAddressLocation", level: Reventless.Plugin.Instance, allowedStates: ["Active"], targets: []},
|
|
16
|
+
{component: "Customer", command: "UpdateAddress", level: Reventless.Plugin.Instance, allowedStates: ["Active"], targets: []},
|
|
17
|
+
{component: "Customer", command: "UpdateEmail", level: Reventless.Plugin.Instance, allowedStates: ["Active"], targets: []},
|
|
18
|
+
{component: "PlaceOrder", command: "PlaceOrder", level: Reventless.Plugin.Collection, allowedStates: [], targets: ["Placed"]},
|
|
19
|
+
{component: "ShipOrder", command: "ShipOrder", level: Reventless.Plugin.Instance, allowedStates: ["Placed"], targets: ["Shipped"]},
|
|
20
|
+
]
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
let model = [
|
|
5
|
+
{
|
|
6
|
+
component: "CancelOrder",
|
|
7
|
+
command: "CancelOrder",
|
|
8
|
+
level: "Instance",
|
|
9
|
+
allowedStates: ["Placed"],
|
|
10
|
+
targets: ["Cancelled"]
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
component: "CancelOrder",
|
|
14
|
+
command: "ReopenOrder",
|
|
15
|
+
level: "Instance",
|
|
16
|
+
allowedStates: ["Cancelled"],
|
|
17
|
+
targets: ["Placed"]
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
component: "Customer",
|
|
21
|
+
command: "Deactivate",
|
|
22
|
+
level: "Instance",
|
|
23
|
+
allowedStates: ["Active"],
|
|
24
|
+
targets: ["Deactivated"]
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
component: "Customer",
|
|
28
|
+
command: "Reactivate",
|
|
29
|
+
level: "Instance",
|
|
30
|
+
allowedStates: ["Deactivated"],
|
|
31
|
+
targets: ["Active"]
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
component: "Customer",
|
|
35
|
+
command: "Register",
|
|
36
|
+
level: "Collection",
|
|
37
|
+
allowedStates: [],
|
|
38
|
+
targets: ["Active"]
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
component: "Customer",
|
|
42
|
+
command: "SetAddressLocation",
|
|
43
|
+
level: "Instance",
|
|
44
|
+
allowedStates: ["Active"],
|
|
45
|
+
targets: []
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
component: "Customer",
|
|
49
|
+
command: "UpdateAddress",
|
|
50
|
+
level: "Instance",
|
|
51
|
+
allowedStates: ["Active"],
|
|
52
|
+
targets: []
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
component: "Customer",
|
|
56
|
+
command: "UpdateEmail",
|
|
57
|
+
level: "Instance",
|
|
58
|
+
allowedStates: ["Active"],
|
|
59
|
+
targets: []
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
component: "PlaceOrder",
|
|
63
|
+
command: "PlaceOrder",
|
|
64
|
+
level: "Collection",
|
|
65
|
+
allowedStates: [],
|
|
66
|
+
targets: ["Placed"]
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
component: "ShipOrder",
|
|
70
|
+
command: "ShipOrder",
|
|
71
|
+
level: "Instance",
|
|
72
|
+
allowedStates: ["Placed"],
|
|
73
|
+
targets: ["Shipped"]
|
|
74
|
+
}
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
export {
|
|
78
|
+
model,
|
|
79
|
+
}
|
|
80
|
+
/* No side effect */
|
|
@@ -9,12 +9,25 @@
|
|
|
9
9
|
// pulled from the catalog would stay orderable forever — the read side deletes
|
|
10
10
|
// the row while the write side keeps saying yes. Both the withdrawal and the way
|
|
11
11
|
// back are consumed, so availability follows the shelf in both directions.
|
|
12
|
+
//
|
|
13
|
+
// The price is read for the same reason the name is, and it is why pricing an
|
|
14
|
+
// order costs nothing here: Ordering already shadows what a placement needs to
|
|
15
|
+
// decide, so the total is computed from this fold rather than from a cross-plugin
|
|
16
|
+
// read or a read-model query issued out of a behaviour.
|
|
12
17
|
@schema
|
|
13
18
|
type consumedEvent =
|
|
14
19
|
| OrderPlaced({orderId: string})
|
|
15
|
-
|
|
20
|
+
// `name` is read as well as `productId`, so the order can record what the
|
|
21
|
+
// product was called at the moment it was placed. The catalog publishes it on
|
|
22
|
+
// both arms already; this slice simply stopped ignoring it.
|
|
23
|
+
| CatalogProductSynced({productId: string, name: string, price: Reventless.Money.t})
|
|
24
|
+
| CatalogProductPriceChanged({productId: string, price: Reventless.Money.t})
|
|
16
25
|
| CatalogProductWithdrawn({productId: string})
|
|
17
|
-
| CatalogProductRelisted({productId: string})
|
|
26
|
+
| CatalogProductRelisted({productId: string, name: string, price: Reventless.Money.t})
|
|
27
|
+
// The picture the shelf currently shows, folded for the same reason the name
|
|
28
|
+
// is: so a placement can copy it onto the order rather than the order having
|
|
29
|
+
// to ask the catalog later.
|
|
30
|
+
| CatalogProductImageChanged({productId: string, productImage?: Reventless.UploadableImage.t})
|
|
18
31
|
|
|
19
32
|
// Declared in the order the UI should present them: the batched default first,
|
|
20
33
|
// then the expedited option, then in-store collection.
|
|
@@ -24,6 +37,19 @@ type shippingMethod =
|
|
|
24
37
|
| Express
|
|
25
38
|
| Pickup
|
|
26
39
|
|
|
40
|
+
// One thing ordered, and how many of it. A **named** record rather than an inline
|
|
41
|
+
// one: the ppx walks a record type declaration, which is what puts the `@ref` (and
|
|
42
|
+
// with it the DCB tag) on `productId` where an inline record would carry neither.
|
|
43
|
+
//
|
|
44
|
+
// The framework finds the marker at this depth — the reference walk names it
|
|
45
|
+
// `lineItems[].productId`, and the tag walk gives it the key `productId`, which is
|
|
46
|
+
// the same tag the catalog's own events write.
|
|
47
|
+
@schema
|
|
48
|
+
type lineItem = {
|
|
49
|
+
@ref("AvailableProducts") productId: string,
|
|
50
|
+
quantity: int,
|
|
51
|
+
}
|
|
52
|
+
|
|
27
53
|
@schema
|
|
28
54
|
type command =
|
|
29
55
|
PlaceOrder({
|
|
@@ -34,7 +60,7 @@ type command =
|
|
|
34
60
|
// client sends here is ignored rather than trusted. An operator placing an
|
|
35
61
|
// order on someone's behalf is exempt and keeps the value they sent.
|
|
36
62
|
@noDcbTag @owner customerId: string,
|
|
37
|
-
|
|
63
|
+
lineItems: array<lineItem>,
|
|
38
64
|
shippingMethod: shippingMethod,
|
|
39
65
|
// A requested delivery slot, chosen at checkout. An optional field — a
|
|
40
66
|
// Pickup order (or a caller that names no preference) simply omits it, and
|
|
@@ -48,13 +74,59 @@ type command =
|
|
|
48
74
|
type error =
|
|
49
75
|
| OrderAlreadyPlaced
|
|
50
76
|
| ProductsNotAvailable({missing: array<string>})
|
|
77
|
+
// An order for nothing is the one validation a shopper could trip before line
|
|
78
|
+
// items existed: an empty product list placed an order and produced no lines.
|
|
79
|
+
| OrderIsEmpty
|
|
80
|
+
| InvalidQuantity({productId: string, quantity: int})
|
|
81
|
+
// Refused rather than silently summed. `Money.add` returns a `result` for
|
|
82
|
+
// exactly this case, and unwrapping it here would invent a total in whichever
|
|
83
|
+
// currency happened to come first. Naming the codes says which shelf entries
|
|
84
|
+
// disagree, the way `ProductsNotAvailable` names the products.
|
|
85
|
+
| MixedCurrencies({currencies: array<string>})
|
|
86
|
+
|
|
87
|
+
// One priced line of a placed order, frozen at placement.
|
|
88
|
+
//
|
|
89
|
+
// **The price is the one the decision model held.** A later
|
|
90
|
+
// `CatalogProductPriceChanged` does not rewrite a placed order, which is why the
|
|
91
|
+
// total belongs on the event rather than in the projection.
|
|
92
|
+
@schema
|
|
93
|
+
type orderLine = {
|
|
94
|
+
productId: string,
|
|
95
|
+
name: string,
|
|
96
|
+
quantity: int,
|
|
97
|
+
unitPrice: Reventless.Money.t,
|
|
98
|
+
lineTotal: Reventless.Money.t,
|
|
99
|
+
}
|
|
51
100
|
|
|
52
101
|
@schema
|
|
53
102
|
type event =
|
|
54
103
|
OrderPlaced({
|
|
55
104
|
@partitionTag orderId: string,
|
|
56
105
|
customerId: string,
|
|
106
|
+
// Redundant against `lines`, and deliberately so. The extension point
|
|
107
|
+
// decomposes this into one published `ItemOrdered` per product and
|
|
108
|
+
// `CancelOrder` folds it, so both keep working untouched — and the public
|
|
109
|
+
// contract in `ordering-spec` does not move, which is what lets the shop
|
|
110
|
+
// show quantities without redeploying Catalog in lockstep.
|
|
57
111
|
productIds: array<string>,
|
|
112
|
+
lines: array<orderLine>,
|
|
113
|
+
total: Reventless.Money.t,
|
|
58
114
|
shippingMethod: shippingMethod,
|
|
59
115
|
deliveryWindow?: Reventless.DateRange.t,
|
|
116
|
+
// What the first product was called when this order was placed.
|
|
117
|
+
//
|
|
118
|
+
// **Captured, not looked up.** An order is a record of what somebody bought,
|
|
119
|
+
// and the catalog goes on changing afterwards — a rename, a withdrawal, a
|
|
120
|
+
// reshoot. Reading the name live would rewrite history every time the shop
|
|
121
|
+
// tidied its shelves, and would leave an order for a withdrawn product with
|
|
122
|
+
// nothing to show at all.
|
|
123
|
+
//
|
|
124
|
+
// Optional because every order placed before this field existed carries no
|
|
125
|
+
// key, which is what makes adding it cost the log nothing. A reader treats
|
|
126
|
+
// absent as "not recorded" rather than as a name.
|
|
127
|
+
firstProductName?: string,
|
|
128
|
+
// The picture as it was when the order was placed, frozen for the reason the
|
|
129
|
+
// name is. A reshoot, a withdrawal or a deletion afterwards leaves this
|
|
130
|
+
// order showing what the shopper actually bought.
|
|
131
|
+
@storageRef("Catalog.productImages") firstProductImage?: Reventless.UploadableImage.t,
|
|
60
132
|
})
|