@open-mercato/core 0.4.2-canary-51881f6bf3 → 0.4.2-canary-5f415b8a44

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 (155) 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 +59 -58
  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 +59 -58
  104. package/generated/entity-fields-registry.ts +2 -0
  105. package/package.json +3 -5
  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/instances/route.ts +21 -7
  122. package/src/modules/workflows/api/tasks/route.ts +7 -1
  123. package/src/modules/workflows/backend/definitions/[id]/page.meta.ts +1 -1
  124. package/src/modules/workflows/backend/definitions/[id]/page.tsx +9 -0
  125. package/src/modules/workflows/backend/definitions/create/page.meta.ts +1 -1
  126. package/src/modules/workflows/backend/definitions/create/page.tsx +9 -0
  127. package/src/modules/workflows/backend/definitions/visual-editor/page.meta.ts +1 -1
  128. package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +21 -3
  129. package/src/modules/workflows/backend/events/[id]/page.meta.ts +2 -2
  130. package/src/modules/workflows/backend/events/[id]/page.tsx +1 -1
  131. package/src/modules/workflows/backend/instances/[id]/page.meta.ts +2 -2
  132. package/src/modules/workflows/backend/tasks/[id]/page.meta.ts +2 -2
  133. package/src/modules/workflows/backend/tasks/[id]/page.tsx +1 -1
  134. package/src/modules/workflows/backend/tasks/page.tsx +5 -6
  135. package/src/modules/workflows/cli.ts +111 -0
  136. package/src/modules/workflows/components/DefinitionTriggersEditor.tsx +581 -0
  137. package/src/modules/workflows/components/EventTriggersEditor.tsx +664 -0
  138. package/src/modules/workflows/data/entities.ts +124 -0
  139. package/src/modules/workflows/data/validators.ts +138 -0
  140. package/src/modules/workflows/events.ts +49 -0
  141. package/src/modules/workflows/examples/checkout-demo-definition.json +1 -5
  142. package/src/modules/workflows/examples/order-approval-definition.json +257 -0
  143. package/src/modules/workflows/examples/order-approval-guard-rules.json +32 -0
  144. package/src/modules/workflows/i18n/en.json +71 -0
  145. package/src/modules/workflows/lib/activity-executor.ts +129 -16
  146. package/src/modules/workflows/lib/event-trigger-service.ts +557 -0
  147. package/src/modules/workflows/lib/graph-utils.ts +117 -2
  148. package/src/modules/workflows/lib/seeds.ts +34 -8
  149. package/src/modules/workflows/lib/start-validator.ts +38 -28
  150. package/src/modules/workflows/lib/transition-handler.ts +208 -55
  151. package/src/modules/workflows/migrations/Migration20260123143500.ts +38 -0
  152. package/src/modules/workflows/subscribers/event-trigger.ts +109 -0
  153. package/src/modules/workflows/widgets/injection/order-approval/widget.client.tsx +446 -0
  154. package/src/modules/workflows/widgets/injection/order-approval/widget.ts +16 -0
  155. package/src/modules/workflows/widgets/injection-table.ts +21 -0
@@ -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' },
@@ -50,6 +50,9 @@ import {
50
50
  emitSalesDocumentTotalsRefresh,
51
51
  subscribeSalesDocumentTotalsRefresh,
52
52
  } from '@open-mercato/core/modules/sales/lib/frontend/documentTotalsEvents'
53
+ import {
54
+ subscribeSalesDocumentDataRefresh,
55
+ } from '@open-mercato/core/modules/sales/lib/frontend/documentDataEvents'
53
56
  import type { CommentSummary, SectionAction } from '@open-mercato/ui/backend/detail'
54
57
  import { ICON_SUGGESTIONS } from '@open-mercato/core/modules/customers/lib/dictionaries'
55
58
  import { readMarkdownPreferenceCookie, writeMarkdownPreferenceCookie } from '@open-mercato/core/modules/customers/lib/markdownPreference'
