@open-mercato/core 0.4.2-canary-15e78de280 → 0.4.2-canary-ed15f2e753

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 (158) hide show
  1. package/dist/generated/entities/workflow_event_trigger/index.js +33 -0
  2. package/dist/generated/entities/workflow_event_trigger/index.js.map +7 -0
  3. package/dist/generated/entities.ids.generated.js +1 -0
  4. package/dist/generated/entities.ids.generated.js.map +2 -2
  5. package/dist/generated/entity-fields-registry.js +2 -0
  6. package/dist/generated/entity-fields-registry.js.map +2 -2
  7. package/dist/modules/auth/events.js +30 -0
  8. package/dist/modules/auth/events.js.map +7 -0
  9. package/dist/modules/business_rules/api/execute/[ruleId]/route.js +145 -0
  10. package/dist/modules/business_rules/api/execute/[ruleId]/route.js.map +7 -0
  11. package/dist/modules/business_rules/data/validators.js +34 -0
  12. package/dist/modules/business_rules/data/validators.js.map +2 -2
  13. package/dist/modules/business_rules/index.js +21 -1
  14. package/dist/modules/business_rules/index.js.map +2 -2
  15. package/dist/modules/business_rules/lib/rule-engine.js +182 -1
  16. package/dist/modules/business_rules/lib/rule-engine.js.map +2 -2
  17. package/dist/modules/catalog/events.js +34 -0
  18. package/dist/modules/catalog/events.js.map +7 -0
  19. package/dist/modules/customers/events.js +49 -0
  20. package/dist/modules/customers/events.js.map +7 -0
  21. package/dist/modules/directory/events.js +23 -0
  22. package/dist/modules/directory/events.js.map +7 -0
  23. package/dist/modules/sales/acl.js +1 -0
  24. package/dist/modules/sales/acl.js.map +2 -2
  25. package/dist/modules/sales/backend/sales/documents/[id]/page.js +12 -0
  26. package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
  27. package/dist/modules/sales/commands/documents.js +62 -0
  28. package/dist/modules/sales/commands/documents.js.map +2 -2
  29. package/dist/modules/sales/events.js +63 -0
  30. package/dist/modules/sales/events.js.map +7 -0
  31. package/dist/modules/sales/lib/dictionaries.js +3 -0
  32. package/dist/modules/sales/lib/dictionaries.js.map +2 -2
  33. package/dist/modules/sales/lib/frontend/documentDataEvents.js +25 -0
  34. package/dist/modules/sales/lib/frontend/documentDataEvents.js.map +7 -0
  35. package/dist/modules/workflows/acl.js +2 -0
  36. package/dist/modules/workflows/acl.js.map +2 -2
  37. package/dist/modules/workflows/api/instances/route.js +18 -6
  38. package/dist/modules/workflows/api/instances/route.js.map +2 -2
  39. package/dist/modules/workflows/api/tasks/route.js +6 -1
  40. package/dist/modules/workflows/api/tasks/route.js.map +2 -2
  41. package/dist/modules/workflows/backend/definitions/[id]/page.js +9 -1
  42. package/dist/modules/workflows/backend/definitions/[id]/page.js.map +2 -2
  43. package/dist/modules/workflows/backend/definitions/[id]/page.meta.js +1 -1
  44. package/dist/modules/workflows/backend/definitions/[id]/page.meta.js.map +2 -2
  45. package/dist/modules/workflows/backend/definitions/create/page.js +24 -15
  46. package/dist/modules/workflows/backend/definitions/create/page.js.map +2 -2
  47. package/dist/modules/workflows/backend/definitions/create/page.meta.js +1 -1
  48. package/dist/modules/workflows/backend/definitions/create/page.meta.js.map +2 -2
  49. package/dist/modules/workflows/backend/definitions/visual-editor/page.js +150 -132
  50. package/dist/modules/workflows/backend/definitions/visual-editor/page.js.map +2 -2
  51. package/dist/modules/workflows/backend/definitions/visual-editor/page.meta.js +1 -1
  52. package/dist/modules/workflows/backend/definitions/visual-editor/page.meta.js.map +2 -2
  53. package/dist/modules/workflows/backend/events/[id]/page.js +1 -1
  54. package/dist/modules/workflows/backend/events/[id]/page.js.map +2 -2
  55. package/dist/modules/workflows/backend/events/[id]/page.meta.js +2 -2
  56. package/dist/modules/workflows/backend/events/[id]/page.meta.js.map +2 -2
  57. package/dist/modules/workflows/backend/instances/[id]/page.meta.js +2 -2
  58. package/dist/modules/workflows/backend/instances/[id]/page.meta.js.map +2 -2
  59. package/dist/modules/workflows/backend/tasks/[id]/page.js +1 -1
  60. package/dist/modules/workflows/backend/tasks/[id]/page.js.map +2 -2
  61. package/dist/modules/workflows/backend/tasks/[id]/page.meta.js +2 -2
  62. package/dist/modules/workflows/backend/tasks/[id]/page.meta.js.map +2 -2
  63. package/dist/modules/workflows/backend/tasks/page.js +5 -6
  64. package/dist/modules/workflows/backend/tasks/page.js.map +2 -2
  65. package/dist/modules/workflows/cli.js +81 -3
  66. package/dist/modules/workflows/cli.js.map +3 -3
  67. package/dist/modules/workflows/components/DefinitionTriggersEditor.js +481 -0
  68. package/dist/modules/workflows/components/DefinitionTriggersEditor.js.map +7 -0
  69. package/dist/modules/workflows/components/EventTriggersEditor.js +553 -0
  70. package/dist/modules/workflows/components/EventTriggersEditor.js.map +7 -0
  71. package/dist/modules/workflows/data/entities.js +64 -1
  72. package/dist/modules/workflows/data/entities.js.map +2 -2
  73. package/dist/modules/workflows/data/validators.js +115 -0
  74. package/dist/modules/workflows/data/validators.js.map +2 -2
  75. package/dist/modules/workflows/events.js +38 -0
  76. package/dist/modules/workflows/events.js.map +7 -0
  77. package/dist/modules/workflows/examples/checkout-demo-definition.json +1 -5
  78. package/dist/modules/workflows/examples/order-approval-definition.json +257 -0
  79. package/dist/modules/workflows/examples/order-approval-guard-rules.json +32 -0
  80. package/dist/modules/workflows/lib/activity-executor.js +75 -13
  81. package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
  82. package/dist/modules/workflows/lib/event-trigger-service.js +308 -0
  83. package/dist/modules/workflows/lib/event-trigger-service.js.map +7 -0
  84. package/dist/modules/workflows/lib/graph-utils.js +71 -2
  85. package/dist/modules/workflows/lib/graph-utils.js.map +2 -2
  86. package/dist/modules/workflows/lib/seeds.js +22 -5
  87. package/dist/modules/workflows/lib/seeds.js.map +2 -2
  88. package/dist/modules/workflows/lib/start-validator.js +33 -23
  89. package/dist/modules/workflows/lib/start-validator.js.map +2 -2
  90. package/dist/modules/workflows/lib/transition-handler.js +157 -45
  91. package/dist/modules/workflows/lib/transition-handler.js.map +3 -3
  92. package/dist/modules/workflows/migrations/Migration20260123143500.js +36 -0
  93. package/dist/modules/workflows/migrations/Migration20260123143500.js.map +7 -0
  94. package/dist/modules/workflows/subscribers/event-trigger.js +78 -0
  95. package/dist/modules/workflows/subscribers/event-trigger.js.map +7 -0
  96. package/dist/modules/workflows/widgets/injection/order-approval/widget.client.js +323 -0
  97. package/dist/modules/workflows/widgets/injection/order-approval/widget.client.js.map +7 -0
  98. package/dist/modules/workflows/widgets/injection/order-approval/widget.js +17 -0
  99. package/dist/modules/workflows/widgets/injection/order-approval/widget.js.map +7 -0
  100. package/dist/modules/workflows/widgets/injection-table.js +19 -0
  101. package/dist/modules/workflows/widgets/injection-table.js.map +7 -0
  102. package/generated/entities/workflow_event_trigger/index.ts +15 -0
  103. package/generated/entities.ids.generated.ts +1 -0
  104. package/generated/entity-fields-registry.ts +2 -0
  105. package/package.json +2 -2
  106. package/src/modules/auth/events.ts +39 -0
  107. package/src/modules/business_rules/api/execute/[ruleId]/route.ts +163 -0
  108. package/src/modules/business_rules/data/validators.ts +40 -0
  109. package/src/modules/business_rules/index.ts +25 -0
  110. package/src/modules/business_rules/lib/rule-engine.ts +281 -1
  111. package/src/modules/catalog/events.ts +45 -0
  112. package/src/modules/customers/events.ts +63 -0
  113. package/src/modules/directory/events.ts +31 -0
  114. package/src/modules/sales/acl.ts +1 -0
  115. package/src/modules/sales/backend/sales/documents/[id]/page.tsx +16 -0
  116. package/src/modules/sales/commands/documents.ts +74 -1
  117. package/src/modules/sales/events.ts +82 -0
  118. package/src/modules/sales/lib/dictionaries.ts +3 -0
  119. package/src/modules/sales/lib/frontend/documentDataEvents.ts +28 -0
  120. package/src/modules/workflows/acl.ts +2 -0
  121. package/src/modules/workflows/api/__tests__/instances.route.test.ts +5 -2
  122. package/src/modules/workflows/api/instances/route.ts +21 -7
  123. package/src/modules/workflows/api/tasks/route.ts +7 -1
  124. package/src/modules/workflows/backend/definitions/[id]/page.meta.ts +1 -1
  125. package/src/modules/workflows/backend/definitions/[id]/page.tsx +9 -0
  126. package/src/modules/workflows/backend/definitions/create/page.meta.ts +1 -1
  127. package/src/modules/workflows/backend/definitions/create/page.tsx +9 -0
  128. package/src/modules/workflows/backend/definitions/visual-editor/page.meta.ts +1 -1
  129. package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +21 -3
  130. package/src/modules/workflows/backend/events/[id]/page.meta.ts +2 -2
  131. package/src/modules/workflows/backend/events/[id]/page.tsx +1 -1
  132. package/src/modules/workflows/backend/instances/[id]/page.meta.ts +2 -2
  133. package/src/modules/workflows/backend/tasks/[id]/page.meta.ts +2 -2
  134. package/src/modules/workflows/backend/tasks/[id]/page.tsx +1 -1
  135. package/src/modules/workflows/backend/tasks/page.tsx +5 -6
  136. package/src/modules/workflows/cli.ts +111 -0
  137. package/src/modules/workflows/components/DefinitionTriggersEditor.tsx +581 -0
  138. package/src/modules/workflows/components/EventTriggersEditor.tsx +664 -0
  139. package/src/modules/workflows/data/entities.ts +124 -0
  140. package/src/modules/workflows/data/validators.ts +138 -0
  141. package/src/modules/workflows/events.ts +49 -0
  142. package/src/modules/workflows/examples/checkout-demo-definition.json +1 -5
  143. package/src/modules/workflows/examples/order-approval-definition.json +257 -0
  144. package/src/modules/workflows/examples/order-approval-guard-rules.json +32 -0
  145. package/src/modules/workflows/i18n/en.json +71 -0
  146. package/src/modules/workflows/lib/__tests__/activity-executor.test.ts +43 -36
  147. package/src/modules/workflows/lib/__tests__/transition-handler.test.ts +170 -90
  148. package/src/modules/workflows/lib/activity-executor.ts +129 -16
  149. package/src/modules/workflows/lib/event-trigger-service.ts +557 -0
  150. package/src/modules/workflows/lib/graph-utils.ts +117 -2
  151. package/src/modules/workflows/lib/seeds.ts +34 -8
  152. package/src/modules/workflows/lib/start-validator.ts +38 -28
  153. package/src/modules/workflows/lib/transition-handler.ts +208 -55
  154. package/src/modules/workflows/migrations/Migration20260123143500.ts +38 -0
  155. package/src/modules/workflows/subscribers/event-trigger.ts +109 -0
  156. package/src/modules/workflows/widgets/injection/order-approval/widget.client.tsx +446 -0
  157. package/src/modules/workflows/widgets/injection/order-approval/widget.ts +16 -0
  158. package/src/modules/workflows/widgets/injection-table.ts +21 -0
