@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.
Files changed (55) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/LICENSE +202 -0
  3. package/lib/bs/.compiler.log +2 -0
  4. package/lib/bs/OnlineShopHybridSeed.cmi +0 -0
  5. package/lib/bs/OnlineShopHybridSeed.cmj +0 -0
  6. package/lib/bs/OnlineShopHybridSeed.cmt +0 -0
  7. package/lib/bs/OnlineShopHybridSeed.mlmap +4 -0
  8. package/lib/bs/build.ninja +0 -0
  9. package/lib/bs/compiler-info.json +8 -0
  10. package/lib/bs/src/DemoCommands-OnlineShopHybridSeed.cmi +0 -0
  11. package/lib/bs/src/DemoCommands-OnlineShopHybridSeed.cmj +0 -0
  12. package/lib/bs/src/DemoCommands-OnlineShopHybridSeed.cmt +0 -0
  13. package/lib/bs/src/DemoCommands.ast +0 -0
  14. package/lib/bs/src/DemoCommands.res +154 -0
  15. package/lib/bs/src/DemoCommands.res.mjs +388 -0
  16. package/lib/bs/src/DemoData-OnlineShopHybridSeed.cmi +0 -0
  17. package/lib/bs/src/DemoData-OnlineShopHybridSeed.cmj +0 -0
  18. package/lib/bs/src/DemoData-OnlineShopHybridSeed.cmt +0 -0
  19. package/lib/bs/src/DemoData.ast +0 -0
  20. package/lib/bs/src/DemoData.res +307 -0
  21. package/lib/bs/src/DemoData.res.mjs +481 -0
  22. package/lib/bs/src/HybridSeedData-OnlineShopHybridSeed.cmi +0 -0
  23. package/lib/bs/src/HybridSeedData-OnlineShopHybridSeed.cmj +0 -0
  24. package/lib/bs/src/HybridSeedData-OnlineShopHybridSeed.cmt +0 -0
  25. package/lib/bs/src/HybridSeedData.ast +0 -0
  26. package/lib/bs/src/HybridSeedData.res +418 -0
  27. package/lib/bs/src/HybridSeedData.res.mjs +317 -0
  28. package/lib/ocaml/.compiler.log +2 -0
  29. package/lib/ocaml/DemoCommands-OnlineShopHybridSeed.cmi +0 -0
  30. package/lib/ocaml/DemoCommands-OnlineShopHybridSeed.cmj +0 -0
  31. package/lib/ocaml/DemoCommands-OnlineShopHybridSeed.cmt +0 -0
  32. package/lib/ocaml/DemoCommands.ast +0 -0
  33. package/lib/ocaml/DemoCommands.res +154 -0
  34. package/lib/ocaml/DemoData-OnlineShopHybridSeed.cmi +0 -0
  35. package/lib/ocaml/DemoData-OnlineShopHybridSeed.cmj +0 -0
  36. package/lib/ocaml/DemoData-OnlineShopHybridSeed.cmt +0 -0
  37. package/lib/ocaml/DemoData.ast +0 -0
  38. package/lib/ocaml/DemoData.res +307 -0
  39. package/lib/ocaml/HybridSeedData-OnlineShopHybridSeed.cmi +0 -0
  40. package/lib/ocaml/HybridSeedData-OnlineShopHybridSeed.cmj +0 -0
  41. package/lib/ocaml/HybridSeedData-OnlineShopHybridSeed.cmt +0 -0
  42. package/lib/ocaml/HybridSeedData.ast +0 -0
  43. package/lib/ocaml/HybridSeedData.res +418 -0
  44. package/lib/ocaml/OnlineShopHybridSeed.cmi +0 -0
  45. package/lib/ocaml/OnlineShopHybridSeed.cmj +0 -0
  46. package/lib/ocaml/OnlineShopHybridSeed.cmt +0 -0
  47. package/lib/ocaml/OnlineShopHybridSeed.mlmap +4 -0
  48. package/package.json +34 -0
  49. package/rescript.json +27 -0
  50. package/src/DemoCommands.res +154 -0
  51. package/src/DemoCommands.res.mjs +388 -0
  52. package/src/DemoData.res +307 -0
  53. package/src/DemoData.res.mjs +481 -0
  54. package/src/HybridSeedData.res +418 -0
  55. 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
@@ -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("&", "&amp;")
136
+ ->String.replaceAll("<", "&lt;")
137
+ ->String.replaceAll(">", "&gt;")
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)