@open-mercato/core 0.4.2-canary-5d2c419a9b → 0.4.2-canary-9e0237de8e

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 +17 -4
  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 +75 -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 +29 -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,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'
@@ -92,6 +92,29 @@ import { SalesDocumentNumberGenerator } from '../services/salesDocumentNumberGen
92
92
  import { loadSalesSettings } from './settings'
93
93
  import { notificationTypes } from '../notifications'
94
94
 
95
+ // CRUD events configuration for workflow triggers
96
+ const orderCrudEvents: CrudEventsConfig<SalesOrder> = {
97
+ module: 'sales',
98
+ entity: 'orders',
99
+ persistent: true,
100
+ buildPayload: (ctx) => ({
101
+ id: ctx.identifiers.id,
102
+ organizationId: ctx.identifiers.organizationId,
103
+ tenantId: ctx.identifiers.tenantId,
104
+ }),
105
+ }
106
+
107
+ const quoteCrudEvents: CrudEventsConfig<SalesQuote> = {
108
+ module: 'sales',
109
+ entity: 'quotes',
110
+ persistent: true,
111
+ buildPayload: (ctx) => ({
112
+ id: ctx.identifiers.id,
113
+ organizationId: ctx.identifiers.organizationId,
114
+ tenantId: ctx.identifiers.tenantId,
115
+ }),
116
+ }
117
+
95
118
  type DocumentAddressSnapshot = {
96
119
  id: string
97
120
  organizationId: string
@@ -3145,6 +3168,32 @@ const createQuoteCommand: CommandHandler<QuoteCreateInput, { quoteId: string }>
3145
3168
  console.error('[sales.quotes.create] Failed to create notification:', err)
3146
3169
  }
3147
3170
 
3171
+ // Emit CRUD side effects to trigger workflow event listeners
3172
+ const dataEngine = ctx.container.resolve('dataEngine') as DataEngine
3173
+ await emitCrudSideEffects({
3174
+ dataEngine,
3175
+ action: 'created',
3176
+ entity: quote,
3177
+ identifiers: {
3178
+ id: quote.id,
3179
+ organizationId: quote.organizationId,
3180
+ tenantId: quote.tenantId,
3181
+ },
3182
+ events: quoteCrudEvents,
3183
+ indexer: { entityType: E.sales.sales_quote },
3184
+ })
3185
+
3186
+ // Invalidate cache
3187
+ const resourceKind = deriveResourceFromCommandId(createQuoteCommand.id) ?? 'sales.quote'
3188
+ await invalidateCrudCache(
3189
+ ctx.container,
3190
+ resourceKind,
3191
+ { id: quote.id, organizationId: quote.organizationId, tenantId: quote.tenantId },
3192
+ ctx.auth?.tenantId ?? null,
3193
+ 'created'
3194
+ )
3195
+
3196
+
3148
3197
  return { quoteId: quote.id }
3149
3198
  },
3150
3199
  captureAfter: async (_input, result, ctx) => {
@@ -3847,6 +3896,31 @@ const createOrderCommand: CommandHandler<OrderCreateInput, { orderId: string }>
3847
3896
  console.error('[sales.orders.create] Failed to create notification:', err)
3848
3897
  }
3849
3898
 
3899
+ // Emit CRUD side effects to trigger workflow event listeners
3900
+ const dataEngine = ctx.container.resolve('dataEngine') as DataEngine
3901
+ await emitCrudSideEffects({
3902
+ dataEngine,
3903
+ action: 'created',
3904
+ entity: order,
3905
+ identifiers: {
3906
+ id: order.id,
3907
+ organizationId: order.organizationId,
3908
+ tenantId: order.tenantId,
3909
+ },
3910
+ events: orderCrudEvents,
3911
+ indexer: { entityType: E.sales.sales_order },
3912
+ })
3913
+
3914
+ // Invalidate cache
3915
+ const resourceKind = deriveResourceFromCommandId(createOrderCommand.id) ?? 'sales.order'
3916
+ await invalidateCrudCache(
3917
+ ctx.container,
3918
+ resourceKind,
3919
+ { id: order.id, organizationId: order.organizationId, tenantId: order.tenantId },
3920
+ ctx.auth?.tenantId ?? null,
3921
+ 'created'
3922
+ )
3923
+
3850
3924
  return { orderId: order.id }