@@ -2601,6 +2604,19 @@ export default function SalesDocumentDetailPage({
2601
2604
  [kind, record?.id, refreshDocumentTotals],
2602
2605
  )
2603
2606
 
2607
+ // Subscribe to document data refresh events (e.g., from workflow status updates)
2608
+ React.useEffect(
2609
+ () =>
2610
+ subscribeSalesDocumentDataRefresh((detail) => {
2611
+ if (!record?.id) return
2612
+ if (detail.documentId !== record.id) return
2613
+ if (detail.kind && detail.kind !== kind) return
2614
+ // Increment reloadKey to trigger a full document reload
2615
+ setReloadKey((prev) => prev + 1)
2616
+ }),
2617
+ [kind, record?.id],
2618
+ )
2619
+
2604
2620
  const statusDictionaryMap = React.useMemo(
2605
2621
  () =>
2606
2622
  createDictionaryMap(
@@ -4,7 +4,7 @@ import { randomUUID } from 'crypto'
4
4
  import { z } from 'zod'
5
5
  import { registerCommand } from '@open-mercato/shared/lib/commands'
6
6
  import type { CommandHandler } from '@open-mercato/shared/lib/commands'
7
- import { emitCrudSideEffects, requireId } from '@open-mercato/shared/lib/commands/helpers'
7
+ import { emitCrudSideEffects, requireId, type CrudEventsConfig } from '@open-mercato/shared/lib/commands/helpers'
8
8
  import type { EntityManager } from '@mikro-orm/postgresql'
9
9
  import type { EventBus } from '@open-mercato/events'
10
10
  import type { DataEngine } from '@open-mercato/shared/lib/data/engine'
@@ -89,6 +89,29 @@ import { resolveStatusEntryIdByValue } from '../lib/statusHelpers'
89
89
  import { SalesDocumentNumberGenerator } from '../services/salesDocumentNumberGenerator'
90
90
  import { loadSalesSettings } from './settings'
91
91
 
92
+ // CRUD events configuration for workflow triggers
93
+ const orderCrudEvents: CrudEventsConfig<SalesOrder> = {
94
+ module: 'sales',
95
+ entity: 'orders',
96
+ persistent: true,
97
+ buildPayload: (ctx) => ({
98
+ id: ctx.identifiers.id,
99
+ organizationId: ctx.identifiers.organizationId,
100
+ tenantId: ctx.identifiers.tenantId,
101
+ }),
102
+ }
103
+
104
+ const quoteCrudEvents: CrudEventsConfig<SalesQuote> = {
105
+ module: 'sales',
106
+ entity: 'quotes',
107
+ persistent: true,
108
+ buildPayload: (ctx) => ({
109
+ id: ctx.identifiers.id,
110
+ organizationId: ctx.identifiers.organizationId,
111
+ tenantId: ctx.identifiers.tenantId,
112
+ }),
113
+ }
114
+
92
115
  type DocumentAddressSnapshot = {
93
116
  id: string
94
117
  organizationId: string
@@ -3111,6 +3134,31 @@ const createQuoteCommand: CommandHandler<QuoteCreateInput, { quoteId: string }>
3111
3134
  })
3112
3135
  await em.flush()
3113
3136
 
3137
+ // Emit CRUD side effects to trigger workflow event listeners
3138
+ const dataEngine = ctx.container.resolve('dataEngine') as DataEngine
3139
+ await emitCrudSideEffects({
3140
+ dataEngine,
3141
+ action: 'created',
3142
+ entity: quote,
3143
+ identifiers: {
3144
+ id: quote.id,
3145
+ organizationId: quote.organizationId,
3146
+ tenantId: quote.tenantId,
3147
+ },
3148
+ events: quoteCrudEvents,
3149
+ indexer: { entityType: E.sales.sales_quote },
3150
+ })
3151
+
3152
+ // Invalidate cache
3153
+ const resourceKind = deriveResourceFromCommandId(createQuoteCommand.id) ?? 'sales.quote'
3154
+ await invalidateCrudCache(
3155
+ ctx.container,
3156
+ resourceKind,
3157
+ { id: quote.id, organizationId: quote.organizationId, tenantId: quote.tenantId },
3158
+ ctx.auth?.tenantId ?? null,
3159
+ 'created'
3160
+ )
3161
+
3114
3162
  return { quoteId: quote.id }
3115
3163
  },
3116
3164
  captureAfter: async (_input, result, ctx) => {
@@ -3782,6 +3830,31 @@ const createOrderCommand: CommandHandler<OrderCreateInput, { orderId: string }>
3782
3830
  })
