@reventlessdev/online-shop-hybrid-ordering 1.0.0-alpha.239 → 1.0.0-alpha.241

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,21 @@
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.241 (2026-09-20)
7
+
8
+ **Note:** Version bump only for package @reventlessdev/online-shop-hybrid-ordering
9
+
10
+
11
+
12
+
13
+
14
+ # 1.0.0-alpha.240 (2026-09-17)
15
+
16
+ ### Features
17
+
18
+ * **examples:** refuse a delivery window that cannot describe a delivery ([4f36ec3](https://github.com/ReventlessDev/reventless-core/commit/4f36ec39b3b73910edce1ca2ad0108efb06aa9de))
19
+
20
+
6
21
  # 1.0.0-alpha.239 (2026-09-16)
7
22
 
8
23
  * feat(spec)!: infer the DCB partition key per slice, and drop @partitionTag where inference agrees ([6c10161](https://github.com/ReventlessDev/reventless-core/commit/6c101611d438beea4bc9175ae8ea0ea1f6f7493b))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/online-shop-hybrid-ordering",
3
- "version": "1.0.0-alpha.239",
3
+ "version": "1.0.0-alpha.241",
4
4
  "description": "Online Shop Hybrid example — Ordering plugin (Customer aggregate + Order/CatalogProduct DCB)",
5
5
  "license": "Apache-2.0",
6
6
  "jest": {
@@ -21,15 +21,15 @@
21
21
  },
22
22
  "dependencies": {
23
23
  "sury": "11.0.0-rc.2",
24
- "@reventlessdev/online-shop-hybrid-ordering-spec": "1.0.0-alpha.132",
24
+ "@reventlessdev/online-shop-hybrid-catalog-spec": "1.0.0-alpha.133",
25
25
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.21",
26
- "@reventlessdev/reventless-infra": "3.0.0-alpha.164",
27
- "@reventlessdev/reventless-gwt": "1.0.0-alpha.210",
28
- "@reventlessdev/online-shop-hybrid-catalog-spec": "1.0.0-alpha.132",
29
- "@reventlessdev/reventless-local": "3.0.0-alpha.252",
30
- "@reventlessdev/reventless-spec": "3.0.0-alpha.136",
31
- "@reventlessdev/trait-notification": "1.0.0-alpha.12",
32
- "@reventlessdev/trait-address-geocoding": "1.0.0-alpha.12"
26
+ "@reventlessdev/reventless-gwt": "1.0.0-alpha.211",
27
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.165",
28
+ "@reventlessdev/online-shop-hybrid-ordering-spec": "1.0.0-alpha.133",
29
+ "@reventlessdev/reventless-local": "3.0.0-alpha.254",
30
+ "@reventlessdev/trait-address-geocoding": "1.0.0-alpha.13",
31
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.137",
32
+ "@reventlessdev/trait-notification": "1.0.0-alpha.13"
33
33
  },
34
34
  "devDependencies": {
35
35
  "rescript": "12.3.0",
@@ -85,6 +85,11 @@ type error =
85
85
  // currency happened to come first. Naming the codes says which shelf entries
86
86
  // disagree, the way `ProductsNotAvailable` names the products.
87
87
  | MixedCurrencies({currencies: array<string>})
88
+ // A pickup is collected in store, so a delivery slot on one describes a
89
+ // delivery that will never happen.
90
+ | DeliveryWindowOnPickup
91
+ // The window ends before it starts. `reason` is `DateRange.validate`'s wording.
92
+ | InvalidDeliveryWindow({reason: string})
88
93
 
89
94
  // One priced line of a placed order, frozen at placement.
90
95
  //
@@ -77,6 +77,11 @@ let errorSchema = Sury.union([
77
77
  Sury.$schema(s => ({
78
78
  TAG: "MixedCurrencies",
79
79
  currencies: s.m(Sury.array(Sury.string))
80
+ })),
81
+ Sury.literal("DeliveryWindowOnPickup"),
82
+ Sury.$schema(s => ({
83
+ TAG: "InvalidDeliveryWindow",
84
+ reason: s.m(Sury.string)
80
85
  }))
81
86
  ]);
82
87
 
@@ -114,6 +114,19 @@ let priceLines = (state, lineItems: array<lineItem>): result<array<orderLine>, e
114
114
  }
115
115
  )
116
116
 
117
+ // The window is display data — nothing ships by it — but a window that cannot
118
+ // describe a delivery is refused rather than shown.
119
+ let deliveryWindowError = (~shippingMethod, ~deliveryWindow) =>
120
+ switch (shippingMethod, deliveryWindow) {
121
+ | (_, None) => None
122
+ | (Pickup, Some(_)) => Some(DeliveryWindowOnPickup)
123
+ | (_, Some(window)) =>
124
+ switch Reventless.DateRange.validate(window) {
125
+ | Ok(_) => None
126
+ | Error(reason) => Some(InvalidDeliveryWindow({reason: reason}))
127
+ }
128
+ }
129
+
117
130
  let decide = (state, command) =>
118
131
  switch command {
119
132
  | PlaceOrder({orderId, customerId, lineItems, shippingMethod, ?deliveryWindow}) =>
@@ -122,9 +135,13 @@ let decide = (state, command) =>
122
135
  } else if lineItems->Array.length == 0 {
123
136
  Error(OrderIsEmpty)
124
137
  } else {
125
- switch lineItems->Array.find(({quantity}) => quantity <= 0) {
126
- | Some({productId, quantity}) => Error(InvalidQuantity({productId, quantity}))
127
- | None =>
138
+ switch (
139
+ lineItems->Array.find(({quantity}) => quantity <= 0),
140
+ deliveryWindowError(~shippingMethod, ~deliveryWindow),
141
+ ) {
142
+ | (Some({productId, quantity}), _) => Error(InvalidQuantity({productId, quantity}))
143
+ | (None, Some(error)) => Error(error)
144
+ | (None, None) =>
128
145
  let merged = mergeLines(lineItems)
129
146
  let missing =
130
147
  merged->Array.filterMap(({productId}) =>
@@ -5,6 +5,7 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
5
  import * as Money$Reventless from "@reventlessdev/reventless-spec/src/semantic/Money.res.mjs";
6
6
  import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
7
7
  import * as Currency$Reventless from "@reventlessdev/reventless-spec/src/semantic/Currency.res.mjs";
8
+ import * as DateRange$Reventless from "@reventlessdev/reventless-spec/src/semantic/DateRange.res.mjs";
8
9
 
9
10
  let initialState_placedOrderIds = [];
10
11
 
@@ -175,6 +176,28 @@ function priceLines(state, lineItems) {
175
176
  });
176
177
  }
177
178
 
179
+ function deliveryWindowError(shippingMethod, deliveryWindow) {
180
+ if (deliveryWindow === undefined) {
181
+ return;
182
+ }
183
+ switch (shippingMethod) {
184
+ case "Standard" :
185
+ case "Express" :
186
+ break;
187
+ case "Pickup" :
188
+ return "DeliveryWindowOnPickup";
189
+ }
190
+ let reason = DateRange$Reventless.validate(deliveryWindow);
191
+ if (reason.TAG === "Ok") {
192
+ return;
193
+ } else {
194
+ return {
195
+ TAG: "InvalidDeliveryWindow",
196
+ reason: reason._0
197
+ };
198
+ }
199
+ }
200
+
178
201
  function decide(state, command) {
179
202
  let orderId = command.orderId;
180
203
  if (state.placedOrderIds.includes(orderId)) {
@@ -190,7 +213,10 @@ function decide(state, command) {
190
213
  _0: "OrderIsEmpty"
191
214
  };
192
215
  }
216
+ let deliveryWindow = command.deliveryWindow;
217
+ let shippingMethod = command.shippingMethod;
193
218
  let match = lineItems.find(param => param.quantity <= 0);
219
+ let match$1 = deliveryWindowError(shippingMethod, deliveryWindow);
194
220
  if (match !== undefined) {
195
221
  return {
196
222
  TAG: "Error",
@@ -201,6 +227,12 @@ function decide(state, command) {
201
227
  }
202
228
  };
203
229
  }
230
+ if (match$1 !== undefined) {
231
+ return {
232
+ TAG: "Error",
233
+ _0: match$1
234
+ };
235
+ }
204
236
  let merged = mergeLines(lineItems);
205
237
  let missing = Stdlib_Array.filterMap(merged, param => {
206
238
  let productId = param.productId;
@@ -224,14 +256,14 @@ function decide(state, command) {
224
256
  return failed;
225
257
  }
226
258
  let lines = failed._0;
227
- let match$1 = Money$Reventless.sum(lines.map(line => line.lineTotal));
228
- if (match$1 === undefined) {
259
+ let match$2 = Money$Reventless.sum(lines.map(line => line.lineTotal));
260
+ if (match$2 === undefined) {
229
261
  return {
230
262
  TAG: "Error",
231
263
  _0: "OrderIsEmpty"
232
264
  };
233
265
  }
234
- if (match$1.TAG !== "Ok") {
266
+ if (match$2.TAG !== "Ok") {
235
267
  return {
236
268
  TAG: "Error",
237
269
  _0: {
@@ -258,9 +290,9 @@ function decide(state, command) {
258
290
  customerId: command.customerId,
259
291
  productIds: lines.map(line => line.productId),
260
292
  lines: lines,
261
- total: match$1._0,
262
- shippingMethod: command.shippingMethod,
263
- deliveryWindow: command.deliveryWindow,
293
+ total: match$2._0,
294
+ shippingMethod: shippingMethod,
295
+ deliveryWindow: deliveryWindow,
264
296
  firstProductName: firstProductName,
265
297
  firstProductImage: firstProductImage
266
298
  }]
@@ -281,6 +313,7 @@ export {
281
313
  mergeLines,
282
314
  priceLine,
283
315
  priceLines,
316
+ deliveryWindowError,
284
317
  decide,
285
318
  moduleUrl,
286
319
  }
@@ -262,6 +262,52 @@ describe("PlaceOrder StateChangeSlice", () => {
262
262
  )
263
263
  })
264
264
 
265
+ test("a delivery window on a pickup order is refused", () => {
266
+ let window =
267
+ Reventless.DateRange.make(
268
+ ~start="2026-03-02T09:00:00Z",
269
+ ~end_="2026-03-02T11:00:00Z",
270
+ )->Result.getOrThrow
271
+ givenEvents([synced(~id="p1", ~name="Fathom Dock")])
272
+ ->whenCmd(
273
+ PlaceOrder({
274
+ orderId: "o1",
275
+ customerId: "c1",
276
+ lineItems: [line(~id="p1")],
277
+ shippingMethod: Pickup,
278
+ deliveryWindow: window,
279
+ }),
280
+ )
281
+ ->thenError(DeliveryWindowOnPickup)
282
+ })
283
+
284
+ // Built as a record rather than through `DateRange.make`, which would refuse
285
+ // it: a client sends JSON, and decoding does not apply the ordering rule.
286
+ test("a delivery window that ends before it starts is refused", () => {
287
+ let reversed = {
288
+ Reventless.DateRange.start: "2026-03-02T11:00:00Z",
289
+ end_: "2026-03-02T09:00:00Z",
290
+ }
291
+ givenEvents([synced(~id="p1", ~name="Fathom Dock")])
292
+ ->whenCmd(
293
+ PlaceOrder({
294
+ orderId: "o1",
295
+ customerId: "c1",
296
+ lineItems: [line(~id="p1")],
297
+ shippingMethod: Standard,
298
+ deliveryWindow: reversed,
299
+ }),
300
+ )
301
+ ->thenError(
302
+ InvalidDeliveryWindow({
303
+ reason: switch Reventless.DateRange.validate(reversed) {
304
+ | Error(reason) => reason
305
+ | Ok(_) => "expected the ordering rule to refuse this range"
306
+ },
307
+ }),
308
+ )
309
+ })
310
+
265
311
  test("partial product availability returns ProductsNotAvailable with missing list", () =>
266
312
  givenEvents([synced(~id="p1", ~name="Fathom Dock")])
267
313
  ->whenCmd(
@@ -336,6 +336,45 @@ describe("PlaceOrder StateChangeSlice", () => {
336
336
  firstProductName: "Fathom Dock"
337
337
  });
338
338
  });
339
+ test("a delivery window on a pickup order is refused", () => {
340
+ let window = Stdlib_Result.getOrThrow(DateRange$Reventless.make("2026-03-02T09:00:00Z", "2026-03-02T11:00:00Z"), undefined);
341
+ let qty = 1;
342
+ return thenError(whenCmd(givenEvents([synced("p1", "Fathom Dock", undefined)]), {
343
+ TAG: "PlaceOrder",
344
+ orderId: "o1",
345
+ customerId: "c1",
346
+ lineItems: [{
347
+ productId: "p1",
348
+ quantity: qty
349
+ }],
350
+ shippingMethod: "Pickup",
351
+ deliveryWindow: window
352
+ }), "DeliveryWindowOnPickup");
353
+ });
354
+ test("a delivery window that ends before it starts is refused", () => {
355
+ let reversed = {
356
+ start: "2026-03-02T11:00:00Z",
357
+ end: "2026-03-02T09:00:00Z"
358
+ };
359
+ let qty = 1;
360
+ let reason = DateRange$Reventless.validate(reversed);
361
+ let tmp;
362
+ tmp = reason.TAG === "Ok" ? "expected the ordering rule to refuse this range" : reason._0;
363
+ return thenError(whenCmd(givenEvents([synced("p1", "Fathom Dock", undefined)]), {
364
+ TAG: "PlaceOrder",
365
+ orderId: "o1",
366
+ customerId: "c1",
367
+ lineItems: [{
368
+ productId: "p1",
369
+ quantity: qty
370
+ }],
371
+ shippingMethod: "Standard",
372
+ deliveryWindow: reversed
373
+ }), {
374
+ TAG: "InvalidDeliveryWindow",
375
+ reason: tmp
376
+ });
377
+ });
339
378
  test("partial product availability returns ProductsNotAvailable with missing list", () => {
340
379
  let qty = 1;
341
380
  let qty$1 = 1;