@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 +15 -0
- package/package.json +9 -9
- package/src/Order/StateChange/PlaceOrder.res +5 -0
- package/src/Order/StateChange/PlaceOrder.res.mjs +5 -0
- package/src/Order/StateChange/PlaceOrder_Behavior.res +20 -3
- package/src/Order/StateChange/PlaceOrder_Behavior.res.mjs +39 -6
- package/tests/Order/StateChange/PlaceOrder_GWT.res +46 -0
- package/tests/Order/StateChange/PlaceOrder_GWT.res.mjs +39 -0
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.
|
|
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-
|
|
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-
|
|
27
|
-
"@reventlessdev/reventless-
|
|
28
|
-
"@reventlessdev/online-shop-hybrid-
|
|
29
|
-
"@reventlessdev/reventless-local": "3.0.0-alpha.
|
|
30
|
-
"@reventlessdev/
|
|
31
|
-
"@reventlessdev/
|
|
32
|
-
"@reventlessdev/trait-
|
|
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
|
|
126
|
-
|
|
127
|
-
|
|
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$
|
|
228
|
-
if (match$
|
|
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$
|
|
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$
|
|
262
|
-
shippingMethod:
|
|
263
|
-
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;
|