@@ -0,0 +1,163 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { z } from 'zod'
3
+ import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
4
+ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
5
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
6
+ import type { EntityManager } from '@mikro-orm/postgresql'
7
+ import * as ruleEngine from '../../../lib/rule-engine'
8
+
9
+ const executeByIdRequestSchema = z.object({
10
+ data: z.any(),
11
+ dryRun: z.boolean().optional().default(false),
12
+ entityType: z.string().optional(),
13
+ entityId: z.string().optional(),
14
+ eventType: z.string().optional(),
15
+ })
16
+
17
+ const executeByIdResponseSchema = z.object({
18
+ success: z.boolean(),
19
+ ruleId: z.string(),
20
+ ruleName: z.string(),
21
+ conditionResult: z.boolean(),
22
+ actionsExecuted: z.object({
23
+ success: z.boolean(),
24
+ results: z.array(z.object({
25
+ type: z.string(),
26
+ success: z.boolean(),
27
+ error: z.string().optional(),
28
+ })),
29
+ }).nullable(),
30
+ executionTime: z.number(),
31
+ error: z.string().optional(),
32
+ logId: z.string().optional(),
33
+ })
34
+
35
+ const errorResponseSchema = z.object({
36
+ error: z.string(),
37
+ })
38
+
39
+ const routeMetadata = {
40
+ POST: { requireAuth: true, requireFeatures: ['business_rules.execute'] },
41
+ }
42
+
43
+ export const metadata = routeMetadata
44
+
45
+ interface RouteContext {
46
+ params: Promise<{ ruleId: string }>
47
+ }
48
+
49
+ export async function POST(req: Request, context: RouteContext) {
50
+ const auth = await getAuthFromRequest(req)
51
+ if (!auth) {
52
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
53
+ }
54
+
55
+ const params = await context.params
56
+ const ruleId = params.ruleId
57
+
58
+ if (!ruleId || !z.uuid().safeParse(ruleId).success) {
59
+ return NextResponse.json({ error: 'Invalid rule ID' }, { status: 400 })
60
+ }
61
+
62
+ const container = await createRequestContainer()
63
+ const em = container.resolve('em') as EntityManager
64
+
65
+ let body: any
66
+ try {
67
+ body = await req.json()
68
+ } catch {
69
+ return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })
70
+ }
71
+
72
+ const parsed = executeByIdRequestSchema.safeParse(body)
73
+ if (!parsed.success) {
74
+ const errors = parsed.error.issues.map(e => `${e.path.join('.')}: ${e.message}`)
75
+ return NextResponse.json({ error: `Validation failed: ${errors.join(', ')}` }, { status: 400 })
76
+ }
77
+
78
+ const { data, dryRun, entityType, entityId, eventType } = parsed.data
79
+
80
+ const execContext: ruleEngine.DirectRuleExecutionContext = {
81
+ ruleId,
82
+ data,
83
+ user: {
84
+ id: auth.sub,
85
+ email: auth.email,
86
+ role: (auth.role as string) ?? undefined,
87
+ },
88
+ tenantId: auth.tenantId ?? '',
89
+ organizationId: auth.orgId ?? '',
90
+ executedBy: auth.sub ?? auth.email ?? undefined,
91
+ dryRun,
92
+ entityType,
93
+ entityId,
94
+ eventType,
95
+ }
96
+
97
+ try {
98
+ const result = await ruleEngine.executeRuleById(em, execContext)
99
+
100
+ const response = {
101
+ success: result.success,
102
+ ruleId: result.ruleId,
103
+ ruleName: result.ruleName,
104
+ conditionResult: result.conditionResult,
105
+ actionsExecuted: result.actionsExecuted ? {
106
+ success: result.actionsExecuted.success,
107
+ results: result.actionsExecuted.results.map(ar => ({
108
+ type: ar.action.type,
109
+ success: ar.success,
110
+ error: ar.error,
111
+ })),
112
+ } : null,
113
+ executionTime: result.executionTime,
114
+ error: result.error,
115
+ logId: result.logId,
116
+ }
117
+
118
+ // Return appropriate status based on result
119
+ const status = result.success ? 200 : (result.error === 'Rule not found' ? 404 : 200)
120
+ return NextResponse.json(response, { status })
121
+ } catch (error) {
122
+ const errorMessage = error instanceof Error ? error.message : String(error)
123
+ return NextResponse.json(
124
+ { error: `Rule execution failed: ${errorMessage}` },
125
+ { status: 500 }
126
+ )
127
+ }
128
+ }
129
+
130
+ export const openApi: OpenApiRouteDoc = {
131
+ tag: 'Business Rules',
132
+ summary: 'Execute a specific business rule by ID',
133
+ methods: {
134
+ POST: {
135
+ summary: 'Execute a specific rule by its database UUID',
136
+ description: 'Directly executes a specific business rule identified by its UUID, bypassing the normal entityType/eventType discovery mechanism. Useful for workflows and targeted rule execution.',
137
+ pathParams: z.object({
138
+ ruleId: z.string().uuid().describe('The database UUID of the business rule to execute'),
139
+ }),
140
+ requestBody: {
141
+ contentType: 'application/json',
142
+ schema: executeByIdRequestSchema,
143
+ },
144
+ responses: [
145
+ {
146
+ status: 200,
147
+ description: 'Rule executed successfully',
148
+ schema: executeByIdResponseSchema,
149
+ },
150
+ {
151
+ status: 404,
152
+ description: 'Rule not found',
153
+ schema: errorResponseSchema,
154
+ },
155
+ ],
156
+ errors: [
157
+ { status: 400, description: 'Invalid request payload or rule ID', schema: errorResponseSchema },
158
+ { status: 401, description: 'Unauthorized', schema: errorResponseSchema },
159
+ { status: 500, description: 'Execution error', schema: errorResponseSchema },
160
+ ],
161
+ },
162
+ },
163
+ }
@@ -287,3 +287,43 @@ export const ruleDiscoveryOptionsSchema = z.object({
287
287
  })
