@open-mercato/core 0.4.2-canary-cf7d9b4116 → 0.4.2-canary-e6bf6a353e

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 (136) hide show
  1. package/dist/modules/auth/lib/setup-app.js +2 -0
  2. package/dist/modules/auth/lib/setup-app.js.map +2 -2
  3. package/dist/modules/catalog/analytics.js +27 -0
  4. package/dist/modules/catalog/analytics.js.map +7 -0
  5. package/dist/modules/customers/analytics.js +50 -0
  6. package/dist/modules/customers/analytics.js.map +7 -0
  7. package/dist/modules/dashboards/acl.js +2 -1
  8. package/dist/modules/dashboards/acl.js.map +2 -2
  9. package/dist/modules/dashboards/api/widgets/data/route.js +187 -0
  10. package/dist/modules/dashboards/api/widgets/data/route.js.map +7 -0
  11. package/dist/modules/dashboards/cli.js +142 -1
  12. package/dist/modules/dashboards/cli.js.map +2 -2
  13. package/dist/modules/dashboards/di.js +11 -0
  14. package/dist/modules/dashboards/di.js.map +7 -0
  15. package/dist/modules/dashboards/lib/aggregations.js +162 -0
  16. package/dist/modules/dashboards/lib/aggregations.js.map +7 -0
  17. package/dist/modules/dashboards/lib/formatters.js +34 -0
  18. package/dist/modules/dashboards/lib/formatters.js.map +7 -0
  19. package/dist/modules/dashboards/seed/analytics.js +383 -0
  20. package/dist/modules/dashboards/seed/analytics.js.map +7 -0
  21. package/dist/modules/dashboards/services/analyticsRegistry.js +52 -0
  22. package/dist/modules/dashboards/services/analyticsRegistry.js.map +7 -0
  23. package/dist/modules/dashboards/services/widgetDataService.js +207 -0
  24. package/dist/modules/dashboards/services/widgetDataService.js.map +7 -0
  25. package/dist/modules/dashboards/widgets/dashboard/aov-kpi/config.js +18 -0
  26. package/dist/modules/dashboards/widgets/dashboard/aov-kpi/config.js.map +7 -0
  27. package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.js +128 -0
  28. package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.js.map +7 -0
  29. package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.js +25 -0
  30. package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.js.map +7 -0
  31. package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/config.js +18 -0
  32. package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/config.js.map +7 -0
  33. package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.client.js +126 -0
  34. package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.client.js.map +7 -0
  35. package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.js +25 -0
  36. package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.js.map +7 -0
  37. package/dist/modules/dashboards/widgets/dashboard/orders-by-status/config.js +18 -0
  38. package/dist/modules/dashboards/widgets/dashboard/orders-by-status/config.js.map +7 -0
  39. package/dist/modules/dashboards/widgets/dashboard/orders-by-status/widget.client.js +151 -0
  40. package/dist/modules/dashboards/widgets/dashboard/orders-by-status/widget.client.js.map +7 -0
  41. package/dist/modules/dashboards/widgets/dashboard/orders-by-status/widget.js +25 -0
  42. package/dist/modules/dashboards/widgets/dashboard/orders-by-status/widget.js.map +7 -0
  43. package/dist/modules/dashboards/widgets/dashboard/orders-kpi/config.js +18 -0
  44. package/dist/modules/dashboards/widgets/dashboard/orders-kpi/config.js.map +7 -0
  45. package/dist/modules/dashboards/widgets/dashboard/orders-kpi/widget.client.js +126 -0
  46. package/dist/modules/dashboards/widgets/dashboard/orders-kpi/widget.client.js.map +7 -0
  47. package/dist/modules/dashboards/widgets/dashboard/orders-kpi/widget.js +25 -0
  48. package/dist/modules/dashboards/widgets/dashboard/orders-kpi/widget.js.map +7 -0
  49. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/config.js +16 -0
  50. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/config.js.map +7 -0
  51. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js +123 -0
  52. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js.map +7 -0
  53. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.js +25 -0
  54. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.js.map +7 -0
  55. package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/config.js +18 -0
  56. package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/config.js.map +7 -0
  57. package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.js +128 -0
  58. package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.js.map +7 -0
  59. package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/widget.js +25 -0
  60. package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/widget.js.map +7 -0
  61. package/dist/modules/dashboards/widgets/dashboard/revenue-trend/config.js +21 -0
  62. package/dist/modules/dashboards/widgets/dashboard/revenue-trend/config.js.map +7 -0
  63. package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.js +211 -0
  64. package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.js.map +7 -0
  65. package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.js +25 -0
  66. package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.js.map +7 -0
  67. package/dist/modules/dashboards/widgets/dashboard/sales-by-region/config.js +19 -0
  68. package/dist/modules/dashboards/widgets/dashboard/sales-by-region/config.js.map +7 -0
  69. package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.js +131 -0
  70. package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.js.map +7 -0
  71. package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.js +25 -0
  72. package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.js.map +7 -0
  73. package/dist/modules/dashboards/widgets/dashboard/top-customers/config.js +19 -0
  74. package/dist/modules/dashboards/widgets/dashboard/top-customers/config.js.map +7 -0
  75. package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.client.js +153 -0
  76. package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.client.js.map +7 -0
  77. package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.js +25 -0
  78. package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.js.map +7 -0
  79. package/dist/modules/dashboards/widgets/dashboard/top-products/config.js +22 -0
  80. package/dist/modules/dashboards/widgets/dashboard/top-products/config.js.map +7 -0
  81. package/dist/modules/dashboards/widgets/dashboard/top-products/widget.client.js +180 -0
  82. package/dist/modules/dashboards/widgets/dashboard/top-products/widget.client.js.map +7 -0
  83. package/dist/modules/dashboards/widgets/dashboard/top-products/widget.js +25 -0
  84. package/dist/modules/dashboards/widgets/dashboard/top-products/widget.js.map +7 -0
  85. package/dist/modules/sales/analytics.js +67 -0
  86. package/dist/modules/sales/analytics.js.map +7 -0
  87. package/package.json +2 -2
  88. package/src/modules/auth/lib/setup-app.ts +2 -0
  89. package/src/modules/catalog/analytics.ts +24 -0
  90. package/src/modules/customers/analytics.ts +47 -0
  91. package/src/modules/dashboards/acl.ts +1 -0
  92. package/src/modules/dashboards/api/widgets/data/route.ts +221 -0
  93. package/src/modules/dashboards/cli.ts +164 -1
  94. package/src/modules/dashboards/di.ts +9 -0
  95. package/src/modules/dashboards/i18n/de.json +115 -1
  96. package/src/modules/dashboards/i18n/en.json +115 -1
  97. package/src/modules/dashboards/i18n/es.json +115 -1
  98. package/src/modules/dashboards/i18n/pl.json +115 -1
  99. package/src/modules/dashboards/lib/__tests__/aggregations.test.ts +327 -0
  100. package/src/modules/dashboards/lib/__tests__/formatters.test.ts +128 -0
  101. package/src/modules/dashboards/lib/aggregations.ts +225 -0
  102. package/src/modules/dashboards/lib/formatters.ts +36 -0
  103. package/src/modules/dashboards/seed/analytics.ts +405 -0
  104. package/src/modules/dashboards/services/analyticsRegistry.ts +79 -0
  105. package/src/modules/dashboards/services/widgetDataService.ts +329 -0
  106. package/src/modules/dashboards/widgets/dashboard/aov-kpi/config.ts +20 -0
  107. package/src/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.tsx +135 -0
  108. package/src/modules/dashboards/widgets/dashboard/aov-kpi/widget.ts +24 -0
  109. package/src/modules/dashboards/widgets/dashboard/new-customers-kpi/config.ts +20 -0
  110. package/src/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.client.tsx +133 -0
  111. package/src/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.ts +24 -0
  112. package/src/modules/dashboards/widgets/dashboard/orders-by-status/config.ts +20 -0
  113. package/src/modules/dashboards/widgets/dashboard/orders-by-status/widget.client.tsx +154 -0
  114. package/src/modules/dashboards/widgets/dashboard/orders-by-status/widget.ts +24 -0
  115. package/src/modules/dashboards/widgets/dashboard/orders-kpi/config.ts +20 -0
  116. package/src/modules/dashboards/widgets/dashboard/orders-kpi/widget.client.tsx +133 -0
  117. package/src/modules/dashboards/widgets/dashboard/orders-kpi/widget.ts +24 -0
  118. package/src/modules/dashboards/widgets/dashboard/pipeline-summary/config.ts +17 -0
  119. package/src/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.tsx +137 -0
  120. package/src/modules/dashboards/widgets/dashboard/pipeline-summary/widget.ts +24 -0
  121. package/src/modules/dashboards/widgets/dashboard/revenue-kpi/config.ts +20 -0
  122. package/src/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.tsx +135 -0
  123. package/src/modules/dashboards/widgets/dashboard/revenue-kpi/widget.ts +24 -0
  124. package/src/modules/dashboards/widgets/dashboard/revenue-trend/config.ts +24 -0
  125. package/src/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.tsx +220 -0
  126. package/src/modules/dashboards/widgets/dashboard/revenue-trend/widget.ts +24 -0
  127. package/src/modules/dashboards/widgets/dashboard/sales-by-region/config.ts +21 -0
  128. package/src/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.tsx +131 -0
  129. package/src/modules/dashboards/widgets/dashboard/sales-by-region/widget.ts +24 -0
  130. package/src/modules/dashboards/widgets/dashboard/top-customers/config.ts +21 -0
  131. package/src/modules/dashboards/widgets/dashboard/top-customers/widget.client.tsx +161 -0
  132. package/src/modules/dashboards/widgets/dashboard/top-customers/widget.ts +24 -0
  133. package/src/modules/dashboards/widgets/dashboard/top-products/config.ts +27 -0
  134. package/src/modules/dashboards/widgets/dashboard/top-products/widget.client.tsx +181 -0
  135. package/src/modules/dashboards/widgets/dashboard/top-products/widget.ts +24 -0
  136. package/src/modules/sales/analytics.ts +64 -0
