@open-mercato/core 0.4.5-develop-636d33c995 → 0.4.5-develop-3d8e759e45

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 (211) hide show
  1. package/dist/modules/catalog/backend/catalog/categories/[id]/edit/page.js +17 -2
  2. package/dist/modules/catalog/backend/catalog/categories/[id]/edit/page.js.map +2 -2
  3. package/dist/modules/catalog/backend/catalog/products/[id]/page.js +15 -0
  4. package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +2 -2
  5. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js +30 -0
  6. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js.map +2 -2
  7. package/dist/modules/catalog/inbox-actions.js +51 -0
  8. package/dist/modules/catalog/inbox-actions.js.map +7 -0
  9. package/dist/modules/catalog/lib/messageObjectPreviews.js +146 -0
  10. package/dist/modules/catalog/lib/messageObjectPreviews.js.map +7 -0
  11. package/dist/modules/catalog/message-objects.js +95 -0
  12. package/dist/modules/catalog/message-objects.js.map +7 -0
  13. package/dist/modules/currencies/backend/currencies/[id]/page.js +21 -0
  14. package/dist/modules/currencies/backend/currencies/[id]/page.js.map +2 -2
  15. package/dist/modules/currencies/lib/messageObjectPreviews.js +51 -0
  16. package/dist/modules/currencies/lib/messageObjectPreviews.js.map +7 -0
  17. package/dist/modules/currencies/message-objects.js +41 -0
  18. package/dist/modules/currencies/message-objects.js.map +7 -0
  19. package/dist/modules/customers/backend/customers/companies/[id]/page.js +20 -0
  20. package/dist/modules/customers/backend/customers/companies/[id]/page.js.map +2 -2
  21. package/dist/modules/customers/backend/customers/deals/[id]/page.js +12 -1
  22. package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
  23. package/dist/modules/customers/backend/customers/people/[id]/page.js +20 -0
  24. package/dist/modules/customers/backend/customers/people/[id]/page.js.map +2 -2
  25. package/dist/modules/customers/components/detail/CompanyHighlights.js +18 -14
  26. package/dist/modules/customers/components/detail/CompanyHighlights.js.map +2 -2
  27. package/dist/modules/customers/components/detail/PersonHighlights.js +18 -14
  28. package/dist/modules/customers/components/detail/PersonHighlights.js.map +2 -2
  29. package/dist/modules/customers/inbox-actions.js +230 -0
  30. package/dist/modules/customers/inbox-actions.js.map +7 -0
  31. package/dist/modules/customers/lib/messageObjectPreviews.js +41 -5
  32. package/dist/modules/customers/lib/messageObjectPreviews.js.map +2 -2
  33. package/dist/modules/customers/message-objects.js +31 -11
  34. package/dist/modules/customers/message-objects.js.map +2 -2
  35. package/dist/modules/inbox_ops/api/emails/[id]/route.js +40 -1
  36. package/dist/modules/inbox_ops/api/emails/[id]/route.js.map +2 -2
  37. package/dist/modules/inbox_ops/api/extract/route.js +87 -0
  38. package/dist/modules/inbox_ops/api/extract/route.js.map +7 -0
  39. package/dist/modules/inbox_ops/api/proposals/[id]/translate/route.js +6 -1
  40. package/dist/modules/inbox_ops/api/proposals/[id]/translate/route.js.map +2 -2
  41. package/dist/modules/inbox_ops/api/proposals/counts/route.js.map +2 -2
  42. package/dist/modules/inbox_ops/backend/inbox-ops/log/page.js +40 -14
  43. package/dist/modules/inbox_ops/backend/inbox-ops/log/page.js.map +2 -2
  44. package/dist/modules/inbox_ops/backend/inbox-ops/log/page.meta.js +2 -2
  45. package/dist/modules/inbox_ops/backend/inbox-ops/log/page.meta.js.map +2 -2
  46. package/dist/modules/inbox_ops/backend/inbox-ops/page.js +161 -79
  47. package/dist/modules/inbox_ops/backend/inbox-ops/page.js.map +2 -2
  48. package/dist/modules/inbox_ops/backend/inbox-ops/page.meta.js +2 -2
  49. package/dist/modules/inbox_ops/backend/inbox-ops/page.meta.js.map +2 -2
  50. package/dist/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.js +109 -62
  51. package/dist/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.js.map +3 -3
  52. package/dist/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.meta.js +2 -2
  53. package/dist/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.meta.js.map +2 -2
  54. package/dist/modules/inbox_ops/backend/inbox-ops/settings/page.js +36 -14
  55. package/dist/modules/inbox_ops/backend/inbox-ops/settings/page.js.map +2 -2
  56. package/dist/modules/inbox_ops/backend/inbox-ops/settings/page.meta.js +2 -2
  57. package/dist/modules/inbox_ops/backend/inbox-ops/settings/page.meta.js.map +2 -2
  58. package/dist/modules/inbox_ops/components/proposals/ActionCard.js +65 -10
  59. package/dist/modules/inbox_ops/components/proposals/ActionCard.js.map +2 -2
  60. package/dist/modules/inbox_ops/components/proposals/EditActionDialog.js +58 -10
  61. package/dist/modules/inbox_ops/components/proposals/EditActionDialog.js.map +2 -2
  62. package/dist/modules/inbox_ops/lib/constants.js.map +2 -2
  63. package/dist/modules/inbox_ops/lib/contactValidation.js +40 -0
  64. package/dist/modules/inbox_ops/lib/contactValidation.js.map +7 -0
  65. package/dist/modules/inbox_ops/lib/executionEngine.js +31 -826
  66. package/dist/modules/inbox_ops/lib/executionEngine.js.map +3 -3
  67. package/dist/modules/inbox_ops/lib/executionHelpers.js +368 -0
  68. package/dist/modules/inbox_ops/lib/executionHelpers.js.map +7 -0
  69. package/dist/modules/inbox_ops/lib/extractionPrompt.js +28 -35
  70. package/dist/modules/inbox_ops/lib/extractionPrompt.js.map +3 -3
  71. package/dist/modules/inbox_ops/lib/inbox-actions-generated.d.js +1 -0
  72. package/dist/modules/inbox_ops/lib/inbox-actions-generated.d.js.map +7 -0
  73. package/dist/modules/inbox_ops/lib/translationProvider.js +15 -10
  74. package/dist/modules/inbox_ops/lib/translationProvider.js.map +2 -2
  75. package/dist/modules/inbox_ops/subscribers/extractionWorker.js +16 -16
  76. package/dist/modules/inbox_ops/subscribers/extractionWorker.js.map +2 -2
  77. package/dist/modules/messages/commands/messages.js +3 -0
  78. package/dist/modules/messages/commands/messages.js.map +2 -2
  79. package/dist/modules/messages/components/message-detail/panels/objects-panel.js +6 -1
  80. package/dist/modules/messages/components/message-detail/panels/objects-panel.js.map +2 -2
  81. package/dist/modules/messages/components/message-detail/panels/thread-panel.js +4 -1
  82. package/dist/modules/messages/components/message-detail/panels/thread-panel.js.map +2 -2
  83. package/dist/modules/messages/frontend/messages/view/[token]/page.js +1 -0
  84. package/dist/modules/messages/frontend/messages/view/[token]/page.js.map +2 -2
  85. package/dist/modules/resources/backend/resources/resources/[id]/page.js +24 -7
  86. package/dist/modules/resources/backend/resources/resources/[id]/page.js.map +2 -2
  87. package/dist/modules/resources/lib/messageObjectPreviews.js +43 -0
  88. package/dist/modules/resources/lib/messageObjectPreviews.js.map +7 -0
  89. package/dist/modules/resources/message-objects.js +37 -0
  90. package/dist/modules/resources/message-objects.js.map +7 -0
  91. package/dist/modules/sales/backend/sales/channels/[channelId]/edit/page.js +19 -0
  92. package/dist/modules/sales/backend/sales/channels/[channelId]/edit/page.js.map +2 -2
  93. package/dist/modules/sales/backend/sales/documents/[id]/page.js +23 -2
  94. package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
  95. package/dist/modules/sales/backend/sales/quotes/[id]/page.js +1 -1
  96. package/dist/modules/sales/backend/sales/quotes/[id]/page.js.map +2 -2
  97. package/dist/modules/sales/inbox-actions.js +278 -0
  98. package/dist/modules/sales/inbox-actions.js.map +7 -0
  99. package/dist/modules/sales/lib/messageObjectPreviews.js +49 -4
  100. package/dist/modules/sales/lib/messageObjectPreviews.js.map +2 -2
  101. package/dist/modules/sales/message-objects.js +44 -2
  102. package/dist/modules/sales/message-objects.js.map +2 -2
  103. package/dist/modules/sales/widgets/messages/SalesDocumentMessageDetail.js +59 -30
  104. package/dist/modules/sales/widgets/messages/SalesDocumentMessageDetail.js.map +2 -2
  105. package/dist/modules/sales/widgets/messages/SalesDocumentMessagePreview.js +1 -1
  106. package/dist/modules/sales/widgets/messages/SalesDocumentMessagePreview.js.map +1 -1
  107. package/dist/modules/staff/backend/staff/leave-requests/[id]/page.js +8 -30
  108. package/dist/modules/staff/backend/staff/leave-requests/[id]/page.js.map +2 -2
  109. package/dist/modules/staff/backend/staff/my-availability/page.js +13 -0
  110. package/dist/modules/staff/backend/staff/my-availability/page.js.map +2 -2
  111. package/dist/modules/staff/backend/staff/my-leave-requests/[id]/page.js +8 -31
  112. package/dist/modules/staff/backend/staff/my-leave-requests/[id]/page.js.map +2 -2
  113. package/dist/modules/staff/backend/staff/team-members/[id]/page.js +32 -10
  114. package/dist/modules/staff/backend/staff/team-members/[id]/page.js.map +2 -2
  115. package/dist/modules/staff/backend/staff/team-roles/[id]/edit/page.js +14 -1
  116. package/dist/modules/staff/backend/staff/team-roles/[id]/edit/page.js.map +2 -2
  117. package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js +14 -1
  118. package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js.map +2 -2
  119. package/dist/modules/staff/components/TeamForm.js +4 -2
  120. package/dist/modules/staff/components/TeamForm.js.map +2 -2
  121. package/dist/modules/staff/components/TeamRoleForm.js +4 -2
  122. package/dist/modules/staff/components/TeamRoleForm.js.map +2 -2
  123. package/dist/modules/staff/lib/messageObjectPreviews.js +111 -2
  124. package/dist/modules/staff/lib/messageObjectPreviews.js.map +2 -2
  125. package/dist/modules/staff/message-objects.js +79 -8
  126. package/dist/modules/staff/message-objects.js.map +2 -2
  127. package/jest.config.cjs +1 -0
  128. package/jest.mocks/inbox-actions.generated.js +5 -0
  129. package/package.json +2 -2
  130. package/src/modules/catalog/backend/catalog/categories/[id]/edit/page.tsx +19 -5
  131. package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +14 -0
  132. package/src/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.tsx +40 -0
  133. package/src/modules/catalog/inbox-actions.ts +60 -0
  134. package/src/modules/catalog/lib/messageObjectPreviews.ts +176 -0
  135. package/src/modules/catalog/message-objects.ts +102 -0
  136. package/src/modules/currencies/backend/currencies/[id]/page.tsx +20 -0
  137. package/src/modules/currencies/lib/messageObjectPreviews.ts +65 -0
  138. package/src/modules/currencies/message-objects.ts +40 -0
  139. package/src/modules/customers/backend/customers/companies/[id]/page.tsx +19 -0
  140. package/src/modules/customers/backend/customers/deals/[id]/page.tsx +13 -0
  141. package/src/modules/customers/backend/customers/people/[id]/page.tsx +19 -0
  142. package/src/modules/customers/components/detail/CompanyHighlights.tsx +14 -9
  143. package/src/modules/customers/components/detail/PersonHighlights.tsx +14 -9
  144. package/src/modules/customers/inbox-actions.ts +285 -0
  145. package/src/modules/customers/lib/messageObjectPreviews.ts +43 -3
  146. package/src/modules/customers/message-objects.ts +31 -11
  147. package/src/modules/inbox_ops/api/emails/[id]/route.ts +44 -0
  148. package/src/modules/inbox_ops/api/extract/route.ts +94 -0
  149. package/src/modules/inbox_ops/api/proposals/[id]/translate/route.ts +6 -1
  150. package/src/modules/inbox_ops/api/proposals/counts/route.ts +2 -0
  151. package/src/modules/inbox_ops/backend/inbox-ops/log/page.meta.ts +2 -2
  152. package/src/modules/inbox_ops/backend/inbox-ops/log/page.tsx +43 -13
  153. package/src/modules/inbox_ops/backend/inbox-ops/page.meta.ts +2 -2
  154. package/src/modules/inbox_ops/backend/inbox-ops/page.tsx +176 -81
  155. package/src/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.meta.ts +2 -2
  156. package/src/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.tsx +122 -68
  157. package/src/modules/inbox_ops/backend/inbox-ops/settings/page.meta.ts +2 -2
  158. package/src/modules/inbox_ops/backend/inbox-ops/settings/page.tsx +36 -14
  159. package/src/modules/inbox_ops/components/proposals/ActionCard.tsx +91 -7
  160. package/src/modules/inbox_ops/components/proposals/EditActionDialog.tsx +64 -12
  161. package/src/modules/inbox_ops/lib/constants.ts +9 -0
  162. package/src/modules/inbox_ops/lib/contactValidation.ts +54 -0
  163. package/src/modules/inbox_ops/lib/executionEngine.ts +47 -1060
  164. package/src/modules/inbox_ops/lib/executionHelpers.ts +527 -0
  165. package/src/modules/inbox_ops/lib/extractionPrompt.ts +45 -34
  166. package/src/modules/inbox_ops/lib/inbox-actions-generated.d.ts +11 -0
  167. package/src/modules/inbox_ops/lib/translationProvider.ts +16 -10
  168. package/src/modules/inbox_ops/subscribers/extractionWorker.ts +16 -18
  169. package/src/modules/messages/commands/messages.ts +4 -0
  170. package/src/modules/messages/components/message-detail/panels/objects-panel.tsx +8 -1
  171. package/src/modules/messages/components/message-detail/panels/thread-panel.tsx +3 -0
  172. package/src/modules/messages/frontend/messages/view/[token]/page.tsx +1 -0
  173. package/src/modules/resources/backend/resources/resources/[id]/page.tsx +20 -4
  174. package/src/modules/resources/lib/messageObjectPreviews.ts +55 -0
  175. package/src/modules/resources/message-objects.ts +36 -0
  176. package/src/modules/sales/backend/sales/channels/[channelId]/edit/page.tsx +18 -0
  177. package/src/modules/sales/backend/sales/documents/[id]/page.tsx +23 -0
  178. package/src/modules/sales/backend/sales/quotes/[id]/page.tsx +1 -1
  179. package/src/modules/sales/inbox-actions.ts +359 -0
  180. package/src/modules/sales/lib/messageObjectPreviews.ts +54 -4
  181. package/src/modules/sales/message-objects.ts +44 -2
  182. package/src/modules/sales/widgets/messages/SalesDocumentMessageDetail.tsx +72 -34
  183. package/src/modules/sales/widgets/messages/SalesDocumentMessagePreview.tsx +1 -1
  184. package/src/modules/staff/backend/staff/leave-requests/[id]/page.tsx +7 -29
  185. package/src/modules/staff/backend/staff/my-availability/page.tsx +14 -0
  186. package/src/modules/staff/backend/staff/my-leave-requests/[id]/page.tsx +8 -30
  187. package/src/modules/staff/backend/staff/team-members/[id]/page.tsx +28 -7
  188. package/src/modules/staff/backend/staff/team-roles/[id]/edit/page.tsx +12 -0
  189. package/src/modules/staff/backend/staff/teams/[id]/edit/page.tsx +12 -0
  190. package/src/modules/staff/components/TeamForm.tsx +3 -0
  191. package/src/modules/staff/components/TeamRoleForm.tsx +3 -0
  192. package/src/modules/staff/lib/messageObjectPreviews.ts +133 -2
  193. package/src/modules/staff/message-objects.ts +79 -8
  194. package/dist/modules/customers/widgets/messages/CustomerMessageObjectDetail.js +0 -51
  195. package/dist/modules/customers/widgets/messages/CustomerMessageObjectDetail.js.map +0 -7
  196. package/dist/modules/customers/widgets/messages/CustomerMessageObjectPreview.js +0 -35
  197. package/dist/modules/customers/widgets/messages/CustomerMessageObjectPreview.js.map +0 -7
  198. package/dist/modules/customers/widgets/messages/index.js +0 -7
  199. package/dist/modules/customers/widgets/messages/index.js.map +0 -7
  200. package/dist/modules/staff/widgets/messages/StaffMessageObjectDetail.js +0 -51
  201. package/dist/modules/staff/widgets/messages/StaffMessageObjectDetail.js.map +0 -7
  202. package/dist/modules/staff/widgets/messages/StaffMessageObjectPreview.js +0 -34
  203. package/dist/modules/staff/widgets/messages/StaffMessageObjectPreview.js.map +0 -7
  204. package/dist/modules/staff/widgets/messages/index.js +0 -7
  205. package/dist/modules/staff/widgets/messages/index.js.map +0 -7
  206. package/src/modules/customers/widgets/messages/CustomerMessageObjectDetail.tsx +0 -57
  207. package/src/modules/customers/widgets/messages/CustomerMessageObjectPreview.tsx +0 -49
  208. package/src/modules/customers/widgets/messages/index.ts +0 -2
  209. package/src/modules/staff/widgets/messages/StaffMessageObjectDetail.tsx +0 -57
  210. package/src/modules/staff/widgets/messages/StaffMessageObjectPreview.tsx +0 -44
  211. package/src/modules/staff/widgets/messages/index.ts +0 -2