288
288
 
289
289
  export type RuleDiscoveryOptionsInput = z.infer<typeof ruleDiscoveryOptionsSchema>
290
+
291
+ // Direct Rule Execution Context Schema (for executing a specific rule by ID)
292
+ export const directRuleExecutionContextSchema = z.object({
293
+ ruleId: z.uuid('ruleId must be a valid UUID'),
294
+ data: z.any(),
295
+ user: z.looseObject({
296
+ id: z.string().optional(),
297
+ email: z.string().optional(),
298
+ role: z.string().optional(),
299
+ }).optional(),
300
+ tenantId: z.uuid('tenantId must be a valid UUID'),
301
+ organizationId: z.uuid('organizationId must be a valid UUID'),
302
+ executedBy: z.string().optional(),
303
+ dryRun: z.boolean().optional(),
304
+ entityType: z.string().optional(),
305
+ entityId: z.string().optional(),
306
+ eventType: z.string().optional(),
307
+ })
308
+
309
+ export type DirectRuleExecutionContextInput = z.infer<typeof directRuleExecutionContextSchema>
310
+
311
+ // Rule ID Execution Context Schema (for executing a specific rule by its string rule_id identifier)
312
+ export const ruleIdExecutionContextSchema = z.object({
313
+ ruleId: z.string().min(1, 'ruleId must be a non-empty string').max(50),
314
+ data: z.any(),
315
+ user: z.looseObject({
316
+ id: z.string().optional(),
317
+ email: z.string().optional(),
318
+ role: z.string().optional(),
319
+ }).optional(),
320
+ tenantId: z.uuid('tenantId must be a valid UUID'),
321
+ organizationId: z.uuid('organizationId must be a valid UUID'),
322
+ executedBy: z.string().optional(),
323
+ dryRun: z.boolean().optional(),
324
+ entityType: z.string().optional(),
325
+ entityId: z.string().optional(),
326
+ eventType: z.string().optional(),
327
+ })
328
+
329
+ export type RuleIdExecutionContextInput = z.infer<typeof ruleIdExecutionContextSchema>
@@ -8,3 +8,28 @@ export const metadata: ModuleInfo = {
8
8
  author: 'Patryk Lewczuk',
9
9
  license: 'Proprietary',
10
10
  }
