@reventlessdev/online-shop-hybrid-ordering 1.0.0-alpha.262 → 1.0.0-alpha.264

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 CHANGED
@@ -3,6 +3,28 @@
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.264 (2026-10-01)
7
+
8
+ * feat!: roles a plugin declares, groups a platform provides ([43daace](https://github.com/ReventlessDev/reventless-core/commit/43daace6ca588afe22562d079af1e6f1da81942e))
9
+
10
+ ### BREAKING CHANGES
11
+
12
+ * AllowGroups and AdminGroup are removed — write AllowRoles over
13
+ the plugin's Roles.t, and use Role.admin / Platform.adminGroup(). Spec module
14
+ types declare `type role`, and their rules are Authorization.rule<role>; a spec
15
+ that writes its own rule declares `type role` (Reventless.Role.name for a rule
16
+ over Authorization.permission). Role.name is a private string: make one from a
17
+ string with Role.make.
18
+
19
+
20
+
21
+ # 1.0.0-alpha.263 (2026-10-01)
22
+
23
+ ### Features
24
+
25
+ * **spec, core, gwt, ppx:** a command acts only on what its caller owns ([2636f0e](https://github.com/ReventlessDev/reventless-core/commit/2636f0ee44797ee2c0b0c40a7107cf306113173f))
26
+
27
+
6
28
  # 1.0.0-alpha.262 (2026-09-29)
7
29
 
8
30
  **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.262",
3
+ "version": "1.0.0-alpha.264",
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",
24
- "@reventlessdev/online-shop-hybrid-ordering-spec": "1.0.0-alpha.150",
25
- "@reventlessdev/online-shop-hybrid-catalog-spec": "1.0.0-alpha.151",
24
+ "@reventlessdev/online-shop-hybrid-catalog-spec": "1.0.0-alpha.153",
26
25
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.22",
27
- "@reventlessdev/reventless-gwt": "1.0.0-alpha.231",
28
- "@reventlessdev/reventless-infra": "3.0.0-alpha.182",
29
- "@reventlessdev/reventless-local": "3.0.0-alpha.274",
30
- "@reventlessdev/reventless-spec": "3.0.0-alpha.154",
31
- "@reventlessdev/trait-address-geocoding": "1.0.0-alpha.33",
32
- "@reventlessdev/trait-notification": "1.0.0-alpha.33"
26
+ "@reventlessdev/online-shop-hybrid-ordering-spec": "1.0.0-alpha.152",
27
+ "@reventlessdev/reventless-gwt": "1.0.0-alpha.233",
28
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.184",
29
+ "@reventlessdev/reventless-local": "3.0.0-alpha.276",
30
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.156",
31
+ "@reventlessdev/trait-address-geocoding": "1.0.0-alpha.35",
32
+ "@reventlessdev/trait-notification": "1.0.0-alpha.35"
33
33
  },
34
34
  "devDependencies": {
35
35
  "rescript": "12.3.1",
36
36
  "sury-ppx": "11.0.0",
37
- "@reventlessdev/reventless-ppx": "1.0.0-alpha.95"
37
+ "@reventlessdev/reventless-ppx": "1.0.0-alpha.96"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "rescript": "^12.3.1"
@@ -7,7 +7,7 @@ module Id = CustomerId
7
7
 
8
8
  // Operator surface: everyone's profile, so operator-only. A shopper reads their
9
9
  // own orders instead.
10
- @@reventless.authorize(AllowGroups(["Admin", "Fulfilment"]))
10
+ @@reventless.authorize(AllowRoles([Admin, Fulfilment]))
11
11
 
12
12
  // A state rather than a flag beside one, so a command can say where it belongs:
13
13
  // `Moves([Deactivated], Active)` on `Reactivate`. `@retired` withdraws
@@ -60,7 +60,7 @@ let stateSchema$2 = Sury.$Metadata_set(stateSchema$1, StateAnnotations$Reventles
60
60
  });
61
61
 
62
62
  let authorization = {
63
- TAG: "AllowGroups",
63
+ TAG: "AllowRoles",
64
64
  _0: [
65
65
  "Admin",
66
66
  "Fulfilment"
@@ -3,9 +3,12 @@
3
3
 
4
4
  @@reventless.spec
5
5
 
6
+ // `@owner` on the consumed `customerId` says whose order this is. The framework
7
+ // reads it before `decide` and refuses a shopper cancelling someone else's order;
8
+ // an operator, or an automation, still acts on anyone's.
6
9
  @schema
7
10
  type consumedEvent =
8
- | OrderPlaced({productIds: array<CatalogSpec.ProductId.t>})
11
+ | OrderPlaced({productIds: array<CatalogSpec.ProductId.t>, @owner customerId: CustomerId.t})
9
12
  | OrderShipped
10
13
  | OrderCancelled
11
14
  | OrderReopened
@@ -1,15 +1,18 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as Sury from "sury";
4
+ import * as Owner$Reventless from "@reventlessdev/reventless-spec/src/components/Owner.res.mjs";
4
5
  import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
5
6
  import * as Api$ReventlessInfra from "@reventlessdev/reventless-infra/src/components/Api.res.mjs";
6
7
  import * as ProductId$CatalogSpec from "@reventlessdev/online-shop-hybrid-catalog-spec/src/ProductId.res.mjs";
7
8
  import * as OrderId$OrderingPlugin from "../OrderId.res.mjs";
9
+ import * as CustomerId$OrderingPlugin from "../../Customer/CustomerId.res.mjs";
8
10
 
9
11
  let consumedEventSchema = Sury.union([
10
12
  Sury.$schema(s => ({
11
13
  TAG: "OrderPlaced",
12
- productIds: s.m(Sury.array(DcbTag$Reventless.mark(ProductId$CatalogSpec.schema)))
14
+ productIds: s.m(Sury.array(DcbTag$Reventless.mark(ProductId$CatalogSpec.schema))),
15
+ customerId: s.m(Owner$Reventless.mark(DcbTag$Reventless.mark(CustomerId$OrderingPlugin.schema)))
13
16
  })),
14
17
  Sury.literal("OrderShipped"),
15
18
  Sury.literal("OrderCancelled"),
@@ -29,7 +29,7 @@ type consumedEvent =
29
29
  | OrderReopened
30
30
 
31
31
  @schema
32
- type command = | @authorize(AllowGroups(["Admin", "Fulfilment"])) ShipOrder({orderId: OrderId.t})
32
+ type command = | @authorize(AllowRoles([Admin, Fulfilment])) ShipOrder({orderId: OrderId.t})
33
33
 
34
34
  @schema
35
35
  type error =
@@ -43,7 +43,7 @@ function commandTransition(command) {
43
43
 
44
44
  function commandAuthorization(command) {
45
45
  return {
46
- TAG: "AllowGroups",
46
+ TAG: "AllowRoles",
47
47
  _0: [
48
48
  "Admin",
49
49
  "Fulfilment"
package/src/Roles.res ADDED
@@ -0,0 +1,5 @@
1
+ // The roles the ordering commands and views require. `Admin` is the framework's
2
+ // administrator; joined by name, so it needs no shared package.
3
+ type t =
4
+ | Admin
5
+ | Fulfilment
@@ -0,0 +1,2 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+ /* This output is empty. Its source's type definitions, externals and/or unused code got optimized away. */
@@ -10,7 +10,8 @@ let include = Behavior_GWT$ReventlessGwt.Make({
10
10
  consumedEventSchema: SyncCatalogProduct$OrderingPlugin.consumedEventSchema,
11
11
  commandSchema: SyncCatalogProduct$OrderingPlugin.commandSchema,
12
12
  errorSchema: SyncCatalogProduct$OrderingPlugin.errorSchema,
13
- eventSchema: SyncCatalogProduct$OrderingPlugin.eventSchema
13
+ eventSchema: SyncCatalogProduct$OrderingPlugin.eventSchema,
14
+ commandAuthorization: SyncCatalogProduct$OrderingPlugin.commandAuthorization
14
15
  })({
15
16
  initialState: SyncCatalogProduct_Behavior$OrderingPlugin.initialState,
16
17
  evolve: SyncCatalogProduct_Behavior$OrderingPlugin.evolve,
@@ -149,6 +150,10 @@ let Spec = include.Spec;
149
150
 
150
151
  let todo = include.todo;
151
152
 
153
+ let Caller = include.Caller;
154
+
155
+ let asCaller = include.asCaller;
156
+
152
157
  let thenEvents = include.thenEvents;
153
158
 
154
159
  let thenEventWithError = include.thenEventWithError;
@@ -157,6 +162,8 @@ let thenEventsWithError = include.thenEventsWithError;
157
162
 
158
163
  let thenError = include.thenError;
159
164
 
165
+ let thenRefused = include.thenRefused;
166
+
160
167
  let thenAppendsConditionedOn = include.thenAppendsConditionedOn;
161
168
 
162
169
  let thenAppendsConditionedOnExactly = include.thenAppendsConditionedOnExactly;
@@ -167,6 +174,8 @@ export {
167
174
  todo,
168
175
  test,
169
176
  givenEvents,
177
+ Caller,
178
+ asCaller,
170
179
  whenCmd,
171
180
  thenEvent,
172
181
  thenEvents,
@@ -174,6 +183,7 @@ export {
174
183
  thenEventWithError,
175
184
  thenEventsWithError,
176
185
  thenError,
186
+ thenRefused,
177
187
  thenAppendsConditionedOn,
178
188
  thenAppendsConditionedOnExactly,
179
189
  }
@@ -147,14 +147,16 @@ let Conformance = AddressGeocoding_Conformance$TraitAddressGeocoding.Make({
147
147
  name: Customer$OrderingPlugin.name,
148
148
  commandSchema: Customer$OrderingPlugin.commandSchema,
149
149
  eventSchema: Customer$OrderingPlugin.eventSchema,
150
- errorSchema: Customer$OrderingPlugin.errorSchema
150
+ errorSchema: Customer$OrderingPlugin.errorSchema,
151
+ commandAuthorization: Customer$OrderingPlugin.commandAuthorization
151
152
  },
152
153
  Behavior: {
153
154
  Spec: {
154
155
  name: Customer$OrderingPlugin.name,
155
156
  commandSchema: Customer$OrderingPlugin.commandSchema,
156
157
  eventSchema: Customer$OrderingPlugin.eventSchema,
157
- errorSchema: Customer$OrderingPlugin.errorSchema
158
+ errorSchema: Customer$OrderingPlugin.errorSchema,
159
+ commandAuthorization: Customer$OrderingPlugin.commandAuthorization
158
160
  },
159
161
  initialState: Customer_Behavior$OrderingPlugin.initialState,
160
162
  evolve: Customer_Behavior$OrderingPlugin.evolve,
@@ -9,13 +9,15 @@ let include = Behavior_GWT$ReventlessGwt.MakeFromAggregate({
9
9
  name: Customer$OrderingPlugin.name,
10
10
  commandSchema: Customer$OrderingPlugin.commandSchema,
11
11
  eventSchema: Customer$OrderingPlugin.eventSchema,
12
- errorSchema: Customer$OrderingPlugin.errorSchema
12
+ errorSchema: Customer$OrderingPlugin.errorSchema,
13
+ commandAuthorization: Customer$OrderingPlugin.commandAuthorization
13
14
  })({
14
15
  Spec: {
15
16
  name: Customer$OrderingPlugin.name,
16
17
  commandSchema: Customer$OrderingPlugin.commandSchema,
17
18
  eventSchema: Customer$OrderingPlugin.eventSchema,
18
- errorSchema: Customer$OrderingPlugin.errorSchema
19
+ errorSchema: Customer$OrderingPlugin.errorSchema,
20
+ commandAuthorization: Customer$OrderingPlugin.commandAuthorization
19
21
  },
20
22
  initialState: Customer_Behavior$OrderingPlugin.initialState,
21
23
  evolve: Customer_Behavior$OrderingPlugin.evolve,
@@ -278,6 +280,10 @@ describe("Customer email verification", () => {
278
280
 
279
281
  let Spec = include.Spec;
280
282
 
283
+ let Caller = include.Caller;
284
+
285
+ let asCaller = include.asCaller;
286
+
281
287
  let thenCompareEvent = include.thenCompareEvent;
282
288
 
283
289
  let thenEventWithError = include.thenEventWithError;
@@ -288,11 +294,15 @@ let thenCompareEvents = include.thenCompareEvents;
288
294
 
289
295
  let thenEventsWithError = include.thenEventsWithError;
290
296
 
297
+ let thenRefused = include.thenRefused;
298
+
291
299
  export {
292
300
  Spec,
293
301
  describe,
294
302
  test,
295
303
  givenEvents,
304
+ Caller,
305
+ asCaller,
296
306
  whenCmd,
297
307
  thenEvent,
298
308
  thenCompareEvent,
@@ -302,5 +312,6 @@ export {
302
312
  thenCompareEvents,
303
313
  thenEventsWithError,
304
314
  thenError,
315
+ thenRefused,
305
316
  }
306
317
  /* include Not a pure module */
@@ -10,7 +10,8 @@ let include = Behavior_GWT$ReventlessGwt.Make({
10
10
  consumedEventSchema: EmailVerificationChallenges$OrderingPlugin.consumedEventSchema,
11
11
  commandSchema: EmailVerificationChallenges$OrderingPlugin.commandSchema,
12
12
  errorSchema: EmailVerificationChallenges$OrderingPlugin.errorSchema,
13
- eventSchema: EmailVerificationChallenges$OrderingPlugin.eventSchema
13
+ eventSchema: EmailVerificationChallenges$OrderingPlugin.eventSchema,
14
+ commandAuthorization: EmailVerificationChallenges$OrderingPlugin.commandAuthorization
14
15
  })({
15
16
  initialState: EmailVerificationChallenges_Behavior$OrderingPlugin.initialState,
16
17
  evolve: EmailVerificationChallenges_Behavior$OrderingPlugin.evolve,
@@ -161,12 +162,18 @@ let Spec = include.Spec;
161
162
 
162
163
  let todo = include.todo;
163
164
 
165
+ let Caller = include.Caller;
166
+
167
+ let asCaller = include.asCaller;
168
+
164
169
  let thenEvents = include.thenEvents;
165
170
 
166
171
  let thenEventWithError = include.thenEventWithError;
167
172
 
168
173
  let thenEventsWithError = include.thenEventsWithError;
169
174
 
175
+ let thenRefused = include.thenRefused;
176
+
170
177
  let thenAppendsConditionedOn = include.thenAppendsConditionedOn;
171
178
 
172
179
  let thenAppendsConditionedOnExactly = include.thenAppendsConditionedOnExactly;
@@ -177,6 +184,8 @@ export {
177
184
  todo,
178
185
  test,
179
186
  givenEvents,
187
+ Caller,
188
+ asCaller,
180
189
  whenCmd,
181
190
  thenEvent,
182
191
  thenEvents,
@@ -184,6 +193,7 @@ export {
184
193
  thenEventWithError,
185
194
  thenEventsWithError,
186
195
  thenError,
196
+ thenRefused,
187
197
  thenAppendsConditionedOn,
188
198
  thenAppendsConditionedOnExactly,
189
199
  issuedAt,
@@ -95,7 +95,8 @@ let Sync = Flow_GWT$ReventlessGwt.CommandStep({
95
95
  consumedEventSchema: SyncCatalogProduct$OrderingPlugin.consumedEventSchema,
96
96
  commandSchema: SyncCatalogProduct$OrderingPlugin.commandSchema,
97
97
  errorSchema: SyncCatalogProduct$OrderingPlugin.errorSchema,
98
- eventSchema: SyncCatalogProduct$OrderingPlugin.eventSchema
98
+ eventSchema: SyncCatalogProduct$OrderingPlugin.eventSchema,
99
+ commandAuthorization: SyncCatalogProduct$OrderingPlugin.commandAuthorization
99
100
  })({
100
101
  initialState: SyncCatalogProduct_Behavior$OrderingPlugin.initialState,
101
102
  evolve: SyncCatalogProduct_Behavior$OrderingPlugin.evolve,
@@ -107,7 +108,8 @@ let Place = Flow_GWT$ReventlessGwt.CommandStep({
107
108
  consumedEventSchema: PlaceOrder$OrderingPlugin.consumedEventSchema,
108
109
  commandSchema: PlaceOrder$OrderingPlugin.commandSchema,
109
110
  errorSchema: PlaceOrder$OrderingPlugin.errorSchema,
110
- eventSchema: PlaceOrder$OrderingPlugin.eventSchema
111
+ eventSchema: PlaceOrder$OrderingPlugin.eventSchema,
112
+ commandAuthorization: PlaceOrder$OrderingPlugin.commandAuthorization
111
113
  })({
112
114
  initialState: PlaceOrder_Behavior$OrderingPlugin.initialState,
113
115
  evolve: PlaceOrder_Behavior$OrderingPlugin.evolve,
@@ -129,7 +131,8 @@ let Ship = Flow_GWT$ReventlessGwt.CommandStep({
129
131
  consumedEventSchema: ShipOrder$OrderingPlugin.consumedEventSchema,
130
132
  commandSchema: ShipOrder$OrderingPlugin.commandSchema,
131
133
  errorSchema: ShipOrder$OrderingPlugin.errorSchema,
132
- eventSchema: ShipOrder$OrderingPlugin.eventSchema
134
+ eventSchema: ShipOrder$OrderingPlugin.eventSchema,
135
+ commandAuthorization: ShipOrder$OrderingPlugin.commandAuthorization
133
136
  })({
134
137
  initialState: ShipOrder_Behavior$OrderingPlugin.initialState,
135
138
  evolve: ShipOrder_Behavior$OrderingPlugin.evolve,
@@ -248,7 +248,8 @@ let Conformance = Notification_Conformance$TraitNotification.Make({
248
248
  consumedEventSchema: NotificationPreferences$OrderingPlugin.consumedEventSchema,
249
249
  commandSchema: NotificationPreferences$OrderingPlugin.commandSchema,
250
250
  errorSchema: NotificationPreferences$OrderingPlugin.errorSchema,
251
- eventSchema: NotificationPreferences$OrderingPlugin.eventSchema
251
+ eventSchema: NotificationPreferences$OrderingPlugin.eventSchema,
252
+ commandAuthorization: NotificationPreferences$OrderingPlugin.commandAuthorization
252
253
  },
253
254
  Behavior: {
254
255
  Spec: {
@@ -256,7 +257,8 @@ let Conformance = Notification_Conformance$TraitNotification.Make({
256
257
  consumedEventSchema: NotificationPreferences$OrderingPlugin.consumedEventSchema,
257
258
  commandSchema: NotificationPreferences$OrderingPlugin.commandSchema,
258
259
  errorSchema: NotificationPreferences$OrderingPlugin.errorSchema,
259
- eventSchema: NotificationPreferences$OrderingPlugin.eventSchema
260
+ eventSchema: NotificationPreferences$OrderingPlugin.eventSchema,
261
+ commandAuthorization: NotificationPreferences$OrderingPlugin.commandAuthorization
260
262
  },
261
263
  initialState: NotificationPreferences_Behavior$OrderingPlugin.initialState,
262
264
  evolve: NotificationPreferences_Behavior$OrderingPlugin.evolve,
@@ -10,7 +10,8 @@ let include = Behavior_GWT$ReventlessGwt.Make({
10
10
  consumedEventSchema: NotificationPreferences$OrderingPlugin.consumedEventSchema,
11
11
  commandSchema: NotificationPreferences$OrderingPlugin.commandSchema,
12
12
  errorSchema: NotificationPreferences$OrderingPlugin.errorSchema,
13
- eventSchema: NotificationPreferences$OrderingPlugin.eventSchema
13
+ eventSchema: NotificationPreferences$OrderingPlugin.eventSchema,
14
+ commandAuthorization: NotificationPreferences$OrderingPlugin.commandAuthorization
14
15
  })({
15
16
  initialState: NotificationPreferences_Behavior$OrderingPlugin.initialState,
16
17
  evolve: NotificationPreferences_Behavior$OrderingPlugin.evolve,
@@ -105,6 +106,10 @@ let Spec = include.Spec;
105
106
 
106
107
  let todo = include.todo;
107
108
 
109
+ let Caller = include.Caller;
110
+
111
+ let asCaller = include.asCaller;
112
+
108
113
  let thenEvents = include.thenEvents;
109
114
 
110
115
  let thenNoEvent = include.thenNoEvent;
@@ -115,6 +120,8 @@ let thenEventsWithError = include.thenEventsWithError;
115
120
 
116
121
  let thenError = include.thenError;
117
122
 
123
+ let thenRefused = include.thenRefused;
124
+
118
125
  let thenAppendsConditionedOn = include.thenAppendsConditionedOn;
119
126
 
120
127
  let thenAppendsConditionedOnExactly = include.thenAppendsConditionedOnExactly;
@@ -125,6 +132,8 @@ export {
125
132
  todo,
126
133
  test,
127
134
  givenEvents,
135
+ Caller,
136
+ asCaller,
128
137
  whenCmd,
129
138
  thenEvent,
130
139
  thenEvents,
@@ -132,6 +141,7 @@ export {
132
141
  thenEventWithError,
133
142
  thenEventsWithError,
134
143
  thenError,
144
+ thenRefused,
135
145
  thenAppendsConditionedOn,
136
146
  thenAppendsConditionedOnExactly,
137
147
  }
@@ -10,7 +10,8 @@ let include = Behavior_GWT$ReventlessGwt.Make({
10
10
  consumedEventSchema: NotificationSourceClaims$OrderingPlugin.consumedEventSchema,
11
11
  commandSchema: NotificationSourceClaims$OrderingPlugin.commandSchema,
12
12
  errorSchema: NotificationSourceClaims$OrderingPlugin.errorSchema,
13
- eventSchema: NotificationSourceClaims$OrderingPlugin.eventSchema
13
+ eventSchema: NotificationSourceClaims$OrderingPlugin.eventSchema,
14
+ commandAuthorization: NotificationSourceClaims$OrderingPlugin.commandAuthorization
14
15
  })({
15
16
  initialState: NotificationSourceClaims_Behavior$OrderingPlugin.initialState,
16
17
  evolve: NotificationSourceClaims_Behavior$OrderingPlugin.evolve,
@@ -76,6 +77,10 @@ let Spec = include.Spec;
76
77
 
77
78
  let todo = include.todo;
78
79
 
80
+ let Caller = include.Caller;
81
+
82
+ let asCaller = include.asCaller;
83
+
79
84
  let thenEvents = include.thenEvents;
80
85
 
81
86
  let thenEventWithError = include.thenEventWithError;
@@ -84,6 +89,8 @@ let thenEventsWithError = include.thenEventsWithError;
84
89
 
85
90
  let thenError = include.thenError;
86
91
 
92
+ let thenRefused = include.thenRefused;
93
+
87
94
  let thenAppendsConditionedOn = include.thenAppendsConditionedOn;
88
95
 
89
96
  let thenAppendsConditionedOnExactly = include.thenAppendsConditionedOnExactly;
@@ -94,6 +101,8 @@ export {
94
101
  todo,
95
102
  test,
96
103
  givenEvents,
104
+ Caller,
105
+ asCaller,
97
106
  whenCmd,
98
107
  thenEvent,
99
108
  thenEvents,
@@ -101,6 +110,7 @@ export {
101
110
  thenEventWithError,
102
111
  thenEventsWithError,
103
112
  thenError,
113
+ thenRefused,
104
114
  thenAppendsConditionedOn,
105
115
  thenAppendsConditionedOnExactly,
106
116
  }
@@ -12,35 +12,35 @@ describe("CancelOrder StateChangeSlice", () => {
12
12
 
13
13
  // scenario-id: 44e2fd1b-d3da-4581-b102-0ccb47e004eb
14
14
  test("placed order produces OrderCancelled with productIds carried over", () =>
15
- givenEvents([OrderPlaced({productIds: [p1, p2]})])
15
+ givenEvents([OrderPlaced({productIds: [p1, p2], customerId: c1})])
16
16
  ->whenCmd(CancelOrder({orderId: o1}))
17
17
  ->thenEvent(OrderCancelled({orderId: o1, productIds: [p1, p2]}))
18
18
  )
19
19
 
20
20
  // scenario-id: 36e7a929-73a3-48ad-96bf-98265877dd9f
21
21
  test("already cancelled order produces no events for CancelOrder (idempotent)", () =>
22
- givenEvents([OrderPlaced({productIds: [p1]}), OrderCancelled])
22
+ givenEvents([OrderPlaced({productIds: [p1], customerId: c1}), OrderCancelled])
23
23
  ->whenCmd(CancelOrder({orderId: o1}))
24
24
  ->thenNoEvent
25
25
  )
26
26
 
27
27
  // scenario-id: a8adfaaf-2a97-487d-b80b-34b2e79a621d
28
28
  test("shipped order returns OrderAlreadyShipped for CancelOrder", () =>
29
- givenEvents([OrderPlaced({productIds: [p1]}), OrderShipped])
29
+ givenEvents([OrderPlaced({productIds: [p1], customerId: c1}), OrderShipped])
30
30
  ->whenCmd(CancelOrder({orderId: o1}))
31
31
  ->thenError(OrderAlreadyShipped)
32
32
  )
33
33
 
34
34
  // scenario-id: da5c2d1d-adaf-412c-ba71-293158b29841
35
35
  test("ReopenOrder on cancelled order produces OrderReopened", () =>
36
- givenEvents([OrderPlaced({productIds: [p1]}), OrderCancelled])
36
+ givenEvents([OrderPlaced({productIds: [p1], customerId: c1}), OrderCancelled])
37
37
  ->whenCmd(ReopenOrder({orderId: o1}))
38
38
  ->thenEvent(OrderReopened({orderId: o1}))
39
39
  )
40
40
 
41
41
  // scenario-id: edebec12-a5ec-4f71-a1fc-f1db65108db9
42
42
  test("ReopenOrder on a placed order produces no events (idempotent)", () =>
43
- givenEvents([OrderPlaced({productIds: [p1]})])
43
+ givenEvents([OrderPlaced({productIds: [p1], customerId: c1})])
44
44
  ->whenCmd(ReopenOrder({orderId: o1}))
45
45
  ->thenNoEvent
46
46
  )
@@ -50,7 +50,7 @@ describe("CancelOrder StateChangeSlice", () => {
50
50
  // shipped — the case no test covered, which is how it survived.
51
51
  // scenario-id: 96bdee04-7518-4297-81f5-7242a0cffa9f
52
52
  test("ReopenOrder on a shipped order returns OrderAlreadyShipped", () =>
53
- givenEvents([OrderPlaced({productIds: [p1]}), OrderShipped])
53
+ givenEvents([OrderPlaced({productIds: [p1], customerId: c1}), OrderShipped])
54
54
  ->whenCmd(ReopenOrder({orderId: o1}))
55
55
  ->thenError(OrderAlreadyShipped)
56
56
  )
@@ -61,4 +61,39 @@ describe("CancelOrder StateChangeSlice", () => {
61
61
  ->whenCmd(ReopenOrder({orderId: o1}))
62
62
  ->thenError(OrderNotFound)
63
63
  )
64
+
65
+ // Only the customer who placed an order may cancel it. The slice says so by
66
+ // marking `@owner` on the `customerId` its `OrderPlaced` carries; an operator
67
+ // still cancels on anyone's behalf.
68
+ // scenario-id: 80c0ad21-a352-43d6-862c-74c5250991d1
69
+ test("the customer who placed the order may cancel it", () =>
70
+ givenEvents([OrderPlaced({productIds: [p1], customerId: c1})])
71
+ ->asCaller(Caller.owner(c1))
72
+ ->whenCmd(CancelOrder({orderId: o1}))
73
+ ->thenEvent(OrderCancelled({orderId: o1, productIds: [p1]}))
74
+ )
75
+
76
+ // scenario-id: 48fc5fa8-449d-40df-b58d-e87f0acea478
77
+ test("another customer cannot cancel the order", () =>
78
+ givenEvents([OrderPlaced({productIds: [p1], customerId: c1})])
79
+ ->asCaller(Caller.owner(c2))
80
+ ->whenCmd(CancelOrder({orderId: o1}))
81
+ ->thenRefused
82
+ )
83
+
84
+ // scenario-id: e7561ebf-5566-4a4e-87c0-9aa403d68310
85
+ test("an operator may cancel any customer's order", () =>
86
+ givenEvents([OrderPlaced({productIds: [p1], customerId: c1})])
87
+ ->asCaller(Caller.operator)
88
+ ->whenCmd(CancelOrder({orderId: o1}))
89
+ ->thenEvent(OrderCancelled({orderId: o1, productIds: [p1]}))
90
+ )
91
+
92
+ // scenario-id: e0df85d2-f997-4833-816e-39ddce5c895e
93
+ test("a caller who is not signed in cannot cancel an order", () =>
94
+ givenEvents([OrderPlaced({productIds: [p1], customerId: c1})])
95
+ ->asCaller(Caller.anonymous)
96
+ ->whenCmd(CancelOrder({orderId: o1}))
97
+ ->thenRefused
98
+ )
64
99
  })
@@ -10,7 +10,8 @@ let include = Behavior_GWT$ReventlessGwt.Make({
10
10
  consumedEventSchema: CancelOrder$OrderingPlugin.consumedEventSchema,
11
11
  commandSchema: CancelOrder$OrderingPlugin.commandSchema,
12
12
  errorSchema: CancelOrder$OrderingPlugin.errorSchema,
13
- eventSchema: CancelOrder$OrderingPlugin.eventSchema
13
+ eventSchema: CancelOrder$OrderingPlugin.eventSchema,
14
+ commandAuthorization: CancelOrder$OrderingPlugin.commandAuthorization
14
15
  })({
15
16
  initialState: CancelOrder_Behavior$OrderingPlugin.initialState,
16
17
  evolve: CancelOrder_Behavior$OrderingPlugin.evolve,
@@ -23,6 +24,10 @@ let test = include.test;
23
24
 
24
25
  let givenEvents = include.givenEvents;
25
26
 
27
+ let Caller = include.Caller;
28
+
29
+ let asCaller = include.asCaller;
30
+
26
31
  let whenCmd = include.whenCmd;
27
32
 
28
33
  let thenEvent = include.thenEvent;
@@ -31,6 +36,8 @@ let thenNoEvent = include.thenNoEvent;
31
36
 
32
37
  let thenError = include.thenError;
33
38
 
39
+ let thenRefused = include.thenRefused;
40
+
34
41
  describe("CancelOrder StateChangeSlice", () => {
35
42
  test("non-existent order returns OrderNotFound for CancelOrder", () => thenError(whenCmd(givenEvents([]), {
36
43
  TAG: "CancelOrder",
@@ -41,7 +48,8 @@ describe("CancelOrder StateChangeSlice", () => {
41
48
  productIds: [
42
49
  Ordering_Examples$OrderingPlugin.p1,
43
50
  Ordering_Examples$OrderingPlugin.p2
44
- ]
51
+ ],
52
+ customerId: Ordering_Examples$OrderingPlugin.c1
45
53
  }]), {
46
54
  TAG: "CancelOrder",
47
55
  orderId: Ordering_Examples$OrderingPlugin.o1
@@ -56,7 +64,8 @@ describe("CancelOrder StateChangeSlice", () => {
56
64
  test("already cancelled order produces no events for CancelOrder (idempotent)", () => thenNoEvent(whenCmd(givenEvents([
57
65
  {
58
66
  TAG: "OrderPlaced",
59
- productIds: [Ordering_Examples$OrderingPlugin.p1]
67
+ productIds: [Ordering_Examples$OrderingPlugin.p1],
68
+ customerId: Ordering_Examples$OrderingPlugin.c1
60
69
  },
61
70
  "OrderCancelled"
62
71
  ]), {
@@ -66,7 +75,8 @@ describe("CancelOrder StateChangeSlice", () => {
66
75
  test("shipped order returns OrderAlreadyShipped for CancelOrder", () => thenError(whenCmd(givenEvents([
67
76
  {
68
77
  TAG: "OrderPlaced",
69
- productIds: [Ordering_Examples$OrderingPlugin.p1]
78
+ productIds: [Ordering_Examples$OrderingPlugin.p1],
79
+ customerId: Ordering_Examples$OrderingPlugin.c1
70
80
  },
71
81
  "OrderShipped"
72
82
  ]), {
@@ -76,7 +86,8 @@ describe("CancelOrder StateChangeSlice", () => {
76
86
  test("ReopenOrder on cancelled order produces OrderReopened", () => thenEvent(whenCmd(givenEvents([
77
87
  {
78
88
  TAG: "OrderPlaced",
79
- productIds: [Ordering_Examples$OrderingPlugin.p1]
89
+ productIds: [Ordering_Examples$OrderingPlugin.p1],
90
+ customerId: Ordering_Examples$OrderingPlugin.c1
80
91
  },
81
92
  "OrderCancelled"
82
93
  ]), {
@@ -88,7 +99,8 @@ describe("CancelOrder StateChangeSlice", () => {
88
99
  }));
89
100
  test("ReopenOrder on a placed order produces no events (idempotent)", () => thenNoEvent(whenCmd(givenEvents([{
90
101
  TAG: "OrderPlaced",
91
- productIds: [Ordering_Examples$OrderingPlugin.p1]
102
+ productIds: [Ordering_Examples$OrderingPlugin.p1],
103
+ customerId: Ordering_Examples$OrderingPlugin.c1
92
104
  }]), {
93
105
  TAG: "ReopenOrder",
94
106
  orderId: Ordering_Examples$OrderingPlugin.o1
@@ -96,7 +108,8 @@ describe("CancelOrder StateChangeSlice", () => {
96
108
  test("ReopenOrder on a shipped order returns OrderAlreadyShipped", () => thenError(whenCmd(givenEvents([
97
109
  {
98
110
  TAG: "OrderPlaced",
99
- productIds: [Ordering_Examples$OrderingPlugin.p1]
111
+ productIds: [Ordering_Examples$OrderingPlugin.p1],
112
+ customerId: Ordering_Examples$OrderingPlugin.c1
100
113
  },
101
114
  "OrderShipped"
102
115
  ]), {
@@ -107,6 +120,46 @@ describe("CancelOrder StateChangeSlice", () => {
107
120
  TAG: "ReopenOrder",
108
121
  orderId: Ordering_Examples$OrderingPlugin.o1
109
122
  }), "OrderNotFound"));
123
+ test("the customer who placed the order may cancel it", () => thenEvent(whenCmd(asCaller(givenEvents([{
124
+ TAG: "OrderPlaced",
125
+ productIds: [Ordering_Examples$OrderingPlugin.p1],
126
+ customerId: Ordering_Examples$OrderingPlugin.c1
127
+ }]), Caller.owner(Ordering_Examples$OrderingPlugin.c1, undefined)), {
128
+ TAG: "CancelOrder",
129
+ orderId: Ordering_Examples$OrderingPlugin.o1
130
+ }), {
131
+ TAG: "OrderCancelled",
132
+ orderId: Ordering_Examples$OrderingPlugin.o1,
133
+ productIds: [Ordering_Examples$OrderingPlugin.p1]
134
+ }));
135
+ test("another customer cannot cancel the order", () => thenRefused(whenCmd(asCaller(givenEvents([{
136
+ TAG: "OrderPlaced",
137
+ productIds: [Ordering_Examples$OrderingPlugin.p1],
138
+ customerId: Ordering_Examples$OrderingPlugin.c1
139
+ }]), Caller.owner(Ordering_Examples$OrderingPlugin.c2, undefined)), {
140
+ TAG: "CancelOrder",
141
+ orderId: Ordering_Examples$OrderingPlugin.o1
142
+ })));
143
+ test("an operator may cancel any customer's order", () => thenEvent(whenCmd(asCaller(givenEvents([{
144
+ TAG: "OrderPlaced",
145
+ productIds: [Ordering_Examples$OrderingPlugin.p1],
146
+ customerId: Ordering_Examples$OrderingPlugin.c1
147
+ }]), Caller.operator), {
148
+ TAG: "CancelOrder",
149
+ orderId: Ordering_Examples$OrderingPlugin.o1
150
+ }), {
151
+ TAG: "OrderCancelled",
152
+ orderId: Ordering_Examples$OrderingPlugin.o1,
153
+ productIds: [Ordering_Examples$OrderingPlugin.p1]
154
+ }));
155
+ test("a caller who is not signed in cannot cancel an order", () => thenRefused(whenCmd(asCaller(givenEvents([{
156
+ TAG: "OrderPlaced",
157
+ productIds: [Ordering_Examples$OrderingPlugin.p1],
158
+ customerId: Ordering_Examples$OrderingPlugin.c1
159
+ }]), Caller.anonymous), {
160
+ TAG: "CancelOrder",
161
+ orderId: Ordering_Examples$OrderingPlugin.o1
162
+ })));
110
163
  });
111
164
 
112
165
  let Spec = include.Spec;
@@ -129,6 +182,8 @@ export {
129
182
  todo,
130
183
  test,
131
184
  givenEvents,
185
+ Caller,
186
+ asCaller,
132
187
  whenCmd,
133
188
  thenEvent,
134
189
  thenEvents,
@@ -136,6 +191,7 @@ export {
136
191
  thenEventWithError,
137
192
  thenEventsWithError,
138
193
  thenError,
194
+ thenRefused,
139
195
  thenAppendsConditionedOn,
140
196
  thenAppendsConditionedOnExactly,
141
197
  }
@@ -14,7 +14,8 @@ let include = Behavior_GWT$ReventlessGwt.Make({
14
14
  consumedEventSchema: PlaceOrder$OrderingPlugin.consumedEventSchema,
15
15
  commandSchema: PlaceOrder$OrderingPlugin.commandSchema,
16
16
  errorSchema: PlaceOrder$OrderingPlugin.errorSchema,
17
- eventSchema: PlaceOrder$OrderingPlugin.eventSchema
17
+ eventSchema: PlaceOrder$OrderingPlugin.eventSchema,
18
+ commandAuthorization: PlaceOrder$OrderingPlugin.commandAuthorization
18
19
  })({
19
20
  initialState: PlaceOrder_Behavior$OrderingPlugin.initialState,
20
21
  evolve: PlaceOrder_Behavior$OrderingPlugin.evolve,
@@ -814,6 +815,10 @@ let Spec = include.Spec;
814
815
 
815
816
  let todo = include.todo;
816
817
 
818
+ let Caller = include.Caller;
819
+
820
+ let asCaller = include.asCaller;
821
+
817
822
  let thenEvents = include.thenEvents;
818
823
 
819
824
  let thenNoEvent = include.thenNoEvent;
@@ -822,6 +827,8 @@ let thenEventWithError = include.thenEventWithError;
822
827
 
823
828
  let thenEventsWithError = include.thenEventsWithError;
824
829
 
830
+ let thenRefused = include.thenRefused;
831
+
825
832
  let thenAppendsConditionedOn = include.thenAppendsConditionedOn;
826
833
 
827
834
  let thenAppendsConditionedOnExactly = include.thenAppendsConditionedOnExactly;
@@ -832,6 +839,8 @@ export {
832
839
  todo,
833
840
  test,
834
841
  givenEvents,
842
+ Caller,
843
+ asCaller,
835
844
  whenCmd,
836
845
  thenEvent,
837
846
  thenEvents,
@@ -839,6 +848,7 @@ export {
839
848
  thenEventWithError,
840
849
  thenEventsWithError,
841
850
  thenError,
851
+ thenRefused,
842
852
  thenAppendsConditionedOn,
843
853
  thenAppendsConditionedOnExactly,
844
854
  }
@@ -43,3 +43,37 @@ describe("ShipOrder StateChangeSlice", () => {
43
43
  ->thenEvent(OrderShipped({orderId: o1, customerId: c1}))
44
44
  )
45
45
  })
46
+
47
+ describe("Who may ShipOrder", () => {
48
+ // scenario-id: a0ce6e35-29df-48c8-81d8-5af12a0f48d1
49
+ test("a Fulfilment may ship an order", () =>
50
+ givenEvents([OrderPlaced({productIds: [p1], customerId: c1})])
51
+ ->asCaller(Caller.inRoles([Fulfilment]))
52
+ ->whenCmd(ShipOrder({orderId: o1}))
53
+ ->thenEvent(OrderShipped({orderId: o1, customerId: c1}))
54
+ )
55
+
56
+ // scenario-id: 6af9d686-1310-43e5-946e-90ed1ec14d18
57
+ test("an Admin may ship an order", () =>
58
+ givenEvents([OrderPlaced({productIds: [p1], customerId: c1})])
59
+ ->asCaller(Caller.inRoles([Admin]))
60
+ ->whenCmd(ShipOrder({orderId: o1}))
61
+ ->thenEvent(OrderShipped({orderId: o1, customerId: c1}))
62
+ )
63
+
64
+ // scenario-id: 5b4f7a52-ec5e-4282-a7c8-a74432af13da
65
+ test("a shopper may not ship an order", () =>
66
+ givenEvents([OrderPlaced({productIds: [p1], customerId: c1})])
67
+ ->asCaller(Caller.owner(c1))
68
+ ->whenCmd(ShipOrder({orderId: o1}))
69
+ ->thenRefused
70
+ )
71
+
72
+ // scenario-id: 020c3695-3e3d-4343-8d38-d9d33ccf22f0
73
+ test("an anonymous caller may not ship an order", () =>
74
+ givenEvents([OrderPlaced({productIds: [p1], customerId: c1})])
75
+ ->asCaller(Caller.anonymous)
76
+ ->whenCmd(ShipOrder({orderId: o1}))
77
+ ->thenRefused
78
+ )
79
+ })
@@ -10,7 +10,8 @@ let include = Behavior_GWT$ReventlessGwt.Make({
10
10
  consumedEventSchema: ShipOrder$OrderingPlugin.consumedEventSchema,
11
11
  commandSchema: ShipOrder$OrderingPlugin.commandSchema,
12
12
  errorSchema: ShipOrder$OrderingPlugin.errorSchema,
13
- eventSchema: ShipOrder$OrderingPlugin.eventSchema
13
+ eventSchema: ShipOrder$OrderingPlugin.eventSchema,
14
+ commandAuthorization: ShipOrder$OrderingPlugin.commandAuthorization
14
15
  })({
15
16
  initialState: ShipOrder_Behavior$OrderingPlugin.initialState,
16
17
  evolve: ShipOrder_Behavior$OrderingPlugin.evolve,
@@ -23,6 +24,10 @@ let test = include.test;
23
24
 
24
25
  let givenEvents = include.givenEvents;
25
26
 
27
+ let Caller = include.Caller;
28
+
29
+ let asCaller = include.asCaller;
30
+
26
31
  let whenCmd = include.whenCmd;
27
32
 
28
33
  let thenEvent = include.thenEvent;
@@ -31,6 +36,8 @@ let thenNoEvent = include.thenNoEvent;
31
36
 
32
37
  let thenError = include.thenError;
33
38
 
39
+ let thenRefused = include.thenRefused;
40
+
34
41
  describe("ShipOrder StateChangeSlice", () => {
35
42
  test("non-existent order returns OrderNotFound", () => thenError(whenCmd(givenEvents([]), {
36
43
  TAG: "ShipOrder",
@@ -88,6 +95,49 @@ describe("ShipOrder StateChangeSlice", () => {
88
95
  }));
89
96
  });
90
97
 
98
+ describe("Who may ShipOrder", () => {
99
+ test("a Fulfilment may ship an order", () => thenEvent(whenCmd(asCaller(givenEvents([{
100
+ TAG: "OrderPlaced",
101
+ productIds: [Ordering_Examples$OrderingPlugin.p1],
102
+ customerId: Ordering_Examples$OrderingPlugin.c1
103
+ }]), Caller.inRoles(["Fulfilment"])), {
104
+ TAG: "ShipOrder",
105
+ orderId: Ordering_Examples$OrderingPlugin.o1
106
+ }), {
107
+ TAG: "OrderShipped",
108
+ orderId: Ordering_Examples$OrderingPlugin.o1,
109
+ customerId: Ordering_Examples$OrderingPlugin.c1
110
+ }));
111
+ test("an Admin may ship an order", () => thenEvent(whenCmd(asCaller(givenEvents([{
112
+ TAG: "OrderPlaced",
113
+ productIds: [Ordering_Examples$OrderingPlugin.p1],
114
+ customerId: Ordering_Examples$OrderingPlugin.c1
115
+ }]), Caller.inRoles(["Admin"])), {
116
+ TAG: "ShipOrder",
117
+ orderId: Ordering_Examples$OrderingPlugin.o1
118
+ }), {
119
+ TAG: "OrderShipped",
120
+ orderId: Ordering_Examples$OrderingPlugin.o1,
121
+ customerId: Ordering_Examples$OrderingPlugin.c1
122
+ }));
123
+ test("a shopper may not ship an order", () => thenRefused(whenCmd(asCaller(givenEvents([{
124
+ TAG: "OrderPlaced",
125
+ productIds: [Ordering_Examples$OrderingPlugin.p1],
126
+ customerId: Ordering_Examples$OrderingPlugin.c1
127
+ }]), Caller.owner(Ordering_Examples$OrderingPlugin.c1, undefined)), {
128
+ TAG: "ShipOrder",
129
+ orderId: Ordering_Examples$OrderingPlugin.o1
130
+ })));
131
+ test("an anonymous caller may not ship an order", () => thenRefused(whenCmd(asCaller(givenEvents([{
132
+ TAG: "OrderPlaced",
133
+ productIds: [Ordering_Examples$OrderingPlugin.p1],
134
+ customerId: Ordering_Examples$OrderingPlugin.c1
135
+ }]), Caller.anonymous), {
136
+ TAG: "ShipOrder",
137
+ orderId: Ordering_Examples$OrderingPlugin.o1
138
+ })));
139
+ });
140
+
91
141
  let Spec = include.Spec;
92
142
 
93
143
  let todo = include.todo;
@@ -108,6 +158,8 @@ export {
108
158
  todo,
109
159
  test,
110
160
  givenEvents,
161
+ Caller,
162
+ asCaller,
111
163
  whenCmd,
112
164
  thenEvent,
113
165
  thenEvents,
@@ -115,6 +167,7 @@ export {
115
167
  thenEventWithError,
116
168
  thenEventsWithError,
117
169
  thenError,
170
+ thenRefused,
118
171
  thenAppendsConditionedOn,
119
172
  thenAppendsConditionedOnExactly,
120
173
  }
@@ -4,6 +4,7 @@ let p1: CatalogSpec.ProductId.t = CatalogSpec.ProductId.makeFromString("p1")
4
4
  let p2: CatalogSpec.ProductId.t = CatalogSpec.ProductId.makeFromString("p2")
5
5
 
6
6
  let c1: CustomerId.t = CustomerId.makeFromString("c1")
7
+ let c2: CustomerId.t = CustomerId.makeFromString("c2")
7
8
  let id: CustomerId.t = CustomerId.makeFromString("id")
8
9
 
9
10
  let o1: OrderId.t = OrderId.makeFromString("o1")
@@ -11,6 +11,8 @@ let p2 = ProductId$CatalogSpec.makeFromString("p2");
11
11
 
12
12
  let c1 = CustomerId$OrderingPlugin.makeFromString("c1");
13
13
 
14
+ let c2 = CustomerId$OrderingPlugin.makeFromString("c2");
15
+
14
16
  let id = CustomerId$OrderingPlugin.makeFromString("id");
15
17
 
16
18
  let o1 = OrderId$OrderingPlugin.makeFromString("o1");
@@ -71,6 +73,7 @@ export {
71
73
  p1,
72
74
  p2,
73
75
  c1,
76
+ c2,
74
77
  id,
75
78
  o1,
76
79
  o2,