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.
- package/dist/ask-preset.js +25 -0
- package/dist/create-project.js +7 -6
- package/dist/index.js +19 -9
- package/dist/next-steps.js +8 -2
- package/dist/presets.js +17 -0
- package/dist/templates/erp/.build-test-abf2a11f/protobase.config.ts +6 -0
- package/dist/templates/erp/.build-test-abf2a11f/resources.ts +2 -0
- package/dist/templates/erp/.build-test-b878b26f/config/index.ts +2 -0
- package/dist/templates/erp/.build-test-b878b26f/config/things/data.ts +2 -0
- package/dist/templates/erp/.build-test-b878b26f/config/things/ui.ts +2 -0
- package/dist/templates/erp/.env.example +14 -0
- package/dist/templates/erp/AGENTS.md +24 -0
- package/dist/templates/erp/auth/auth.ts +68 -0
- package/dist/templates/erp/components/orders-per-day.tsx +21 -0
- package/dist/templates/erp/config/bottom-bar.ts +9 -0
- package/dist/templates/erp/config/categories/data.ts +20 -0
- package/dist/templates/erp/config/categories/ui.ts +11 -0
- package/dist/templates/erp/config/companies/data.ts +23 -0
- package/dist/templates/erp/config/companies/ui.ts +22 -0
- package/dist/templates/erp/config/company_tags/data.ts +16 -0
- package/dist/templates/erp/config/company_tags/ui.ts +7 -0
- package/dist/templates/erp/config/countries/data.ts +13 -0
- package/dist/templates/erp/config/countries/ui.ts +6 -0
- package/dist/templates/erp/config/currencies/data.ts +14 -0
- package/dist/templates/erp/config/currencies/ui.ts +6 -0
- package/dist/templates/erp/config/emp_master/data.ts +19 -0
- package/dist/templates/erp/config/emp_master/ui.ts +7 -0
- package/dist/templates/erp/config/index.ts +42 -0
- package/dist/templates/erp/config/invoice_lines/data.ts +23 -0
- package/dist/templates/erp/config/invoice_lines/ui.ts +7 -0
- package/dist/templates/erp/config/invoices/data.ts +26 -0
- package/dist/templates/erp/config/invoices/ui.ts +38 -0
- package/dist/templates/erp/config/jobs.test.ts +23 -0
- package/dist/templates/erp/config/jobs.ts +29 -0
- package/dist/templates/erp/config/locations/data.ts +18 -0
- package/dist/templates/erp/config/locations/ui.ts +6 -0
- package/dist/templates/erp/config/order_lines/data.ts +20 -0
- package/dist/templates/erp/config/order_lines/ui.ts +7 -0
- package/dist/templates/erp/config/orders/data.ts +27 -0
- package/dist/templates/erp/config/orders/ui.ts +41 -0
- package/dist/templates/erp/config/organizations/data.ts +17 -0
- package/dist/templates/erp/config/organizations/ui.ts +6 -0
- package/dist/templates/erp/config/overview/page.tsx +80 -0
- package/dist/templates/erp/config/pages.test.ts +14 -0
- package/dist/templates/erp/config/people/data.ts +21 -0
- package/dist/templates/erp/config/people/ui.ts +6 -0
- package/dist/templates/erp/config/products/data.ts +25 -0
- package/dist/templates/erp/config/products/ui.ts +6 -0
- package/dist/templates/erp/config/roles.test.ts +56 -0
- package/dist/templates/erp/config/roles.ts +54 -0
- package/dist/templates/erp/config/shell.test.ts +24 -0
- package/dist/templates/erp/config/stock_levels/data.ts +16 -0
- package/dist/templates/erp/config/stock_levels/ui.ts +6 -0
- package/dist/templates/erp/config/stock_moves/data.ts +22 -0
- package/dist/templates/erp/config/stock_moves/ui.ts +25 -0
- package/dist/templates/erp/config/tags/data.ts +17 -0
- package/dist/templates/erp/config/tags/ui.ts +6 -0
- package/dist/templates/erp/config/user-menu.ts +9 -0
- package/dist/templates/erp/config/users/data.ts +19 -0
- package/dist/templates/erp/config/users/ui.ts +9 -0
- package/dist/templates/erp/config/warehouses/data.ts +19 -0
- package/dist/templates/erp/config/warehouses/ui.ts +6 -0
- package/dist/templates/erp/data.ts +3 -0
- package/dist/templates/erp/db/connection.ts +7 -0
- package/dist/templates/erp/db/drop.ts +9 -0
- package/dist/templates/erp/db/kysely.ts +13 -0
- package/dist/templates/erp/db/migrations/001_core.sql +48 -0
- package/dist/templates/erp/db/migrations/002_crm.sql +59 -0
- package/dist/templates/erp/db/migrations/003_catalog.sql +38 -0
- package/dist/templates/erp/db/migrations/004_sales.sql +86 -0
- package/dist/templates/erp/db/migrations/005_inventory.sql +65 -0
- package/dist/templates/erp/db/migrations/006_hr_legacy.sql +16 -0
- package/dist/templates/erp/db/migrations/007_filter_indexes.sql +4 -0
- package/dist/templates/erp/db/migrations/008_calculated_totals.sql +98 -0
- package/dist/templates/erp/db/migrations/011_auth_organizations.sql +22 -0
- package/dist/templates/erp/db/migrations/012_jobs.sql +531 -0
- package/dist/templates/erp/db/migrations/013_jobs.sql +98 -0
- package/dist/templates/erp/db/migrations/016_jobs.sql +99 -0
- package/dist/templates/erp/db/migrations/017_filter_indexes.sql +5 -0
- package/dist/templates/erp/db/migrations/018_sort_indexes.sql +4 -0
- package/dist/templates/erp/db/migrations/019_auth_users.sql +18 -0
- package/dist/templates/erp/functions/unpaid-orders.ts +13 -0
- package/dist/templates/erp/package.json +38 -0
- package/dist/templates/erp/protobase.config.ts +22 -0
- package/dist/templates/erp/protobase.ui.tsx +14 -0
- package/dist/templates/erp/seed/auth-organizations.ts +11 -0
- package/dist/templates/erp/seed/base-organization.test.ts +17 -0
- package/dist/templates/erp/seed/base-organization.ts +10 -0
- package/dist/templates/erp/seed/base.ts +22 -0
- package/dist/templates/erp/seed/builders/categories.ts +18 -0
- package/dist/templates/erp/seed/builders/company.ts +61 -0
- package/dist/templates/erp/seed/builders/invoice.ts +31 -0
- package/dist/templates/erp/seed/builders/order.ts +66 -0
- package/dist/templates/erp/seed/builders/product.ts +46 -0
- package/dist/templates/erp/seed/calendar.ts +60 -0
- package/dist/templates/erp/seed/cli.ts +54 -0
- package/dist/templates/erp/seed/copy.ts +22 -0
- package/dist/templates/erp/seed/data/catalogTree.ts +72 -0
- package/dist/templates/erp/seed/data/layout.ts +19 -0
- package/dist/templates/erp/seed/data/names.ts +44 -0
- package/dist/templates/erp/seed/data/reference.ts +53 -0
- package/dist/templates/erp/seed/format.ts +24 -0
- package/dist/templates/erp/seed/maintenance.ts +63 -0
- package/dist/templates/erp/seed/rng.ts +36 -0
- package/dist/templates/erp/seed/scales.ts +9 -0
- package/dist/templates/erp/seed/steps/catalog.ts +34 -0
- package/dist/templates/erp/seed/steps/crm.ts +62 -0
- package/dist/templates/erp/seed/steps/hr.ts +26 -0
- package/dist/templates/erp/seed/steps/ledger.ts +88 -0
- package/dist/templates/erp/seed/steps/reference.ts +24 -0
- package/dist/templates/erp/seed/steps/sales.ts +58 -0
- package/dist/templates/erp/seed/steps/warehouses.ts +42 -0
- package/dist/templates/erp/seed/world.ts +86 -0
- package/dist/templates/erp/vitest.config.ts +4 -0
- package/dist/templates/real-estate/.env.example +9 -0
- package/dist/templates/real-estate/AGENTS.md +24 -0
- package/dist/templates/real-estate/_.gitignore +3 -0
- package/dist/templates/real-estate/_bunfig.toml +4 -0
- package/dist/templates/real-estate/auth/auth.ts +36 -0
- package/dist/templates/real-estate/config/amenities/data.ts +12 -0
- package/dist/templates/real-estate/config/amenities/ui.ts +7 -0
- package/dist/templates/real-estate/config/arrears/data.ts +19 -0
- package/dist/templates/real-estate/config/arrears/ui.ts +21 -0
- package/dist/templates/real-estate/config/deposits/data.ts +21 -0
- package/dist/templates/real-estate/config/deposits/ui.ts +17 -0
- package/dist/templates/real-estate/config/index.ts +41 -0
- package/dist/templates/real-estate/config/inspections/data.ts +22 -0
- package/dist/templates/real-estate/config/inspections/ui.ts +15 -0
- package/dist/templates/real-estate/config/lease_tenants/data.ts +16 -0
- package/dist/templates/real-estate/config/lease_tenants/ui.ts +8 -0
- package/dist/templates/real-estate/config/leases/data.ts +29 -0
- package/dist/templates/real-estate/config/leases/ui.ts +33 -0
- package/dist/templates/real-estate/config/organizations/data.ts +17 -0
- package/dist/templates/real-estate/config/organizations/ui.ts +7 -0
- package/dist/templates/real-estate/config/overview/page.tsx +57 -0
- package/dist/templates/real-estate/config/owners/data.ts +22 -0
- package/dist/templates/real-estate/config/owners/ui.ts +17 -0
- package/dist/templates/real-estate/config/pages.test.ts +49 -0
- package/dist/templates/real-estate/config/payments/data.ts +20 -0
- package/dist/templates/real-estate/config/payments/ui.ts +14 -0
- package/dist/templates/real-estate/config/properties/data.ts +26 -0
- package/dist/templates/real-estate/config/properties/ui.ts +21 -0
- package/dist/templates/real-estate/config/rent_charges/data.ts +22 -0
- package/dist/templates/real-estate/config/rent_charges/ui.ts +27 -0
- package/dist/templates/real-estate/config/roles.test.ts +49 -0
- package/dist/templates/real-estate/config/roles.ts +45 -0
- package/dist/templates/real-estate/config/shell.test.ts +18 -0
- package/dist/templates/real-estate/config/tenants/data.ts +23 -0
- package/dist/templates/real-estate/config/tenants/ui.ts +27 -0
- package/dist/templates/real-estate/config/tickets/data.ts +33 -0
- package/dist/templates/real-estate/config/tickets/ui.ts +58 -0
- package/dist/templates/real-estate/config/unit_amenities/data.ts +15 -0
- package/dist/templates/real-estate/config/unit_amenities/ui.ts +8 -0
- package/dist/templates/real-estate/config/units/data.ts +24 -0
- package/dist/templates/real-estate/config/units/ui.ts +41 -0
- package/dist/templates/real-estate/config/user-menu.ts +9 -0
- package/dist/templates/real-estate/config/users/data.ts +19 -0
- package/dist/templates/real-estate/config/users/ui.ts +11 -0
- package/dist/templates/real-estate/config/valuations/data.ts +19 -0
- package/dist/templates/real-estate/config/valuations/ui.ts +13 -0
- package/dist/templates/real-estate/config/vendors/data.ts +21 -0
- package/dist/templates/real-estate/config/vendors/ui.ts +18 -0
- package/dist/templates/real-estate/config/work_orders/data.ts +22 -0
- package/dist/templates/real-estate/config/work_orders/ui.ts +18 -0
- package/dist/templates/real-estate/db/connection.ts +7 -0
- package/dist/templates/real-estate/db/drop.ts +9 -0
- package/dist/templates/real-estate/db/kysely.ts +13 -0
- package/dist/templates/real-estate/db/migrate.ts +30 -0
- package/dist/templates/real-estate/db/migrations/001_core.sql +33 -0
- package/dist/templates/real-estate/db/migrations/002_portfolio.sql +97 -0
- package/dist/templates/real-estate/db/migrations/003_leasing.sql +78 -0
- package/dist/templates/real-estate/db/migrations/004_billing.sql +88 -0
- package/dist/templates/real-estate/db/migrations/005_maintenance.sql +105 -0
- package/dist/templates/real-estate/db/migrations/006_outstanding.sql +14 -0
- package/dist/templates/real-estate/db/migrations/007_auth.sql +36 -0
- package/dist/templates/real-estate/db/migrations/008_auth_snake_case.sql +109 -0
- package/dist/templates/real-estate/db/migrations/009_auth.sql +6 -0
- package/dist/templates/real-estate/db/migrations/010_auth.sql +4 -0
- package/dist/templates/real-estate/db/migrations/011_filter_indexes.sql +8 -0
- package/dist/templates/real-estate/db/migrations/012_sort_indexes.sql +2 -0
- package/dist/templates/real-estate/db/migrations/013_auth_users.sql +17 -0
- package/dist/templates/real-estate/node-types.d.ts +1 -0
- package/dist/templates/real-estate/package.json +38 -0
- package/dist/templates/real-estate/protobase.config.ts +5 -0
- package/dist/templates/real-estate/protobase.ui.tsx +16 -0
- package/dist/templates/real-estate/seed/base-organization.test.ts +17 -0
- package/dist/templates/real-estate/seed/base-organization.ts +10 -0
- package/dist/templates/real-estate/seed/base.ts +18 -0
- package/dist/templates/real-estate/seed/builders/allocation.ts +47 -0
- package/dist/templates/real-estate/seed/builders/ledger.ts +52 -0
- package/dist/templates/real-estate/seed/builders/property.ts +98 -0
- package/dist/templates/real-estate/seed/builders/tenancy.ts +99 -0
- package/dist/templates/real-estate/seed/builders/ticket.ts +115 -0
- package/dist/templates/real-estate/seed/calendar.ts +77 -0
- package/dist/templates/real-estate/seed/cli.ts +50 -0
- package/dist/templates/real-estate/seed/copy.ts +22 -0
- package/dist/templates/real-estate/seed/data/names.ts +31 -0
- package/dist/templates/real-estate/seed/data/reference.ts +63 -0
- package/dist/templates/real-estate/seed/format.ts +24 -0
- package/dist/templates/real-estate/seed/iban.ts +15 -0
- package/dist/templates/real-estate/seed/maintenance.ts +63 -0
- package/dist/templates/real-estate/seed/rng.ts +36 -0
- package/dist/templates/real-estate/seed/scales.ts +9 -0
- package/dist/templates/real-estate/seed/steps/billing.ts +44 -0
- package/dist/templates/real-estate/seed/steps/leasing.ts +60 -0
- package/dist/templates/real-estate/seed/steps/maintenance.ts +77 -0
- package/dist/templates/real-estate/seed/steps/portfolio.ts +97 -0
- package/dist/templates/real-estate/seed/steps/reference.ts +21 -0
- package/dist/templates/real-estate/seed/world.ts +92 -0
- package/dist/templates/real-estate/tsconfig.json +16 -0
- package/dist/templates/real-estate/vitest.config.ts +4 -0
- package/dist/templates/scratch/_.gitignore +3 -0
- package/dist/templates/scratch/_bunfig.toml +4 -0
- package/dist/templates/scratch/db/migrate.ts +30 -0
- package/dist/templates/scratch/db/migrations/001_auth.sql +36 -0
- package/dist/templates/scratch/db/migrations/002_auth_snake_case.sql +109 -0
- package/dist/templates/scratch/db/migrations/003_auth.sql +6 -0
- package/dist/templates/scratch/db/migrations/004_auth.sql +4 -0
- package/dist/templates/scratch/node-types.d.ts +1 -0
- package/dist/{template → templates/scratch}/package.json +5 -5
- package/dist/templates/scratch/tsconfig.json +16 -0
- package/package.json +2 -2
- /package/dist/{template → templates/erp}/_.gitignore +0 -0
- /package/dist/{template → templates/erp}/_bunfig.toml +0 -0
- /package/dist/{template → templates/erp}/db/migrate.ts +0 -0
- /package/dist/{template/db/migrations/001_auth.sql → templates/erp/db/migrations/009_auth.sql} +0 -0
- /package/dist/{template/db/migrations/002_auth_snake_case.sql → templates/erp/db/migrations/010_auth_snake_case.sql} +0 -0
- /package/dist/{template/db/migrations/003_auth.sql → templates/erp/db/migrations/014_auth.sql} +0 -0
- /package/dist/{template/db/migrations/004_auth.sql → templates/erp/db/migrations/015_auth.sql} +0 -0
- /package/dist/{template → templates/erp}/node-types.d.ts +0 -0
- /package/dist/{template → templates/erp}/tsconfig.json +0 -0
- /package/dist/{template → templates/scratch}/.env.example +0 -0
- /package/dist/{template → templates/scratch}/AGENTS.md +0 -0
- /package/dist/{template → templates/scratch}/auth/auth.ts +0 -0
- /package/dist/{template → templates/scratch}/config/index.ts +0 -0
- /package/dist/{template → templates/scratch}/config/user-menu.ts +0 -0
- /package/dist/{template → templates/scratch}/db/connection.ts +0 -0
- /package/dist/{template → templates/scratch}/db/migrations/.gitkeep +0 -0
- /package/dist/{template → templates/scratch}/protobase.config.ts +0 -0
- /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
|
+
}
|