@@ -0,0 +1,527 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql'
2
+ import type { EntityClass } from '@mikro-orm/core'
3
+ import type { AwilixContainer } from 'awilix'
4
+ import type { EventBus } from '@open-mercato/events/types'
5
+ import type { AuthContext } from '@open-mercato/shared/lib/auth/server'
6
+ import type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
7
+ import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
8
+ import type { InboxActionExecutionContext } from '@open-mercato/shared/modules/inbox-actions'
9
+ import type { CrossModuleEntities } from './executionEngine'
10
+ export { formatZodErrors } from './validation'
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Context type used by helper functions (concrete types for ORM/DI access)
14
+ // ---------------------------------------------------------------------------
15
+
16
+ export interface ExecutionHelperContext {
17
+ em: EntityManager
18
+ userId: string
19
+ tenantId: string
20
+ organizationId: string
21
+ eventBus?: EventBus | null
22
+ container: AwilixContainer
23
+ auth?: AuthContext
24
+ entities?: CrossModuleEntities
25
+ }
26
+
27
+ /**
28
+ * Cast InboxActionExecutionContext (from shared) to the concrete helper context.
29
+ * The inbox-actions.ts handlers receive InboxActionExecutionContext but helpers
30
+ * need concrete EntityManager / AwilixContainer types.
31
+ */
32
+ export function asHelperContext(ctx: InboxActionExecutionContext): ExecutionHelperContext {
33
+ return ctx as unknown as ExecutionHelperContext
34
+ }
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // Error
38
+ // ---------------------------------------------------------------------------
39
+
40
+ export class ExecutionError extends Error {
41
+ statusCode: number
42
+
43
+ constructor(message: string, statusCode = 400) {
44
+ super(message)
45
+ this.statusCode = statusCode
46
+ }
47
+ }
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // Command execution
51
+ // ---------------------------------------------------------------------------
52
+
53
+ export async function executeCommand<TInput, TResult>(
54
+ ctx: ExecutionHelperContext,
55
+ commandId: string,
56
+ input: TInput,
57
+ ): Promise<TResult> {
58
+ const commandBus = ctx.container.resolve('commandBus') as CommandBus
59
+ if (!commandBus || typeof commandBus.execute !== 'function') {
60
+ throw new ExecutionError('Command bus is not available', 503)
61
+ }
62
+
63
+ const auth =
64
+ ctx.auth ??
65
+ ({
66
+ sub: ctx.userId,
67
+ userId: ctx.userId,
68
+ tenantId: ctx.tenantId,
69
+ orgId: ctx.organizationId,
70
+ isSuperAdmin: false,
71
+ } satisfies Exclude<AuthContext, null>)
72
+
73
+ const commandContext: CommandRuntimeContext = {
74
+ container: ctx.container,
75
+ auth,
76
+ organizationScope: null,
77
+ selectedOrganizationId: ctx.organizationId,
78
+ organizationIds: [ctx.organizationId],
79
+ }
80
+
81
+ const { result } = await commandBus.execute<TInput, TResult>(commandId, {
82
+ input,
83
+ ctx: commandContext,
84
+ })
85
+
86
+ return result
87
+ }
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // Entity resolution
91
+ // ---------------------------------------------------------------------------
92
+
93
+ export function resolveEntityClass<K extends keyof CrossModuleEntities>(
94
+ ctx: ExecutionHelperContext,
95
+ key: K,
96
+ ): CrossModuleEntities[K] | null {
97
+ const fromEntities = ctx.entities?.[key]
98
+ if (fromEntities) return fromEntities
99
+ try { return ctx.container.resolve(key) } catch { return null }
100
+ }
101
+
102
+ // ---------------------------------------------------------------------------
103
+ // Source metadata
104
+ // ---------------------------------------------------------------------------
105
+
106
+ export function buildSourceMetadata(actionId: string, proposalId: string): Record<string, unknown> {
107
+ return {
108
+ source: 'inbox_ops',
109
+ inboxOpsActionId: actionId,
110
+ inboxOpsProposalId: proposalId,
111
+ }
112
+ }
113
+
114
+ // ---------------------------------------------------------------------------
115
+ // Order resolution
116
+ // ---------------------------------------------------------------------------
117
+
118
+ export async function resolveOrderByReference(
119
+ ctx: ExecutionHelperContext,
120
+ orderId?: string,
121
+ orderNumber?: string,
122
+ ): Promise<{ id: string; orderNumber: string; currencyCode: string; comments?: string | null }> {
123
+ const SalesOrderClass = resolveEntityClass(ctx, 'SalesOrder')
124
+ if (!SalesOrderClass) {
125
+ throw new ExecutionError('Sales module entities not available', 503)
126
+ }
127
+
128
+ const where: Record<string, unknown> = {
129
+ tenantId: ctx.tenantId,
130
+ organizationId: ctx.organizationId,
131
+ deletedAt: null,
132
+ }
133
+ if (orderId) {
134
+ where.id = orderId
135
+ } else if (orderNumber && orderNumber.trim().length > 0) {
136
+ where.orderNumber = orderNumber.trim()
137
+ } else {
138
+ throw new ExecutionError('Order reference is required', 400)
139
+ }
140
+
141
+ const order = await findOneWithDecryption(
142
+ ctx.em,
143
+ SalesOrderClass,
144
+ where,
145
+ undefined,
146
+ { tenantId: ctx.tenantId, organizationId: ctx.organizationId },
147
+ )
148
+ if (!order) {
149
+ throw new ExecutionError('Referenced order not found', 404)
150
+ }
151
+ return order
152
+ }
153
+
154
+ // ---------------------------------------------------------------------------
155
+ // Channel resolution
156
+ // ---------------------------------------------------------------------------
157
+
158
+ export async function resolveFirstChannelId(ctx: ExecutionHelperContext): Promise<string | null> {
159
+ const SalesChannelClass = resolveEntityClass(ctx, 'SalesChannel')
160
+ if (!SalesChannelClass) return null
161
+
162
+ try {
163
+ const channel = await findOneWithDecryption(
164
+ ctx.em,
165
+ SalesChannelClass,
166
+ {
167
+ tenantId: ctx.tenantId,
168
+ organizationId: ctx.organizationId,
169
+ deletedAt: null,
170
+ },
171
+ { orderBy: { name: 'ASC' } },
172
+ { tenantId: ctx.tenantId, organizationId: ctx.organizationId },
173
+ )
174
+ return channel?.id ?? null
175
+ } catch {
176
+ return null
177
+ }
178
+ }
179
+
180
+ export async function resolveChannelCurrency(
181
+ ctx: ExecutionHelperContext,
182
+ channelId: string | null,
183
+ ): Promise<string | null> {
184
+ const SalesChannelClass = resolveEntityClass(ctx, 'SalesChannel')
185
+ if (!SalesChannelClass) return null
186
+
187
+ try {
188
+ const where: Record<string, unknown> = {
189
+ tenantId: ctx.tenantId,
190
+ organizationId: ctx.organizationId,
191
+ deletedAt: null,
192
+ }
193
+ if (channelId) where.id = channelId
194
+ const channel = await findOneWithDecryption(
195
+ ctx.em,
196
+ SalesChannelClass,
197
+ where,
198
+ channelId ? undefined : { orderBy: { name: 'ASC' } },
199
+ { tenantId: ctx.tenantId, organizationId: ctx.organizationId },
200
+ )
201
+ return channel?.currencyCode ?? null
202
+ } catch {
203
+ return null
204
+ }
205
+ }
206
+
207
+ export async function resolveEffectiveDocumentKind(
208
+ ctx: ExecutionHelperContext,
209
+ channelId: string,
210
+ ): Promise<'order' | 'quote'> {
211
+ const SalesChannelClass = resolveEntityClass(ctx, 'SalesChannel')
212
+ if (!SalesChannelClass) return 'order'
213
+
214
+ const channel = await findOneWithDecryption(
215
+ ctx.em,
216
+ SalesChannelClass,
217
+ {
218
+ id: channelId,
219
+ tenantId: ctx.tenantId,
220
+ organizationId: ctx.organizationId,
221
+ deletedAt: null,
222
+ },
223
+ undefined,
224
+ { tenantId: ctx.tenantId, organizationId: ctx.organizationId },
225
+ )
226
+ if (!channel) return 'order'
227
+
228
+ const metadata = channel.metadata as Record<string, unknown> | null
229
+ if (metadata?.quotesRequired === true) {
230
+ return 'quote'
231
+ }
232
+ return 'order'
233
+ }
234
+
235
+ // ---------------------------------------------------------------------------
236
+ // Shipment status resolution
237
+ // ---------------------------------------------------------------------------
238
+
239
+ const SALES_SHIPMENT_STATUS_DICTIONARY_KEY = 'sales.shipment_status'
240
+
241
+ export async function resolveShipmentStatusEntryId(
242
+ ctx: ExecutionHelperContext,
243
+ statusLabel: string,
244
+ ): Promise<string | null> {
245
+ const DictionaryClass = resolveEntityClass(ctx, 'Dictionary')
246
+ const DictionaryEntryClass = resolveEntityClass(ctx, 'DictionaryEntry')
247
+ if (!DictionaryClass || !DictionaryEntryClass) return null
248
+
249
+ const encryptionScope = { tenantId: ctx.tenantId, organizationId: ctx.organizationId }
250
+
251
+ const dictionary = await findOneWithDecryption(
252
+ ctx.em,
253
+ DictionaryClass,
254
+ {
255
+ key: SALES_SHIPMENT_STATUS_DICTIONARY_KEY,
256
+ tenantId: ctx.tenantId,
257
+ organizationId: ctx.organizationId,
258
+ deletedAt: null,
259
+ },
260
+ undefined,
261
+ encryptionScope,
262
+ )
263
+ if (!dictionary) return null
264
+
265
+ const entries = await findWithDecryption(
266
+ ctx.em,
267
+ DictionaryEntryClass,
268
+ {
269
+ dictionary: dictionary.id,
270
+ tenantId: ctx.tenantId,
271
+ organizationId: ctx.organizationId,
272
+ },
273
+ undefined,
274
+ encryptionScope,
275
+ )
276
+ if (!entries.length) return null
277
+
278
+ const normalizedTarget = normalizeDictionaryToken(statusLabel)
279
+ const loweredTarget = statusLabel.trim().toLowerCase()
280
+
281
+ const match = entries.find((entry) => {
282
+ const label = entry.label.trim().toLowerCase()
283
+ const value = entry.value.trim().toLowerCase()
284
+ return (
285
+ entry.normalizedValue === normalizedTarget ||
286
+ label === loweredTarget ||
287
+ value === loweredTarget
288
+ )
289
+ })
290
+
291
+ return match?.id ?? null
292
+ }
293
+
294
+ // ---------------------------------------------------------------------------
295
+ // Customer / contact resolution
296
+ // ---------------------------------------------------------------------------
297
+
298
+ export async function resolveCustomerEntityIdByEmail(
299
+ ctx: ExecutionHelperContext,
300
+ email: string,
301
+ ): Promise<string | null> {
302
+ const normalized = email.trim().toLowerCase()
303
+ if (!normalized) return null
304
+
305
+ const CustomerEntityClass = resolveEntityClass(ctx, 'CustomerEntity')
306
+ if (!CustomerEntityClass) return null
307
+
308
+ const entity = await findOneWithDecryption(
309
+ ctx.em,
310
+ CustomerEntityClass,
311
+ {
312
+ primaryEmail: normalized,
313
+ tenantId: ctx.tenantId,
314
+ organizationId: ctx.organizationId,
315
+ deletedAt: null,
316
+ },
317
+ undefined,
318
+ { tenantId: ctx.tenantId, organizationId: ctx.organizationId },
319
+ )
320
+ if (entity) return entity.id
321
+
322
+ const candidates = await findWithDecryption(
323
+ ctx.em,
324
+ CustomerEntityClass,
325
+ {
326
+ tenantId: ctx.tenantId,
327
+ organizationId: ctx.organizationId,
328
+ deletedAt: null,
329
+ },
330
+ { limit: 100, orderBy: { createdAt: 'DESC' } },
331
+ { tenantId: ctx.tenantId, organizationId: ctx.organizationId },
332
+ )
333
+ const match = candidates.find(
334
+ (e) => e.primaryEmail && e.primaryEmail.toLowerCase() === normalized,
335
+ )
336
+ return match?.id ?? null
337
+ }
338
+
339
+ export async function resolveContactIdByNameAndType(
340
+ ctx: ExecutionHelperContext,
341
+ contactName: string,
342
+ contactType: string,
343
+ ): Promise<string | null> {
344
+ const CustomerEntityClass = resolveEntityClass(ctx, 'CustomerEntity')
345
+ if (!CustomerEntityClass) return null
346
+
347
+ const normalized = contactName.trim()
348
+ if (!normalized) return null
349
+
350
+ const entity = await findOneWithDecryption(
351
+ ctx.em,
352
+ CustomerEntityClass,
353
+ {
354
+ displayName: normalized,
355
+ kind: contactType,
356
+ tenantId: ctx.tenantId,
357
+ organizationId: ctx.organizationId,
358
+ deletedAt: null,
359
+ },
360
+ undefined,
361
+ { tenantId: ctx.tenantId, organizationId: ctx.organizationId },
362
+ )
363
+
364
+ return entity?.id ?? null
365
+ }
366
+
367
+ // ---------------------------------------------------------------------------
368
+ // Order line items
369
+ // ---------------------------------------------------------------------------
370
+
371
+ export interface OrderLineItem {
372
+ id: string
373
+ name?: string | null
374
+ }
375
+
376
+ export async function loadOrderLineItems(
377
+ ctx: ExecutionHelperContext,
378
+ orderId: string,
379
+ ): Promise<OrderLineItem[]> {
380
+ try {
381
+ const result = await executeCommand<Record<string, unknown>, { lines?: OrderLineItem[] }>(
382
+ ctx,
383
+ 'sales.orders.lines.list',
384
+ { orderId, organizationId: ctx.organizationId, tenantId: ctx.tenantId },
385
+ )
386
+ return result.lines ?? []
387
+ } catch {
388
+ return []
389
+ }
390
+ }
391
+
392
+ export function matchLineItemByName(
393
+ orderLines: OrderLineItem[],
394
+ lineItemName: string,
395
+ ): string | null {
396
+ const target = lineItemName.trim().toLowerCase()
397
+ if (!target) return null
398
+
399
+ const exact = orderLines.find((l) => (l.name || '').trim().toLowerCase() === target)
400
+ if (exact) return exact.id
401
+
402
+ const partial = orderLines.find((l) => {
403
+ const name = (l.name || '').trim().toLowerCase()
404
+ return name.includes(target) || target.includes(name)
405
+ })
406
+ return partial?.id ?? null
407
+ }
408
+
409
+ // ---------------------------------------------------------------------------
410
+ // Data normalization utilities
411
+ // ---------------------------------------------------------------------------
412
+
413
+ export function normalizeAddressSnapshot(
414
+ address: Record<string, unknown>,
415
+ ): Record<string, unknown> {
416
+ return {
417
+ addressLine1: address.line1 ?? address.addressLine1 ?? '',
418
+ addressLine2: address.line2 ?? address.addressLine2 ?? null,
419
+ companyName: address.company ?? address.companyName ?? null,
420
+ name: address.contactName ?? address.name ?? null,
421
+ city: address.city ?? null,
422
+ region: address.state ?? address.region ?? null,
423
+ postalCode: address.postalCode ?? null,
424
+ country: address.country ?? null,
425
+ }
426
+ }
427
+
428
+ export function parseDateToken(value?: string | null): Date | undefined {
429
+ if (!value) return undefined
430
+ const parsed = new Date(value)
431
+ if (Number.isNaN(parsed.getTime())) return undefined
432
+ return parsed
433
+ }
434
+
435
+ export function parseNumberToken(value: string, fieldName: string): number {
436
+ const parsed = Number(value)
437
+ if (!Number.isFinite(parsed)) {
438
+ throw new ExecutionError(`Invalid numeric value for ${fieldName}`, 400)
439
+ }
440
+ return parsed
441
+ }
442
+
443
+ export function normalizeDictionaryToken(value: string): string {
444
+ return value.trim().toLowerCase().replace(/[\s-]+/g, '_')
445
+ }
446
+
447
+ // ---------------------------------------------------------------------------
448
+ // Product discrepancy resolution (used by catalog inbox action handler)
449
+ // ---------------------------------------------------------------------------
450
+
451
+ export async function resolveProductDiscrepanciesInProposal(
452
+ em: EntityManager,
453
+ proposalId: string,
454
+ productTitle: string,
455
+ productId: string,
456
+ scope: { tenantId: string; organizationId: string },
457
+ ): Promise<void> {
458
+ const { InboxDiscrepancy, InboxProposalAction } = await import('../data/entities')
459
+
460
+ const discrepancies = await findWithDecryption(
461
+ em,
462
+ InboxDiscrepancy,
463
+ {
464
+ proposalId,
465
+ type: 'product_not_found',
466
+ resolved: false,
467
+ tenantId: scope.tenantId,
468
+ organizationId: scope.organizationId,
469
+ },
470
+ undefined,
471
+ scope,
472
+ )
473
+
474
+ const normalizedTitle = productTitle.toLowerCase().trim()
475
+ const matchingDiscrepancies = discrepancies.filter((d) => {
476
+ const foundValue = (d.foundValue || '').toLowerCase().trim()
477
+ return foundValue === normalizedTitle
478
+ })
479
+
480
+ if (matchingDiscrepancies.length === 0) return
481
+
482
+ // Phase 1: flush scalar mutations before any queries to avoid UoW tracking loss (SPEC-018)
483
+ for (const discrepancy of matchingDiscrepancies) {
484
+ discrepancy.resolved = true
485
+ }
486
+ await em.flush()
487
+
488
+ // Phase 2: update line item product IDs (involves findOneWithDecryption queries)
489
+ const actionIds = matchingDiscrepancies
490
+ .map((d) => d.actionId)
491
+ .filter((id): id is string => !!id)
492
+
493
+ for (const actionId of actionIds) {
494
+ const action = await findOneWithDecryption(
495
+ em,
496
+ InboxProposalAction,
497
+ { id: actionId, deletedAt: null },
498
+ undefined,
499
+ scope,
500
+ )
501
+ if (!action) continue
502
+
503
+ const payload = action.payload as Record<string, unknown>
504
+ const lineItems = Array.isArray(payload?.lineItems)
505
+ ? (payload.lineItems as Record<string, unknown>[])
506
+ : []
507
+
508
+ let updated = false
509
+ for (const item of lineItems) {
510
+ if (item.productId) continue
511
+ const itemName = (typeof item.productName === 'string' ? item.productName : '').toLowerCase().trim()
512
+ if (itemName === normalizedTitle) {
513
+ item.productId = productId
514
+ updated = true
515
+ break
516
+ }
517
+ }
518
+
519
+ if (updated) {
520
+ action.payload = { ...payload, lineItems }
521
+ }
522
+ }
523
+
524
+ if (actionIds.length > 0) {
525
+ await em.flush()
526
+ }
527
+ }
@@ -1,14 +1,52 @@
1
1
  import type { ContactMatchResult } from './contactMatcher'