3783
3831
  await em.flush()
3784
3832
 
3833
+ // Emit CRUD side effects to trigger workflow event listeners
3834
+ const dataEngine = ctx.container.resolve('dataEngine') as DataEngine
3835
+ await emitCrudSideEffects({
3836
+ dataEngine,
3837
+ action: 'created',
3838
+ entity: order,
3839
+ identifiers: {
3840
+ id: order.id,
3841
+ organizationId: order.organizationId,
3842
+ tenantId: order.tenantId,
3843
+ },
3844
+ events: orderCrudEvents,
3845
+ indexer: { entityType: E.sales.sales_order },
3846
+ })
3847
+
3848
+ // Invalidate cache
3849
+ const resourceKind = deriveResourceFromCommandId(createOrderCommand.id) ?? 'sales.order'
3850
+ await invalidateCrudCache(
3851
+ ctx.container,
3852
+ resourceKind,
3853
+ { id: order.id, organizationId: order.organizationId, tenantId: order.tenantId },
3854
+ ctx.auth?.tenantId ?? null,
3855
+ 'created'
3856
+ )
3857
+
3785
3858
  return { orderId: order.id }
3786
3859
  },
3787
3860
  captureAfter: async (_input, result, ctx) => {
@@ -0,0 +1,82 @@
1
+ import { createModuleEvents } from '@open-mercato/shared/modules/events'
2
+
3
+ /**
4
+ * Sales Module Events
5
+ *
6
+ * Declares all events that can be emitted by the sales module.
7
+ */
8
+ const events = [
9
+ // Orders
10
+ { id: 'sales.orders.created', label: 'Sales Order Created', entity: 'orders', category: 'crud' },
11
+ { id: 'sales.orders.updated', label: 'Sales Order Updated', entity: 'orders', category: 'crud' },
12
+ { id: 'sales.orders.deleted', label: 'Sales Order Deleted', entity: 'orders', category: 'crud' },
13
+
14
+ // Quotes
15
+ { id: 'sales.quotes.created', label: 'Quote Created', entity: 'quotes', category: 'crud' },
16
+ { id: 'sales.quotes.updated', label: 'Quote Updated', entity: 'quotes', category: 'crud' },
17
+ { id: 'sales.quotes.deleted', label: 'Quote Deleted', entity: 'quotes', category: 'crud' },
18
+
19
+ // Invoices
20
+ { id: 'sales.invoices.created', label: 'Invoice Created', entity: 'invoices', category: 'crud' },
21
+ { id: 'sales.invoices.updated', label: 'Invoice Updated', entity: 'invoices', category: 'crud' },
22
+ { id: 'sales.invoices.deleted', label: 'Invoice Deleted', entity: 'invoices', category: 'crud' },
23
+
24
+ // Order Lines
25
+ { id: 'sales.lines.created', label: 'Order Line Created', entity: 'lines', category: 'crud' },
26
+ { id: 'sales.lines.updated', label: 'Order Line Updated', entity: 'lines', category: 'crud' },
27
+ { id: 'sales.lines.deleted', label: 'Order Line Deleted', entity: 'lines', category: 'crud' },
28
+
29
+ // Payments
30
+ { id: 'sales.payments.created', label: 'Payment Created', entity: 'payments', category: 'crud' },
31
+ { id: 'sales.payments.updated', label: 'Payment Updated', entity: 'payments', category: 'crud' },
32
+ { id: 'sales.payments.deleted', label: 'Payment Deleted', entity: 'payments', category: 'crud' },
33
+
34
+ // Shipments
35
+ { id: 'sales.shipments.created', label: 'Shipment Created', entity: 'shipments', category: 'crud' },
36
+ { id: 'sales.shipments.updated', label: 'Shipment Updated', entity: 'shipments', category: 'crud' },
37
+ { id: 'sales.shipments.deleted', label: 'Shipment Deleted', entity: 'shipments', category: 'crud' },
38
+
39
+ // Notes
40
+ { id: 'sales.notes.created', label: 'Note Created', entity: 'notes', category: 'crud' },
41
+ { id: 'sales.notes.updated', label: 'Note Updated', entity: 'notes', category: 'crud' },
42
+ { id: 'sales.notes.deleted', label: 'Note Deleted', entity: 'notes', category: 'crud' },
43
+
44
+ // Configuration
45
+ { id: 'sales.configuration.created', label: 'Configuration Created', entity: 'configuration', category: 'crud' },
46
+ { id: 'sales.configuration.updated', label: 'Configuration Updated', entity: 'configuration', category: 'crud' },
47
+ { id: 'sales.configuration.deleted', label: 'Configuration Deleted', entity: 'configuration', category: 'crud' },
48
+
49
+ // Lifecycle events - Document calculations
50
+ { id: 'sales.document.totals.calculated', label: 'Document Totals Calculated', category: 'lifecycle' },
51
+ { id: 'sales.document.calculate.before', label: 'Before Document Calculate', category: 'lifecycle', excludeFromTriggers: true },
52
+ { id: 'sales.document.calculate.after', label: 'After Document Calculate', category: 'lifecycle', excludeFromTriggers: true },
53
+
54
+ // Lifecycle events - Line calculations
55
+ { id: 'sales.line.calculate.before', label: 'Before Line Calculate', category: 'lifecycle', excludeFromTriggers: true },
56
+ { id: 'sales.line.calculate.after', label: 'After Line Calculate', category: 'lifecycle', excludeFromTriggers: true },
57
+
58
+ // Lifecycle events - Tax calculations
59
+ { id: 'sales.tax.calculate.before', label: 'Before Tax Calculate', category: 'lifecycle', excludeFromTriggers: true },
60
+ { id: 'sales.tax.calculate.after', label: 'After Tax Calculate', category: 'lifecycle', excludeFromTriggers: true },
61
+
62
+ // Lifecycle events - Shipping adjustments
63
+ { id: 'sales.shipping.adjustments.apply.before', label: 'Before Shipping Adjustments', category: 'lifecycle', excludeFromTriggers: true },
64
+ { id: 'sales.shipping.adjustments.apply.after', label: 'After Shipping Adjustments', category: 'lifecycle', excludeFromTriggers: true },
65
+
66
+ // Lifecycle events - Payment adjustments
67
+ { id: 'sales.payment.adjustments.apply.before', label: 'Before Payment Adjustments', category: 'lifecycle', excludeFromTriggers: true },
68
+ { id: 'sales.payment.adjustments.apply.after', label: 'After Payment Adjustments', category: 'lifecycle', excludeFromTriggers: true },
69
+ ] as const
70
+
71
+ export const eventsConfig = createModuleEvents({
72
+ moduleId: 'sales',
73
+ events,
74
+ })
75
+
76
+ /** Type-safe event emitter for sales module */
77
+ export const emitSalesEvent = eventsConfig.emit
78
+
79
+ /** Event IDs that can be emitted by the sales module */
80
+ export type SalesEventId = typeof events[number]['id']
81
+
82
+ export default eventsConfig
@@ -125,6 +125,9 @@ type SeedScope = { tenantId: string; organizationId: string }
125
125
 
126
126
  const ORDER_STATUS_DEFAULTS: SalesDictionarySeed[] = [
127
127
  { value: 'draft', label: 'Draft', color: '#94a3b8', icon: 'lucide:file-pen-line' },
128
+ { value: 'pending_approval', label: 'Pending Approval', color: '#f59e0b', icon: 'lucide:hourglass' },
129
+ { value: 'approved', label: 'Approved', color: '#16a34a', icon: 'lucide:check-circle' },
130
+ { value: 'rejected', label: 'Rejected', color: '#ef4444', icon: 'lucide:x-circle' },
128
131
  { value: 'sent', label: 'Sent', color: '#0ea5e9', icon: 'lucide:send' },
129
132
  { value: 'confirmed', label: 'Confirmed', color: '#2563eb', icon: 'lucide:badge-check' },
130
133
  { value: 'in_fulfillment', label: 'In fulfillment', color: '#f59e0b', icon: 'lucide:loader-2' },