create-protobase 0.23.13 → 0.24.0

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 (240) hide show
  1. package/dist/ask-preset.js +25 -0
  2. package/dist/create-project.js +7 -6
  3. package/dist/index.js +19 -9
  4. package/dist/next-steps.js +8 -2
  5. package/dist/presets.js +17 -0
  6. package/dist/templates/erp/.build-test-abf2a11f/protobase.config.ts +6 -0
  7. package/dist/templates/erp/.build-test-abf2a11f/resources.ts +2 -0
  8. package/dist/templates/erp/.build-test-b878b26f/config/index.ts +2 -0
  9. package/dist/templates/erp/.build-test-b878b26f/config/things/data.ts +2 -0
  10. package/dist/templates/erp/.build-test-b878b26f/config/things/ui.ts +2 -0
  11. package/dist/templates/erp/.env.example +14 -0
  12. package/dist/templates/erp/AGENTS.md +24 -0
  13. package/dist/templates/erp/auth/auth.ts +68 -0
  14. package/dist/templates/erp/components/orders-per-day.tsx +21 -0
  15. package/dist/templates/erp/config/bottom-bar.ts +9 -0
  16. package/dist/templates/erp/config/categories/data.ts +20 -0
  17. package/dist/templates/erp/config/categories/ui.ts +11 -0
  18. package/dist/templates/erp/config/companies/data.ts +23 -0
  19. package/dist/templates/erp/config/companies/ui.ts +22 -0
  20. package/dist/templates/erp/config/company_tags/data.ts +16 -0
  21. package/dist/templates/erp/config/company_tags/ui.ts +7 -0
  22. package/dist/templates/erp/config/countries/data.ts +13 -0
  23. package/dist/templates/erp/config/countries/ui.ts +6 -0
  24. package/dist/templates/erp/config/currencies/data.ts +14 -0
  25. package/dist/templates/erp/config/currencies/ui.ts +6 -0
  26. package/dist/templates/erp/config/emp_master/data.ts +19 -0
  27. package/dist/templates/erp/config/emp_master/ui.ts +7 -0
  28. package/dist/templates/erp/config/index.ts +42 -0
  29. package/dist/templates/erp/config/invoice_lines/data.ts +23 -0
  30. package/dist/templates/erp/config/invoice_lines/ui.ts +7 -0
  31. package/dist/templates/erp/config/invoices/data.ts +26 -0
  32. package/dist/templates/erp/config/invoices/ui.ts +38 -0
  33. package/dist/templates/erp/config/jobs.test.ts +23 -0
  34. package/dist/templates/erp/config/jobs.ts +29 -0
  35. package/dist/templates/erp/config/locations/data.ts +18 -0
  36. package/dist/templates/erp/config/locations/ui.ts +6 -0
  37. package/dist/templates/erp/config/order_lines/data.ts +20 -0
  38. package/dist/templates/erp/config/order_lines/ui.ts +7 -0
  39. package/dist/templates/erp/config/orders/data.ts +27 -0
  40. package/dist/templates/erp/config/orders/ui.ts +41 -0
  41. package/dist/templates/erp/config/organizations/data.ts +17 -0
  42. package/dist/templates/erp/config/organizations/ui.ts +6 -0
  43. package/dist/templates/erp/config/overview/page.tsx +80 -0
  44. package/dist/templates/erp/config/pages.test.ts +14 -0
  45. package/dist/templates/erp/config/people/data.ts +21 -0
  46. package/dist/templates/erp/config/people/ui.ts +6 -0
  47. package/dist/templates/erp/config/products/data.ts +25 -0
  48. package/dist/templates/erp/config/products/ui.ts +6 -0
  49. package/dist/templates/erp/config/roles.test.ts +56 -0
  50. package/dist/templates/erp/config/roles.ts +54 -0
  51. package/dist/templates/erp/config/shell.test.ts +24 -0
  52. package/dist/templates/erp/config/stock_levels/data.ts +16 -0
  53. package/dist/templates/erp/config/stock_levels/ui.ts +6 -0
  54. package/dist/templates/erp/config/stock_moves/data.ts +22 -0
  55. package/dist/templates/erp/config/stock_moves/ui.ts +25 -0
  56. package/dist/templates/erp/config/tags/data.ts +17 -0
  57. package/dist/templates/erp/config/tags/ui.ts +6 -0
  58. package/dist/templates/erp/config/user-menu.ts +9 -0
  59. package/dist/templates/erp/config/users/data.ts +19 -0
  60. package/dist/templates/erp/config/users/ui.ts +9 -0
  61. package/dist/templates/erp/config/warehouses/data.ts +19 -0
  62. package/dist/templates/erp/config/warehouses/ui.ts +6 -0
  63. package/dist/templates/erp/data.ts +3 -0
  64. package/dist/templates/erp/db/connection.ts +7 -0
  65. package/dist/templates/erp/db/drop.ts +9 -0
  66. package/dist/templates/erp/db/kysely.ts +13 -0
  67. package/dist/templates/erp/db/migrations/001_core.sql +48 -0
  68. package/dist/templates/erp/db/migrations/002_crm.sql +59 -0
  69. package/dist/templates/erp/db/migrations/003_catalog.sql +38 -0
  70. package/dist/templates/erp/db/migrations/004_sales.sql +86 -0
  71. package/dist/templates/erp/db/migrations/005_inventory.sql +65 -0
  72. package/dist/templates/erp/db/migrations/006_hr_legacy.sql +16 -0
  73. package/dist/templates/erp/db/migrations/007_filter_indexes.sql +4 -0
  74. package/dist/templates/erp/db/migrations/008_calculated_totals.sql +98 -0
  75. package/dist/templates/erp/db/migrations/011_auth_organizations.sql +22 -0
  76. package/dist/templates/erp/db/migrations/012_jobs.sql +531 -0
  77. package/dist/templates/erp/db/migrations/013_jobs.sql +98 -0
  78. package/dist/templates/erp/db/migrations/016_jobs.sql +99 -0
  79. package/dist/templates/erp/db/migrations/017_filter_indexes.sql +5 -0
  80. package/dist/templates/erp/db/migrations/018_sort_indexes.sql +4 -0
  81. package/dist/templates/erp/db/migrations/019_auth_users.sql +18 -0
  82. package/dist/templates/erp/functions/unpaid-orders.ts +13 -0
  83. package/dist/templates/erp/package.json +38 -0
  84. package/dist/templates/erp/protobase.config.ts +22 -0
  85. package/dist/templates/erp/protobase.ui.tsx +14 -0
  86. package/dist/templates/erp/seed/auth-organizations.ts +11 -0
  87. package/dist/templates/erp/seed/base-organization.test.ts +17 -0
  88. package/dist/templates/erp/seed/base-organization.ts +10 -0
  89. package/dist/templates/erp/seed/base.ts +22 -0
  90. package/dist/templates/erp/seed/builders/categories.ts +18 -0
  91. package/dist/templates/erp/seed/builders/company.ts +61 -0
  92. package/dist/templates/erp/seed/builders/invoice.ts +31 -0
  93. package/dist/templates/erp/seed/builders/order.ts +66 -0
  94. package/dist/templates/erp/seed/builders/product.ts +46 -0
  95. package/dist/templates/erp/seed/calendar.ts +60 -0
  96. package/dist/templates/erp/seed/cli.ts +54 -0
  97. package/dist/templates/erp/seed/copy.ts +22 -0
  98. package/dist/templates/erp/seed/data/catalogTree.ts +72 -0
  99. package/dist/templates/erp/seed/data/layout.ts +19 -0
  100. package/dist/templates/erp/seed/data/names.ts +44 -0
  101. package/dist/templates/erp/seed/data/reference.ts +53 -0
  102. package/dist/templates/erp/seed/format.ts +24 -0
  103. package/dist/templates/erp/seed/maintenance.ts +63 -0
  104. package/dist/templates/erp/seed/rng.ts +36 -0
  105. package/dist/templates/erp/seed/scales.ts +9 -0
  106. package/dist/templates/erp/seed/steps/catalog.ts +34 -0
  107. package/dist/templates/erp/seed/steps/crm.ts +62 -0
  108. package/dist/templates/erp/seed/steps/hr.ts +26 -0
  109. package/dist/templates/erp/seed/steps/ledger.ts +88 -0
  110. package/dist/templates/erp/seed/steps/reference.ts +24 -0
  111. package/dist/templates/erp/seed/steps/sales.ts +58 -0
  112. package/dist/templates/erp/seed/steps/warehouses.ts +42 -0
  113. package/dist/templates/erp/seed/world.ts +86 -0
  114. package/dist/templates/erp/vitest.config.ts +4 -0
  115. package/dist/templates/real-estate/.env.example +9 -0
  116. package/dist/templates/real-estate/AGENTS.md +24 -0
  117. package/dist/templates/real-estate/_.gitignore +3 -0
  118. package/dist/templates/real-estate/_bunfig.toml +4 -0
  119. package/dist/templates/real-estate/auth/auth.ts +36 -0
  120. package/dist/templates/real-estate/config/amenities/data.ts +12 -0
  121. package/dist/templates/real-estate/config/amenities/ui.ts +7 -0
  122. package/dist/templates/real-estate/config/arrears/data.ts +19 -0
  123. package/dist/templates/real-estate/config/arrears/ui.ts +21 -0
  124. package/dist/templates/real-estate/config/deposits/data.ts +21 -0
  125. package/dist/templates/real-estate/config/deposits/ui.ts +17 -0
  126. package/dist/templates/real-estate/config/index.ts +41 -0
  127. package/dist/templates/real-estate/config/inspections/data.ts +22 -0
  128. package/dist/templates/real-estate/config/inspections/ui.ts +15 -0
  129. package/dist/templates/real-estate/config/lease_tenants/data.ts +16 -0
  130. package/dist/templates/real-estate/config/lease_tenants/ui.ts +8 -0
  131. package/dist/templates/real-estate/config/leases/data.ts +29 -0
  132. package/dist/templates/real-estate/config/leases/ui.ts +33 -0
  133. package/dist/templates/real-estate/config/organizations/data.ts +17 -0
  134. package/dist/templates/real-estate/config/organizations/ui.ts +7 -0
  135. package/dist/templates/real-estate/config/overview/page.tsx +57 -0
  136. package/dist/templates/real-estate/config/owners/data.ts +22 -0
  137. package/dist/templates/real-estate/config/owners/ui.ts +17 -0
  138. package/dist/templates/real-estate/config/pages.test.ts +49 -0
  139. package/dist/templates/real-estate/config/payments/data.ts +20 -0
  140. package/dist/templates/real-estate/config/payments/ui.ts +14 -0
  141. package/dist/templates/real-estate/config/properties/data.ts +26 -0
  142. package/dist/templates/real-estate/config/properties/ui.ts +21 -0
  143. package/dist/templates/real-estate/config/rent_charges/data.ts +22 -0
  144. package/dist/templates/real-estate/config/rent_charges/ui.ts +27 -0
  145. package/dist/templates/real-estate/config/roles.test.ts +49 -0
  146. package/dist/templates/real-estate/config/roles.ts +45 -0
  147. package/dist/templates/real-estate/config/shell.test.ts +18 -0
  148. package/dist/templates/real-estate/config/tenants/data.ts +23 -0
  149. package/dist/templates/real-estate/config/tenants/ui.ts +27 -0
  150. package/dist/templates/real-estate/config/tickets/data.ts +33 -0
  151. package/dist/templates/real-estate/config/tickets/ui.ts +58 -0
  152. package/dist/templates/real-estate/config/unit_amenities/data.ts +15 -0
  153. package/dist/templates/real-estate/config/unit_amenities/ui.ts +8 -0
  154. package/dist/templates/real-estate/config/units/data.ts +24 -0
  155. package/dist/templates/real-estate/config/units/ui.ts +41 -0
  156. package/dist/templates/real-estate/config/user-menu.ts +9 -0
  157. package/dist/templates/real-estate/config/users/data.ts +19 -0
  158. package/dist/templates/real-estate/config/users/ui.ts +11 -0
  159. package/dist/templates/real-estate/config/valuations/data.ts +19 -0
  160. package/dist/templates/real-estate/config/valuations/ui.ts +13 -0
  161. package/dist/templates/real-estate/config/vendors/data.ts +21 -0
  162. package/dist/templates/real-estate/config/vendors/ui.ts +18 -0
  163. package/dist/templates/real-estate/config/work_orders/data.ts +22 -0
  164. package/dist/templates/real-estate/config/work_orders/ui.ts +18 -0
  165. package/dist/templates/real-estate/db/connection.ts +7 -0
  166. package/dist/templates/real-estate/db/drop.ts +9 -0
  167. package/dist/templates/real-estate/db/kysely.ts +13 -0
  168. package/dist/templates/real-estate/db/migrate.ts +30 -0
  169. package/dist/templates/real-estate/db/migrations/001_core.sql +33 -0
  170. package/dist/templates/real-estate/db/migrations/002_portfolio.sql +97 -0
  171. package/dist/templates/real-estate/db/migrations/003_leasing.sql +78 -0
  172. package/dist/templates/real-estate/db/migrations/004_billing.sql +88 -0
  173. package/dist/templates/real-estate/db/migrations/005_maintenance.sql +105 -0
  174. package/dist/templates/real-estate/db/migrations/006_outstanding.sql +14 -0
  175. package/dist/templates/real-estate/db/migrations/007_auth.sql +36 -0
  176. package/dist/templates/real-estate/db/migrations/008_auth_snake_case.sql +109 -0
  177. package/dist/templates/real-estate/db/migrations/009_auth.sql +6 -0
  178. package/dist/templates/real-estate/db/migrations/010_auth.sql +4 -0
  179. package/dist/templates/real-estate/db/migrations/011_filter_indexes.sql +8 -0
  180. package/dist/templates/real-estate/db/migrations/012_sort_indexes.sql +2 -0
  181. package/dist/templates/real-estate/db/migrations/013_auth_users.sql +17 -0
  182. package/dist/templates/real-estate/node-types.d.ts +1 -0
  183. package/dist/templates/real-estate/package.json +38 -0
  184. package/dist/templates/real-estate/protobase.config.ts +5 -0
  185. package/dist/templates/real-estate/protobase.ui.tsx +16 -0
  186. package/dist/templates/real-estate/seed/base-organization.test.ts +17 -0
  187. package/dist/templates/real-estate/seed/base-organization.ts +10 -0
  188. package/dist/templates/real-estate/seed/base.ts +18 -0
  189. package/dist/templates/real-estate/seed/builders/allocation.ts +47 -0
  190. package/dist/templates/real-estate/seed/builders/ledger.ts +52 -0
  191. package/dist/templates/real-estate/seed/builders/property.ts +98 -0
  192. package/dist/templates/real-estate/seed/builders/tenancy.ts +99 -0
  193. package/dist/templates/real-estate/seed/builders/ticket.ts +115 -0
  194. package/dist/templates/real-estate/seed/calendar.ts +77 -0
  195. package/dist/templates/real-estate/seed/cli.ts +50 -0
  196. package/dist/templates/real-estate/seed/copy.ts +22 -0
  197. package/dist/templates/real-estate/seed/data/names.ts +31 -0
  198. package/dist/templates/real-estate/seed/data/reference.ts +63 -0
  199. package/dist/templates/real-estate/seed/format.ts +24 -0
  200. package/dist/templates/real-estate/seed/iban.ts +15 -0
  201. package/dist/templates/real-estate/seed/maintenance.ts +63 -0
  202. package/dist/templates/real-estate/seed/rng.ts +36 -0
  203. package/dist/templates/real-estate/seed/scales.ts +9 -0
  204. package/dist/templates/real-estate/seed/steps/billing.ts +44 -0
  205. package/dist/templates/real-estate/seed/steps/leasing.ts +60 -0
  206. package/dist/templates/real-estate/seed/steps/maintenance.ts +77 -0
  207. package/dist/templates/real-estate/seed/steps/portfolio.ts +97 -0
  208. package/dist/templates/real-estate/seed/steps/reference.ts +21 -0
  209. package/dist/templates/real-estate/seed/world.ts +92 -0
  210. package/dist/templates/real-estate/tsconfig.json +16 -0
  211. package/dist/templates/real-estate/vitest.config.ts +4 -0
  212. package/dist/templates/scratch/_.gitignore +3 -0
  213. package/dist/templates/scratch/_bunfig.toml +4 -0
  214. package/dist/templates/scratch/db/migrate.ts +30 -0
  215. package/dist/templates/scratch/db/migrations/001_auth.sql +36 -0
  216. package/dist/templates/scratch/db/migrations/002_auth_snake_case.sql +109 -0
  217. package/dist/templates/scratch/db/migrations/003_auth.sql +6 -0
  218. package/dist/templates/scratch/db/migrations/004_auth.sql +4 -0
  219. package/dist/templates/scratch/node-types.d.ts +1 -0
  220. package/dist/{template → templates/scratch}/package.json +5 -5
  221. package/dist/templates/scratch/tsconfig.json +16 -0
  222. package/package.json +2 -2
  223. /package/dist/{template → templates/erp}/_.gitignore +0 -0
  224. /package/dist/{template → templates/erp}/_bunfig.toml +0 -0
  225. /package/dist/{template → templates/erp}/db/migrate.ts +0 -0
  226. /package/dist/{template/db/migrations/001_auth.sql → templates/erp/db/migrations/009_auth.sql} +0 -0
  227. /package/dist/{template/db/migrations/002_auth_snake_case.sql → templates/erp/db/migrations/010_auth_snake_case.sql} +0 -0
  228. /package/dist/{template/db/migrations/003_auth.sql → templates/erp/db/migrations/014_auth.sql} +0 -0
  229. /package/dist/{template/db/migrations/004_auth.sql → templates/erp/db/migrations/015_auth.sql} +0 -0
  230. /package/dist/{template → templates/erp}/node-types.d.ts +0 -0
  231. /package/dist/{template → templates/erp}/tsconfig.json +0 -0
  232. /package/dist/{template → templates/scratch}/.env.example +0 -0
  233. /package/dist/{template → templates/scratch}/AGENTS.md +0 -0
  234. /package/dist/{template → templates/scratch}/auth/auth.ts +0 -0
  235. /package/dist/{template → templates/scratch}/config/index.ts +0 -0
  236. /package/dist/{template → templates/scratch}/config/user-menu.ts +0 -0
  237. /package/dist/{template → templates/scratch}/db/connection.ts +0 -0
  238. /package/dist/{template → templates/scratch}/db/migrations/.gitkeep +0 -0
  239. /package/dist/{template → templates/scratch}/protobase.config.ts +0 -0
  240. /package/dist/{template → templates/scratch}/vitest.config.ts +0 -0