11
+
12
+ // Export rule engine types and functions for programmatic usage
13
+ export {
14
+ executeRules,
15
+ executeRuleById,
16
+ executeRuleByRuleId,
17
+ executeSingleRule,
18
+ findApplicableRules,
19
+ logRuleExecution,
20
+ type RuleEngineContext,
21
+ type RuleEngineResult,
22
+ type RuleExecutionResult,
23
+ type RuleDiscoveryOptions,
24
+ type DirectRuleExecutionContext,
25
+ type DirectRuleExecutionResult,
26
+ type RuleIdExecutionContext,
27
+ } from './lib/rule-engine'
28
+
29
+ // Export validator schemas
30
+ export {
31
+ directRuleExecutionContextSchema,
32
+ ruleIdExecutionContextSchema,
33
+ type DirectRuleExecutionContextInput,
34
+ type RuleIdExecutionContextInput,
35
+ } from './data/validators'
@@ -4,7 +4,7 @@ import * as ruleEvaluator from './rule-evaluator'
4
4
  import * as actionExecutor from './action-executor'
5
5
  import type { RuleEvaluationContext } from './rule-evaluator'
6
6
  import type { ActionContext, ActionExecutionOutcome } from './action-executor'
7
- import { ruleEngineContextSchema, ruleDiscoveryOptionsSchema } from '../data/validators'
7
+ import { ruleEngineContextSchema, ruleDiscoveryOptionsSchema, directRuleExecutionContextSchema, ruleIdExecutionContextSchema } from '../data/validators'
8
8
 