@@ -0,0 +1,36 @@
1
+ export type FormatCurrencyOptions = {
2
+ currency?: string
3
+ minimumFractionDigits?: number
4
+ maximumFractionDigits?: number
5
+ }
6
+
7
+ export function formatCurrency(value: number, options: FormatCurrencyOptions = {}): string {
8
+ const { currency = 'USD', minimumFractionDigits = 0, maximumFractionDigits = 0 } = options
9
+ return new Intl.NumberFormat(undefined, {
10
+ style: 'currency',
11
+ currency,
12
+ minimumFractionDigits,
13
+ maximumFractionDigits,
14
+ }).format(value)
15
+ }
16
+
17
+ export function formatCurrencyWithDecimals(value: number, options: FormatCurrencyOptions = {}): string {
18
+ return formatCurrency(value, { minimumFractionDigits: 2, maximumFractionDigits: 2, ...options })
19
+ }
20
+
21
+ export function formatCurrencyCompact(value: number, currencySymbol = '$'): string {
22
+ if (Math.abs(value) >= 1_000_000) {
23
+ return `${currencySymbol}${(value / 1_000_000).toFixed(1)}M`
24
+ }
25
+ if (Math.abs(value) >= 1_000) {
26
+ return `${currencySymbol}${(value / 1_000).toFixed(1)}K`
27
+ }
28
+ return `${currencySymbol}${value.toFixed(0)}`
29
+ }
30
+
31
+ export function formatCurrencySafe(value: unknown, fallback = '--'): string {
32
+ if (value === null || value === undefined) return fallback
33
+ const num = Number(value)
34
+ if (!Number.isFinite(num)) return fallback
35
+ return formatCurrency(num)
36
+ }
@@ -0,0 +1,405 @@
1
+ import { randomUUID } from 'crypto'
2
+ import type { EntityManager } from '@mikro-orm/postgresql'
3
+ import {
4
+ SalesOrder,
5
+ SalesOrderLine,
6
+ } from '@open-mercato/core/modules/sales/data/entities'
7
+ import {
8
+ CustomerEntity,
9
+ CustomerCompanyProfile,
10
+ CustomerDeal,
11
+ } from '@open-mercato/core/modules/customers/data/entities'
12
+ import {
13
+ CatalogProduct,
14
+ CatalogProductVariant,
15
+ } from '@open-mercato/core/modules/catalog/data/entities'
16
+
17
+ export type AnalyticsSeedScope = {
18
+ tenantId: string
19
+ organizationId: string
20
+ }
21
+
22
+ export type AnalyticsSeedOptions = {
23
+ months?: number
24
+ ordersPerMonth?: number
25
+ customersCount?: number
26
+ productsCount?: number
27
+ dealsCount?: number
28
+ }
29
+
30
+ const ORDER_STATUSES = ['draft', 'pending', 'confirmed', 'processing', 'shipped', 'delivered', 'cancelled'] as const
31
+ const FULFILLMENT_STATUSES = ['pending', 'in_fulfillment', 'partially_fulfilled', 'fulfilled'] as const
32
+ const PAYMENT_STATUSES = ['unpaid', 'partial', 'paid', 'refunded'] as const
33
+ const DEAL_PIPELINE_STAGES = ['lead', 'qualified', 'proposal', 'negotiation', 'closed_won', 'closed_lost'] as const
34
+ const COUNTRIES = ['US', 'GB', 'DE', 'FR', 'CA', 'AU', 'NL', 'ES', 'IT', 'PL'] as const
35
+ const REGIONS_BY_COUNTRY: Record<string, string[]> = {
36
+ US: ['California', 'New York', 'Texas', 'Florida', 'Illinois', 'Washington', 'Massachusetts'],
37
+ GB: ['England', 'Scotland', 'Wales'],
38
+ DE: ['Bavaria', 'Berlin', 'Hamburg', 'Hessen'],
39
+ FR: ['Île-de-France', 'Provence', 'Rhône-Alpes'],
40
+ CA: ['Ontario', 'Quebec', 'British Columbia'],
41
+ AU: ['New South Wales', 'Victoria', 'Queensland'],
42
+ NL: ['North Holland', 'South Holland'],
43
+ ES: ['Madrid', 'Catalonia', 'Andalusia'],
44
+ IT: ['Lombardy', 'Lazio', 'Veneto'],
45
+ PL: ['Mazovia', 'Lesser Poland', 'Silesia'],
46
+ }
47
+
48
+ const COMPANY_NAMES = [
49
+ 'Acme Corp', 'Global Industries', 'Tech Solutions', 'Prime Services',
50
+ 'Northern Analytics', 'Blue Ocean Trading', 'Summit Enterprises', 'Horizon Dynamics',
51
+ 'Vertex Systems', 'Atlas Logistics', 'Pinnacle Group', 'Quantum Labs',
52
+ 'Stellar Innovations', 'Pacific Partners', 'Apex Manufacturing', 'Nexus Technologies',
53
+ 'Eclipse Ventures', 'Titan Holdings', 'Vanguard Solutions', 'Momentum Corp',
54
+ 'Crystal Clear Media', 'Silver Line Transport', 'Golden Gate Imports', 'Red Rock Mining',
55
+ 'Green Valley Foods', 'Blue Sky Aviation', 'White Mountain Retail', 'Black Diamond Sports',
56
+ ]
57
+
58
+ const PRODUCT_NAMES = [
59
+ 'Premium Widget', 'Standard Component', 'Professional Kit', 'Enterprise Module',
60
+ 'Basic Starter Pack', 'Advanced System', 'Deluxe Bundle', 'Essential Tools',
61
+ 'Pro Series Device', 'Ultra Performance Unit', 'Classic Edition', 'Limited Series',
62
+ 'Industrial Grade Part', 'Consumer Package', 'Business Solution', 'Home Edition',
63
+ ]
64
+
65
+ const DEAL_TITLES = [
66
+ 'Enterprise License Deal', 'Annual Subscription', 'Pilot Program', 'Strategic Partnership',
67
+ 'Volume Purchase Agreement', 'Service Contract', 'Implementation Project', 'Expansion Deal',
68
+ 'Renewal Opportunity', 'Upsell Initiative', 'Cross-sell Package', 'Custom Solution',
69
+ ]
70
+
71
+ function randomInt(min: number, max: number): number {
72
+ return Math.floor(Math.random() * (max - min + 1)) + min
73
+ }
74
+
75
+ function randomFloat(min: number, max: number, decimals = 2): number {
76
+ const value = Math.random() * (max - min) + min
77
+ return Number(value.toFixed(decimals))
78
+ }
79
+
80
+ function randomElement<T>(arr: readonly T[]): T {
81
+ return arr[Math.floor(Math.random() * arr.length)]
82
+ }
83
+
84
+ function randomElements<T>(arr: readonly T[], count: number): T[] {
85
+ const shuffled = [...arr].sort(() => Math.random() - 0.5)
86
+ return shuffled.slice(0, Math.min(count, arr.length))
87
+ }
88
+
89
+ function toAmount(value: number): string {
90
+ return value.toFixed(2)
91
+ }
92
+
93
+ function daysAgo(days: number): Date {
94
+ const date = new Date()
95
+ date.setDate(date.getDate() - days)
96
+ return date
97
+ }
98
+
99
+ function randomDateInRange(startDaysAgo: number, endDaysAgo: number): Date {
100
+ const daysOffset = randomInt(endDaysAgo, startDaysAgo)
101
+ return daysAgo(daysOffset)
102
+ }
103
+
104
+ function generateOrderNumber(index: number): string {
105
+ return `SO-ANALYTICS-${String(index).padStart(5, '0')}`
106
+ }
107
+
108
+ export async function seedAnalyticsData(
109
+ em: EntityManager,
110
+ scope: AnalyticsSeedScope,
111
+ options: AnalyticsSeedOptions = {}
112
+ ): Promise<{ orders: number; customers: number; products: number; deals: number }> {
113
+ const {
114
+ months = 6,
115
+ ordersPerMonth = 50,
116
+ customersCount = 25,
117
+ productsCount = 15,
118
+ dealsCount = 20,
119
+ } = options
120
+
121
+ const existingOrders = await em.count(SalesOrder, {
122
+ tenantId: scope.tenantId,
123
+ organizationId: scope.organizationId,
124
+ orderNumber: { $like: 'SO-ANALYTICS-%' },
125
+ })
126
+
127
+ if (existingOrders > 0) {
128
+ return { orders: 0, customers: 0, products: 0, deals: 0 }
129
+ }
130
+
131
+ const customers: CustomerEntity[] = []
132
+ const products: CatalogProduct[] = []
133
+ const variants: CatalogProductVariant[] = []
134
+
135
+ for (let i = 0; i < customersCount; i++) {
136
+ const companyName = COMPANY_NAMES[i % COMPANY_NAMES.length]
137
+ const customerCreatedAt = randomDateInRange(months * 30 + 60, 0)
138
+
139
+ const customer = em.create(CustomerEntity, {
140
+ id: randomUUID(),
141
+ organizationId: scope.organizationId,
142
+ tenantId: scope.tenantId,
143
+ kind: 'company',
144
+ displayName: `${companyName} #${i + 1}`,
145
+ primaryEmail: `contact${i + 1}@${companyName.toLowerCase().replace(/\s+/g, '')}.example.com`,
146
+ status: 'active',
147
+ lifecycleStage: randomElement(['lead', 'customer', 'opportunity']),
148
+ isActive: true,
149
+ createdAt: customerCreatedAt,
150
+ updatedAt: customerCreatedAt,
151
+ })
152
+ em.persist(customer)
153
+ customers.push(customer)
154
+
155
+ const companyProfile = em.create(CustomerCompanyProfile, {
156
+ id: randomUUID(),
157
+ organizationId: scope.organizationId,
158
+ tenantId: scope.tenantId,
159
+ entity: customer,
160
+ legalName: `${companyName} Inc.`,
161
+ brandName: companyName,
162
+ industry: randomElement(['Technology', 'Manufacturing', 'Retail', 'Services', 'Healthcare']),
163
+ sizeBucket: randomElement(['small', 'medium', 'large', 'enterprise']),
164
+ annualRevenue: toAmount(randomFloat(100000, 50000000)),
165
+ createdAt: customerCreatedAt,
166
+ updatedAt: customerCreatedAt,
167
+ })
168
+ em.persist(companyProfile)
169
+ }
170
+
171
+ for (let i = 0; i < productsCount; i++) {
172
+ const productName = PRODUCT_NAMES[i % PRODUCT_NAMES.length]
173
+ const productCreatedAt = daysAgo(months * 30 + randomInt(0, 30))
174
+
175
+ const product = em.create(CatalogProduct, {
176
+ id: randomUUID(),
177
+ organizationId: scope.organizationId,
178
+ tenantId: scope.tenantId,
179
+ title: `${productName} ${i + 1}`,
180
+ handle: `analytics-product-${i + 1}`,
181
+ sku: `SKU-ANALYTICS-${String(i + 1).padStart(3, '0')}`,
182
+ productType: 'simple',
183
+ isConfigurable: false,
184
+ isActive: true,
185
+ createdAt: productCreatedAt,
186
+ updatedAt: productCreatedAt,
187
+ })
188
+ em.persist(product)
189
+ products.push(product)
190
+
191
+ const variant = em.create(CatalogProductVariant, {
192
+ id: randomUUID(),
193
+ organizationId: scope.organizationId,
194
+ tenantId: scope.tenantId,
195
+ product,
196
+ name: 'Default',
197
+ sku: `${product.sku}-DEFAULT`,
198
+ isDefault: true,
199
+ isActive: true,
200
+ createdAt: productCreatedAt,
201
+ updatedAt: productCreatedAt,
202
+ })
203
+ em.persist(variant)
204
+ variants.push(variant)
205
+ }
206
+
207
+ let orderIndex = 1
208
+ const totalDays = months * 30
209
+ const orders: SalesOrder[] = []
210
+
211
+ for (let dayOffset = totalDays; dayOffset >= 0; dayOffset--) {
212
+ const ordersToday = Math.round(ordersPerMonth / 30 * randomFloat(0.5, 1.5))
213
+
214
+ for (let j = 0; j < ordersToday; j++) {
215
+ const orderDate = daysAgo(dayOffset)
216
+ const customer = randomElement(customers)
217
+ const country = randomElement(COUNTRIES)
218
+ const region = randomElement(REGIONS_BY_COUNTRY[country] || [''])
219
+
220
+ const lineCount = randomInt(1, 5)
221
+ const selectedProducts = randomElements(products, lineCount)
222
+
223
+ let subtotalNet = 0
224
+ let subtotalGross = 0
225
+ let taxTotal = 0
226
+
227
+ const orderLines: Array<{
228
+ product: CatalogProduct
229
+ variant: CatalogProductVariant
230
+ quantity: number
231
+ unitPriceNet: number
232
+ unitPriceGross: number
233
+ taxRate: number
234
+ lineNetAmount: number
235
+ lineGrossAmount: number
236
+ lineTaxAmount: number
237
+ }> = []
238
+
239
+ for (let k = 0; k < selectedProducts.length; k++) {
240
+ const product = selectedProducts[k]
241
+ const variant = variants.find((v) => v.product.id === product.id) || variants[0]
242
+ const quantity = randomInt(1, 10)
243
+ const unitPriceNet = randomFloat(10, 500)
244
+ const taxRate = randomElement([0, 5, 10, 20, 23])
245
+ const unitPriceGross = unitPriceNet * (1 + taxRate / 100)
246
+ const lineNetAmount = unitPriceNet * quantity
247
+ const lineGrossAmount = unitPriceGross * quantity
248
+ const lineTaxAmount = lineGrossAmount - lineNetAmount
249
+
250
+ subtotalNet += lineNetAmount
251
+ subtotalGross += lineGrossAmount
252
+ taxTotal += lineTaxAmount
253
+
254
+ orderLines.push({
255
+ product,
256
+ variant,
257
+ quantity,
258
+ unitPriceNet,
259
+ unitPriceGross,
260
+ taxRate,
261
+ lineNetAmount,
262
+ lineGrossAmount,
263
+ lineTaxAmount,
264
+ })
265
+ }
266
+
267
+ const order = em.create(SalesOrder, {
268
+ id: randomUUID(),
269
+ organizationId: scope.organizationId,
270
+ tenantId: scope.tenantId,
271
+ orderNumber: generateOrderNumber(orderIndex++),
272
+ status: randomElement(ORDER_STATUSES),
273
+ fulfillmentStatus: randomElement(FULFILLMENT_STATUSES),
274
+ paymentStatus: randomElement(PAYMENT_STATUSES),
275
+ customerEntityId: customer.id,
276
+ customerSnapshot: {
277
+ customer: {
278
+ id: customer.id,
279
+ kind: customer.kind,
280
+ displayName: customer.displayName,
281
+ },
282
+ },
283
+ currencyCode: 'USD',
284
+ placedAt: orderDate,
285
+ shippingAddressSnapshot: {
286
+ country,
287
+ region,
288
+ city: `City ${randomInt(1, 100)}`,
289
+ postalCode: String(randomInt(10000, 99999)),
290
+ },
291
+ billingAddressSnapshot: {
292
+ country,
293
+ region,
294
+ city: `City ${randomInt(1, 100)}`,
295
+ postalCode: String(randomInt(10000, 99999)),
296
+ },
297
+ subtotalNetAmount: toAmount(subtotalNet),
298
+ subtotalGrossAmount: toAmount(subtotalGross),
299
+ discountTotalAmount: '0.00',
300
+ taxTotalAmount: toAmount(taxTotal),
301
+ shippingNetAmount: '0.00',
302
+ shippingGrossAmount: '0.00',
303
+ surchargeTotalAmount: '0.00',
304
+ grandTotalNetAmount: toAmount(subtotalNet),
305
+ grandTotalGrossAmount: toAmount(subtotalGross),
306
+ paidTotalAmount: '0.00',
307
+ refundedTotalAmount: '0.00',
308
+ outstandingAmount: toAmount(subtotalGross),
309
+ lineItemCount: orderLines.length,
310
+ metadata: { seed: 'dashboards.analytics' },
311
+ createdAt: orderDate,
312
+ updatedAt: orderDate,
313
+ })
314
+ em.persist(order)
315
+ orders.push(order)
316
+
317
+ for (let k = 0; k < orderLines.length; k++) {
318
+ const lineData = orderLines[k]
319
+ const line = em.create(SalesOrderLine, {
320
+ id: randomUUID(),
321
+ order,
322
+ organizationId: scope.organizationId,
323
+ tenantId: scope.tenantId,
324
+ lineNumber: k + 1,
325
+ kind: 'product',
326
+ name: lineData.product.title,
327
+ quantity: toAmount(lineData.quantity),
328
+ currencyCode: 'USD',
329
+ unitPriceNet: toAmount(lineData.unitPriceNet),
330
+ unitPriceGross: toAmount(lineData.unitPriceGross),
331
+ discountAmount: '0.00',
332
+ discountPercent: '0.00',
333
+ taxRate: toAmount(lineData.taxRate),
334
+ taxAmount: toAmount(lineData.lineTaxAmount),
335
+ totalNetAmount: toAmount(lineData.lineNetAmount),
336
+ totalGrossAmount: toAmount(lineData.lineGrossAmount),
337
+ reservedQuantity: '0',
338
+ fulfilledQuantity: '0',
339
+ invoicedQuantity: '0',
340
+ returnedQuantity: '0',
341
+ productId: lineData.product.id,
342
+ productVariantId: lineData.variant?.id ?? null,
343
+ catalogSnapshot: {
344
+ product: {
345
+ id: lineData.product.id,
346
+ title: lineData.product.title,
347
+ sku: lineData.product.sku,
348
+ },
349
+ variant: lineData.variant
350
+ ? {
351
+ id: lineData.variant.id,
352
+ name: lineData.variant.name,
353
+ sku: lineData.variant.sku,
354
+ }
355
+ : null,
356
+ },
357
+ createdAt: orderDate,
358
+ updatedAt: orderDate,
359
+ })
360
+ em.persist(line)
361
+ }
362
+ }
363
+ }
364
+
365
+ for (let i = 0; i < dealsCount; i++) {
366
+ const customer = randomElement(customers)
367
+ const dealCreatedAt = randomDateInRange(months * 30, 0)
368
+ const pipelineStage = randomElement(DEAL_PIPELINE_STAGES)
369
+
370
+ const probabilityByStage: Record<string, number> = {
371
+ lead: 10,
372
+ qualified: 25,
373
+ proposal: 50,
374
+ negotiation: 75,
375
+ closed_won: 100,
376
+ closed_lost: 0,
377
+ }
378
+
379
+ const deal = em.create(CustomerDeal, {
380
+ id: randomUUID(),
381
+ organizationId: scope.organizationId,
382
+ tenantId: scope.tenantId,
383
+ title: `${randomElement(DEAL_TITLES)} - ${customer.displayName}`,
384
+ status: pipelineStage === 'closed_won' || pipelineStage === 'closed_lost' ? 'closed' : 'open',
385
+ pipelineStage,
386
+ valueAmount: toAmount(randomFloat(5000, 500000)),
387
+ valueCurrency: 'USD',
388
+ probability: probabilityByStage[pipelineStage],
389
+ expectedCloseAt: daysAgo(randomInt(-60, 90)),
390
+ source: randomElement(['inbound', 'outbound', 'referral', 'partner']),
391
+ createdAt: dealCreatedAt,
392
+ updatedAt: dealCreatedAt,
393
+ })
394
+ em.persist(deal)
395
+ }
396
+
397
+ await em.flush()
398
+
399
+ return {
400
+ orders: orders.length,
401
+ customers: customers.length,
402
+ products: products.length,
403
+ deals: dealsCount,
404
+ }
405
+ }
@@ -0,0 +1,79 @@
1
+ import type {
2
+ AnalyticsEntityConfig,
3
+ AnalyticsEntityTypeConfig,
4
+ AnalyticsFieldMapping,
5
+ AnalyticsLabelResolverConfig,
6
+ AnalyticsModuleConfig,
7
+ } from '@open-mercato/shared/modules/analytics'
8
+ import { getAnalyticsModuleConfigs } from '@open-mercato/shared/modules/analytics'
9
+
10
+ export interface AnalyticsRegistry {
11
+ getAllEntityConfigs(): AnalyticsEntityConfig[]
12
+ getEntityConfig(entityId: string): AnalyticsEntityConfig | null
13
+ isValidEntityType(entityId: string): boolean
14
+ getEntityTypeConfig(entityId: string): AnalyticsEntityTypeConfig | null
15
+ getFieldMapping(entityId: string, field: string): AnalyticsFieldMapping | null
16
+ getRequiredFeatures(entityId: string): string[] | null
17
+ getLabelResolverConfig(entityId: string, field: string): AnalyticsLabelResolverConfig | null
18
+ getAllFieldMappings(entityId: string): Record<string, AnalyticsFieldMapping> | null
19
+ }
20
+
21
+ export class DefaultAnalyticsRegistry implements AnalyticsRegistry {
22
+ private configs: AnalyticsModuleConfig[]
23
+ private entityConfigMap: Map<string, AnalyticsEntityConfig>
24
+
25
+ constructor(configs?: AnalyticsModuleConfig[]) {
26
+ this.configs = configs ?? getAnalyticsModuleConfigs()
27
+ this.entityConfigMap = new Map()
28
+ this.buildEntityConfigMap()
29
+ }
30
+
31
+ private buildEntityConfigMap(): void {
32
+ for (const moduleConfig of this.configs) {
33
+ for (const entityConfig of moduleConfig.entities) {
34
+ this.entityConfigMap.set(entityConfig.entityId, entityConfig)
35
+ }
36
+ }
37
+ }
38
+
39
+ getAllEntityConfigs(): AnalyticsEntityConfig[] {
40
+ return Array.from(this.entityConfigMap.values())
41
+ }
42
+
43
+ getEntityConfig(entityId: string): AnalyticsEntityConfig | null {
44
+ return this.entityConfigMap.get(entityId) ?? null
45
+ }
46
+
47
+ isValidEntityType(entityId: string): boolean {
48
+ return this.entityConfigMap.has(entityId)
49
+ }
50
+
51
+ getEntityTypeConfig(entityId: string): AnalyticsEntityTypeConfig | null {
52
+ const config = this.getEntityConfig(entityId)
53
+ return config?.entityConfig ?? null
54
+ }
55
+
56
+ getFieldMapping(entityId: string, field: string): AnalyticsFieldMapping | null {
57
+ const config = this.getEntityConfig(entityId)
58
+ return config?.fieldMappings[field] ?? null
59
+ }
60
+
61
+ getRequiredFeatures(entityId: string): string[] | null {
62
+ const config = this.getEntityConfig(entityId)
63
+ return config?.requiredFeatures ?? null
64
+ }
65
+
66
+ getLabelResolverConfig(entityId: string, field: string): AnalyticsLabelResolverConfig | null {
67
+ const config = this.getEntityConfig(entityId)
68
+ return config?.labelResolvers?.[field] ?? null
69
+ }
70
+
71
+ getAllFieldMappings(entityId: string): Record<string, AnalyticsFieldMapping> | null {
72
+ const config = this.getEntityConfig(entityId)
73
+ return config?.fieldMappings ?? null
74
+ }
75
+ }
76
+
77
+ export function createAnalyticsRegistry(configs?: AnalyticsModuleConfig[]): AnalyticsRegistry {
78
+ return new DefaultAnalyticsRegistry(configs)
79
+ }