2
- import { REQUIRED_FEATURES_MAP } from './constants'
2
+ import type { InboxActionDefinition } from '@open-mercato/shared/modules/inbox-actions'
3
3
 
4
4
  const LANGUAGE_NAMES: Record<string, string> = { en: 'English', de: 'German', es: 'Spanish', pl: 'Polish' }
5
5
 
6
- export function buildExtractionSystemPrompt(
6
+ /**
7
+ * Lazily load registered inbox action definitions from the generated registry.
8
+ * Uses dynamic import to avoid circular dependencies at module load time.
9
+ */
10
+ async function loadRegisteredActions(): Promise<InboxActionDefinition[]> {
11
+ try {
12
+ const registry = await import('@/.mercato/generated/inbox-actions.generated')
13
+ return registry.inboxActions ?? []
14
+ } catch {
15
+ return []
16
+ }
17
+ }
18
+
19
+ function buildFeaturesSection(actions: InboxActionDefinition[]): string {
20
+ return actions
21
+ .map((a) => `- ${a.type} (requires: ${a.requiredFeature})`)
22
+ .join('\n')
23
+ }
24
+
25
+ function buildPayloadSchemasSection(actions: InboxActionDefinition[]): string {
26
+ return actions
27
+ .filter((a) => a.promptSchema && a.promptSchema !== '(shared with create_order)' && a.promptSchema !== '(shared with create_order above)')
28
+ .map((a) => a.promptSchema)
29
+ .join('\n\n')
30
+ }
31
+
32
+ function buildActionRulesSection(actions: InboxActionDefinition[]): string {
33
+ const rules = actions.flatMap((a) => a.promptRules ?? [])
34
+ return rules.map((r) => `- ${r}`).join('\n')
35
+ }
36
+
37
+ export async function buildExtractionSystemPrompt(
7
38
  matchedContacts: ContactMatchResult[],
8
39
  catalogProducts: { id: string; name: string; sku?: string; price?: string }[],
9
40
  channelId?: string,
10
41
  workingLanguage?: string,
11
- ): string {
42
+ registeredActions?: InboxActionDefinition[],
43
+ ): Promise<string> {
44
+ const actions = registeredActions ?? await loadRegisteredActions()
45
+
46
+ const featuresSection = buildFeaturesSection(actions)
47
+ const payloadSchemasSection = buildPayloadSchemasSection(actions)
48
+ const actionRulesSection = buildActionRulesSection(actions)
49
+
12
50
  const contactsSection = matchedContacts.length > 0
13
51
  ? `\nPre-matched contacts from CRM:\n${JSON.stringify(
14
52
  matchedContacts.map((match) => ({
@@ -36,7 +74,7 @@ You are an email-to-ERP extraction agent.
36
74
  </role>
37
75
 
38
76
  <required_features>
39
- ${Object.entries(REQUIRED_FEATURES_MAP).map(([actionType, feature]) => `- ${actionType} (requires: ${feature})`).join('\n')}
77
+ ${featuresSection}
40
78
  </required_features>
41
79
 
42
80
  <safety>
@@ -46,41 +84,13 @@ ${Object.entries(REQUIRED_FEATURES_MAP).map(([actionType, feature]) => `- ${acti
46
84
  </safety>
47
85
 
48
86
  <payload_schemas>
49
- create_order / create_quote payload:
50
- { customerName: string, customerEmail?: string, customerEntityId?: uuid, channelId?: uuid, currencyCode: string (3-letter ISO), taxRateId?: uuid, lineItems: [{ productName: string (REQUIRED), productId?: uuid, variantId?: uuid, sku?: string, quantity: string, unitPrice?: string, kind?: "product"|"service", description?: string }], requestedDeliveryDate?: string, notes?: string, customerReference?: string (customer's own PO number or reference code — only set if explicitly stated in the email, do NOT use the email subject), shippingAddress?: { line1?: string, line2?: string, city?: string, state?: string, postalCode?: string, country?: string, company?: string, contactName?: string }, billingAddress?: { line1?: string, line2?: string, city?: string, state?: string, postalCode?: string, country?: string, company?: string, contactName?: string } }
51
-
52
- create_contact payload:
53
- { type: "person"|"company", name: string, email?: string, phone?: string, companyName?: string, role?: string, source: "inbox_ops" }
54
-
55
- create_product payload:
56
- { title: string, sku?: string, unitPrice?: string, currencyCode?: string (3-letter ISO), kind?: "product"|"service", description?: string }
57
-
58
- link_contact payload:
59
- { emailAddress: string (email), contactId: uuid, contactType: "person"|"company", contactName: string }
60
-
61
- update_order payload:
62
- { orderId?: uuid, orderNumber?: string, quantityChanges?: [{ lineItemName: string, lineItemId?: uuid, oldQuantity?: string, newQuantity: string }], deliveryDateChange?: { oldDate?: string, newDate: string }, noteAdditions?: string[] }
63
-
64
- update_shipment payload:
65
- { orderId?: uuid, orderNumber?: string, trackingNumbers?: string[], carrierName?: string, statusLabel: string, shippedAt?: string, deliveredAt?: string, estimatedDelivery?: string, notes?: string }
66
-
67
- log_activity payload:
68
- { contactId?: uuid, contactType: "person"|"company", contactName: string, activityType: "email"|"call"|"meeting"|"note", subject: string, body: string }
69
-
70
- draft_reply payload:
71
- { to: string (email), toName?: string, subject: string, body: string, context?: string }
87
+ ${payloadSchemasSection}
72
88
  </payload_schemas>
73
89
 
74
90
  <rules>
75
91
  - Extract only details explicitly stated or strongly implied in the thread.
76
92
  - Do not fabricate values; omit values that are not present.
77
- - ALWAYS propose a create_order or create_quote action when the customer expresses interest in buying, even if some product names are uncertain or not in the catalog. Use the best product name available; the system will flag unmatched products as discrepancies. Do NOT replace an order with a draft_reply asking for clarification — propose both if needed.
78
- - Use create_order when the customer has clearly confirmed they want to proceed (e.g., "let's go ahead", "please process", "confirmed"). Use create_quote when the customer is still inquiring, requesting pricing, asking for a proposal, or negotiating (e.g., "could you send a quote", "what would it cost", "we're interested in", "can you offer"). When in doubt, prefer create_quote.
79
- - For create_order / create_quote: each line item MUST have "productName" (the product name goes here, NOT in "description"). Include currencyCode and customerName.
80
- - For update_shipment: use statusLabel text only.
81
- - For create_order / create_quote: extract shippingAddress and billingAddress as structured objects when addresses are mentioned. Parse street, city, postal code, country from the text. Do NOT put address data in notes.
82
- - For create_contact: always include email when available from the thread. Set source to "inbox_ops", type must be lowercase "person" or "company".
83
- - For draft_reply: include ERP context when available.
93
+ ${actionRulesSection}
84
94
  - Set requiredFeature on each action from the mapping above.
85
95
  - Set confidence in [0.0, 1.0].
86
96
  - Write summary and all action descriptions in ${LANGUAGE_NAMES[workingLanguage || 'en'] || 'English'} even if the original thread is in another language.
@@ -110,4 +120,5 @@ ${cleanedText}
110
120
  </output_requirements>`
111
121
  }
112
122
 
123
+ /** @deprecated Use the generated inbox action registry instead */
113
124
  export { REQUIRED_FEATURES_MAP } from './constants'
@@ -0,0 +1,11 @@
1
+ declare module '@/.mercato/generated/inbox-actions.generated' {
2
+ import type { InboxActionDefinition } from '@open-mercato/shared/modules/inbox-actions'
3
+
4
+ export const inboxActionConfigEntries: Array<{
5
+ moduleId: string
6
+ actions: InboxActionDefinition[]
7
+ }>
8
+ export const inboxActions: InboxActionDefinition[]
9
+ export function getInboxAction(type: string): InboxActionDefinition | undefined
10
+ export function getRegisteredActionTypes(): string[]
11
+ }