9
9
  /**
10
10
  * Constants
@@ -85,6 +85,65 @@ export interface RuleDiscoveryOptions {
85
85
  ruleType?: RuleType
86
86
  }
87
87
 
88
+ /**
89
+ * Direct rule execution context (for executing a specific rule by ID)
90
+ */
91
+ export interface DirectRuleExecutionContext {
92
+ ruleId: string // Database UUID of the rule
93
+ data: any
94
+ user?: {
95
+ id?: string
96
+ email?: string
97
+ role?: string
98
+ [key: string]: any
99
+ }
100
+ tenantId: string
101
+ organizationId: string
102
+ executedBy?: string
103
+ dryRun?: boolean
104
+ // Optional for logging (falls back to rule's entityType)
105
+ entityType?: string
106
+ entityId?: string
107
+ eventType?: string
108
+ }
109
+
110
+ /**
111
+ * Direct rule execution result
112
+ */
113
+ export interface DirectRuleExecutionResult {
114
+ success: boolean
115
+ ruleId: string
116
+ ruleName: string
117
+ conditionResult: boolean
118
+ actionsExecuted: ActionExecutionOutcome | null
119
+ executionTime: number
120
+ error?: string
121
+ logId?: string
122
+ }
123
+
124
+ /**
125
+ * Context for executing a rule by its string rule_id identifier
126
+ * Unlike DirectRuleExecutionContext which uses database UUID,
127
+ * this uses the string identifier (e.g., "workflow_checkout_inventory_available")
128
+ */
129
+ export interface RuleIdExecutionContext {
130
+ ruleId: string // String identifier (e.g., "workflow_checkout_inventory_available")
131
+ data: any
132
+ user?: {
133
+ id?: string
134
+ email?: string
135
+ role?: string
136
+ [key: string]: any
137
+ }
138
+ tenantId: string
139
+ organizationId: string
140
+ executedBy?: string
141
+ dryRun?: boolean
142
+ entityType?: string
143
+ entityId?: string
144
+ eventType?: string
145
+ }
146
+
88
147
  /**
89
148
  * Execute a function with a timeout
90
149
  */
@@ -404,6 +463,227 @@ export async function findApplicableRules(
404
463
  })
405
464
  }
406
465
 