3851
3925
  },
3852
3926
  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' },
@@ -0,0 +1,28 @@
1
+ "use client"
2
+
3
+ export const SALES_DOCUMENT_DATA_REFRESH_EVENT = 'sales:document:data:refresh'
4
+
5
+ export type SalesDocumentDataRefreshDetail = {
6
+ documentId: string
7
+ kind?: 'order' | 'quote'
8
+ }
9
+
10
+ export function emitSalesDocumentDataRefresh(detail: SalesDocumentDataRefreshDetail): void {
11
+ if (typeof window === 'undefined' || typeof CustomEvent === 'undefined') return
12
+ window.dispatchEvent(
13
+ new CustomEvent<SalesDocumentDataRefreshDetail>(SALES_DOCUMENT_DATA_REFRESH_EVENT, { detail }),
14
+ )
15
+ }
16
+
17
+ export function subscribeSalesDocumentDataRefresh(
18
+ handler: (detail: SalesDocumentDataRefreshDetail) => void,
19
+ ): () => void {
20
+ if (typeof window === 'undefined') return () => {}
21
+ const listener = (event: Event) => {
22
+ const detail = (event as CustomEvent<SalesDocumentDataRefreshDetail>).detail
23
+ if (!detail) return
24
+ handler(detail)
25
+ }
26
+ window.addEventListener(SALES_DOCUMENT_DATA_REFRESH_EVENT, listener as EventListener)
27
+ return () => window.removeEventListener(SALES_DOCUMENT_DATA_REFRESH_EVENT, listener as EventListener)
28
+ }
@@ -19,6 +19,8 @@ export const features = [
19
19
  { id: 'workflows.tasks.complete', title: 'Complete workflow tasks', module: moduleId },
20
20
  { id: 'workflows.signals.send', title: 'Send workflow signals', module: moduleId },
21
21
  { id: 'workflows.events.view', title: 'View workflow events', module: moduleId },
22
+ // Note: Event triggers are now embedded in workflow definitions.
23
+ // Trigger management permissions are covered by workflows.definitions.edit
22
24
  ]
23
25
 
24
26
  export default features
