@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.
Files changed (40) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/package.json +10 -10
  3. package/src/CatalogProduct/StateChangeSlice/SyncCatalogProduct.res +13 -0
  4. package/src/CatalogProduct/StateChangeSlice/SyncCatalogProduct.res.mjs +17 -2
  5. package/src/CatalogProduct/StateChangeSlice/SyncCatalogProduct_Behavior.res +23 -3
  6. package/src/CatalogProduct/StateChangeSlice/SyncCatalogProduct_Behavior.res.mjs +46 -15
  7. package/src/Customer/Aggregate/Customer.res +4 -3
  8. package/src/Customer/Aggregate/Customer.res.mjs +4 -0
  9. package/src/Extension/Products_Extension.res +5 -0
  10. package/src/Extension/Products_Extension.res.mjs +13 -0
  11. package/src/ExtensionPoint/Orders_ExtensionPointMapping.res.mjs +1 -1
  12. package/src/LifecycleModel.res +20 -0
  13. package/src/LifecycleModel.res.mjs +80 -0
  14. package/src/Notification/AutomationSlice/NotificationIntake.res.mjs +1 -1
  15. package/src/Notification/StateChangeSlice/NotificationPreferences.res.mjs +1 -1
  16. package/src/Notification/StateChangeSlice/NotificationSourceClaims.res.mjs +1 -1
  17. package/src/Order/AutomationSlice/AutoShipOrder.res.mjs +1 -1
  18. package/src/Order/StateChangeSlice/PlaceOrder.res +75 -3
  19. package/src/Order/StateChangeSlice/PlaceOrder.res.mjs +50 -5
  20. package/src/Order/StateChangeSlice/PlaceOrder_Behavior.res +140 -10
  21. package/src/Order/StateChangeSlice/PlaceOrder_Behavior.res.mjs +224 -19
  22. package/src/Order/StateViewSliceStream/Orders.res +57 -2
  23. package/src/Order/StateViewSliceStream/Orders.res.mjs +61 -4
  24. package/src/Order/StateViewSliceStream/Orders_Projection.res +21 -1
  25. package/src/Order/StateViewSliceStream/Orders_Projection.res.mjs +24 -31
  26. package/src/Plugin.res +1 -0
  27. package/src/Plugin.res.mjs +2 -1
  28. package/src/capabilities.json +21 -0
  29. package/tests/Customer/Aggregate/Customer_GWT.res +22 -0
  30. package/tests/Customer/Aggregate/Customer_GWT.res.mjs +14 -0
  31. package/tests/Customer/ReadModelStream/Customers_GWT.res +19 -0
  32. package/tests/Customer/ReadModelStream/Customers_GWT.res.mjs +18 -0
  33. package/tests/Flow/OrderingFlow_GWT.res +31 -2
  34. package/tests/Flow/OrderingFlow_GWT.res.mjs +30 -4
  35. package/tests/Notification/NotificationConformance_GWT.res +25 -24
  36. package/tests/Notification/NotificationConformance_GWT.res.mjs +27 -28
  37. package/tests/Order/StateChangeSlice/PlaceOrder_GWT.res +467 -29
  38. package/tests/Order/StateChangeSlice/PlaceOrder_GWT.res.mjs +652 -192
  39. package/tests/Order/StateViewSliceStream/Orders_GWT.res +75 -0
  40. 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.232",
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.125",
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/online-shop-hybrid-ordering-spec": "1.0.0-alpha.125",
27
- "@reventlessdev/reventless-gwt": "1.0.0-alpha.203",
28
- "@reventlessdev/reventless-infra": "3.0.0-alpha.157",
29
- "@reventlessdev/reventless-local": "3.0.0-alpha.245",
30
- "@reventlessdev/reventless-spec": "3.0.0-alpha.129",
31
- "@reventlessdev/trait-notification": "1.0.0-alpha.5",
32
- "@reventlessdev/trait-address-geocoding": "1.0.0-alpha.5"
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.77"
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 "Unrestricted";
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 = {name: string, price: option<Reventless.Money.t>, withdrawn: bool}
10
- let initialState = {name: "", price: None, withdrawn: false}
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
- | CatalogProductSynced({name, price}) => {name, price: Some(price), withdrawn: false}
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
- } else if (event.TAG === "CatalogProductSynced") {
21
- return {
22
- name: event.name,
23
- price: event.price,
24
- withdrawn: false
25
- };
26
- } else {
27
- return {
28
- name: state.name,
29
- price: event.price,
30
- withdrawn: state.withdrawn
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 — and it names no target either, because the row's status is the
97
- // view's to derive.
98
- | Register(_) => Unrestricted
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
@@ -55,6 +55,10 @@ function commandTransition(command) {
55
55
  }
56
56
  switch (command.TAG) {
57
57
  case "Register" :
58
+ return {
59
+ TAG: "Creates",
60
+ _0: "Active"
61
+ };
58
62
  case "SetLocation" :
59
63
  case "MarkAddressUnresolvable" :
60
64
  return "Unrestricted";
@@ -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
 
@@ -26,7 +26,7 @@ function commandAuthorization(param) {
26
26
  }
27
27
 
28
28
  function commandTransition(param) {
29
- return "Unrestricted";
29
+ return "Undeclared";
30
30
  }
31
31
 
32
32
  let traits = [];
@@ -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 */
@@ -28,7 +28,7 @@ function commandAuthorization(param) {
28
28
  }
29
29
 
30
30
  function commandTransition(param) {
31
- return "Unrestricted";
31
+ return "Undeclared";
32
32
  }
33
33
 
34
34
  let traits = [];
@@ -185,7 +185,7 @@ function commandAuthorization(param) {
185
185
  }
186
186
 
187
187
  function commandTransition(param) {
188
- return "Unrestricted";
188
+ return "Undeclared";
189
189
  }
190
190
 
191
191
  let name = "NotificationPreferences";
@@ -55,7 +55,7 @@ function commandAuthorization(param) {
55
55
  }
56
56
 
57
57
  function commandTransition(param) {
58
- return "Unrestricted";
58
+ return "Undeclared";
59
59
  }
60
60
 
61
61
  let name = "NotificationSourceClaims";
@@ -17,7 +17,7 @@ function commandAuthorization(param) {
17
17
  }
18
18
 
19
19
  function commandTransition(param) {
20
- return "Unrestricted";
20
+ return "Undeclared";
21
21
  }
22
22
 
23
23
  let traits = [];
@@ -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
- | CatalogProductSynced({productId: string})
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
- @ref("AvailableProducts") productIds: array<string>,
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
  })