466
+ /**
467
+ * Execute a specific rule by its database UUID
468
+ * This bypasses the entityType/eventType discovery mechanism and directly executes the rule
469
+ */
470
+ export async function executeRuleById(
471
+ em: EntityManager,
472
+ context: DirectRuleExecutionContext
473
+ ): Promise<DirectRuleExecutionResult> {
474
+ const startTime = Date.now()
475
+
476
+ // Validate input
477
+ const validation = directRuleExecutionContextSchema.safeParse(context)
478
+ if (!validation.success) {
479
+ const validationErrors = validation.error.issues.map(e => `${e.path.join('.')}: ${e.message}`)
480
+ return {
481
+ success: false,
482
+ ruleId: context.ruleId,
483
+ ruleName: 'Unknown',
484
+ conditionResult: false,
485
+ actionsExecuted: null,
486
+ executionTime: Date.now() - startTime,
487
+ error: `Validation failed: ${validationErrors.join(', ')}`,
488
+ }
489
+ }
490
+
491
+ // Fetch rule by ID with tenant/org validation
492
+ const rule = await em.findOne(BusinessRule, {
493
+ id: context.ruleId,
494
+ tenantId: context.tenantId,
495
+ organizationId: context.organizationId,
496
+ deletedAt: null,
497
+ })
498
+
499
+ if (!rule) {
500
+ return {
501
+ success: false,
502
+ ruleId: context.ruleId,
503
+ ruleName: 'Unknown',
504
+ conditionResult: false,
505
+ actionsExecuted: null,
506
+ executionTime: Date.now() - startTime,
507
+ error: 'Rule not found',
508
+ }
509
+ }
510
+
511
+ if (!rule.enabled) {
512
+ return {
513
+ success: false,
514
+ ruleId: rule.ruleId,
515
+ ruleName: rule.ruleName,
516
+ conditionResult: false,
517
+ actionsExecuted: null,
518
+ executionTime: Date.now() - startTime,
519
+ error: 'Rule is disabled',
520
+ }
521
+ }
522
+
523
+ // Check effective date range
524
+ const now = new Date()
525
+ if (rule.effectiveFrom && rule.effectiveFrom > now) {
526
+ return {
527
+ success: false,
528
+ ruleId: rule.ruleId,
529
+ ruleName: rule.ruleName,
530
+ conditionResult: false,
531
+ actionsExecuted: null,
532
+ executionTime: Date.now() - startTime,
533
+ error: `Rule is not yet effective (starts ${rule.effectiveFrom.toISOString()})`,
534
+ }
535
+ }
536
+ if (rule.effectiveTo && rule.effectiveTo < now) {
537
+ return {
538
+ success: false,
539
+ ruleId: rule.ruleId,
540
+ ruleName: rule.ruleName,
541
+ conditionResult: false,
542
+ actionsExecuted: null,
543
+ executionTime: Date.now() - startTime,
544
+ error: `Rule has expired (ended ${rule.effectiveTo.toISOString()})`,
545
+ }
546
+ }
547
+
548
+ // Build RuleEngineContext (use provided entityType or fall back to rule's)
549
+ const engineContext: RuleEngineContext = {
550
+ entityType: context.entityType || rule.entityType,
551
+ entityId: context.entityId,
552
+ eventType: context.eventType || rule.eventType || undefined,
553
+ data: context.data,
554
+ user: context.user,
555
+ tenantId: context.tenantId,
556
+ organizationId: context.organizationId,
557
+ executedBy: context.executedBy,
558
+ dryRun: context.dryRun,
559
+ }
560
+
561
+ // Execute via existing executeSingleRule
562
+ const result = await executeSingleRule(em, rule, engineContext)
563
+
564
+ return {
565
+ success: !result.error,
566
+ ruleId: rule.ruleId,
567
+ ruleName: rule.ruleName,
568
+ conditionResult: result.conditionResult,
569
+ actionsExecuted: result.actionsExecuted,
570
+ executionTime: result.executionTime,
571
+ error: result.error,
572
+ logId: result.logId,
573
+ }
574
+ }
575
+
576
+ /**
577
+ * Execute a rule by its string rule_id identifier
578
+ * Looks up rule by rule_id (string column) + tenant_id (unique constraint)
579
+ * This is useful for workflow conditions that reference rules by their string identifiers
580
+ */
581
+ export async function executeRuleByRuleId(
582
+ em: EntityManager,
583
+ context: RuleIdExecutionContext
584
+ ): Promise<DirectRuleExecutionResult> {
585
+ const startTime = Date.now()
586
+
587
+ // Validate input
588
+ const validation = ruleIdExecutionContextSchema.safeParse(context)
589
+ if (!validation.success) {
590
+ const validationErrors = validation.error.issues.map(e => `${e.path.join('.')}: ${e.message}`)
591
+ return {
592
+ success: false,
593
+ ruleId: context.ruleId || 'unknown',
594
+ ruleName: 'Unknown',
595
+ conditionResult: false,
596
+ actionsExecuted: null,
597
+ executionTime: Date.now() - startTime,
598
+ error: `Validation failed: ${validationErrors.join(', ')}`,
599
+ }
600
+ }
601
+
602
+ // Fetch rule by rule_id (string identifier) + tenant/org
603
+ const rule = await em.findOne(BusinessRule, {
604
+ ruleId: context.ruleId, // String identifier column
605
+ tenantId: context.tenantId,
606
+ organizationId: context.organizationId,
607
+ deletedAt: null,
608
+ })
609
+
610
+ if (!rule) {
611
+ return {
612
+ success: false,
613
+ ruleId: context.ruleId,
614
+ ruleName: 'Unknown',
615
+ conditionResult: false,
616
+ actionsExecuted: null,
617
+ executionTime: Date.now() - startTime,
618
+ error: 'Rule not found',
619
+ }
620
+ }
621
+
622
+ if (!rule.enabled) {
623
+ return {
624
+ success: false,
625
+ ruleId: rule.ruleId,
626
+ ruleName: rule.ruleName,
627
+ conditionResult: false,
628
+ actionsExecuted: null,
629
+ executionTime: Date.now() - startTime,
630
+ error: 'Rule is disabled',
631
+ }
632
+ }
633
+
634
+ // Check effective date range
635
+ const now = new Date()
636
+ if (rule.effectiveFrom && rule.effectiveFrom > now) {
637
+ return {
638
+ success: false,
639
+ ruleId: rule.ruleId,
640
+ ruleName: rule.ruleName,
641
+ conditionResult: false,
642
+ actionsExecuted: null,
643
+ executionTime: Date.now() - startTime,
644
+ error: `Rule is not yet effective (starts ${rule.effectiveFrom.toISOString()})`,
645
+ }
646
+ }
647
+ if (rule.effectiveTo && rule.effectiveTo < now) {
648
+ return {
649
+ success: false,
650
+ ruleId: rule.ruleId,
651
+ ruleName: rule.ruleName,
652
+ conditionResult: false,
653
+ actionsExecuted: null,
654
+ executionTime: Date.now() - startTime,
655
+ error: `Rule has expired (ended ${rule.effectiveTo.toISOString()})`,
656
+ }
657
+ }
658
+
659
+ // Build RuleEngineContext (use provided entityType or fall back to rule's)
660
+ const engineContext: RuleEngineContext = {
661
+ entityType: context.entityType || rule.entityType,
662
+ entityId: context.entityId,
663
+ eventType: context.eventType || rule.eventType || undefined,
664
+ data: context.data,
665
+ user: context.user,
666
+ tenantId: context.tenantId,
667
+ organizationId: context.organizationId,
668
+ executedBy: context.executedBy,
669
+ dryRun: context.dryRun,
670
+ }
671
+
672
+ // Execute via existing executeSingleRule
673
+ const result = await executeSingleRule(em, rule, engineContext)
674
+
675
+ return {
676
+ success: !result.error,
677
+ ruleId: rule.ruleId,
678
+ ruleName: rule.ruleName,
679
+ conditionResult: result.conditionResult,
680
+ actionsExecuted: result.actionsExecuted,
681
+ executionTime: result.executionTime,
682
+ error: result.error,
683
+ logId: result.logId,
684
+ }
685
+ }
686
+
407
687
  /**
408
688
  * Sensitive field patterns to exclude from logs
409
689
  */