@@ -194,11 +194,14 @@ describe('Workflow Instances API', () => {
194
194
  const request = new NextRequest('http://localhost/api/workflows/instances?entityType=order&entityId=order-123')
195
195
  await listInstances(request)
196
196
 
197
+ // The implementation uses JSONB $contains queries for metadata filtering
197
198
  expect(mockEm.findAndCount).toHaveBeenCalledWith(
198
199
  WorkflowInstance,
199
200
  expect.objectContaining({
200
- 'metadata.entityType': 'order',
201
- 'metadata.entityId': 'order-123',
201
+ $and: expect.arrayContaining([
202
+ { metadata: { $contains: { entityType: 'order' } } },
203
+ { metadata: { $contains: { entityId: 'order-123' } } },
204
+ ]),
202
205
  }),
203
206
  expect.any(Object)
204
207
  )
@@ -70,19 +70,33 @@ export async function GET(request: NextRequest) {
70
70
  }
71
71
 
72
72
  if (status) {
73
- where.status = status
73
+ // Support comma-separated status values (e.g., "RUNNING,PAUSED,WAITING_FOR_ACTIVITIES")
74
+ const statuses = status.split(',').map(s => s.trim()).filter(Boolean)
75
+ if (statuses.length === 1) {
76
+ where.status = statuses[0]
77
+ } else if (statuses.length > 1) {
78
+ where.status = { $in: statuses }
79
+ }
74
80
  }
75
81
 
76
82
  if (correlationKey) {
77
83
  where.correlationKey = correlationKey
78
84
  }
79
85
 
80
- if (entityType) {
81
- where['metadata.entityType'] = entityType
82
- }
83
-
84
- if (entityId) {
85
- where['metadata.entityId'] = entityId
86
+ // For JSONB metadata filtering, use $contains with explicit key-value pairs
87
+ // MikroORM's dot notation creates table joins, not JSON access
88
+ if (entityType || entityId) {
89
+ where.$and = where.$and || []
90
+ if (entityType) {
91
+ where.$and.push({
92
+ metadata: { $contains: { entityType: entityType } }
93
+ })
94
+ }
95
+ if (entityId) {
96
+ where.$and.push({
97
+ metadata: { $contains: { entityId: entityId } }
98
+ })
99
+ }
86
100
  }
87
101
 
88
102
  const [instances, total] = await em.findAndCount(
@@ -67,7 +67,13 @@ export async function GET(request: NextRequest) {
67
67
  }
68
68
 
69
69
  if (status) {
70
- where.status = status
70
+ // Handle comma-separated status values
71
+ const statusValues = status.split(',').map(s => s.trim()).filter(Boolean)
72
+ if (statusValues.length === 1) {
73
+ where.status = statusValues[0]
74
+ } else if (statusValues.length > 1) {
75
+ where.status = { $in: statusValues }
76
+ }
71
77
  }
72
78
 
73
79
  if (assignedTo) {
@@ -4,7 +4,7 @@ export const metadata = {
4
4
  pageTitle: 'Edit Workflow Definition',
5
5
  pageTitleKey: 'workflows.edit.title',
6
6
  breadcrumb: [
7
- { label: 'Workflows', labelKey: 'workflows.module.name', href: '/backend/workflows/definitions' },
7
+ { label: 'Workflows', labelKey: 'workflows.module.name', href: '/backend/definitions' },
8
8
  { label: 'Edit', labelKey: 'workflows.common.edit' },
9
9
  ],
10
10
  }
@@ -19,6 +19,7 @@ import {
19
19
  } from '../../../components/formConfig'
20
20
  import { StepsEditor } from '../../../components/StepsEditor'
21
21
  import { TransitionsEditor } from '../../../components/TransitionsEditor'
22
+ import { EventTriggersEditor } from '../../../components/EventTriggersEditor'
22
23
 
23
24
  export default function EditWorkflowDefinitionPage() {
24
25
  const router = useRouter()
@@ -145,6 +146,14 @@ export default function EditWorkflowDefinitionPage() {
145
146
  groups={formGroups}
146
147
  submitLabel={t('workflows.form.update')}
147
148
  />
149
+
150
+ {/* Event Triggers Section */}
151
+ <div className="mt-8">
152
+ <EventTriggersEditor
153
+ workflowDefinitionId={definitionId!}
154
+ workflowId={definition?.workflowId}
155
+ />
156
+ </div>
148
157
  </PageBody>
149
158
  </Page>
150
159
  )
@@ -20,7 +20,7 @@ export const metadata = {
20
20
  pageOrder: 100,
21
21
  icon: createIcon,
22
22
  breadcrumb: [
23
- { label: 'Workflows', labelKey: 'workflows.module.name', href: '/backend/workflows/definitions' },
23
+ { label: 'Workflows', labelKey: 'workflows.module.name', href: '/backend/definitions' },
24
24
  { label: 'Create', labelKey: 'workflows.common.create' },
25
25
  ],
26
26
  }
@@ -16,6 +16,8 @@ import {
16
16
  } from '../../../components/formConfig'
17
17
  import { StepsEditor } from '../../../components/StepsEditor'
18
18
  import { TransitionsEditor } from '../../../components/TransitionsEditor'
19
+ import { Alert, AlertDescription, AlertTitle } from '@open-mercato/ui/primitives/alert'
20
+ import { Zap } from 'lucide-react'
19
21
 
20
22
  export default function CreateWorkflowDefinitionPage() {
21
23
  const router = useRouter()
@@ -50,6 +52,13 @@ export default function CreateWorkflowDefinitionPage() {
50
52
  return (
51
53
  <Page>
52
54
  <PageBody>
55
+ <Alert variant="info" className="mb-6">
56
+ <Zap className="w-4 h-4" />
57
+ <AlertTitle>Event Triggers</AlertTitle>
58
+ <AlertDescription>
59
+ After creating this workflow definition, you can configure event triggers to automatically start the workflow when specific events occur in the system.
60
+ </AlertDescription>
61
+ </Alert>
53
62
  <CrudForm
54
63
  title={t('workflows.create.title')}
55
64
  backHref="/backend/definitions"
@@ -19,7 +19,7 @@ export const metadata = {
19
19
  pageOrder: 150,
20
20
  icon: visualEditorIcon,
21
21
  breadcrumb: [
22
- { label: 'Workflows', labelKey: 'workflows.module.name', href: '/backend/workflows/definitions' },
22
+ { label: 'Workflows', labelKey: 'workflows.module.name', href: '/backend/definitions' },
23
23
  { label: 'Visual Editor', labelKey: 'workflows.backend.definitions.visual_editor.title' },
24
24
  ],
25
25
  }
@@ -34,6 +34,8 @@ import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
34
34
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
35
35
  import {CircleQuestionMark, Info, PanelTopClose, PanelTopOpen, Play, Save, Trash2} from 'lucide-react'
36
36
  import { NODE_TYPE_ICONS, NODE_TYPE_COLORS, NODE_TYPE_LABELS } from '../../../lib/node-type-icons'
37
+ import { DefinitionTriggersEditor } from '../../../components/DefinitionTriggersEditor'
38
+ import type { WorkflowDefinitionTrigger } from '../../../data/entities'
37
39
  import * as React from "react";
38
40
 
39
41
  /**
@@ -76,6 +78,7 @@ export default function VisualEditorPage() {
76
78
  const [icon, setIcon] = useState('')
77
79
  const [effectiveFrom, setEffectiveFrom] = useState('')
78
80
  const [effectiveTo, setEffectiveTo] = useState('')
81
+ const [triggers, setTriggers] = useState<WorkflowDefinitionTrigger[]>([])
79
82
 
80
83
  // Load existing definition if ID is provided
81
84
  useEffect(() => {
@@ -113,6 +116,9 @@ export default function VisualEditorPage() {
113
116
  setNodes(graph.nodes)
114
117
  setEdges(graph.edges)
115
118
 
119
+ // Load embedded triggers from definition
120
+ setTriggers(definition.definition?.triggers || [])
121
+
116
122
  flash('Workflow loaded successfully', 'success')
117
123
  } catch (error) {
118
124
  console.error('Error loading workflow definition:', error)
@@ -284,8 +290,12 @@ export default function VisualEditorPage() {
284
290
  return
285
291
  }
286
292
 
287
- // Generate definition data
288
- const definitionData = graphToDefinition(nodes, edges, { includePositions: true })
293
+ // Generate definition data and include triggers
294
+ const graphDefinition = graphToDefinition(nodes, edges, { includePositions: true })
295
+ const definitionData = {
296
+ ...graphDefinition,
297
+ triggers: triggers.length > 0 ? triggers : undefined,
298
+ }
289
299
 
290
300
  // Run Zod schema validation before saving
291
301
  const schemaResult = workflowDefinitionDataSchema.safeParse(definitionData)
@@ -357,7 +367,7 @@ export default function VisualEditorPage() {
357
367
  } finally {
358
368
  setIsSaving(false)
359
369
  }
360
- }, [nodes, edges, workflowId, workflowName, description, version, enabled, category, tags, definitionId, router])
370
+ }, [nodes, edges, workflowId, workflowName, description, version, enabled, category, tags, triggers, definitionId, router])
361
371
 
362
372
  // Test workflow
363
373
  const handleTest = useCallback(() => {
@@ -475,6 +485,7 @@ export default function VisualEditorPage() {
475
485
  setIcon('')
476
486
  setEffectiveFrom('')
477
487
  setEffectiveTo('')
488
+ setTriggers([])
478
489
  setShowClearConfirm(false)
479
490
  flash('Canvas cleared', 'success')
480
491
  }, [])
@@ -709,6 +720,13 @@ export default function VisualEditorPage() {
709
720
  </div>
710
721
  </div>
711
722
  </div>
723
+
724
+ {/* Event Triggers - Always visible, managed as part of definition */}
725
+ <DefinitionTriggersEditor
726
+ value={triggers}
727
+ onChange={setTriggers}
728
+ className="mt-4"
729
+ />
712
730
  </div>
713
731
  )}
714
732
 
@@ -4,8 +4,8 @@ export const metadata = {
4
4
  pageTitle: 'Event Details',
5
5
  pageTitleKey: 'workflows.events.detail.title',
6
6
  breadcrumb: [
7
- { label: 'Workflows', labelKey: 'workflows.module.name', href: '/backend/workflows/definitions' },
8
- { label: 'Events', labelKey: 'workflows.events.plural', href: '/backend/workflows/events' },
7
+ { label: 'Workflows', labelKey: 'workflows.module.name', href: '/backend/definitions' },
8
+ { label: 'Events', labelKey: 'workflows.events.plural', href: '/backend/events' },
9
9
  { label: 'Details', labelKey: 'common.details' },
10
10
  ],
11
11
  }
@@ -212,7 +212,7 @@ export default function WorkflowEventDetailPage() {
212
212
  </dt>
213
213
  <dd className="mt-1">
214
214
  <Link
215
- href={`/backend/workflows/instances/${event.workflowInstance.id}`}
215
+ href={`/backend/instances/${event.workflowInstance.id}`}
216
216
  className="text-sm text-primary hover:underline font-mono"
217
217
  >
218
218
  {event.workflowInstance.id}
@@ -4,8 +4,8 @@ export const metadata = {
4
4
  pageTitle: 'Workflow Instance Details',
5
5
  pageTitleKey: 'workflows.instances.singular',
6
6
  breadcrumb: [
7
- { label: 'Workflows', labelKey: 'workflows.module.name', href: '/backend/workflows/definitions' },
8
- { label: 'Instances', labelKey: 'workflows.instances.plural', href: '/backend/workflows/instances' },
7
+ { label: 'Workflows', labelKey: 'workflows.module.name', href: '/backend/definitions' },
8
+ { label: 'Instances', labelKey: 'workflows.instances.plural', href: '/backend/instances' },
9
9
  { label: 'Details', labelKey: 'common.details' },
10
10
  ],
11
11
  }
@@ -4,8 +4,8 @@ export const metadata = {
4
4
  pageTitle: 'Task Details',
5
5
  pageTitleKey: 'workflows.tasks.singular',
6
6
  breadcrumb: [
7
- { label: 'Workflows', labelKey: 'workflows.module.name', href: '/backend/workflows/definitions' },
8
- { label: 'Tasks', labelKey: 'workflows.tasks.plural', href: '/backend/workflows/tasks' },
7
+ { label: 'Workflows', labelKey: 'workflows.module.name', href: '/backend/definitions' },
8
+ { label: 'Tasks', labelKey: 'workflows.tasks.plural', href: '/backend/tasks' },
9
9
  { label: 'Details', labelKey: 'common.details' },
10
10
  ],
11
11
  }