@reventlessdev/online-shop-hybrid-seed 1.0.0-alpha.2
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 +10 -0
- package/LICENSE +202 -0
- package/lib/bs/.compiler.log +2 -0
- package/lib/bs/OnlineShopHybridSeed.cmi +0 -0
- package/lib/bs/OnlineShopHybridSeed.cmj +0 -0
- package/lib/bs/OnlineShopHybridSeed.cmt +0 -0
- package/lib/bs/OnlineShopHybridSeed.mlmap +4 -0
- package/lib/bs/build.ninja +0 -0
- package/lib/bs/compiler-info.json +8 -0
- package/lib/bs/src/DemoCommands-OnlineShopHybridSeed.cmi +0 -0
- package/lib/bs/src/DemoCommands-OnlineShopHybridSeed.cmj +0 -0
- package/lib/bs/src/DemoCommands-OnlineShopHybridSeed.cmt +0 -0
- package/lib/bs/src/DemoCommands.ast +0 -0
- package/lib/bs/src/DemoCommands.res +154 -0
- package/lib/bs/src/DemoCommands.res.mjs +388 -0
- package/lib/bs/src/DemoData-OnlineShopHybridSeed.cmi +0 -0
- package/lib/bs/src/DemoData-OnlineShopHybridSeed.cmj +0 -0
- package/lib/bs/src/DemoData-OnlineShopHybridSeed.cmt +0 -0
- package/lib/bs/src/DemoData.ast +0 -0
- package/lib/bs/src/DemoData.res +307 -0
- package/lib/bs/src/DemoData.res.mjs +481 -0
- package/lib/bs/src/HybridSeedData-OnlineShopHybridSeed.cmi +0 -0
- package/lib/bs/src/HybridSeedData-OnlineShopHybridSeed.cmj +0 -0
- package/lib/bs/src/HybridSeedData-OnlineShopHybridSeed.cmt +0 -0
- package/lib/bs/src/HybridSeedData.ast +0 -0
- package/lib/bs/src/HybridSeedData.res +418 -0
- package/lib/bs/src/HybridSeedData.res.mjs +317 -0
- package/lib/ocaml/.compiler.log +2 -0
- package/lib/ocaml/DemoCommands-OnlineShopHybridSeed.cmi +0 -0
- package/lib/ocaml/DemoCommands-OnlineShopHybridSeed.cmj +0 -0
- package/lib/ocaml/DemoCommands-OnlineShopHybridSeed.cmt +0 -0
- package/lib/ocaml/DemoCommands.ast +0 -0
- package/lib/ocaml/DemoCommands.res +154 -0
- package/lib/ocaml/DemoData-OnlineShopHybridSeed.cmi +0 -0
- package/lib/ocaml/DemoData-OnlineShopHybridSeed.cmj +0 -0
- package/lib/ocaml/DemoData-OnlineShopHybridSeed.cmt +0 -0
- package/lib/ocaml/DemoData.ast +0 -0
- package/lib/ocaml/DemoData.res +307 -0
- package/lib/ocaml/HybridSeedData-OnlineShopHybridSeed.cmi +0 -0
- package/lib/ocaml/HybridSeedData-OnlineShopHybridSeed.cmj +0 -0
- package/lib/ocaml/HybridSeedData-OnlineShopHybridSeed.cmt +0 -0
- package/lib/ocaml/HybridSeedData.ast +0 -0
- package/lib/ocaml/HybridSeedData.res +418 -0
- package/lib/ocaml/OnlineShopHybridSeed.cmi +0 -0
- package/lib/ocaml/OnlineShopHybridSeed.cmj +0 -0
- package/lib/ocaml/OnlineShopHybridSeed.cmt +0 -0
- package/lib/ocaml/OnlineShopHybridSeed.mlmap +4 -0
- package/package.json +34 -0
- package/rescript.json +27 -0
- package/src/DemoCommands.res +154 -0
- package/src/DemoCommands.res.mjs +388 -0
- package/src/DemoData.res +307 -0
- package/src/DemoData.res.mjs +481 -0
- package/src/HybridSeedData.res +418 -0
- package/src/HybridSeedData.res.mjs +317 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Seed$ReventlessSeed from "@reventlessdev/reventless-seed/src/Seed.res.mjs";
|
|
4
|
+
|
|
5
|
+
function catalog(name) {
|
|
6
|
+
return `Catalog_` + name;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function ordering(name) {
|
|
10
|
+
return `Ordering_` + name;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function addCategory(command) {
|
|
14
|
+
return Seed$ReventlessSeed.mutation(`Catalog_` + "AddCategory", [
|
|
15
|
+
[
|
|
16
|
+
"categoryId",
|
|
17
|
+
{
|
|
18
|
+
TAG: "Id",
|
|
19
|
+
_0: command.categoryId
|
|
20
|
+
}
|
|
21
|
+
],
|
|
22
|
+
[
|
|
23
|
+
"name",
|
|
24
|
+
{
|
|
25
|
+
TAG: "String",
|
|
26
|
+
_0: command.name
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
]);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function renameCategory(command) {
|
|
33
|
+
return Seed$ReventlessSeed.mutation(`Catalog_` + "RenameCategory", [
|
|
34
|
+
[
|
|
35
|
+
"categoryId",
|
|
36
|
+
{
|
|
37
|
+
TAG: "Id",
|
|
38
|
+
_0: command.categoryId
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
[
|
|
42
|
+
"name",
|
|
43
|
+
{
|
|
44
|
+
TAG: "String",
|
|
45
|
+
_0: command.name
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
]);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function archiveCategory(command) {
|
|
52
|
+
return Seed$ReventlessSeed.mutation(`Catalog_` + "ArchiveCategory", [[
|
|
53
|
+
"categoryId",
|
|
54
|
+
{
|
|
55
|
+
TAG: "Id",
|
|
56
|
+
_0: command.categoryId
|
|
57
|
+
}
|
|
58
|
+
]]);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function addProduct(command) {
|
|
62
|
+
return Seed$ReventlessSeed.mutation(`Catalog_` + "AddProduct", [
|
|
63
|
+
[
|
|
64
|
+
"productId",
|
|
65
|
+
{
|
|
66
|
+
TAG: "Id",
|
|
67
|
+
_0: command.productId
|
|
68
|
+
}
|
|
69
|
+
],
|
|
70
|
+
[
|
|
71
|
+
"name",
|
|
72
|
+
{
|
|
73
|
+
TAG: "String",
|
|
74
|
+
_0: command.name
|
|
75
|
+
}
|
|
76
|
+
],
|
|
77
|
+
[
|
|
78
|
+
"description",
|
|
79
|
+
{
|
|
80
|
+
TAG: "String",
|
|
81
|
+
_0: command.description
|
|
82
|
+
}
|
|
83
|
+
],
|
|
84
|
+
[
|
|
85
|
+
"price",
|
|
86
|
+
{
|
|
87
|
+
TAG: "Float",
|
|
88
|
+
_0: command.price
|
|
89
|
+
}
|
|
90
|
+
],
|
|
91
|
+
[
|
|
92
|
+
"imageUrl",
|
|
93
|
+
{
|
|
94
|
+
TAG: "String",
|
|
95
|
+
_0: command.imageUrl
|
|
96
|
+
}
|
|
97
|
+
],
|
|
98
|
+
[
|
|
99
|
+
"categoryId",
|
|
100
|
+
{
|
|
101
|
+
TAG: "Id",
|
|
102
|
+
_0: command.categoryId
|
|
103
|
+
}
|
|
104
|
+
]
|
|
105
|
+
]);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function changeProductPrice(command) {
|
|
109
|
+
return Seed$ReventlessSeed.mutation(`Catalog_` + "ChangeProductPrice", [
|
|
110
|
+
[
|
|
111
|
+
"productId",
|
|
112
|
+
{
|
|
113
|
+
TAG: "Id",
|
|
114
|
+
_0: command.productId
|
|
115
|
+
}
|
|
116
|
+
],
|
|
117
|
+
[
|
|
118
|
+
"price",
|
|
119
|
+
{
|
|
120
|
+
TAG: "Float",
|
|
121
|
+
_0: command.price
|
|
122
|
+
}
|
|
123
|
+
]
|
|
124
|
+
]);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function changeProductDescription(command) {
|
|
128
|
+
return Seed$ReventlessSeed.mutation(`Catalog_` + "ChangeProductDescription", [
|
|
129
|
+
[
|
|
130
|
+
"productId",
|
|
131
|
+
{
|
|
132
|
+
TAG: "Id",
|
|
133
|
+
_0: command.productId
|
|
134
|
+
}
|
|
135
|
+
],
|
|
136
|
+
[
|
|
137
|
+
"description",
|
|
138
|
+
{
|
|
139
|
+
TAG: "String",
|
|
140
|
+
_0: command.description
|
|
141
|
+
}
|
|
142
|
+
]
|
|
143
|
+
]);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function importProduct(input) {
|
|
147
|
+
return Seed$ReventlessSeed.mutation(`Catalog_` + "ImportProduct", [
|
|
148
|
+
[
|
|
149
|
+
"sku",
|
|
150
|
+
{
|
|
151
|
+
TAG: "String",
|
|
152
|
+
_0: input.sku
|
|
153
|
+
}
|
|
154
|
+
],
|
|
155
|
+
[
|
|
156
|
+
"title",
|
|
157
|
+
{
|
|
158
|
+
TAG: "String",
|
|
159
|
+
_0: input.title
|
|
160
|
+
}
|
|
161
|
+
],
|
|
162
|
+
[
|
|
163
|
+
"desc",
|
|
164
|
+
{
|
|
165
|
+
TAG: "String",
|
|
166
|
+
_0: input.desc
|
|
167
|
+
}
|
|
168
|
+
],
|
|
169
|
+
[
|
|
170
|
+
"unitPrice",
|
|
171
|
+
{
|
|
172
|
+
TAG: "Int",
|
|
173
|
+
_0: input.unitPrice
|
|
174
|
+
}
|
|
175
|
+
],
|
|
176
|
+
[
|
|
177
|
+
"currency",
|
|
178
|
+
{
|
|
179
|
+
TAG: "String",
|
|
180
|
+
_0: input.currency
|
|
181
|
+
}
|
|
182
|
+
],
|
|
183
|
+
[
|
|
184
|
+
"category",
|
|
185
|
+
{
|
|
186
|
+
TAG: "Id",
|
|
187
|
+
_0: input.category
|
|
188
|
+
}
|
|
189
|
+
]
|
|
190
|
+
]);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function shippingMethod(method) {
|
|
194
|
+
switch (method) {
|
|
195
|
+
case "Standard" :
|
|
196
|
+
return {
|
|
197
|
+
TAG: "Enum",
|
|
198
|
+
_0: "Standard"
|
|
199
|
+
};
|
|
200
|
+
case "Express" :
|
|
201
|
+
return {
|
|
202
|
+
TAG: "Enum",
|
|
203
|
+
_0: "Express"
|
|
204
|
+
};
|
|
205
|
+
case "Pickup" :
|
|
206
|
+
return {
|
|
207
|
+
TAG: "Enum",
|
|
208
|
+
_0: "Pickup"
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function placeOrder(command) {
|
|
214
|
+
return Seed$ReventlessSeed.mutation(`Ordering_` + "PlaceOrder", [
|
|
215
|
+
[
|
|
216
|
+
"orderId",
|
|
217
|
+
{
|
|
218
|
+
TAG: "Id",
|
|
219
|
+
_0: command.orderId
|
|
220
|
+
}
|
|
221
|
+
],
|
|
222
|
+
[
|
|
223
|
+
"customerId",
|
|
224
|
+
{
|
|
225
|
+
TAG: "Id",
|
|
226
|
+
_0: command.customerId
|
|
227
|
+
}
|
|
228
|
+
],
|
|
229
|
+
[
|
|
230
|
+
"productIds",
|
|
231
|
+
Seed$ReventlessSeed.ids(command.productIds)
|
|
232
|
+
],
|
|
233
|
+
[
|
|
234
|
+
"shippingMethod",
|
|
235
|
+
shippingMethod(command.shippingMethod)
|
|
236
|
+
]
|
|
237
|
+
]);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function shipOrder(command) {
|
|
241
|
+
return Seed$ReventlessSeed.mutation(`Ordering_` + "ShipOrder", [[
|
|
242
|
+
"orderId",
|
|
243
|
+
{
|
|
244
|
+
TAG: "Id",
|
|
245
|
+
_0: command.orderId
|
|
246
|
+
}
|
|
247
|
+
]]);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function cancelOrder(command) {
|
|
251
|
+
if (command.TAG === "CancelOrder") {
|
|
252
|
+
return Seed$ReventlessSeed.mutation(`Ordering_` + "CancelOrder", [[
|
|
253
|
+
"orderId",
|
|
254
|
+
{
|
|
255
|
+
TAG: "Id",
|
|
256
|
+
_0: command.orderId
|
|
257
|
+
}
|
|
258
|
+
]]);
|
|
259
|
+
}
|
|
260
|
+
throw {
|
|
261
|
+
RE_EXN_ID: Seed$ReventlessSeed.Failed,
|
|
262
|
+
_1: "ReopenOrder is @noApi — it cannot be seeded through the GraphQL API",
|
|
263
|
+
Error: new Error()
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function customer(id, command) {
|
|
268
|
+
if (typeof command !== "object") {
|
|
269
|
+
return Seed$ReventlessSeed.mutation(`Ordering_` + "Customer_Deactivate", [[
|
|
270
|
+
"id",
|
|
271
|
+
{
|
|
272
|
+
TAG: "Id",
|
|
273
|
+
_0: id
|
|
274
|
+
}
|
|
275
|
+
]]);
|
|
276
|
+
}
|
|
277
|
+
switch (command.TAG) {
|
|
278
|
+
case "Register" :
|
|
279
|
+
return Seed$ReventlessSeed.mutation(`Ordering_` + "Customer_Register", [
|
|
280
|
+
[
|
|
281
|
+
"id",
|
|
282
|
+
{
|
|
283
|
+
TAG: "Id",
|
|
284
|
+
_0: id
|
|
285
|
+
}
|
|
286
|
+
],
|
|
287
|
+
[
|
|
288
|
+
"email",
|
|
289
|
+
{
|
|
290
|
+
TAG: "String",
|
|
291
|
+
_0: command.email
|
|
292
|
+
}
|
|
293
|
+
],
|
|
294
|
+
[
|
|
295
|
+
"address",
|
|
296
|
+
{
|
|
297
|
+
TAG: "String",
|
|
298
|
+
_0: command.address
|
|
299
|
+
}
|
|
300
|
+
]
|
|
301
|
+
]);
|
|
302
|
+
case "UpdateEmail" :
|
|
303
|
+
return Seed$ReventlessSeed.mutation(`Ordering_` + "Customer_UpdateEmail", [
|
|
304
|
+
[
|
|
305
|
+
"id",
|
|
306
|
+
{
|
|
307
|
+
TAG: "Id",
|
|
308
|
+
_0: id
|
|
309
|
+
}
|
|
310
|
+
],
|
|
311
|
+
[
|
|
312
|
+
"email",
|
|
313
|
+
{
|
|
314
|
+
TAG: "String",
|
|
315
|
+
_0: command.email
|
|
316
|
+
}
|
|
317
|
+
]
|
|
318
|
+
]);
|
|
319
|
+
case "UpdateAddress" :
|
|
320
|
+
return Seed$ReventlessSeed.mutation(`Ordering_` + "Customer_UpdateAddress", [
|
|
321
|
+
[
|
|
322
|
+
"id",
|
|
323
|
+
{
|
|
324
|
+
TAG: "Id",
|
|
325
|
+
_0: id
|
|
326
|
+
}
|
|
327
|
+
],
|
|
328
|
+
[
|
|
329
|
+
"address",
|
|
330
|
+
{
|
|
331
|
+
TAG: "String",
|
|
332
|
+
_0: command.address
|
|
333
|
+
}
|
|
334
|
+
]
|
|
335
|
+
]);
|
|
336
|
+
case "SetLocation" :
|
|
337
|
+
let location = command.location;
|
|
338
|
+
return Seed$ReventlessSeed.mutation(`Ordering_` + "Customer_SetLocation", [
|
|
339
|
+
[
|
|
340
|
+
"id",
|
|
341
|
+
{
|
|
342
|
+
TAG: "Id",
|
|
343
|
+
_0: id
|
|
344
|
+
}
|
|
345
|
+
],
|
|
346
|
+
[
|
|
347
|
+
"location",
|
|
348
|
+
{
|
|
349
|
+
TAG: "Object",
|
|
350
|
+
_0: [
|
|
351
|
+
[
|
|
352
|
+
"lat",
|
|
353
|
+
{
|
|
354
|
+
TAG: "Float",
|
|
355
|
+
_0: location.lat
|
|
356
|
+
}
|
|
357
|
+
],
|
|
358
|
+
[
|
|
359
|
+
"lng",
|
|
360
|
+
{
|
|
361
|
+
TAG: "Float",
|
|
362
|
+
_0: location.lng
|
|
363
|
+
}
|
|
364
|
+
]
|
|
365
|
+
]
|
|
366
|
+
}
|
|
367
|
+
]
|
|
368
|
+
]);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export {
|
|
373
|
+
catalog,
|
|
374
|
+
ordering,
|
|
375
|
+
addCategory,
|
|
376
|
+
renameCategory,
|
|
377
|
+
archiveCategory,
|
|
378
|
+
addProduct,
|
|
379
|
+
changeProductPrice,
|
|
380
|
+
changeProductDescription,
|
|
381
|
+
importProduct,
|
|
382
|
+
shippingMethod,
|
|
383
|
+
placeOrder,
|
|
384
|
+
shipOrder,
|
|
385
|
+
cancelOrder,
|
|
386
|
+
customer,
|
|
387
|
+
}
|
|
388
|
+
/* No side effect */
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
// The demo dataset for the online-shop-hybrid example.
|
|
2
|
+
//
|
|
3
|
+
// This file holds *what* to seed: literal data plus the deterministic
|
|
4
|
+
// generation that turns it into entities. Every command it produces is a real
|
|
5
|
+
// plugin command value — `CatalogPlugin.AddProduct.command`,
|
|
6
|
+
// `OrderingPlugin.PlaceOrder.command`, and so on — so a renamed or re-shaped
|
|
7
|
+
// command breaks the build here rather than failing at runtime against a
|
|
8
|
+
// half-seeded store.
|
|
9
|
+
//
|
|
10
|
+
// The mapping from those command values onto GraphQL mutations lives in
|
|
11
|
+
// `DemoCommands.res`; the run itself lives in `DemoSeed.res`.
|
|
12
|
+
|
|
13
|
+
// A fixed seed and fixed literal data: two runs against a fresh store produce
|
|
14
|
+
// identical rows, so a seeded store is a usable baseline for comparison.
|
|
15
|
+
let random = ReventlessSeed.Seed.Random.make(~seed=0x5eed)
|
|
16
|
+
|
|
17
|
+
let productCount = 60
|
|
18
|
+
let customerCount = 20
|
|
19
|
+
let orderCount = 150
|
|
20
|
+
|
|
21
|
+
// ── Categories ──────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
type category = {
|
|
24
|
+
id: string,
|
|
25
|
+
name: string,
|
|
26
|
+
weight: int,
|
|
27
|
+
nouns: array<string>,
|
|
28
|
+
archive: bool,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let categories: array<category> = [
|
|
32
|
+
{id: "cat-01", name: "Laptops", weight: 9, nouns: ["Notebook", "Ultrabook", "Workstation"], archive: false},
|
|
33
|
+
{id: "cat-02", name: "Phones", weight: 10, nouns: ["Handset", "Smartphone", "Phone"], archive: false},
|
|
34
|
+
{id: "cat-03", name: "Audio", weight: 9, nouns: ["Headphones", "Earbuds", "Speaker"], archive: false},
|
|
35
|
+
{id: "cat-04", name: "Cameras", weight: 7, nouns: ["Camera", "Lens", "Gimbal"], archive: false},
|
|
36
|
+
{id: "cat-05", name: "Wearables", weight: 7, nouns: ["Watch", "Tracker", "Band"], archive: false},
|
|
37
|
+
{id: "cat-06", name: "Home Office", weight: 8, nouns: ["Desk Lamp", "Monitor", "Keyboard"], archive: false},
|
|
38
|
+
{id: "cat-07", name: "Accessories", weight: 6, nouns: ["Cable", "Adapter", "Case"], archive: false},
|
|
39
|
+
// Archived at the end of the catalog phase — after its products exist, since
|
|
40
|
+
// AddProduct rejects an archived category with CategoryNotFound.
|
|
41
|
+
{id: "cat-08", name: "Clearance", weight: 4, nouns: ["Bundle", "Refurb Kit"], archive: true},
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
let renamedCategoryId = "cat-06"
|
|
45
|
+
let renamedCategoryName = "Home & Office"
|
|
46
|
+
|
|
47
|
+
// ── Word pools ──────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
let brands = ["Aurora", "Basalt", "Cirrus", "Dovetail", "Ember", "Fathom", "Granite", "Halcyon"]
|
|
50
|
+
let qualifiers = ["Pro", "Air", "Max", "Lite", "Studio", "Go", "Plus", ""]
|
|
51
|
+
let blurbs = ["refreshed", "best-selling", "entry-level", "flagship", "compact"]
|
|
52
|
+
|
|
53
|
+
let firstNames = [
|
|
54
|
+
"Ada", "Bruno", "Chiara", "Diego", "Elif", "Farid", "Greta", "Hugo", "Iris", "Jonas",
|
|
55
|
+
"Kaja", "Luca", "Maya", "Noor", "Olof", "Pia", "Rafael", "Sana", "Tomas", "Vera",
|
|
56
|
+
]
|
|
57
|
+
let lastNames = [
|
|
58
|
+
"Almeida", "Beck", "Costa", "Duarte", "Engel", "Ferrer", "Gruber", "Haas", "Ivanov", "Jansen",
|
|
59
|
+
"Klein", "Lindqvist", "Moreau", "Nagy", "Olsen", "Petrov", "Rossi", "Sandberg", "Tamm", "Vogel",
|
|
60
|
+
]
|
|
61
|
+
let streets = ["Bakergasse", "Cedar Lane", "Dockside Way", "Elm Row", "Foundry Street"]
|
|
62
|
+
let cities = ["Bruges", "Cortona", "Delft", "Espoo", "Freiburg", "Gdansk"]
|
|
63
|
+
|
|
64
|
+
// Real coordinates for each entry in `cities`, index-aligned, so the Customers
|
|
65
|
+
// map view drops each pin on the customer's actual city rather than at (0, 0).
|
|
66
|
+
let cityCoords: array<(float, float)> = [
|
|
67
|
+
(51.2093, 3.2247), // Bruges
|
|
68
|
+
(43.2749, 11.9853), // Cortona
|
|
69
|
+
(52.0116, 4.3571), // Delft
|
|
70
|
+
(60.2055, 24.6559), // Espoo
|
|
71
|
+
(47.999, 7.8421), // Freiburg
|
|
72
|
+
(54.352, 18.6466), // Gdansk
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
// ── Supplier feed ───────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
// Rows for the ImportProduct InboundTranslationSlice. The last one is
|
|
78
|
+
// deliberately invalid so the ImportProductAudit view shows both outcomes.
|
|
79
|
+
let supplierFeed: array<CatalogPlugin.ImportProduct.externalInput> = [
|
|
80
|
+
{sku: "SKU-4410", title: "Fathom Dock 4-Port", desc: "Supplier-fed docking station.", unitPrice: 8990, currency: "USD", category: "cat-07"},
|
|
81
|
+
{sku: "SKU-4411", title: "Cirrus Travel Charger", desc: "Supplier-fed 65W charger.", unitPrice: 4550, currency: "USD", category: "cat-07"},
|
|
82
|
+
{sku: "SKU-4412", title: "Granite Laptop Sleeve", desc: "Supplier-fed protective sleeve.", unitPrice: 3200, currency: "USD", category: "cat-07"},
|
|
83
|
+
{sku: "SKU-4413", title: "Halcyon Desk Riser", desc: "Supplier-fed monitor riser.", unitPrice: 12400, currency: "USD", category: "cat-06"},
|
|
84
|
+
{sku: "SKU-4414", title: "Ember Cable Set", desc: "Rejected: non-USD supplier row.", unitPrice: 1900, currency: "EUR", category: "cat-07"},
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
let importedSkus =
|
|
88
|
+
supplierFeed->Array.filter(row => row.currency == "USD")->Array.map(row => row.sku)
|
|
89
|
+
|
|
90
|
+
let expectedImportSuccesses = importedSkus->Array.length
|
|
91
|
+
let expectedImportFailures = supplierFeed->Array.length - expectedImportSuccesses
|
|
92
|
+
|
|
93
|
+
// ── Generation helpers ──────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
let pad = (n: int, width: int): string => n->Int.toString->String.padStart(width, "0")
|
|
96
|
+
|
|
97
|
+
let pick = (xs: array<string>): string =>
|
|
98
|
+
ReventlessSeed.Seed.Random.pickOr(random, ~fallback="", xs)
|
|
99
|
+
|
|
100
|
+
let address = (): string => {
|
|
101
|
+
let number = ReventlessSeed.Seed.Random.int(random, ~min=1, ~max=180)
|
|
102
|
+
`${number->Int.toString} ${pick(streets)}, ${pick(cities)}`
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// An address paired with the coordinates of the city it names, so a customer's
|
|
106
|
+
// map pin lands on the same city that appears in its address text.
|
|
107
|
+
let locatedAddress = (): (string, float, float) => {
|
|
108
|
+
let number = ReventlessSeed.Seed.Random.int(random, ~min=1, ~max=180)
|
|
109
|
+
let cityIndex = ReventlessSeed.Seed.Random.int(random, ~min=0, ~max=cities->Array.length - 1)
|
|
110
|
+
let city = cities->Array.get(cityIndex)->Option.getOr("")
|
|
111
|
+
let (lat, lng) = cityCoords->Array.get(cityIndex)->Option.getOr((0.0, 0.0))
|
|
112
|
+
(`${number->Int.toString} ${pick(streets)}, ${city}`, lat, lng)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ── Products ────────────────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
type product = {
|
|
118
|
+
id: string,
|
|
119
|
+
name: string,
|
|
120
|
+
description: string,
|
|
121
|
+
price: float,
|
|
122
|
+
imageUrl: string,
|
|
123
|
+
categoryId: string,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// A deterministic placeholder image per product: a distinct fill colour derived
|
|
127
|
+
// from the product index plus the product name as a label. SVG is tiny,
|
|
128
|
+
// text-based (no repo binaries, no third-party service), renders in `<img>`, and
|
|
129
|
+
// serves cleanly through both the AWS CloudFront read path and the local dev
|
|
130
|
+
// serve route. Uploaded at seed time (DemoSeed) so each product's `imageUrl`
|
|
131
|
+
// travels the real upload → store → serve loop instead of an external URL. The
|
|
132
|
+
// Image semantic thumbnails the served ref in the generated Products view.
|
|
133
|
+
let escapeXml = (s: string): string =>
|
|
134
|
+
s
|
|
135
|
+
->String.replaceAll("&", "&")
|
|
136
|
+
->String.replaceAll("<", "<")
|
|
137
|
+
->String.replaceAll(">", ">")
|
|
138
|
+
|
|
139
|
+
let productSvg = (~name: string, ~index: int): string => {
|
|
140
|
+
let hue = mod(index * 47, 360)
|
|
141
|
+
let bg = `hsl(${hue->Int.toString}, 62%, 52%)`
|
|
142
|
+
let label = escapeXml(name)
|
|
143
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="400" height="300" viewBox="0 0 400 300">` ++
|
|
144
|
+
`<rect width="400" height="300" fill="${bg}"/>` ++
|
|
145
|
+
`<text x="200" y="160" fill="#ffffff" font-family="sans-serif" font-size="22" font-weight="600" text-anchor="middle">${label}</text>` ++
|
|
146
|
+
`</svg>`
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let buildProducts = (~count=productCount, ()): array<product> => {
|
|
150
|
+
// Category share proportional to weight, so the catalog is lopsided the way a
|
|
151
|
+
// real one is rather than eight even buckets.
|
|
152
|
+
let totalWeight = categories->Array.reduce(0, (sum, c) => sum + c.weight)
|
|
153
|
+
let products = []
|
|
154
|
+
let n = ref(0)
|
|
155
|
+
categories->Array.forEach(category => {
|
|
156
|
+
let exact = Int.toFloat(category.weight) /. Int.toFloat(totalWeight) *. Int.toFloat(count)
|
|
157
|
+
let share = Math.round(exact)->Int.fromFloat
|
|
158
|
+
let share = share < 2 ? 2 : share
|
|
159
|
+
for _ in 1 to share {
|
|
160
|
+
if products->Array.length < count {
|
|
161
|
+
n := n.contents + 1
|
|
162
|
+
let qualifier = pick(qualifiers)
|
|
163
|
+
let suffix = qualifier == "" ? "" : ` ${qualifier}`
|
|
164
|
+
let name = `${pick(brands)} ${pick(category.nouns)}${suffix}`
|
|
165
|
+
// Log-uniform over ~5..900 so the price axis has a long right tail
|
|
166
|
+
// instead of clustering in the middle of a linear range.
|
|
167
|
+
let low = Math.log(5.0)
|
|
168
|
+
let high = Math.log(900.0)
|
|
169
|
+
let raw = Math.exp(low +. ReventlessSeed.Seed.Random.float(random) *. (high -. low))
|
|
170
|
+
let price = Math.round(raw *. 100.0) /. 100.0
|
|
171
|
+
products->Array.push({
|
|
172
|
+
id: `prd-${pad(n.contents, 3)}`,
|
|
173
|
+
name,
|
|
174
|
+
description: `${name} — ${pick(blurbs)} ${category.name->String.toLowerCase} pick.`,
|
|
175
|
+
price,
|
|
176
|
+
// Filled by DemoSeed's upload phase with the served `/{prefix}/{key}`
|
|
177
|
+
// ref once the product's placeholder SVG is uploaded.
|
|
178
|
+
imageUrl: "",
|
|
179
|
+
categoryId: category.id,
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
products
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** A little post-creation churn, so views are not uniformly "created once and
|
|
188
|
+
never touched" and the price-change path through to Ordering is exercised. */
|
|
189
|
+
let repricedProducts = (products: array<product>): array<product> =>
|
|
190
|
+
products->Array.filterWithIndex((_, i) => mod(i, 11) == 4)
|
|
191
|
+
|
|
192
|
+
let redescribedProducts = (products: array<product>): array<product> =>
|
|
193
|
+
products->Array.filterWithIndex((_, i) => mod(i, 17) == 9)
|
|
194
|
+
|
|
195
|
+
let discountedPrice = (p: product): float => Math.round(p.price *. 0.85 *. 100.0) /. 100.0
|
|
196
|
+
|
|
197
|
+
// ── Customers ───────────────────────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
type customer = {
|
|
200
|
+
id: string,
|
|
201
|
+
email: string,
|
|
202
|
+
address: string,
|
|
203
|
+
lat: float,
|
|
204
|
+
lng: float,
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
let buildCustomers = (~count=customerCount, ()): array<customer> =>
|
|
208
|
+
Array.fromInitializer(~length=count, i => {
|
|
209
|
+
let first = firstNames->Array.get(mod(i, firstNames->Array.length))->Option.getOr("Ada")
|
|
210
|
+
let last =
|
|
211
|
+
lastNames->Array.get(mod(i * 7 + 3, lastNames->Array.length))->Option.getOr("Beck")
|
|
212
|
+
let id = `cust-${pad(i + 1, 2)}`
|
|
213
|
+
let (address, lat, lng) = locatedAddress()
|
|
214
|
+
{
|
|
215
|
+
id,
|
|
216
|
+
email: `${first}.${last}@example.com`->String.toLowerCase,
|
|
217
|
+
address,
|
|
218
|
+
lat,
|
|
219
|
+
lng,
|
|
220
|
+
}
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
let movedCustomers = (customers: array<customer>): array<customer> =>
|
|
224
|
+
customers->Array.filterWithIndex((_, i) => mod(i, 6) == 2)
|
|
225
|
+
|
|
226
|
+
let deactivatedCustomers = (customers: array<customer>): array<customer> =>
|
|
227
|
+
customers->Array.filterWithIndex((_, i) => mod(i, 9) == 5)
|
|
228
|
+
|
|
229
|
+
let newAddress = () => address()
|
|
230
|
+
|
|
231
|
+
// ── Orders ──────────────────────────────────────────────────────────────────
|
|
232
|
+
|
|
233
|
+
type order = {
|
|
234
|
+
id: string,
|
|
235
|
+
customerId: string,
|
|
236
|
+
productIds: array<string>,
|
|
237
|
+
shippingMethod: OrderingPlugin.PlaceOrder.shippingMethod,
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
let buildOrders = (
|
|
241
|
+
products: array<product>,
|
|
242
|
+
customers: array<customer>,
|
|
243
|
+
~count=orderCount,
|
|
244
|
+
(),
|
|
245
|
+
): array<order> => {
|
|
246
|
+
// Zipf over a fixed shuffle: a handful of products carry most of the demand
|
|
247
|
+
// and the tail is long, so ProductDemand reads as a real leaderboard.
|
|
248
|
+
let shuffled = ReventlessSeed.Seed.Random.sampleWeighted(
|
|
249
|
+
random,
|
|
250
|
+
products->Array.map(p => (p, 1.0)),
|
|
251
|
+
~count=products->Array.length,
|
|
252
|
+
)
|
|
253
|
+
let productWeights = ReventlessSeed.Seed.Random.zipfWeights(shuffled, ~exponent=1.1)
|
|
254
|
+
// Milder skew on customers: a few repeat buyers, nobody with zero.
|
|
255
|
+
let customerWeights = ReventlessSeed.Seed.Random.zipfWeights(customers, ~exponent=0.45)
|
|
256
|
+
|
|
257
|
+
Array.fromInitializer(~length=count, i => {
|
|
258
|
+
let sizeRoll = ReventlessSeed.Seed.Random.float(random)
|
|
259
|
+
let size = if sizeRoll < 0.5 {
|
|
260
|
+
1
|
|
261
|
+
} else if sizeRoll < 0.75 {
|
|
262
|
+
2
|
|
263
|
+
} else if sizeRoll < 0.9 {
|
|
264
|
+
3
|
|
265
|
+
} else {
|
|
266
|
+
4
|
|
267
|
+
}
|
|
268
|
+
let customerId =
|
|
269
|
+
ReventlessSeed.Seed.Random.sampleWeighted(random, customerWeights, ~count=1)
|
|
270
|
+
->Array.get(0)
|
|
271
|
+
->Option.mapOr("cust-01", c => c.id)
|
|
272
|
+
let productIds =
|
|
273
|
+
ReventlessSeed.Seed.Random.sampleWeighted(random, productWeights, ~count=size)
|
|
274
|
+
->Array.map(p => p.id)
|
|
275
|
+
// Drives the whole downstream lifecycle: Express is auto-shipped by the
|
|
276
|
+
// AutoShipOrder automation, Standard waits for the batch dispatch, Pickup
|
|
277
|
+
// never ships. This split is what gives the board three columns.
|
|
278
|
+
let methodRoll = ReventlessSeed.Seed.Random.float(random)
|
|
279
|
+
let shippingMethod: OrderingPlugin.PlaceOrder.shippingMethod = if methodRoll < 0.35 {
|
|
280
|
+
Express
|
|
281
|
+
} else if methodRoll < 0.8 {
|
|
282
|
+
Standard
|
|
283
|
+
} else {
|
|
284
|
+
Pickup
|
|
285
|
+
}
|
|
286
|
+
{id: `ord-${pad(i + 1, 3)}`, customerId, productIds, shippingMethod}
|
|
287
|
+
})
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** The warehouse batch run: Standard orders are not the automation's business,
|
|
291
|
+
so they sit in Placed until something dispatches them. Most are shipped;
|
|
292
|
+
the rest stay Placed. */
|
|
293
|
+
let batchDispatched = (orders: array<order>): array<order> =>
|
|
294
|
+
orders
|
|
295
|
+
->Array.filter(o => o.shippingMethod == Standard)
|
|
296
|
+
->Array.filterWithIndex((_, i) => mod(i, 5) != 0)
|
|
297
|
+
|
|
298
|
+
/** Cancellations are drawn only from orders still in Placed — Standard orders
|
|
299
|
+
the batch skipped, and Pickup orders, which never ship. An Express order is
|
|
300
|
+
already Shipped and could not be cancelled. */
|
|
301
|
+
let cancellable = (orders: array<order>, ~dispatched: array<string>): array<order> =>
|
|
302
|
+
orders->Array.filter(o =>
|
|
303
|
+
o.shippingMethod != Express && !(dispatched->Array.includes(o.id))
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
let cancelled = (orders: array<order>): array<order> =>
|
|
307
|
+
orders->Array.filterWithIndex((_, i) => mod(i, 3) == 1)
|