@@ -0,0 +1,45 @@
1
+ import { createModuleEvents } from '@open-mercato/shared/modules/events'
2
+
3
+ /**
4
+ * Catalog Module Events
5
+ *
6
+ * Declares all events that can be emitted by the catalog module.
7
+ */
8
+ const events = [
9
+ // Products
10
+ { id: 'catalog.product.created', label: 'Product Created', entity: 'product', category: 'crud' },
11
+ { id: 'catalog.product.updated', label: 'Product Updated', entity: 'product', category: 'crud' },
12
+ { id: 'catalog.product.deleted', label: 'Product Deleted', entity: 'product', category: 'crud' },
13
+
14
+ // Categories
15
+ { id: 'catalog.category.created', label: 'Category Created', entity: 'category', category: 'crud' },
16
+ { id: 'catalog.category.updated', label: 'Category Updated', entity: 'category', category: 'crud' },
17
+ { id: 'catalog.category.deleted', label: 'Category Deleted', entity: 'category', category: 'crud' },
18
+
19
+ // Variants
20
+ { id: 'catalog.variant.created', label: 'Variant Created', entity: 'variant', category: 'crud' },
21
+ { id: 'catalog.variant.updated', label: 'Variant Updated', entity: 'variant', category: 'crud' },
22
+ { id: 'catalog.variant.deleted', label: 'Variant Deleted', entity: 'variant', category: 'crud' },
23
+
24
+ // Prices
25
+ { id: 'catalog.price.created', label: 'Price Created', entity: 'price', category: 'crud' },
26
+ { id: 'catalog.price.updated', label: 'Price Updated', entity: 'price', category: 'crud' },
27
+ { id: 'catalog.price.deleted', label: 'Price Deleted', entity: 'price', category: 'crud' },
28
+
29
+ // Lifecycle events - Pricing resolution
30
+ { id: 'catalog.pricing.resolve.before', label: 'Before Pricing Resolve', category: 'lifecycle', excludeFromTriggers: true },
31
+ { id: 'catalog.pricing.resolve.after', label: 'After Pricing Resolve', category: 'lifecycle', excludeFromTriggers: true },
32
+ ] as const
33
+
34
+ export const eventsConfig = createModuleEvents({
35
+ moduleId: 'catalog',
36
+ events,
37
+ })
38
+
39
+ /** Type-safe event emitter for catalog module */
40
+ export const emitCatalogEvent = eventsConfig.emit
41
+
42
+ /** Event IDs that can be emitted by the catalog module */
43
+ export type CatalogEventId = typeof events[number]['id']
44
+
45
+ export default eventsConfig
@@ -0,0 +1,63 @@
1
+ import { createModuleEvents } from '@open-mercato/shared/modules/events'
2
+
3
+ /**
4
+ * Customers Module Events
5
+ *
6
+ * Declares all events that can be emitted by the customers module.
7
+ */
8
+ const events = [
9
+ // People
10
+ { id: 'customers.people.created', label: 'Customer (Person) Created', entity: 'people', category: 'crud' },
11
+ { id: 'customers.people.updated', label: 'Customer (Person) Updated', entity: 'people', category: 'crud' },
12
+ { id: 'customers.people.deleted', label: 'Customer (Person) Deleted', entity: 'people', category: 'crud' },
13
+
14
+ // Companies
15
+ { id: 'customers.companies.created', label: 'Customer (Company) Created', entity: 'companies', category: 'crud' },
16
+ { id: 'customers.companies.updated', label: 'Customer (Company) Updated', entity: 'companies', category: 'crud' },
17
+ { id: 'customers.companies.deleted', label: 'Customer (Company) Deleted', entity: 'companies', category: 'crud' },
18
+
19
+ // Deals
20
+ { id: 'customers.deals.created', label: 'Deal Created', entity: 'deals', category: 'crud' },
21
+ { id: 'customers.deals.updated', label: 'Deal Updated', entity: 'deals', category: 'crud' },
22
+ { id: 'customers.deals.deleted', label: 'Deal Deleted', entity: 'deals', category: 'crud' },
23
+
24
+ // Comments
25
+ { id: 'customers.comments.created', label: 'Comment Created', entity: 'comments', category: 'crud' },
26
+ { id: 'customers.comments.updated', label: 'Comment Updated', entity: 'comments', category: 'crud' },
27
+ { id: 'customers.comments.deleted', label: 'Comment Deleted', entity: 'comments', category: 'crud' },
28
+
29
+ // Addresses
30
+ { id: 'customers.addresses.created', label: 'Address Created', entity: 'addresses', category: 'crud' },
31
+ { id: 'customers.addresses.updated', label: 'Address Updated', entity: 'addresses', category: 'crud' },
32
+ { id: 'customers.addresses.deleted', label: 'Address Deleted', entity: 'addresses', category: 'crud' },
33
+
34
+ // Activities
35
+ { id: 'customers.activities.created', label: 'Activity Created', entity: 'activities', category: 'crud' },
36
+ { id: 'customers.activities.updated', label: 'Activity Updated', entity: 'activities', category: 'crud' },
37
+ { id: 'customers.activities.deleted', label: 'Activity Deleted', entity: 'activities', category: 'crud' },
38
+
39
+ // Tags
40
+ { id: 'customers.tags.created', label: 'Tag Created', entity: 'tags', category: 'crud' },
41
+ { id: 'customers.tags.updated', label: 'Tag Updated', entity: 'tags', category: 'crud' },
42
+ { id: 'customers.tags.deleted', label: 'Tag Deleted', entity: 'tags', category: 'crud' },
43
+ { id: 'customers.tags.assigned', label: 'Tag Assigned', entity: 'tags', category: 'crud' },
44
+ { id: 'customers.tags.removed', label: 'Tag Removed', entity: 'tags', category: 'crud' },
45
+
46
+ // Todos
47
+ { id: 'customers.todos.created', label: 'Todo Created', entity: 'todos', category: 'crud' },
48
+ { id: 'customers.todos.updated', label: 'Todo Updated', entity: 'todos', category: 'crud' },
49
+ { id: 'customers.todos.deleted', label: 'Todo Deleted', entity: 'todos', category: 'crud' },
50
+ ] as const
51
+
52
+ export const eventsConfig = createModuleEvents({
53
+ moduleId: 'customers',
54
+ events,
55
+ })
56
+
57
+ /** Type-safe event emitter for customers module */
58
+ export const emitCustomersEvent = eventsConfig.emit
59
+
60
+ /** Event IDs that can be emitted by the customers module */
61
+ export type CustomersEventId = typeof events[number]['id']
62
+
63
+ export default eventsConfig
@@ -0,0 +1,31 @@
1
+ import { createModuleEvents } from '@open-mercato/shared/modules/events'
2
+
3
+ /**
4
+ * Directory Module Events
5
+ *
6
+ * Declares all events that can be emitted by the directory module.
7
+ */
8
+ const events = [
9
+ // Tenants
10
+ { id: 'directory.tenants.created', label: 'Tenant Created', entity: 'tenants', category: 'crud' },
11
+ { id: 'directory.tenants.updated', label: 'Tenant Updated', entity: 'tenants', category: 'crud' },
12
+ { id: 'directory.tenants.deleted', label: 'Tenant Deleted', entity: 'tenants', category: 'crud' },
13
+
14
+ // Organizations
15
+ { id: 'directory.organizations.created', label: 'Organization Created', entity: 'organizations', category: 'crud' },
16
+ { id: 'directory.organizations.updated', label: 'Organization Updated', entity: 'organizations', category: 'crud' },
17
+ { id: 'directory.organizations.deleted', label: 'Organization Deleted', entity: 'organizations', category: 'crud' },
18
+ ] as const
19
+
20
+ export const eventsConfig = createModuleEvents({
21
+ moduleId: 'directory',
22
+ events,
23
+ })
24
+
25
+ /** Type-safe event emitter for directory module */
26
+ export const emitDirectoryEvent = eventsConfig.emit
27
+
28
+ /** Event IDs that can be emitted by the directory module */
29
+ export type DirectoryEventId = typeof events[number]['id']
30
+
31
+ export default eventsConfig
@@ -1,6 +1,7 @@
1
1
  export const features = [
2
2
  { id: 'sales.orders.view', title: 'View sales orders', module: 'sales' },
3
3
  { id: 'sales.orders.manage', title: 'Manage sales orders', module: 'sales' },
4
+ { id: 'sales.orders.approve', title: 'Approve sales orders', module: 'sales' },
4
5
  { id: 'sales.quotes.view', title: 'View sales quotes', module: 'sales' },
5
6
  { id: 'sales.quotes.manage', title: 'Manage sales quotes', module: 'sales' },
6
7
  { id: 'sales.shipments.manage', title: 'Manage order shipments', module: 'sales' },