@@ -0,0 +1,72 @@
1
+ type Item = readonly [name: string, priceCents: number]
2
+
3
+ export const tree: ReadonlyArray<{ name: string; code: string; children: ReadonlyArray<{ name: string; items: readonly Item[] }> }> = [
4
+ {
5
+ name: 'Fasteners',
6
+ code: 'FST',
7
+ children: [
8
+ { name: 'Bolts', items: [['M8 hex bolt A2, 100 pcs', 1890], ['M12 hex bolt 8.8, 50 pcs', 2150], ['Countersunk screw M5, 200 pcs', 1260]] },
9
+ { name: 'Nuts and washers', items: [['M10 hex nut A2, 100 pcs', 1240], ['Stainless washer 8 mm, 250 pcs', 990], ['Nyloc nut M8, 100 pcs', 1175]] },
10
+ { name: 'Anchors', items: [['Wedge anchor M10 x 100, 20 pcs', 2480], ['Wall plug 8 mm, 200 pcs', 760]] },
11
+ ],
12
+ },
13
+ {
14
+ name: 'Electrical',
15
+ code: 'ELC',
16
+ children: [
17
+ { name: 'Cable management', items: [['Cable tie 300 mm, 100 pcs', 650], ['Cable gland M20, 25 pcs', 2340], ['Cable tray 100 mm, 3 m', 3890]] },
18
+ { name: 'Enclosures', items: [['Junction box IP65', 1480], ['Control cabinet 600 x 400', 12900]] },
19
+ { name: 'Lighting', items: [['LED panel 600 x 600, 40 W', 3290], ['LED high bay 150 W', 8990]] },
20
+ ],
21
+ },
22
+ {
23
+ name: 'Hydraulics',
24
+ code: 'HYD',
25
+ children: [
26
+ { name: 'Hoses', items: [['Hydraulic hose 1/2 in, 5 m', 8650], ['Hydraulic hose 3/4 in, 5 m', 11450]] },
27
+ { name: 'Couplings', items: [['Hydraulic coupling 3/8 in', 1895], ['Quick coupling 1/2 in', 2640], ['Banjo bolt M14', 540]] },
28
+ ],
29
+ },
30
+ {
31
+ name: 'Packaging',
32
+ code: 'PKG',
33
+ children: [
34
+ { name: 'Wrap and tape', items: [['Pallet wrap 500 mm', 740], ['Bubble wrap roll 50 m', 1990], ['Packing tape 48 mm, 6 rolls', 1380]] },
35
+ { name: 'Boxes', items: [['Corrugated box 400 x 300', 215], ['Corrugated box 600 x 400', 340]] },
36
+ { name: 'Labels', items: [['Pallet label roll 100 x 150', 1175], ['Thermal printer ribbon', 890]] },
37
+ ],
38
+ },
39
+ {
40
+ name: 'Safety',
41
+ code: 'PPE',
42
+ children: [
43
+ { name: 'Hand protection', items: [['Safety gloves, size L', 395], ['Cut resistant gloves, size M', 885]] },
44
+ { name: 'Eye and ear', items: [['Safety glasses, clear', 450], ['Ear defenders SNR 31', 1295], ['Earplugs, 200 pairs', 2290]] },
45
+ ],
46
+ },
47
+ {
48
+ name: 'Bearings and belts',
49
+ code: 'BRG',
50
+ children: [
51
+ { name: 'Bearings', items: [['Ball bearing 6204-2RS', 685], ['Taper roller bearing 30205', 1420], ['Pillow block UCP205', 2260]] },
52
+ { name: 'Belts', items: [['Timing belt HTD 5M, 600 mm', 2780], ['V-belt SPA 1250', 1360]] },
53
+ ],
54
+ },
55
+ {
56
+ name: 'Lubricants',
57
+ code: 'LUB',
58
+ children: [
59
+ { name: 'Grease', items: [['Grease cartridge, 400 g', 920], ['High temperature grease, 1 kg', 2150]] },
60
+ { name: 'Oil', items: [['Machine oil ISO 68, 5 l', 3950], ['Hydraulic oil HLP 46, 20 l', 8450], ['Cutting fluid, 10 l', 5480]] },
61
+ ],
62
+ },
63
+ {
64
+ name: 'Tools and pneumatics',
65
+ code: 'TLS',
66
+ children: [
67
+ { name: 'Hand tools', items: [['Drill bit set HSS, 19 pcs', 3490], ['Torque wrench 20-100 Nm', 14900], ['Angle grinder disc 125 mm, 10 pcs', 1880]] },
68
+ { name: 'Pneumatics', items: [['Pneumatic cylinder 40 x 100', 6120], ['Air hose PU 8 mm, 20 m', 2475]] },
69
+ { name: 'Pallets', items: [['Euro pallet EPAL, used', 1150], ['Roll container, galvanised', 9800]] },
70
+ ],
71
+ },
72
+ ]
@@ -0,0 +1,19 @@
1
+ // Every warehouse gets 3 zones x 4 aisles x 5 bins, stored depth-first:
2
+ // zone, then each aisle followed by its bins. Only bins hold stock.
3
+ export const zones = 3
4
+ export const aislesPerZone = 4
5
+ export const binsPerAisle = 5
6
+
7
+ const zoneSpan = 1 + aislesPerZone * (1 + binsPerAisle)
8
+
9
+ export const locationsPerWarehouse = zones * zoneSpan
10
+
11
+ export const zoneOffset = (zone: number) => zone * zoneSpan
12
+ export const aisleOffset = (zone: number, aisle: number) => zoneOffset(zone) + 1 + aisle * (1 + binsPerAisle)
13
+ export const binOffset = (zone: number, aisle: number, bin: number) => aisleOffset(zone, aisle) + 1 + bin
14
+
15
+ export const binOffsets = Array.from({ length: zones * aislesPerZone * binsPerAisle }, (_, k) => {
16
+ const zone = Math.floor(k / (aislesPerZone * binsPerAisle))
17
+ const aisle = Math.floor(k / binsPerAisle) % aislesPerZone
18
+ return binOffset(zone, aisle, k % binsPerAisle)
19
+ })
@@ -0,0 +1,44 @@
1
+ export const locales = {
2
+ NL: {
3
+ stems: ['Polder', 'Rijnland', 'Kuststaal', 'Delta', 'Veluwe', 'Maasvallei', 'Tulpen', 'Grachten', 'Noordzee', 'Waterland', 'Brabant', 'Zeeuwse', 'Hollandse', 'Zuiderzee', 'Amstel', 'Vechtstreek', 'Gelderse', 'Friese'],
4
+ nouns: ['Logistiek', 'Techniek', 'Verpakking', 'Installatie', 'Groothandel', 'Metaal', 'Bouwstoffen', 'Fietsen', 'Machinebouw', 'Transport', 'Scheepsbouw', 'Staalbouw', 'Elektro', 'Kunststof'],
5
+ suffix: 'B.V.',
6
+ cities: ['Rotterdam', 'Eindhoven', 'Utrecht', 'Zwolle', 'Breda', 'Groningen', 'Tilburg', 'Almere', 'Nijmegen', 'Arnhem', 'Enschede', 'Venlo'],
7
+ surnames: ['de Vries', 'van den Berg', 'Bakker', 'Janssen', 'Visser', 'Smit', 'Meijer', 'de Boer', 'Mulder', 'de Groot', 'Bos', 'Vos', 'Peters', 'Hendriks', 'van Dijk', 'Dekker', 'Kok', 'Jacobs'],
8
+ firstNames: ['Sanne', 'Daan', 'Lieke', 'Jeroen', 'Femke', 'Bram', 'Anouk', 'Ruben', 'Eva', 'Thijs', 'Noor', 'Pieter', 'Mirjam', 'Joost'],
9
+ andWord: 'en Zonen',
10
+ tld: 'nl',
11
+ dial: '+31',
12
+ vat: (n: number) => `NL${n}B01`,
13
+ },
14
+ DE: {
15
+ stems: ['Nordlicht', 'Eifel', 'Rhein-Main', 'Schwarzwald', 'Hansa', 'Alpen', 'Elbe', 'Bergisch', 'Ruhr', 'Isar', 'Saar', 'Harz', 'Bodensee', 'Franken', 'Westfalen', 'Mosel'],
16
+ nouns: ['Werkzeuge', 'Präzision', 'Maschinenbau', 'Handel', 'Elektro', 'Logistik', 'Verpackung', 'Systemtechnik', 'Metallbau', 'Antriebstechnik', 'Industriebedarf', 'Hydraulik', 'Kunststofftechnik'],
17
+ suffix: 'GmbH',
18
+ cities: ['Düsseldorf', 'Köln', 'Stuttgart', 'Hamburg', 'Münster', 'Nürnberg', 'Dortmund', 'Bremen', 'Hannover', 'Leipzig', 'Essen', 'Mannheim'],
19
+ surnames: ['Müller', 'Schmidt', 'Schneider', 'Fischer', 'Weber', 'Meyer', 'Wagner', 'Becker', 'Hoffmann', 'Schäfer', 'Koch', 'Richter', 'Klein', 'Wolf', 'Neumann', 'Schwarz', 'Zimmermann', 'Braun'],
20
+ firstNames: ['Lena', 'Felix', 'Marie', 'Jan', 'Katrin', 'Paul', 'Julia', 'Lukas', 'Anna', 'Moritz', 'Sabine', 'Stefan', 'Heike', 'Matthias'],
21
+ andWord: 'und Söhne',
22
+ tld: 'de',
23
+ dial: '+49',
24
+ vat: (n: number) => `DE${n}`,
25
+ },
26
+ GB: {
27
+ stems: ['Brightwater', 'Ironbridge', 'Harbour Street', 'Kestrel', 'Northgate', 'Oakfield', 'Redwood', 'Stonebridge', 'Millbrook', 'Ashford', 'Pennine', 'Thornbury', 'Greenway', 'Hartley'],
28
+ nouns: ['Trading', 'Supplies', 'Engineering', 'Fabrication', 'Distribution', 'Packaging', 'Industrial', 'Components', 'Tooling', 'Hydraulics', 'Fasteners'],
29
+ suffix: 'Ltd',
30
+ cities: ['Leeds', 'Bristol', 'Birmingham', 'Manchester', 'Sheffield', 'Nottingham', 'Liverpool', 'Newcastle', 'Cardiff', 'Coventry'],
31
+ surnames: ['Smith', 'Jones', 'Taylor', 'Brown', 'Williams', 'Wilson', 'Evans', 'Thomas', 'Roberts', 'Walker', 'Wright', 'Robinson', 'Thompson', 'Hughes', 'Harrison', 'Clarke', 'Bennett', 'Hall'],
32
+ firstNames: ['Oliver', 'Amelia', 'George', 'Isla', 'Harry', 'Ava', 'Jack', 'Emily', 'Thomas', 'Grace', 'James', 'Charlotte', 'William', 'Poppy'],
33
+ andWord: '& Sons',
34
+ tld: 'co.uk',
35
+ dial: '+44',
36
+ vat: (n: number) => `GB${n}`,
37
+ },
38
+ } as const
39
+
40
+ export type LocaleCode = keyof typeof locales
41
+
42
+ export const qualifiers = ['Holding', 'Group', 'International', 'Benelux', 'Europe', 'Services', 'Solutions']
43
+
44
+ export const jobTitles = ['Purchasing manager', 'Buyer', 'Operations manager', 'Warehouse lead', 'Finance controller', 'Owner', 'Procurement specialist', 'Logistics coordinator', 'Maintenance engineer', 'Office manager']
@@ -0,0 +1,53 @@
1
+ export const countries = [
2
+ ['NL', 'Netherlands', true],
3
+ ['DE', 'Germany', true],
4
+ ['BE', 'Belgium', true],
5
+ ['FR', 'France', true],
6
+ ['AT', 'Austria', true],
7
+ ['PL', 'Poland', true],
8
+ ['IT', 'Italy', true],
9
+ ['ES', 'Spain', true],
10
+ ['DK', 'Denmark', true],
11
+ ['GB', 'United Kingdom', false],
12
+ ['CH', 'Switzerland', false],
13
+ ['NO', 'Norway', false],
14
+ ] as const
15
+
16
+ export const currencies = [
17
+ ['EUR', 'Euro', '€', 2],
18
+ ['GBP', 'Pound sterling', '£', 2],
19
+ ['CHF', 'Swiss franc', 'CHF', 2],
20
+ ['DKK', 'Danish krone', 'kr', 2],
21
+ ['PLN', 'Polish zloty', 'zł', 2],
22
+ ] as const
23
+
24
+ export const tagNames = [
25
+ ['VIP', '#7c3aed'], ['Key account', '#2563eb'], ['Slow payer', '#dc2626'], ['Reseller', '#0891b2'],
26
+ ['Export', '#059669'], ['Prospect', '#d97706'], ['Partner', '#4f46e5'], ['Seasonal', '#ca8a04'],
27
+ ['Government', '#475569'], ['Education', '#0d9488'], ['Credit hold', '#b91c1c'], ['Framework contract', '#9333ea'],
28
+ ] as const
29
+
30
+ export const orgSpecs = [
31
+ {
32
+ id: 1,
33
+ name: 'Rijnland Industrial Supply B.V.',
34
+ slug: 'rijnland',
35
+ country: 'NL',
36
+ share: 0.7,
37
+ seed: 101,
38
+ locales: [['NL', 55], ['DE', 25], ['GB', 20]] as const,
39
+ users: [['Sanne', 'M.', 'admin'], ['Jonas', 'K.', 'sales'], ['Lieke', 'D.', 'finance'], ['Tobias', 'R.', 'warehouse'], ['Imke', 'V.', 'sales'], ['Marek', 'W.', 'warehouse'], ['Hanna', 'B.', 'finance'], ['Daan', 'P.', 'sales']] as const,
40
+ warehouses: [['AMS-01', 'Amsterdam main', 'Amsterdam', 'NL'], ['RTM-02', 'Rotterdam port', 'Rotterdam', 'NL'], ['DUS-01', 'Düsseldorf hub', 'Düsseldorf', 'DE'], ['ANT-03', 'Antwerp overflow', 'Antwerp', 'BE']] as const,
41
+ },
42
+ {
43
+ id: 2,
44
+ name: 'Nordlicht Werkzeuge GmbH',
45
+ slug: 'nordlicht',
46
+ country: 'DE',
47
+ share: 0.3,
48
+ seed: 202,
49
+ locales: [['DE', 60], ['NL', 20], ['GB', 20]] as const,
50
+ users: [['Katrin', 'L.', 'admin'], ['Felix', 'B.', 'sales'], ['Marie', 'S.', 'sales'], ['Jan', 'T.', 'finance'], ['Paul', 'W.', 'warehouse'], ['Lena', 'H.', 'warehouse']] as const,
51
+ warehouses: [['HAM-01', 'Hamburg central', 'Hamburg', 'DE'], ['MUC-01', 'München depot', 'München', 'DE'], ['VIE-01', 'Vienna cross-dock', 'Vienna', 'AT']] as const,
52
+ },
53
+ ] as const
@@ -0,0 +1,24 @@
1
+ const escapes: Record<string, string> = { '\\': '\\\\', '\t': '\\t', '\n': '\\n', '\r': '\\r' }
2
+
3
+ const cell = (value: unknown) => {
4
+ if (value === null || value === undefined) return '\\N'
5
+ return String(value).replace(/[\\\t\n\r]/g, (char) => escapes[char]!)
6
+ }
7
+
8
+ // One COPY text-format row.
9
+ export const row = (...values: unknown[]) => values.map(cell).join('\t') + '\n'
10
+
11
+ export const money = (cents: number) => `${Math.floor(cents / 100)}.${String(cents % 100).padStart(2, '0')}`
12
+
13
+ export const iso = (ms: number) => new Date(ms).toISOString()
14
+
15
+ export const isoDate = (ms: number) => iso(ms).slice(0, 10)
16
+
17
+ export const slugify = (text: string) =>
18
+ text
19
+ .normalize('NFD')
20
+ .replace(/[̀-ͯ]/g, '')
21
+ .toLowerCase()
22
+ .replace(/&/g, 'and')
23
+ .replace(/[^a-z0-9]+/g, '-')
24
+ .replace(/^-|-$/g, '')
@@ -0,0 +1,63 @@
1
+ import type { Db } from '../db/connection'
2
+
3
+ const schemas = ['core', 'crm', 'catalog', 'sales', 'inventory', 'hr']
4
+
5
+ export const truncateAll = async (sql: Db) => {
6
+ const tables = await sql`
7
+ select format('%I.%I', schemaname, tablename) as name from pg_tables
8
+ where schemaname in ${sql(schemas)} or tablename = 'seed_info'
9
+ `
10
+ await sql.unsafe(`truncate ${tables.map((table) => table.name).join(', ')} restart identity cascade`)
11
+ }
12
+
13
+ // Secondary indexes only: primary keys and unique constraints stay in place.
14
+ const secondaryIndexes = (sql: Db) => sql`
15
+ select format('%I.%I', n.nspname, c.relname) as name, pg_get_indexdef(i.indexrelid) as definition
16
+ from pg_index i
17
+ join pg_class c on c.oid = i.indexrelid
18
+ join pg_namespace n on n.oid = c.relnamespace
19
+ where n.nspname in ${sql(schemas)}
20
+ and not i.indisprimary
21
+ and not exists (select 1 from pg_constraint k where k.conindid = i.indexrelid)
22
+ `
23
+
24
+ // Loading into indexed tables is several times slower than building the indexes afterwards.
25
+ export const dropIndexes = async (sql: Db) => {
26
+ const indexes = await secondaryIndexes(sql)
27
+ for (const index of indexes) await sql.unsafe(`drop index ${index.name}`)
28
+ return indexes.map((index) => index.definition as string)
29
+ }
30
+
31
+ export const createIndexes = async (sql: Db, definitions: string[]) => {
32
+ await Promise.all(definitions.map((definition) => sql.unsafe(definition)))
33
+ }
34
+
35
+ // The triggers that keep stored totals in step with their rows. The seed writes those totals itself, and recomputing
36
+ // them during the load, with the secondary indexes gone, takes minutes at small scale and hours above it. A table's
37
+ // owner may switch its own triggers off, unlike the foreign key checks, which stay on.
38
+ export const setTriggers = async (sql: Db, enabled: boolean) => {
39
+ const tables = await sql`
40
+ select distinct format('%I.%I', n.nspname, c.relname) as name
41
+ from pg_trigger t
42
+ join pg_class c on c.oid = t.tgrelid
43
+ join pg_namespace n on n.oid = c.relnamespace
44
+ where not t.tgisinternal and n.nspname in ${sql(schemas)}
45
+ `
46
+ for (const table of tables) await sql.unsafe(`alter table ${table.name} ${enabled ? 'enable' : 'disable'} trigger user`)
47
+ }
48
+
49
+ // COPY with explicit ids leaves identity sequences at 1.
50
+ export const resetSequences = async (sql: Db) => {
51
+ const columns = await sql`
52
+ select table_schema as schema, table_name as table, column_name as column from information_schema.columns
53
+ where is_identity = 'YES' and table_schema in ${sql(schemas)}
54
+ `
55
+ for (const { schema, table, column } of columns) {
56
+ const target = `${schema}.${table}`
57
+ await sql.unsafe(`select setval(pg_get_serial_sequence('${target}', '${column}'), coalesce(max(${column}), 1), max(${column}) is not null) from ${target}`)
58
+ }
59
+ }
60
+
61
+ export const analyzeAll = async (sql: Db) => {
62
+ await sql.unsafe('analyze')
63
+ }
@@ -0,0 +1,36 @@
1
+ export const hash = (seed: number, index: number) => {
2
+ let h = (seed ^ Math.imul(index + 0x9e3779b9, 0x85ebca6b)) >>> 0
3
+ h = Math.imul(h ^ (h >>> 16), 0x85ebca6b)
4
+ h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35)
5
+ return (h ^ (h >>> 16)) >>> 0
6
+ }
7
+
8
+ // Independent stream per (seed, index): lets any row be rebuilt without generating its neighbours.
9
+ export const rngAt = (seed: number, index: number) => {
10
+ let state = hash(seed, index)
11
+ return () => {
12
+ state = (state + 0x6d2b79f5) >>> 0
13
+ let t = state
14
+ t = Math.imul(t ^ (t >>> 15), t | 1)
15
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61)
16
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296
17
+ }
18
+ }
19
+
20
+ export type Rand = ReturnType<typeof rngAt>
21
+
22
+ export const pick = <T>(rand: Rand, items: readonly T[]) => items[Math.floor(rand() * items.length)]!
23
+
24
+ export const int = (rand: Rand, min: number, max: number) => min + Math.floor(rand() * (max - min + 1))
25
+
26
+ export const weighted = <T>(rand: Rand, entries: ReadonlyArray<readonly [T, number]>) => {
27
+ const total = entries.reduce((sum, [, weight]) => sum + weight, 0)
28
+ let roll = rand() * total
29
+ for (const [value, weight] of entries) {
30
+ roll -= weight
31
+ if (roll < 0) return value
32
+ }
33
+ return entries[entries.length - 1]![0]
34
+ }
35
+
36
+ export const makeRng = (seed: number) => rngAt(seed, 0)
@@ -0,0 +1,9 @@
1
+ export const scales = {
2
+ small: { companies: 200, orders: 2_000, moves: 20_000, productsPerOrg: 100 },
3
+ medium: { companies: 20_000, orders: 200_000, moves: 1_000_000, productsPerOrg: 1_000 },
4
+ large: { companies: 20_000, orders: 200_000, moves: 10_000_000, productsPerOrg: 1_000 },
5
+ } as const
6
+
7
+ export type ScaleName = keyof typeof scales
8
+
9
+ export const isScale = (name: string | undefined): name is ScaleName => name !== undefined && name in scales
@@ -0,0 +1,34 @@
1
+ import type { Db } from '../../db/connection'
2
+ import { categories } from '../builders/categories'
3
+ import { productAt } from '../builders/product'
4
+ import { DAY, windowStart } from '../calendar'
5
+ import { copyRows } from '../copy'
6
+ import { iso, money, row } from '../format'
7
+ import type { World } from '../world'
8
+
9
+ function* categoryRows(world: World) {
10
+ const createdAt = iso(windowStart - 4 * 365 * DAY)
11
+ for (const org of world.orgs) {
12
+ for (const [index, category] of categories.entries()) {
13
+ const parentId = category.parent === null ? null : org.categoryFirstId + category.parent
14
+ yield row(org.categoryFirstId + index, org.id, parentId, category.name, category.slug, category.position, createdAt, createdAt)
15
+ }
16
+ }
17
+ }
18
+
19
+ function* productRows(world: World) {
20
+ for (const org of world.orgs) {
21
+ for (let ordinal = 0; ordinal < org.productCount; ordinal++) {
22
+ const product = productAt(org, ordinal)
23
+ const createdAt = iso(product.createdAt)
24
+ const deletedAt = product.deletedAt === null ? null : iso(product.deletedAt)
25
+ const updatedAt = iso(Math.max(product.createdAt, product.deletedAt ?? windowStart - 30 * DAY))
26
+ yield row(product.id, org.id, product.categoryId, product.sku, product.name, product.description, money(product.priceCents), 'EUR', JSON.stringify(product.attributes), deletedAt === null, deletedAt, createdAt, updatedAt)
27
+ }
28
+ }
29
+ }
30
+
31
+ export const seedCatalog = async (sql: Db, world: World) => {
32
+ await copyRows(sql, 'catalog.categories (id, organization_id, parent_id, name, slug, position, created_at, updated_at)', categoryRows(world))
33
+ await copyRows(sql, 'catalog.products (id, organization_id, category_id, sku, name, description, price, currency_code, attributes, active, deleted_at, created_at, updated_at)', productRows(world))
34
+ }
@@ -0,0 +1,62 @@
1
+ import type { Db } from '../../db/connection'
2
+ import { companyAt, peopleOf, personAt } from '../builders/company'
3
+ import { DAY, windowEnd, within } from '../calendar'
4
+ import { copyRows } from '../copy'
5
+ import { tagNames } from '../data/reference'
6
+ import { iso, row } from '../format'
7
+ import { int, rngAt, weighted } from '../rng'
8
+ import type { World } from '../world'
9
+
10
+ function* companyRows(world: World) {
11
+ for (const org of world.orgs) {
12
+ for (let ordinal = 0; ordinal < org.companyCount; ordinal++) {
13
+ const company = companyAt(org, ordinal)
14
+ const createdAt = iso(company.createdAt)
15
+ yield row(company.id, org.id, company.name, company.country, company.city, company.vatNumber, company.email, company.phone, company.status, createdAt, createdAt)
16
+ }
17
+ }
18
+ }
19
+
20
+ function* personRows(world: World) {
21
+ for (const org of world.orgs) {
22
+ for (let ordinal = 0; ordinal < org.companyCount; ordinal++) {
23
+ const company = companyAt(org, ordinal)
24
+ for (let slot = 0; slot < peopleOf(org, ordinal); slot++) {
25
+ const person = personAt(org, company, ordinal, slot)
26
+ const createdAt = iso(person.createdAt)
27
+ yield row(person.id, org.id, company.id, person.first, person.last, person.email, person.phone, person.title, createdAt, createdAt)
28
+ }
29
+ }
30
+ }
31
+ }
32
+
33
+ function* tagRows(world: World) {
34
+ const createdAt = iso(windowEnd - 3 * 365 * DAY)
35
+ for (const org of world.orgs) {
36
+ for (const [index, [name, color]] of tagNames.entries()) yield row(org.tagFirstId + index, org.id, name, color, createdAt, createdAt)
37
+ }
38
+ }
39
+
40
+ function* companyTagRows(world: World) {
41
+ for (const org of world.orgs) {
42
+ const staff = org.users.filter((user) => user.role === 'sales' || user.role === 'admin')
43
+ for (let ordinal = 0; ordinal < org.companyCount; ordinal++) {
44
+ const rand = rngAt(org.seed + 6, ordinal)
45
+ const count = weighted(rand, [[0, 55], [1, 25], [2, 15], [3, 5]] as const)
46
+ const company = companyAt(org, ordinal)
47
+ const chosen = new Set<number>()
48
+ while (chosen.size < count) chosen.add(int(rand, 0, tagNames.length - 1))
49
+ for (const tag of chosen) {
50
+ const addedAt = within(company.createdAt, int(rand, 1, 400) * DAY + Math.floor(rand() * DAY), rand)
51
+ yield row(company.id, org.tagFirstId + tag, org.id, staff[int(rand, 0, staff.length - 1)]!.id, iso(addedAt))
52
+ }
53
+ }
54
+ }
55
+ }
56
+
57
+ export const seedCrm = async (sql: Db, world: World) => {
58
+ await copyRows(sql, 'crm.companies (id, organization_id, name, country_code, city, vat_number, email, phone, status, created_at, updated_at)', companyRows(world))
59
+ await copyRows(sql, 'crm.people (id, organization_id, company_id, first_name, last_name, email, phone, job_title, created_at, updated_at)', personRows(world))
60
+ await copyRows(sql, 'crm.tags (id, organization_id, name, color, created_at, updated_at)', tagRows(world))
61
+ await copyRows(sql, 'crm.company_tags (company_id, tag_id, organization_id, added_by, added_at)', companyTagRows(world))
62
+ }
@@ -0,0 +1,26 @@
1
+ import type { Db } from '../../db/connection'
2
+ import { DAY, windowEnd } from '../calendar'
3
+ import { copyRows } from '../copy'
4
+ import { locales } from '../data/names'
5
+ import { isoDate, money, row } from '../format'
6
+ import { int, pick, rngAt } from '../rng'
7
+ import { employeesPerOrg, type World } from '../world'
8
+
9
+ const departments = ['SAL', 'FIN', 'WHS', 'ADM', 'PUR']
10
+ const managers = 3
11
+
12
+ function* employeeRows(world: World) {
13
+ for (const org of world.orgs) {
14
+ const locale = locales[org.country]
15
+ for (let k = 0; k < employeesPerOrg; k++) {
16
+ const rand = rngAt(org.seed + 8, k)
17
+ const name = `${pick(rand, locale.surnames)}, ${pick(rand, locale.firstNames)}`.toUpperCase()
18
+ const managerId = k < managers ? null : org.employeeFirstId + int(rand, 0, managers - 1)
19
+ const hired = windowEnd - int(rand, 30, 15 * 365) * DAY
20
+ yield row(org.employeeFirstId + k, org.id, name, pick(rand, departments), isoDate(hired), money(int(rand, 2_800_000, 9_500_000)), managerId, rand() < 0.92 ? 'Y' : 'N')
21
+ }
22
+ }
23
+ }
24
+
25
+ export const seedHr = (sql: Db, world: World) =>
26
+ copyRows(sql, 'hr."EMP_MASTER" ("EMP_ID", "ORGANIZATION_ID", "EMP_NAME", "DEPT_CD", "HIRE_DT", "SAL_AMT", "MGR_ID", "ACTIVE_FLG")', employeeRows(world))
@@ -0,0 +1,88 @@
1
+ import type { Db } from '../../db/connection'
2
+ import { productPriceCents } from '../builders/product'
3
+ import { timestamps, yearOf } from '../calendar'
4
+ import { copyRows } from '../copy'
5
+ import { binOffsets } from '../data/layout'
6
+ import { iso, money, row } from '../format'
7
+ import { int, makeRng, pick, weighted } from '../rng'
8
+ import type { Org, World } from '../world'
9
+
10
+ const binsPerWarehouse = binOffsets.length
11
+
12
+ // Moves are generated in time order against a running stock table, so issues and
13
+ // transfers never take a bin below zero and the final table is the stock_levels seed.
14
+ const createLedger = (org: Org) => {
15
+ const rand = makeRng(org.seed + 11)
16
+ const times = timestamps(org.moveCount, org.seed + 12)
17
+ const bins = org.warehouses.length * binsPerWarehouse
18
+ const stock = new Int32Array(org.productCount * bins)
19
+ const touched = new Uint8Array(stock.length)
20
+ const lastMovedAt = new Float64Array(stock.length)
21
+ const prices = Int32Array.from({ length: org.productCount }, (_, ordinal) => productPriceCents(ordinal))
22
+ const locationOf = (bin: number) => org.warehouses[Math.floor(bin / binsPerWarehouse)]!.firstLocationId + binOffsets[bin % binsPerWarehouse]!
23
+ const warehouseOf = (bin: number) => org.warehouses[Math.floor(bin / binsPerWarehouse)]!.id
24
+
25
+ // Each product lives in three home bins, so issues usually find stock.
26
+ const homeBin = (product: number) => (product * 13 + int(rand, 0, 2) * 97) % bins
27
+ const pickProduct = () => Math.floor(rand() ** 1.8 * org.productCount)
28
+
29
+ function* moves() {
30
+ let id = org.moveFirstId
31
+ let index = 0
32
+ const move = (product: number, bin: number, kind: string, quantity: number, reference: string | null, at: number, userId: number) => {
33
+ stock[product * bins + bin]! += quantity
34
+ touched[product * bins + bin] = 1
35
+ lastMovedAt[product * bins + bin] = at
36
+ return row(id++, org.id, org.productFirstId + product, warehouseOf(bin), locationOf(bin), kind, quantity, money(Math.round(prices[product]! * 0.62)), reference, iso(at), userId)
37
+ }
38
+
39
+ while (index < org.moveCount) {
40
+ const at = times[index]!
41
+ const userId = pick(rand, org.warehouseUsers).id
42
+ const product = pickProduct()
43
+ const bin = homeBin(product)
44
+ const available = stock[product * bins + bin]!
45
+ const wanted = weighted(rand, [['issue', 55], ['receipt', 28], ['transfer', 13], ['adjustment', 4]] as const)
46
+ const kind = wanted !== 'receipt' && available < 1 && wanted !== 'adjustment' ? 'receipt' : wanted
47
+ const orderOrdinal = Math.max(0, Math.min(org.orderCount - 1, Math.floor((index / org.moveCount) * org.orderCount) - int(rand, 0, 50)))
48
+
49
+ if (kind === 'issue') {
50
+ const reference = `SO-${yearOf(org.orderTimes[orderOrdinal]!)}-${String(orderOrdinal + 1).padStart(6, '0')}`
51
+ yield move(product, bin, kind, -Math.min(int(rand, 1, 40), available), reference, at, userId)
52
+ index += 1
53
+ } else if (kind === 'transfer' && org.moveCount - index >= 2) {
54
+ const quantity = Math.min(int(rand, 1, 40), available)
55
+ const target = (bin + 1 + int(rand, 0, bins - 2)) % bins
56
+ const reference = `TR-${int(rand, 1000, 9999)}`
57
+ yield move(product, bin, kind, -quantity, reference, at, userId)
58
+ yield move(product, target, kind, quantity, reference, at, userId)
59
+ index += 2
60
+ } else if (kind === 'adjustment') {
61
+ const size = int(rand, 1, 6)
62
+ yield move(product, bin, kind, rand() < 0.5 && available >= size ? -size : size, `CC-${int(rand, 100, 999)}`, at, userId)
63
+ index += 1
64
+ } else {
65
+ yield move(product, bin, 'receipt', int(rand, 5, 100), `PO-${int(rand, 10000, 99999)}`, at, userId)
66
+ index += 1
67
+ }
68
+ }
69
+ }
70
+
71
+ function* levels() {
72
+ for (let slot = 0; slot < stock.length; slot++) {
73
+ if (!touched[slot]) continue
74
+ const bin = slot % bins
75
+ yield row(org.productFirstId + Math.floor(slot / bins), locationOf(bin), org.id, stock[slot], iso(lastMovedAt[slot]!))
76
+ }
77
+ }
78
+
79
+ return { moves, levels }
80
+ }
81
+
82
+ export const seedLedger = async (sql: Db, world: World) => {
83
+ for (const org of world.orgs) {
84
+ const ledger = createLedger(org)
85
+ await copyRows(sql, 'inventory.stock_moves (id, organization_id, product_id, warehouse_id, location_id, kind, quantity, unit_cost, reference, moved_at, user_id)', ledger.moves())
86
+ await copyRows(sql, 'inventory.stock_levels (product_id, location_id, organization_id, quantity, updated_at)', ledger.levels())
87
+ }
88
+ }
@@ -0,0 +1,24 @@
1
+ import type { Db } from '../../db/connection'
2
+ import { DAY, windowStart } from '../calendar'
3
+ import { copyRows } from '../copy'
4
+ import { countries, currencies } from '../data/reference'
5
+ import { iso, row } from '../format'
6
+ import type { World } from '../world'
7
+ import { seedAuthOrganizations } from '../auth-organizations'
8
+
9
+ export const seedReference = async (sql: Db, world: World) => {
10
+ const createdAt = iso(windowStart - 5 * 365 * DAY)
11
+ await copyRows(sql, 'core.countries (code, name, eu_member)', countries.map((country) => row(...country)))
12
+ await copyRows(sql, 'core.currencies (code, name, symbol, decimals)', currencies.map((currency) => row(...currency)))
13
+ await copyRows(
14
+ sql,
15
+ 'core.organizations (id, name, slug, country_code, currency_code, created_at, updated_at)',
16
+ world.orgs.map((org) => row(org.id, org.name, org.slug, org.country, 'EUR', createdAt, createdAt)),
17
+ )
18
+ await seedAuthOrganizations(sql, world.orgs)
19
+ await copyRows(
20
+ sql,
21
+ 'core.users (id, organization_id, email, name, role, active, created_at, updated_at)',
22
+ world.orgs.flatMap((org) => org.users.map((user) => row(user.id, org.id, user.email, user.name, user.role, true, createdAt, createdAt))),
23
+ )
24
+ }
@@ -0,0 +1,58 @@
1
+ import type { Db } from '../../db/connection'
2
+ import { invoiceFor } from '../builders/invoice'
3
+ import { orderAt } from '../builders/order'
4
+ import { copyRows } from '../copy'
5
+ import { iso, isoDate, money, row } from '../format'
6
+ import type { World } from '../world'
7
+
8
+ function* orderRows(world: World) {
9
+ for (const org of world.orgs) {
10
+ for (let ordinal = 0; ordinal < org.orderCount; ordinal++) {
11
+ const order = orderAt(org, ordinal)
12
+ yield row(order.id, org.id, order.number, order.company.id, order.personId, order.ownerId, order.status, 'EUR', money(order.discount * 100), money(order.totalCents), order.paid, order.notes, iso(order.createdAt), iso(order.updatedAt))
13
+ }
14
+ }
15
+ }
16
+
17
+ function* orderLineRows(world: World) {
18
+ for (const org of world.orgs) {
19
+ for (let ordinal = 0; ordinal < org.orderCount; ordinal++) {
20
+ const order = orderAt(org, ordinal)
21
+ for (const line of order.lines) {
22
+ yield row(order.id, line.lineNo, org.id, line.productId, line.description, line.quantity, money(line.unitPriceCents), iso(order.createdAt), iso(order.createdAt))
23
+ }
24
+ }
25
+ }
26
+ }
27
+
28
+ function* invoiceRows(world: World) {
29
+ for (const org of world.orgs) {
30
+ for (let ordinal = 0; ordinal < org.orderCount; ordinal++) {
31
+ const order = orderAt(org, ordinal)
32
+ const invoice = invoiceFor(org, ordinal, order)
33
+ if (!invoice) continue
34
+ yield row(invoice.id, org.id, invoice.number, order.id, order.company.id, invoice.status, isoDate(invoice.issuedAt), isoDate(invoice.dueAt), money(invoice.vatRate * 100), money(invoice.subtotalCents), money(invoice.vatCents), money(invoice.totalCents), iso(invoice.createdAt), iso(invoice.updatedAt))
35
+ }
36
+ }
37
+ }
38
+
39
+ // Line ids leave room for nine positions per invoice; invoice lines mirror the order lines.
40
+ function* invoiceLineRows(world: World) {
41
+ for (const org of world.orgs) {
42
+ for (let ordinal = 0; ordinal < org.orderCount; ordinal++) {
43
+ const order = orderAt(org, ordinal)
44
+ const invoice = invoiceFor(org, ordinal, order)
45
+ if (!invoice) continue
46
+ for (const line of order.lines) {
47
+ yield row((invoice.id - 1) * 10 + line.lineNo, invoice.id, org.id, line.lineNo, line.productId, line.description, line.quantity, money(line.unitPriceCents), iso(invoice.createdAt), iso(invoice.createdAt))
48
+ }
49
+ }
50
+ }
51
+ }
52
+
53
+ export const seedSales = async (sql: Db, world: World) => {
54
+ await copyRows(sql, 'sales.orders (id, organization_id, number, company_id, person_id, owner_id, status, currency_code, discount_percent, total, paid, notes, created_at, updated_at)', orderRows(world))
55
+ await copyRows(sql, 'sales.order_lines (order_id, line_no, organization_id, product_id, description, quantity, unit_price, created_at, updated_at)', orderLineRows(world))
56
+ await copyRows(sql, 'sales.invoices (id, organization_id, number, order_id, company_id, status, issued_at, due_at, vat_rate, subtotal, vat, total, created_at, updated_at)', invoiceRows(world))
57
+ await copyRows(sql, 'sales.invoice_lines (id, invoice_id, organization_id, position, product_id, description, quantity, unit_price, created_at, updated_at)', invoiceLineRows(world))
58
+ }