@open-mercato/core 0.6.5-develop.4384.1.ce2ec6eaaa → 0.6.5-develop.4393.1.de282b5dfd

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 (533) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/generated/entities/channel_ingest_dead_letter/index.js +25 -0
  3. package/dist/generated/entities/channel_ingest_dead_letter/index.js.map +7 -0
  4. package/dist/generated/entities/channel_thread_mapping/index.js +25 -0
  5. package/dist/generated/entities/channel_thread_mapping/index.js.map +7 -0
  6. package/dist/generated/entities/channel_thread_token/index.js +17 -0
  7. package/dist/generated/entities/channel_thread_token/index.js.map +7 -0
  8. package/dist/generated/entities/communication_channel/index.js +43 -0
  9. package/dist/generated/entities/communication_channel/index.js.map +7 -0
  10. package/dist/generated/entities/customer_interaction/index.js +4 -0
  11. package/dist/generated/entities/customer_interaction/index.js.map +2 -2
  12. package/dist/generated/entities/external_conversation/index.js +25 -0
  13. package/dist/generated/entities/external_conversation/index.js.map +7 -0
  14. package/dist/generated/entities/external_message/index.js +25 -0
  15. package/dist/generated/entities/external_message/index.js.map +7 -0
  16. package/dist/generated/entities/integration_credentials/index.js +3 -1
  17. package/dist/generated/entities/integration_credentials/index.js.map +2 -2
  18. package/dist/generated/entities/message/index.js +2 -0
  19. package/dist/generated/entities/message/index.js.map +2 -2
  20. package/dist/generated/entities/message_channel_link/index.js +33 -0
  21. package/dist/generated/entities/message_channel_link/index.js.map +7 -0
  22. package/dist/generated/entities/message_reaction/index.js +25 -0
  23. package/dist/generated/entities/message_reaction/index.js.map +7 -0
  24. package/dist/generated/entities.ids.generated.js +11 -0
  25. package/dist/generated/entities.ids.generated.js.map +2 -2
  26. package/dist/generated/entity-fields-registry.js +117 -0
  27. package/dist/generated/entity-fields-registry.js.map +2 -2
  28. package/dist/helpers/integration/authFixtures.js +2 -1
  29. package/dist/helpers/integration/authFixtures.js.map +2 -2
  30. package/dist/helpers/integration/communicationChannelsFixtures.js +58 -0
  31. package/dist/helpers/integration/communicationChannelsFixtures.js.map +7 -0
  32. package/dist/modules/communication_channels/acl.js +47 -0
  33. package/dist/modules/communication_channels/acl.js.map +7 -0
  34. package/dist/modules/communication_channels/api/delete/channels/[id]/route.js +133 -0
  35. package/dist/modules/communication_channels/api/delete/channels/[id]/route.js.map +7 -0
  36. package/dist/modules/communication_channels/api/delete/messages/[messageId]/reactions/[reactionId]/route.js +113 -0
  37. package/dist/modules/communication_channels/api/delete/messages/[messageId]/reactions/[reactionId]/route.js.map +7 -0
  38. package/dist/modules/communication_channels/api/get/channels/[id]/health/route.js +138 -0
  39. package/dist/modules/communication_channels/api/get/channels/[id]/health/route.js.map +7 -0
  40. package/dist/modules/communication_channels/api/get/channels/[id]/route.js +93 -0
  41. package/dist/modules/communication_channels/api/get/channels/[id]/route.js.map +7 -0
  42. package/dist/modules/communication_channels/api/get/channels/route.js +96 -0
  43. package/dist/modules/communication_channels/api/get/channels/route.js.map +7 -0
  44. package/dist/modules/communication_channels/api/get/me/channels/route.js +82 -0
  45. package/dist/modules/communication_channels/api/get/me/channels/route.js.map +7 -0
  46. package/dist/modules/communication_channels/api/get/oauth/[provider]/callback/route.js +274 -0
  47. package/dist/modules/communication_channels/api/get/oauth/[provider]/callback/route.js.map +7 -0
  48. package/dist/modules/communication_channels/api/post/channels/[id]/import-history/route.js +168 -0
  49. package/dist/modules/communication_channels/api/post/channels/[id]/import-history/route.js.map +7 -0
  50. package/dist/modules/communication_channels/api/post/channels/[id]/poll-now/route.js +143 -0
  51. package/dist/modules/communication_channels/api/post/channels/[id]/poll-now/route.js.map +7 -0
  52. package/dist/modules/communication_channels/api/post/channels/[id]/push/register/route.js +127 -0
  53. package/dist/modules/communication_channels/api/post/channels/[id]/push/register/route.js.map +7 -0
  54. package/dist/modules/communication_channels/api/post/channels/[id]/set-primary/route.js +99 -0
  55. package/dist/modules/communication_channels/api/post/channels/[id]/set-primary/route.js.map +7 -0
  56. package/dist/modules/communication_channels/api/post/channels/[id]/test-send/route.js +197 -0
  57. package/dist/modules/communication_channels/api/post/channels/[id]/test-send/route.js.map +7 -0
  58. package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js +124 -0
  59. package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js.map +7 -0
  60. package/dist/modules/communication_channels/api/post/messages/[messageId]/reactions/route.js +120 -0
  61. package/dist/modules/communication_channels/api/post/messages/[messageId]/reactions/route.js.map +7 -0
  62. package/dist/modules/communication_channels/api/post/oauth/[provider]/initiate/route.js +157 -0
  63. package/dist/modules/communication_channels/api/post/oauth/[provider]/initiate/route.js.map +7 -0
  64. package/dist/modules/communication_channels/api/post/send-as-user/route.js +115 -0
  65. package/dist/modules/communication_channels/api/post/send-as-user/route.js.map +7 -0
  66. package/dist/modules/communication_channels/api/post/test-seed/route.js +217 -0
  67. package/dist/modules/communication_channels/api/post/test-seed/route.js.map +7 -0
  68. package/dist/modules/communication_channels/api/post/webhook/[provider]/route.js +175 -0
  69. package/dist/modules/communication_channels/api/post/webhook/[provider]/route.js.map +7 -0
  70. package/dist/modules/communication_channels/api/post/webhooks/gmail/route.js +123 -0
  71. package/dist/modules/communication_channels/api/post/webhooks/gmail/route.js.map +7 -0
  72. package/dist/modules/communication_channels/api/put/threads/[threadId]/assign/route.js +117 -0
  73. package/dist/modules/communication_channels/api/put/threads/[threadId]/assign/route.js.map +7 -0
  74. package/dist/modules/communication_channels/backend/communication_channels/channels/[id]/page.js +180 -0
  75. package/dist/modules/communication_channels/backend/communication_channels/channels/[id]/page.js.map +7 -0
  76. package/dist/modules/communication_channels/backend/communication_channels/channels/[id]/page.meta.js +36 -0
  77. package/dist/modules/communication_channels/backend/communication_channels/channels/[id]/page.meta.js.map +7 -0
  78. package/dist/modules/communication_channels/backend/communication_channels/channels/page.js +107 -0
  79. package/dist/modules/communication_channels/backend/communication_channels/channels/page.js.map +7 -0
  80. package/dist/modules/communication_channels/backend/communication_channels/channels/page.meta.js +38 -0
  81. package/dist/modules/communication_channels/backend/communication_channels/channels/page.meta.js.map +7 -0
  82. package/dist/modules/communication_channels/backend/profile/communication-channels/page.js +727 -0
  83. package/dist/modules/communication_channels/backend/profile/communication-channels/page.js.map +7 -0
  84. package/dist/modules/communication_channels/backend/profile/communication-channels/page.meta.js +38 -0
  85. package/dist/modules/communication_channels/backend/profile/communication-channels/page.meta.js.map +7 -0
  86. package/dist/modules/communication_channels/commands/connect-credential-channel.js +154 -0
  87. package/dist/modules/communication_channels/commands/connect-credential-channel.js.map +7 -0
  88. package/dist/modules/communication_channels/commands/delete-channel.js +137 -0
  89. package/dist/modules/communication_channels/commands/delete-channel.js.map +7 -0
  90. package/dist/modules/communication_channels/commands/deliver-outbound-message.js +400 -0
  91. package/dist/modules/communication_channels/commands/deliver-outbound-message.js.map +7 -0
  92. package/dist/modules/communication_channels/commands/disconnect-channel.js +163 -0
  93. package/dist/modules/communication_channels/commands/disconnect-channel.js.map +7 -0
  94. package/dist/modules/communication_channels/commands/ingest-inbound-message.js +413 -0
  95. package/dist/modules/communication_channels/commands/ingest-inbound-message.js.map +7 -0
  96. package/dist/modules/communication_channels/commands/interceptors.js +68 -0
  97. package/dist/modules/communication_channels/commands/interceptors.js.map +7 -0
  98. package/dist/modules/communication_channels/commands/process-inbound-reaction.js +198 -0
  99. package/dist/modules/communication_channels/commands/process-inbound-reaction.js.map +7 -0
  100. package/dist/modules/communication_channels/commands/push-register.js +146 -0
  101. package/dist/modules/communication_channels/commands/push-register.js.map +7 -0
  102. package/dist/modules/communication_channels/commands/push-renew.js +23 -0
  103. package/dist/modules/communication_channels/commands/push-renew.js.map +7 -0
  104. package/dist/modules/communication_channels/commands/push-unregister.js +108 -0
  105. package/dist/modules/communication_channels/commands/push-unregister.js.map +7 -0
  106. package/dist/modules/communication_channels/commands/queue-import-history.js +113 -0
  107. package/dist/modules/communication_channels/commands/queue-import-history.js.map +7 -0
  108. package/dist/modules/communication_channels/commands/reassign-conversation.js +193 -0
  109. package/dist/modules/communication_channels/commands/reassign-conversation.js.map +7 -0
  110. package/dist/modules/communication_channels/commands/set-primary-channel.js +114 -0
  111. package/dist/modules/communication_channels/commands/set-primary-channel.js.map +7 -0
  112. package/dist/modules/communication_channels/commands/toggle-outbound-reaction.js +260 -0
  113. package/dist/modules/communication_channels/commands/toggle-outbound-reaction.js.map +7 -0
  114. package/dist/modules/communication_channels/data/enrichers.js +286 -0
  115. package/dist/modules/communication_channels/data/enrichers.js.map +7 -0
  116. package/dist/modules/communication_channels/data/entities.js +447 -0
  117. package/dist/modules/communication_channels/data/entities.js.map +7 -0
  118. package/dist/modules/communication_channels/data/extensions.js +67 -0
  119. package/dist/modules/communication_channels/data/extensions.js.map +7 -0
  120. package/dist/modules/communication_channels/data/validators.js +123 -0
  121. package/dist/modules/communication_channels/data/validators.js.map +7 -0
  122. package/dist/modules/communication_channels/di.js +35 -0
  123. package/dist/modules/communication_channels/di.js.map +7 -0
  124. package/dist/modules/communication_channels/encryption.js +12 -0
  125. package/dist/modules/communication_channels/encryption.js.map +7 -0
  126. package/dist/modules/communication_channels/events.js +124 -0
  127. package/dist/modules/communication_channels/events.js.map +7 -0
  128. package/dist/modules/communication_channels/index.js +20 -0
  129. package/dist/modules/communication_channels/index.js.map +7 -0
  130. package/dist/modules/communication_channels/lib/access-control.js +43 -0
  131. package/dist/modules/communication_channels/lib/access-control.js.map +7 -0
  132. package/dist/modules/communication_channels/lib/adapter-compat.js +36 -0
  133. package/dist/modules/communication_channels/lib/adapter-compat.js.map +7 -0
  134. package/dist/modules/communication_channels/lib/adapter-registry-singleton.js +22 -0
  135. package/dist/modules/communication_channels/lib/adapter-registry-singleton.js.map +7 -0
  136. package/dist/modules/communication_channels/lib/adapter.js +1 -0
  137. package/dist/modules/communication_channels/lib/adapter.js.map +7 -0
  138. package/dist/modules/communication_channels/lib/connect-channel.js +95 -0
  139. package/dist/modules/communication_channels/lib/connect-channel.js.map +7 -0
  140. package/dist/modules/communication_channels/lib/contact-resolver.js +79 -0
  141. package/dist/modules/communication_channels/lib/contact-resolver.js.map +7 -0
  142. package/dist/modules/communication_channels/lib/credential-refresh.js +97 -0
  143. package/dist/modules/communication_channels/lib/credential-refresh.js.map +7 -0
  144. package/dist/modules/communication_channels/lib/dead-letter.js +62 -0
  145. package/dist/modules/communication_channels/lib/dead-letter.js.map +7 -0
  146. package/dist/modules/communication_channels/lib/email-capabilities.js +47 -0
  147. package/dist/modules/communication_channels/lib/email-capabilities.js.map +7 -0
  148. package/dist/modules/communication_channels/lib/email-contact.js +14 -0
  149. package/dist/modules/communication_channels/lib/email-contact.js.map +7 -0
  150. package/dist/modules/communication_channels/lib/email-mime.js +259 -0
  151. package/dist/modules/communication_channels/lib/email-mime.js.map +7 -0
  152. package/dist/modules/communication_channels/lib/error-classification.js +101 -0
  153. package/dist/modules/communication_channels/lib/error-classification.js.map +7 -0
  154. package/dist/modules/communication_channels/lib/gmail-pubsub-jwt.js +185 -0
  155. package/dist/modules/communication_channels/lib/gmail-pubsub-jwt.js.map +7 -0
  156. package/dist/modules/communication_channels/lib/mutation-guards.js +114 -0
  157. package/dist/modules/communication_channels/lib/mutation-guards.js.map +7 -0
  158. package/dist/modules/communication_channels/lib/oauth-client-config.js +32 -0
  159. package/dist/modules/communication_channels/lib/oauth-client-config.js.map +7 -0
  160. package/dist/modules/communication_channels/lib/oauth-state.js +128 -0
  161. package/dist/modules/communication_channels/lib/oauth-state.js.map +7 -0
  162. package/dist/modules/communication_channels/lib/oauth-token.js +45 -0
  163. package/dist/modules/communication_channels/lib/oauth-token.js.map +7 -0
  164. package/dist/modules/communication_channels/lib/pg-errors.js +11 -0
  165. package/dist/modules/communication_channels/lib/pg-errors.js.map +7 -0
  166. package/dist/modules/communication_channels/lib/provider-health.js +24 -0
  167. package/dist/modules/communication_channels/lib/provider-health.js.map +7 -0
  168. package/dist/modules/communication_channels/lib/push-state.js +19 -0
  169. package/dist/modules/communication_channels/lib/push-state.js.map +7 -0
  170. package/dist/modules/communication_channels/lib/queue.js +54 -0
  171. package/dist/modules/communication_channels/lib/queue.js.map +7 -0
  172. package/dist/modules/communication_channels/lib/reaction-processor-types.js +5 -0
  173. package/dist/modules/communication_channels/lib/reaction-processor-types.js.map +7 -0
  174. package/dist/modules/communication_channels/lib/reaction-semantics.js +11 -0
  175. package/dist/modules/communication_channels/lib/reaction-semantics.js.map +7 -0
  176. package/dist/modules/communication_channels/lib/registry.js +67 -0
  177. package/dist/modules/communication_channels/lib/registry.js.map +7 -0
  178. package/dist/modules/communication_channels/lib/route-mutation-guard.js +43 -0
  179. package/dist/modules/communication_channels/lib/route-mutation-guard.js.map +7 -0
  180. package/dist/modules/communication_channels/lib/sanitize-channel-html.js +96 -0
  181. package/dist/modules/communication_channels/lib/sanitize-channel-html.js.map +7 -0
  182. package/dist/modules/communication_channels/lib/send-as-user.js +194 -0
  183. package/dist/modules/communication_channels/lib/send-as-user.js.map +7 -0
  184. package/dist/modules/communication_channels/lib/system-user.js +22 -0
  185. package/dist/modules/communication_channels/lib/system-user.js.map +7 -0
  186. package/dist/modules/communication_channels/lib/test-seed.js +68 -0
  187. package/dist/modules/communication_channels/lib/test-seed.js.map +7 -0
  188. package/dist/modules/communication_channels/lib/thread-matcher.js +263 -0
  189. package/dist/modules/communication_channels/lib/thread-matcher.js.map +7 -0
  190. package/dist/modules/communication_channels/lib/thread-token.js +219 -0
  191. package/dist/modules/communication_channels/lib/thread-token.js.map +7 -0
  192. package/dist/modules/communication_channels/lib/use-connect-channel.js +61 -0
  193. package/dist/modules/communication_channels/lib/use-connect-channel.js.map +7 -0
  194. package/dist/modules/communication_channels/migrations/Migration20260526134719_communication_channels.js +50 -0
  195. package/dist/modules/communication_channels/migrations/Migration20260526134719_communication_channels.js.map +7 -0
  196. package/dist/modules/communication_channels/migrations/Migration20260527195446_communication_channels.js +19 -0
  197. package/dist/modules/communication_channels/migrations/Migration20260527195446_communication_channels.js.map +7 -0
  198. package/dist/modules/communication_channels/migrations/Migration20260529231848_communication_channels.js +13 -0
  199. package/dist/modules/communication_channels/migrations/Migration20260529231848_communication_channels.js.map +7 -0
  200. package/dist/modules/communication_channels/migrations/Migration20260531120000_communication_channels.js +17 -0
  201. package/dist/modules/communication_channels/migrations/Migration20260531120000_communication_channels.js.map +7 -0
  202. package/dist/modules/communication_channels/notifications.client.js +51 -0
  203. package/dist/modules/communication_channels/notifications.client.js.map +7 -0
  204. package/dist/modules/communication_channels/notifications.handlers.js +53 -0
  205. package/dist/modules/communication_channels/notifications.handlers.js.map +7 -0
  206. package/dist/modules/communication_channels/notifications.js +56 -0
  207. package/dist/modules/communication_channels/notifications.js.map +7 -0
  208. package/dist/modules/communication_channels/setup.js +105 -0
  209. package/dist/modules/communication_channels/setup.js.map +7 -0
  210. package/dist/modules/communication_channels/subscribers/channel-requires-reauth-notification.js +71 -0
  211. package/dist/modules/communication_channels/subscribers/channel-requires-reauth-notification.js.map +7 -0
  212. package/dist/modules/communication_channels/subscribers/outbound-bridge.js +103 -0
  213. package/dist/modules/communication_channels/subscribers/outbound-bridge.js.map +7 -0
  214. package/dist/modules/communication_channels/subscribers/user-deleted-cascade.js +51 -0
  215. package/dist/modules/communication_channels/subscribers/user-deleted-cascade.js.map +7 -0
  216. package/dist/modules/communication_channels/widgets/components.js +7 -0
  217. package/dist/modules/communication_channels/widgets/components.js.map +7 -0
  218. package/dist/modules/communication_channels/widgets/injection/channel-badge/widget.client.js +18 -0
  219. package/dist/modules/communication_channels/widgets/injection/channel-badge/widget.client.js.map +7 -0
  220. package/dist/modules/communication_channels/widgets/injection/channel-badge/widget.js +30 -0
  221. package/dist/modules/communication_channels/widgets/injection/channel-badge/widget.js.map +7 -0
  222. package/dist/modules/communication_channels/widgets/injection/channel-info-panel/widget.client.js +185 -0
  223. package/dist/modules/communication_channels/widgets/injection/channel-info-panel/widget.client.js.map +7 -0
  224. package/dist/modules/communication_channels/widgets/injection/channel-info-panel/widget.js +17 -0
  225. package/dist/modules/communication_channels/widgets/injection/channel-info-panel/widget.js.map +7 -0
  226. package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.js +44 -0
  227. package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.js.map +7 -0
  228. package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.js +17 -0
  229. package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.js.map +7 -0
  230. package/dist/modules/communication_channels/widgets/injection/profile-channels-menu/widget.js +23 -0
  231. package/dist/modules/communication_channels/widgets/injection/profile-channels-menu/widget.js.map +7 -0
  232. package/dist/modules/communication_channels/widgets/injection/reaction-bar/widget.client.js +141 -0
  233. package/dist/modules/communication_channels/widgets/injection/reaction-bar/widget.client.js.map +7 -0
  234. package/dist/modules/communication_channels/widgets/injection/reaction-bar/widget.js +17 -0
  235. package/dist/modules/communication_channels/widgets/injection/reaction-bar/widget.js.map +7 -0
  236. package/dist/modules/communication_channels/widgets/injection-table.js +38 -0
  237. package/dist/modules/communication_channels/widgets/injection-table.js.map +7 -0
  238. package/dist/modules/communication_channels/widgets/notifications/ChannelRequiresReauthRenderer.js +25 -0
  239. package/dist/modules/communication_channels/widgets/notifications/ChannelRequiresReauthRenderer.js.map +7 -0
  240. package/dist/modules/communication_channels/widgets/notifications/MessageReceivedRenderer.js +19 -0
  241. package/dist/modules/communication_channels/widgets/notifications/MessageReceivedRenderer.js.map +7 -0
  242. package/dist/modules/communication_channels/widgets/notifications/index.js +7 -0
  243. package/dist/modules/communication_channels/widgets/notifications/index.js.map +7 -0
  244. package/dist/modules/communication_channels/workers/channel-import-history.js +185 -0
  245. package/dist/modules/communication_channels/workers/channel-import-history.js.map +7 -0
  246. package/dist/modules/communication_channels/workers/gmail-history-sync.js +154 -0
  247. package/dist/modules/communication_channels/workers/gmail-history-sync.js.map +7 -0
  248. package/dist/modules/communication_channels/workers/gmail-renew-watch.js +95 -0
  249. package/dist/modules/communication_channels/workers/gmail-renew-watch.js.map +7 -0
  250. package/dist/modules/communication_channels/workers/inbound-processor.js +56 -0
  251. package/dist/modules/communication_channels/workers/inbound-processor.js.map +7 -0
  252. package/dist/modules/communication_channels/workers/outbound-delivery.js +85 -0
  253. package/dist/modules/communication_channels/workers/outbound-delivery.js.map +7 -0
  254. package/dist/modules/communication_channels/workers/poll-channel.js +240 -0
  255. package/dist/modules/communication_channels/workers/poll-channel.js.map +7 -0
  256. package/dist/modules/communication_channels/workers/poll-tick.js +132 -0
  257. package/dist/modules/communication_channels/workers/poll-tick.js.map +7 -0
  258. package/dist/modules/communication_channels/workers/reaction-processor.js +192 -0
  259. package/dist/modules/communication_channels/workers/reaction-processor.js.map +7 -0
  260. package/dist/modules/customers/acl.js +18 -0
  261. package/dist/modules/customers/acl.js.map +2 -2
  262. package/dist/modules/customers/api/activities/route.js +9 -0
  263. package/dist/modules/customers/api/activities/route.js.map +2 -2
  264. package/dist/modules/customers/api/companies/[id]/route.js +18 -7
  265. package/dist/modules/customers/api/companies/[id]/route.js.map +2 -2
  266. package/dist/modules/customers/api/interactions/[id]/visibility/route.js +151 -0
  267. package/dist/modules/customers/api/interactions/[id]/visibility/route.js.map +7 -0
  268. package/dist/modules/customers/api/interactions/counts/route.js +6 -0
  269. package/dist/modules/customers/api/interactions/counts/route.js.map +2 -2
  270. package/dist/modules/customers/api/interactions/route.js +26 -7
  271. package/dist/modules/customers/api/interactions/route.js.map +2 -2
  272. package/dist/modules/customers/api/people/[id]/email-threads/route.js +82 -0
  273. package/dist/modules/customers/api/people/[id]/email-threads/route.js.map +7 -0
  274. package/dist/modules/customers/api/people/[id]/emails/route.js +157 -0
  275. package/dist/modules/customers/api/people/[id]/emails/route.js.map +7 -0
  276. package/dist/modules/customers/api/people/[id]/route.js +12 -4
  277. package/dist/modules/customers/api/people/[id]/route.js.map +2 -2
  278. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +10 -0
  279. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
  280. package/dist/modules/customers/commands/deals.js +46 -5
  281. package/dist/modules/customers/commands/deals.js.map +2 -2
  282. package/dist/modules/customers/commands/interactions.js +16 -0
  283. package/dist/modules/customers/commands/interactions.js.map +2 -2
  284. package/dist/modules/customers/components/detail/ActivityCard.js +32 -0
  285. package/dist/modules/customers/components/detail/ActivityCard.js.map +2 -2
  286. package/dist/modules/customers/components/detail/ComposeEmailDialog.js +242 -0
  287. package/dist/modules/customers/components/detail/ComposeEmailDialog.js.map +7 -0
  288. package/dist/modules/customers/components/detail/DealForm.js +2 -1
  289. package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
  290. package/dist/modules/customers/components/detail/DealsSection.js +10 -0
  291. package/dist/modules/customers/components/detail/DealsSection.js.map +2 -2
  292. package/dist/modules/customers/components/detail/EmailCardActions.js +179 -0
  293. package/dist/modules/customers/components/detail/EmailCardActions.js.map +7 -0
  294. package/dist/modules/customers/components/detail/EmailReplyForwardActions.js +52 -0
  295. package/dist/modules/customers/components/detail/EmailReplyForwardActions.js.map +7 -0
  296. package/dist/modules/customers/components/detail/PersonDetailTabs.js +7 -1
  297. package/dist/modules/customers/components/detail/PersonDetailTabs.js.map +2 -2
  298. package/dist/modules/customers/components/detail/PersonEmailThreadsTab.js +366 -0
  299. package/dist/modules/customers/components/detail/PersonEmailThreadsTab.js.map +7 -0
  300. package/dist/modules/customers/data/enrichers.js +133 -2
  301. package/dist/modules/customers/data/enrichers.js.map +2 -2
  302. package/dist/modules/customers/data/entities.js +18 -0
  303. package/dist/modules/customers/data/entities.js.map +2 -2
  304. package/dist/modules/customers/data/extensions.js +16 -0
  305. package/dist/modules/customers/data/extensions.js.map +7 -0
  306. package/dist/modules/customers/encryption.js +11 -0
  307. package/dist/modules/customers/encryption.js.map +2 -2
  308. package/dist/modules/customers/events.js +4 -1
  309. package/dist/modules/customers/events.js.map +2 -2
  310. package/dist/modules/customers/lib/findPeopleByAddresses.js +64 -0
  311. package/dist/modules/customers/lib/findPeopleByAddresses.js.map +7 -0
  312. package/dist/modules/customers/lib/kysely.js.map +2 -2
  313. package/dist/modules/customers/lib/link-channel-message-handler.js +303 -0
  314. package/dist/modules/customers/lib/link-channel-message-handler.js.map +7 -0
  315. package/dist/modules/customers/lib/personEmailThreads.js +205 -0
  316. package/dist/modules/customers/lib/personEmailThreads.js.map +7 -0
  317. package/dist/modules/customers/lib/visibilityFilter.js +51 -0
  318. package/dist/modules/customers/lib/visibilityFilter.js.map +7 -0
  319. package/dist/modules/customers/migrations/Migration20260527012240_customers.js +20 -0
  320. package/dist/modules/customers/migrations/Migration20260527012240_customers.js.map +7 -0
  321. package/dist/modules/customers/setup.js +2 -1
  322. package/dist/modules/customers/setup.js.map +2 -2
  323. package/dist/modules/customers/subscribers/link-channel-message-received.js +12 -0
  324. package/dist/modules/customers/subscribers/link-channel-message-received.js.map +7 -0
  325. package/dist/modules/customers/subscribers/link-channel-message-sent.js +12 -0
  326. package/dist/modules/customers/subscribers/link-channel-message-sent.js.map +7 -0
  327. package/dist/modules/integrations/data/entities.js +8 -1
  328. package/dist/modules/integrations/data/entities.js.map +2 -2
  329. package/dist/modules/integrations/lib/credentials-service.js +29 -14
  330. package/dist/modules/integrations/lib/credentials-service.js.map +2 -2
  331. package/dist/modules/integrations/migrations/Migration20260526154136_integrations.js +15 -0
  332. package/dist/modules/integrations/migrations/Migration20260526154136_integrations.js.map +7 -0
  333. package/dist/modules/messages/commands/messages.js +70 -8
  334. package/dist/modules/messages/commands/messages.js.map +2 -2
  335. package/dist/modules/messages/components/ComposeMessagePageClient.js +24 -13
  336. package/dist/modules/messages/components/ComposeMessagePageClient.js.map +2 -2
  337. package/dist/modules/messages/components/MessageDetailPageClient.js +39 -2
  338. package/dist/modules/messages/components/MessageDetailPageClient.js.map +2 -2
  339. package/dist/modules/messages/components/MessagesInboxPageClient.js +1 -0
  340. package/dist/modules/messages/components/MessagesInboxPageClient.js.map +2 -2
  341. package/dist/modules/messages/data/entities.js +8 -1
  342. package/dist/modules/messages/data/entities.js.map +2 -2
  343. package/dist/modules/messages/migrations/Migration20260531130000.js +15 -0
  344. package/dist/modules/messages/migrations/Migration20260531130000.js.map +7 -0
  345. package/dist/modules/messages/widgets/injection-table.js +7 -0
  346. package/dist/modules/messages/widgets/injection-table.js.map +7 -0
  347. package/generated/entities/channel_ingest_dead_letter/index.ts +11 -0
  348. package/generated/entities/channel_thread_mapping/index.ts +11 -0
  349. package/generated/entities/channel_thread_token/index.ts +7 -0
  350. package/generated/entities/communication_channel/index.ts +20 -0
  351. package/generated/entities/customer_interaction/index.ts +2 -0
  352. package/generated/entities/external_conversation/index.ts +11 -0
  353. package/generated/entities/external_message/index.ts +11 -0
  354. package/generated/entities/integration_credentials/index.ts +1 -0
  355. package/generated/entities/message/index.ts +1 -0
  356. package/generated/entities/message_channel_link/index.ts +15 -0
  357. package/generated/entities/message_reaction/index.ts +11 -0
  358. package/generated/entities.ids.generated.ts +11 -0
  359. package/generated/entity-fields-registry.ts +117 -0
  360. package/package.json +9 -7
  361. package/src/helpers/integration/authFixtures.ts +4 -1
  362. package/src/helpers/integration/communicationChannelsFixtures.ts +124 -0
  363. package/src/modules/communication_channels/acl.ts +43 -0
  364. package/src/modules/communication_channels/api/delete/channels/[id]/route.ts +163 -0
  365. package/src/modules/communication_channels/api/delete/messages/[messageId]/reactions/[reactionId]/route.ts +143 -0
  366. package/src/modules/communication_channels/api/get/channels/[id]/health/route.ts +173 -0
  367. package/src/modules/communication_channels/api/get/channels/[id]/route.ts +111 -0
  368. package/src/modules/communication_channels/api/get/channels/route.ts +109 -0
  369. package/src/modules/communication_channels/api/get/me/channels/route.ts +100 -0
  370. package/src/modules/communication_channels/api/get/oauth/[provider]/callback/route.ts +355 -0
  371. package/src/modules/communication_channels/api/post/channels/[id]/import-history/route.ts +206 -0
  372. package/src/modules/communication_channels/api/post/channels/[id]/poll-now/route.ts +174 -0
  373. package/src/modules/communication_channels/api/post/channels/[id]/push/register/route.ts +158 -0
  374. package/src/modules/communication_channels/api/post/channels/[id]/set-primary/route.ts +114 -0
  375. package/src/modules/communication_channels/api/post/channels/[id]/test-send/route.ts +241 -0
  376. package/src/modules/communication_channels/api/post/channels/connect/credentials/route.ts +134 -0
  377. package/src/modules/communication_channels/api/post/messages/[messageId]/reactions/route.ts +143 -0
  378. package/src/modules/communication_channels/api/post/oauth/[provider]/initiate/route.ts +192 -0
  379. package/src/modules/communication_channels/api/post/send-as-user/route.ts +125 -0
  380. package/src/modules/communication_channels/api/post/test-seed/route.ts +267 -0
  381. package/src/modules/communication_channels/api/post/webhook/[provider]/route.ts +227 -0
  382. package/src/modules/communication_channels/api/post/webhooks/gmail/route.ts +161 -0
  383. package/src/modules/communication_channels/api/put/threads/[threadId]/assign/route.ts +132 -0
  384. package/src/modules/communication_channels/backend/communication_channels/channels/[id]/page.meta.ts +34 -0
  385. package/src/modules/communication_channels/backend/communication_channels/channels/[id]/page.tsx +250 -0
  386. package/src/modules/communication_channels/backend/communication_channels/channels/page.meta.ts +36 -0
  387. package/src/modules/communication_channels/backend/communication_channels/channels/page.tsx +137 -0
  388. package/src/modules/communication_channels/backend/profile/communication-channels/page.meta.ts +36 -0
  389. package/src/modules/communication_channels/backend/profile/communication-channels/page.tsx +907 -0
  390. package/src/modules/communication_channels/commands/connect-credential-channel.ts +243 -0
  391. package/src/modules/communication_channels/commands/delete-channel.ts +193 -0
  392. package/src/modules/communication_channels/commands/deliver-outbound-message.ts +579 -0
  393. package/src/modules/communication_channels/commands/disconnect-channel.ts +241 -0
  394. package/src/modules/communication_channels/commands/ingest-inbound-message.ts +602 -0
  395. package/src/modules/communication_channels/commands/interceptors.ts +104 -0
  396. package/src/modules/communication_channels/commands/process-inbound-reaction.ts +265 -0
  397. package/src/modules/communication_channels/commands/push-register.ts +203 -0
  398. package/src/modules/communication_channels/commands/push-renew.ts +49 -0
  399. package/src/modules/communication_channels/commands/push-unregister.ts +168 -0
  400. package/src/modules/communication_channels/commands/queue-import-history.ts +180 -0
  401. package/src/modules/communication_channels/commands/reassign-conversation.ts +273 -0
  402. package/src/modules/communication_channels/commands/set-primary-channel.ts +154 -0
  403. package/src/modules/communication_channels/commands/toggle-outbound-reaction.ts +347 -0
  404. package/src/modules/communication_channels/data/enrichers.ts +413 -0
  405. package/src/modules/communication_channels/data/entities.ts +546 -0
  406. package/src/modules/communication_channels/data/extensions.ts +76 -0
  407. package/src/modules/communication_channels/data/validators.ts +138 -0
  408. package/src/modules/communication_channels/di.ts +40 -0
  409. package/src/modules/communication_channels/encryption.ts +44 -0
  410. package/src/modules/communication_channels/events.ts +122 -0
  411. package/src/modules/communication_channels/i18n/de.json +138 -0
  412. package/src/modules/communication_channels/i18n/en.json +138 -0
  413. package/src/modules/communication_channels/i18n/es.json +138 -0
  414. package/src/modules/communication_channels/i18n/pl.json +138 -0
  415. package/src/modules/communication_channels/index.ts +19 -0
  416. package/src/modules/communication_channels/lib/access-control.ts +110 -0
  417. package/src/modules/communication_channels/lib/adapter-compat.ts +57 -0
  418. package/src/modules/communication_channels/lib/adapter-registry-singleton.ts +35 -0
  419. package/src/modules/communication_channels/lib/adapter.ts +605 -0
  420. package/src/modules/communication_channels/lib/connect-channel.ts +163 -0
  421. package/src/modules/communication_channels/lib/contact-resolver.ts +162 -0
  422. package/src/modules/communication_channels/lib/credential-refresh.ts +197 -0
  423. package/src/modules/communication_channels/lib/dead-letter.ts +87 -0
  424. package/src/modules/communication_channels/lib/email-capabilities.ts +60 -0
  425. package/src/modules/communication_channels/lib/email-contact.ts +17 -0
  426. package/src/modules/communication_channels/lib/email-mime.ts +425 -0
  427. package/src/modules/communication_channels/lib/error-classification.ts +144 -0
  428. package/src/modules/communication_channels/lib/gmail-pubsub-jwt.ts +278 -0
  429. package/src/modules/communication_channels/lib/mutation-guards.ts +215 -0
  430. package/src/modules/communication_channels/lib/oauth-client-config.ts +79 -0
  431. package/src/modules/communication_channels/lib/oauth-state.ts +228 -0
  432. package/src/modules/communication_channels/lib/oauth-token.ts +81 -0
  433. package/src/modules/communication_channels/lib/pg-errors.ts +12 -0
  434. package/src/modules/communication_channels/lib/provider-health.ts +47 -0
  435. package/src/modules/communication_channels/lib/push-state.ts +38 -0
  436. package/src/modules/communication_channels/lib/queue.ts +66 -0
  437. package/src/modules/communication_channels/lib/reaction-processor-types.ts +51 -0
  438. package/src/modules/communication_channels/lib/reaction-semantics.ts +48 -0
  439. package/src/modules/communication_channels/lib/registry.ts +99 -0
  440. package/src/modules/communication_channels/lib/route-mutation-guard.ts +68 -0
  441. package/src/modules/communication_channels/lib/sanitize-channel-html.ts +129 -0
  442. package/src/modules/communication_channels/lib/send-as-user.ts +284 -0
  443. package/src/modules/communication_channels/lib/system-user.ts +74 -0
  444. package/src/modules/communication_channels/lib/test-seed.ts +140 -0
  445. package/src/modules/communication_channels/lib/thread-matcher.ts +430 -0
  446. package/src/modules/communication_channels/lib/thread-token.ts +355 -0
  447. package/src/modules/communication_channels/lib/use-connect-channel.ts +73 -0
  448. package/src/modules/communication_channels/migrations/.snapshot-open-mercato.json +2142 -0
  449. package/src/modules/communication_channels/migrations/Migration20260526134719_communication_channels.ts +55 -0
  450. package/src/modules/communication_channels/migrations/Migration20260527195446_communication_channels.ts +20 -0
  451. package/src/modules/communication_channels/migrations/Migration20260529231848_communication_channels.ts +13 -0
  452. package/src/modules/communication_channels/migrations/Migration20260531120000_communication_channels.ts +24 -0
  453. package/src/modules/communication_channels/notifications.client.ts +50 -0
  454. package/src/modules/communication_channels/notifications.handlers.ts +86 -0
  455. package/src/modules/communication_channels/notifications.ts +52 -0
  456. package/src/modules/communication_channels/setup.ts +158 -0
  457. package/src/modules/communication_channels/subscribers/channel-requires-reauth-notification.ts +118 -0
  458. package/src/modules/communication_channels/subscribers/outbound-bridge.ts +175 -0
  459. package/src/modules/communication_channels/subscribers/user-deleted-cascade.ts +100 -0
  460. package/src/modules/communication_channels/widgets/components.ts +36 -0
  461. package/src/modules/communication_channels/widgets/injection/channel-badge/widget.client.tsx +38 -0
  462. package/src/modules/communication_channels/widgets/injection/channel-badge/widget.ts +51 -0
  463. package/src/modules/communication_channels/widgets/injection/channel-info-panel/widget.client.tsx +278 -0
  464. package/src/modules/communication_channels/widgets/injection/channel-info-panel/widget.ts +24 -0
  465. package/src/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.tsx +63 -0
  466. package/src/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.ts +29 -0
  467. package/src/modules/communication_channels/widgets/injection/profile-channels-menu/widget.ts +34 -0
  468. package/src/modules/communication_channels/widgets/injection/reaction-bar/widget.client.tsx +177 -0
  469. package/src/modules/communication_channels/widgets/injection/reaction-bar/widget.ts +26 -0
  470. package/src/modules/communication_channels/widgets/injection-table.ts +47 -0
  471. package/src/modules/communication_channels/widgets/notifications/ChannelRequiresReauthRenderer.tsx +48 -0
  472. package/src/modules/communication_channels/widgets/notifications/MessageReceivedRenderer.tsx +45 -0
  473. package/src/modules/communication_channels/widgets/notifications/index.ts +2 -0
  474. package/src/modules/communication_channels/workers/channel-import-history.ts +252 -0
  475. package/src/modules/communication_channels/workers/gmail-history-sync.ts +223 -0
  476. package/src/modules/communication_channels/workers/gmail-renew-watch.ts +141 -0
  477. package/src/modules/communication_channels/workers/inbound-processor.ts +114 -0
  478. package/src/modules/communication_channels/workers/outbound-delivery.ts +155 -0
  479. package/src/modules/communication_channels/workers/poll-channel.ts +391 -0
  480. package/src/modules/communication_channels/workers/poll-tick.ts +210 -0
  481. package/src/modules/communication_channels/workers/reaction-processor.ts +264 -0
  482. package/src/modules/customers/acl.ts +18 -0
  483. package/src/modules/customers/api/activities/route.ts +13 -0
  484. package/src/modules/customers/api/companies/[id]/route.ts +21 -1
  485. package/src/modules/customers/api/interactions/[id]/visibility/route.ts +179 -0
  486. package/src/modules/customers/api/interactions/counts/route.ts +10 -0
  487. package/src/modules/customers/api/interactions/route.ts +51 -5
  488. package/src/modules/customers/api/people/[id]/email-threads/route.ts +92 -0
  489. package/src/modules/customers/api/people/[id]/emails/route.ts +184 -0
  490. package/src/modules/customers/api/people/[id]/route.ts +17 -2
  491. package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +11 -1
  492. package/src/modules/customers/commands/deals.ts +65 -6
  493. package/src/modules/customers/commands/interactions.ts +30 -0
  494. package/src/modules/customers/components/detail/ActivityCard.tsx +48 -0
  495. package/src/modules/customers/components/detail/ComposeEmailDialog.tsx +329 -0
  496. package/src/modules/customers/components/detail/DealForm.tsx +2 -1
  497. package/src/modules/customers/components/detail/DealsSection.tsx +26 -0
  498. package/src/modules/customers/components/detail/EmailCardActions.tsx +258 -0
  499. package/src/modules/customers/components/detail/EmailReplyForwardActions.tsx +53 -0
  500. package/src/modules/customers/components/detail/PersonDetailTabs.tsx +8 -1
  501. package/src/modules/customers/components/detail/PersonEmailThreadsTab.tsx +448 -0
  502. package/src/modules/customers/data/enrichers.ts +252 -1
  503. package/src/modules/customers/data/entities.ts +46 -1
  504. package/src/modules/customers/data/extensions.ts +26 -0
  505. package/src/modules/customers/encryption.ts +11 -0
  506. package/src/modules/customers/events.ts +4 -0
  507. package/src/modules/customers/i18n/de.json +41 -0
  508. package/src/modules/customers/i18n/en.json +41 -0
  509. package/src/modules/customers/i18n/es.json +41 -0
  510. package/src/modules/customers/i18n/pl.json +41 -0
  511. package/src/modules/customers/lib/findPeopleByAddresses.ts +107 -0
  512. package/src/modules/customers/lib/kysely.ts +16 -0
  513. package/src/modules/customers/lib/link-channel-message-handler.ts +571 -0
  514. package/src/modules/customers/lib/personEmailThreads.ts +325 -0
  515. package/src/modules/customers/lib/visibilityFilter.ts +152 -0
  516. package/src/modules/customers/migrations/.snapshot-open-mercato.json +61 -0
  517. package/src/modules/customers/migrations/Migration20260527012240_customers.ts +23 -0
  518. package/src/modules/customers/setup.ts +1 -0
  519. package/src/modules/customers/subscribers/link-channel-message-received.ts +21 -0
  520. package/src/modules/customers/subscribers/link-channel-message-sent.ts +21 -0
  521. package/src/modules/integrations/AGENTS.md +9 -0
  522. package/src/modules/integrations/data/entities.ts +21 -1
  523. package/src/modules/integrations/lib/credentials-service.ts +49 -13
  524. package/src/modules/integrations/migrations/.snapshot-open-mercato.json +26 -1
  525. package/src/modules/integrations/migrations/Migration20260526154136_integrations.ts +15 -0
  526. package/src/modules/messages/commands/messages.ts +101 -8
  527. package/src/modules/messages/components/ComposeMessagePageClient.tsx +17 -0
  528. package/src/modules/messages/components/MessageDetailPageClient.tsx +43 -0
  529. package/src/modules/messages/components/MessagesInboxPageClient.tsx +4 -0
  530. package/src/modules/messages/data/entities.ts +11 -0
  531. package/src/modules/messages/migrations/.snapshot-open-mercato.json +18 -0
  532. package/src/modules/messages/migrations/Migration20260531130000.ts +15 -0
  533. package/src/modules/messages/widgets/injection-table.ts +29 -0
@@ -59,7 +59,10 @@ const events = [
59
59
  // Person-Company Links
60
60
  { id: "customers.person_company_link.created", label: "Person Linked To Company", entity: "person_company_link", category: "crud", clientBroadcast: true },
61
61
  { id: "customers.person_company_link.updated", label: "Person-Company Link Updated", entity: "person_company_link", category: "crud", clientBroadcast: true },
62
- { id: "customers.person_company_link.deleted", label: "Person Unlinked From Company", entity: "person_company_link", category: "crud", clientBroadcast: true }
62
+ { id: "customers.person_company_link.deleted", label: "Person Unlinked From Company", entity: "person_company_link", category: "crud", clientBroadcast: true },
63
+ // ── Email integration (2026-05-27) ────────────────────────────────────────
64
+ { id: "customers.email.linked", label: "Email Linked To Person", entity: "email_link", category: "lifecycle", clientBroadcast: true },
65
+ { id: "customers.email.visibility_changed", label: "Email Visibility Changed", entity: "email_link", category: "lifecycle", clientBroadcast: true }
63
66
  ];
64
67
  const eventsConfig = createModuleEvents({
65
68
  moduleId: "customers",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/modules/customers/events.ts"],
4
- "sourcesContent": ["import { createModuleEvents } from '@open-mercato/shared/modules/events'\n\n/**\n * Customers Module Events\n *\n * Declares all events that can be emitted by the customers module.\n */\nconst events = [\n // People\n { id: 'customers.person.created', label: 'Customer (Person) Created', entity: 'person', category: 'crud' },\n { id: 'customers.person.updated', label: 'Customer (Person) Updated', entity: 'person', category: 'crud' },\n { id: 'customers.person.deleted', label: 'Customer (Person) Deleted', entity: 'person', category: 'crud' },\n\n // Companies\n { id: 'customers.company.created', label: 'Customer (Company) Created', entity: 'company', category: 'crud' },\n { id: 'customers.company.updated', label: 'Customer (Company) Updated', entity: 'company', category: 'crud' },\n { id: 'customers.company.deleted', label: 'Customer (Company) Deleted', entity: 'company', category: 'crud' },\n\n // Deals\n { id: 'customers.deal.created', label: 'Deal Created', entity: 'deal', category: 'crud' },\n { id: 'customers.deal.updated', label: 'Deal Updated', entity: 'deal', category: 'crud' },\n { id: 'customers.deal.deleted', label: 'Deal Deleted', entity: 'deal', category: 'crud' },\n { id: 'customers.deal.won', label: 'Deal Won', entity: 'deal', category: 'lifecycle' },\n { id: 'customers.deal.lost', label: 'Deal Lost', entity: 'deal', category: 'lifecycle' },\n\n // Comments\n { id: 'customers.comment.created', label: 'Comment Created', entity: 'comment', category: 'crud' },\n { id: 'customers.comment.updated', label: 'Comment Updated', entity: 'comment', category: 'crud' },\n { id: 'customers.comment.deleted', label: 'Comment Deleted', entity: 'comment', category: 'crud' },\n\n // Addresses\n { id: 'customers.address.created', label: 'Address Created', entity: 'address', category: 'crud' },\n { id: 'customers.address.updated', label: 'Address Updated', entity: 'address', category: 'crud' },\n { id: 'customers.address.deleted', label: 'Address Deleted', entity: 'address', category: 'crud' },\n\n // Activities\n { id: 'customers.activity.created', label: 'Activity Created', entity: 'activity', category: 'crud' },\n { id: 'customers.activity.updated', label: 'Activity Updated', entity: 'activity', category: 'crud' },\n { id: 'customers.activity.deleted', label: 'Activity Deleted', entity: 'activity', category: 'crud' },\n\n // Tags\n { id: 'customers.tag.created', label: 'Tag Created', entity: 'tag', category: 'crud' },\n { id: 'customers.tag.updated', label: 'Tag Updated', entity: 'tag', category: 'crud' },\n { id: 'customers.tag.deleted', label: 'Tag Deleted', entity: 'tag', category: 'crud' },\n { id: 'customers.tag.assigned', label: 'Tag Assigned', entity: 'tag', category: 'crud' },\n { id: 'customers.tag.removed', label: 'Tag Removed', entity: 'tag', category: 'crud' },\n\n // Todos\n { id: 'customers.todo.created', label: 'Todo Created', entity: 'todo', category: 'crud' },\n { id: 'customers.todo.updated', label: 'Todo Updated', entity: 'todo', category: 'crud' },\n { id: 'customers.todo.deleted', label: 'Todo Deleted', entity: 'todo', category: 'crud' },\n\n // Interactions (canonical)\n { id: 'customers.interaction.created', label: 'Interaction Created', entity: 'interaction', category: 'crud' },\n { id: 'customers.interaction.updated', label: 'Interaction Updated', entity: 'interaction', category: 'crud' },\n { id: 'customers.interaction.completed', label: 'Interaction Completed', entity: 'interaction', category: 'lifecycle' },\n { id: 'customers.interaction.canceled', label: 'Interaction Canceled', entity: 'interaction', category: 'lifecycle' },\n { id: 'customers.interaction.reverted', label: 'Interaction Reverted', entity: 'interaction', category: 'lifecycle' },\n { id: 'customers.interaction.deleted', label: 'Interaction Deleted', entity: 'interaction', category: 'crud' },\n { id: 'customers.next_interaction.updated', label: 'Next Interaction Updated', entity: 'interaction', category: 'lifecycle' },\n\n // Entity Roles\n { id: 'customers.entity_role.created', label: 'Entity Role Created', entity: 'entity_role', category: 'crud' },\n { id: 'customers.entity_role.updated', label: 'Entity Role Updated', entity: 'entity_role', category: 'crud' },\n { id: 'customers.entity_role.deleted', label: 'Entity Role Deleted', entity: 'entity_role', category: 'crud' },\n\n // Labels\n { id: 'customers.label.created', label: 'Label Created', entity: 'label', category: 'crud' },\n { id: 'customers.label.updated', label: 'Label Updated', entity: 'label', category: 'crud' },\n { id: 'customers.label.deleted', label: 'Label Deleted', entity: 'label', category: 'crud' },\n\n // Label Assignments\n { id: 'customers.label_assignment.created', label: 'Label Assigned', entity: 'label_assignment', category: 'crud' },\n { id: 'customers.label_assignment.updated', label: 'Label Assignment Updated', entity: 'label_assignment', category: 'crud' },\n { id: 'customers.label_assignment.deleted', label: 'Label Unassigned', entity: 'label_assignment', category: 'crud' },\n\n // Person-Company Links\n { id: 'customers.person_company_link.created', label: 'Person Linked To Company', entity: 'person_company_link', category: 'crud', clientBroadcast: true },\n { id: 'customers.person_company_link.updated', label: 'Person-Company Link Updated', entity: 'person_company_link', category: 'crud', clientBroadcast: true },\n { id: 'customers.person_company_link.deleted', label: 'Person Unlinked From Company', entity: 'person_company_link', category: 'crud', clientBroadcast: true },\n] as const\n\nexport const eventsConfig = createModuleEvents({\n moduleId: 'customers',\n events,\n})\n\n/** Type-safe event emitter for customers module */\nexport const emitCustomersEvent = eventsConfig.emit\n\n/** Event IDs that can be emitted by the customers module */\nexport type CustomersEventId = typeof events[number]['id']\n\nexport default eventsConfig\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { createModuleEvents } from '@open-mercato/shared/modules/events'\n\n/**\n * Customers Module Events\n *\n * Declares all events that can be emitted by the customers module.\n */\nconst events = [\n // People\n { id: 'customers.person.created', label: 'Customer (Person) Created', entity: 'person', category: 'crud' },\n { id: 'customers.person.updated', label: 'Customer (Person) Updated', entity: 'person', category: 'crud' },\n { id: 'customers.person.deleted', label: 'Customer (Person) Deleted', entity: 'person', category: 'crud' },\n\n // Companies\n { id: 'customers.company.created', label: 'Customer (Company) Created', entity: 'company', category: 'crud' },\n { id: 'customers.company.updated', label: 'Customer (Company) Updated', entity: 'company', category: 'crud' },\n { id: 'customers.company.deleted', label: 'Customer (Company) Deleted', entity: 'company', category: 'crud' },\n\n // Deals\n { id: 'customers.deal.created', label: 'Deal Created', entity: 'deal', category: 'crud' },\n { id: 'customers.deal.updated', label: 'Deal Updated', entity: 'deal', category: 'crud' },\n { id: 'customers.deal.deleted', label: 'Deal Deleted', entity: 'deal', category: 'crud' },\n { id: 'customers.deal.won', label: 'Deal Won', entity: 'deal', category: 'lifecycle' },\n { id: 'customers.deal.lost', label: 'Deal Lost', entity: 'deal', category: 'lifecycle' },\n\n // Comments\n { id: 'customers.comment.created', label: 'Comment Created', entity: 'comment', category: 'crud' },\n { id: 'customers.comment.updated', label: 'Comment Updated', entity: 'comment', category: 'crud' },\n { id: 'customers.comment.deleted', label: 'Comment Deleted', entity: 'comment', category: 'crud' },\n\n // Addresses\n { id: 'customers.address.created', label: 'Address Created', entity: 'address', category: 'crud' },\n { id: 'customers.address.updated', label: 'Address Updated', entity: 'address', category: 'crud' },\n { id: 'customers.address.deleted', label: 'Address Deleted', entity: 'address', category: 'crud' },\n\n // Activities\n { id: 'customers.activity.created', label: 'Activity Created', entity: 'activity', category: 'crud' },\n { id: 'customers.activity.updated', label: 'Activity Updated', entity: 'activity', category: 'crud' },\n { id: 'customers.activity.deleted', label: 'Activity Deleted', entity: 'activity', category: 'crud' },\n\n // Tags\n { id: 'customers.tag.created', label: 'Tag Created', entity: 'tag', category: 'crud' },\n { id: 'customers.tag.updated', label: 'Tag Updated', entity: 'tag', category: 'crud' },\n { id: 'customers.tag.deleted', label: 'Tag Deleted', entity: 'tag', category: 'crud' },\n { id: 'customers.tag.assigned', label: 'Tag Assigned', entity: 'tag', category: 'crud' },\n { id: 'customers.tag.removed', label: 'Tag Removed', entity: 'tag', category: 'crud' },\n\n // Todos\n { id: 'customers.todo.created', label: 'Todo Created', entity: 'todo', category: 'crud' },\n { id: 'customers.todo.updated', label: 'Todo Updated', entity: 'todo', category: 'crud' },\n { id: 'customers.todo.deleted', label: 'Todo Deleted', entity: 'todo', category: 'crud' },\n\n // Interactions (canonical)\n { id: 'customers.interaction.created', label: 'Interaction Created', entity: 'interaction', category: 'crud' },\n { id: 'customers.interaction.updated', label: 'Interaction Updated', entity: 'interaction', category: 'crud' },\n { id: 'customers.interaction.completed', label: 'Interaction Completed', entity: 'interaction', category: 'lifecycle' },\n { id: 'customers.interaction.canceled', label: 'Interaction Canceled', entity: 'interaction', category: 'lifecycle' },\n { id: 'customers.interaction.reverted', label: 'Interaction Reverted', entity: 'interaction', category: 'lifecycle' },\n { id: 'customers.interaction.deleted', label: 'Interaction Deleted', entity: 'interaction', category: 'crud' },\n { id: 'customers.next_interaction.updated', label: 'Next Interaction Updated', entity: 'interaction', category: 'lifecycle' },\n\n // Entity Roles\n { id: 'customers.entity_role.created', label: 'Entity Role Created', entity: 'entity_role', category: 'crud' },\n { id: 'customers.entity_role.updated', label: 'Entity Role Updated', entity: 'entity_role', category: 'crud' },\n { id: 'customers.entity_role.deleted', label: 'Entity Role Deleted', entity: 'entity_role', category: 'crud' },\n\n // Labels\n { id: 'customers.label.created', label: 'Label Created', entity: 'label', category: 'crud' },\n { id: 'customers.label.updated', label: 'Label Updated', entity: 'label', category: 'crud' },\n { id: 'customers.label.deleted', label: 'Label Deleted', entity: 'label', category: 'crud' },\n\n // Label Assignments\n { id: 'customers.label_assignment.created', label: 'Label Assigned', entity: 'label_assignment', category: 'crud' },\n { id: 'customers.label_assignment.updated', label: 'Label Assignment Updated', entity: 'label_assignment', category: 'crud' },\n { id: 'customers.label_assignment.deleted', label: 'Label Unassigned', entity: 'label_assignment', category: 'crud' },\n\n // Person-Company Links\n { id: 'customers.person_company_link.created', label: 'Person Linked To Company', entity: 'person_company_link', category: 'crud', clientBroadcast: true },\n { id: 'customers.person_company_link.updated', label: 'Person-Company Link Updated', entity: 'person_company_link', category: 'crud', clientBroadcast: true },\n { id: 'customers.person_company_link.deleted', label: 'Person Unlinked From Company', entity: 'person_company_link', category: 'crud', clientBroadcast: true },\n\n // \u2500\u2500 Email integration (2026-05-27) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n { id: 'customers.email.linked', label: 'Email Linked To Person', entity: 'email_link', category: 'lifecycle', clientBroadcast: true },\n { id: 'customers.email.visibility_changed', label: 'Email Visibility Changed', entity: 'email_link', category: 'lifecycle', clientBroadcast: true },\n] as const\n\nexport const eventsConfig = createModuleEvents({\n moduleId: 'customers',\n events,\n})\n\n/** Type-safe event emitter for customers module */\nexport const emitCustomersEvent = eventsConfig.emit\n\n/** Event IDs that can be emitted by the customers module */\nexport type CustomersEventId = typeof events[number]['id']\n\nexport default eventsConfig\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,64 @@
1
+ import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
2
+ import { CustomerEntity } from "../data/entities.js";
3
+ function normalizeAddresses(input) {
4
+ if (!Array.isArray(input)) return [];
5
+ const seen = /* @__PURE__ */ new Set();
6
+ const out = [];
7
+ for (const value of input) {
8
+ if (typeof value !== "string") continue;
9
+ const trimmed = value.trim().toLowerCase();
10
+ if (!trimmed) continue;
11
+ const at = trimmed.indexOf("@");
12
+ if (at <= 0 || at === trimmed.length - 1 || trimmed.lastIndexOf("@") !== at) continue;
13
+ if (seen.has(trimmed)) continue;
14
+ seen.add(trimmed);
15
+ out.push(trimmed);
16
+ }
17
+ return out;
18
+ }
19
+ const MATCH_CANDIDATE_LIMIT = 500;
20
+ async function findPeopleByAddresses(em, addresses, tenantId, organizationId = null) {
21
+ const normalized = normalizeAddresses(addresses);
22
+ if (normalized.length === 0) return [];
23
+ if (!organizationId) return [];
24
+ const dscope = { tenantId, organizationId };
25
+ const resolved = /* @__PURE__ */ new Map();
26
+ const direct = await findWithDecryption(
27
+ em,
28
+ CustomerEntity,
29
+ { primaryEmail: { $in: normalized }, kind: "person", tenantId, organizationId, deletedAt: null },
30
+ void 0,
31
+ dscope
32
+ );
33
+ for (const row of direct) {
34
+ const rowEmail = row.primaryEmail?.trim().toLowerCase();
35
+ if (rowEmail && !resolved.has(rowEmail)) resolved.set(rowEmail, row.id);
36
+ }
37
+ if (normalized.some((email) => !resolved.has(email))) {
38
+ const candidates = await findWithDecryption(
39
+ em,
40
+ CustomerEntity,
41
+ { kind: "person", tenantId, organizationId, deletedAt: null },
42
+ { limit: MATCH_CANDIDATE_LIMIT, orderBy: { createdAt: "DESC" } },
43
+ dscope
44
+ );
45
+ for (const row of candidates) {
46
+ const rowEmail = row.primaryEmail?.trim().toLowerCase();
47
+ if (rowEmail && !resolved.has(rowEmail)) resolved.set(rowEmail, row.id);
48
+ }
49
+ }
50
+ const seen = /* @__PURE__ */ new Set();
51
+ const out = [];
52
+ for (const email of normalized) {
53
+ const id = resolved.get(email);
54
+ if (!id || seen.has(id)) continue;
55
+ seen.add(id);
56
+ out.push({ id, email });
57
+ }
58
+ return out;
59
+ }
60
+ export {
61
+ findPeopleByAddresses,
62
+ normalizeAddresses
63
+ };
64
+ //# sourceMappingURL=findPeopleByAddresses.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/customers/lib/findPeopleByAddresses.ts"],
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { CustomerEntity } from '../data/entities'\n\n/**\n * Lower-cases, trims, and dedupes a list of email-shaped strings.\n * Rejects anything that doesn't look like `x@y` (single `@`, not at the start\n * or end, no consecutive `@`).\n */\nexport function normalizeAddresses(input: unknown): string[] {\n if (!Array.isArray(input)) return []\n const seen = new Set<string>()\n const out: string[] = []\n for (const value of input) {\n if (typeof value !== 'string') continue\n const trimmed = value.trim().toLowerCase()\n if (!trimmed) continue\n const at = trimmed.indexOf('@')\n if (at <= 0 || at === trimmed.length - 1 || trimmed.lastIndexOf('@') !== at) continue\n if (seen.has(trimmed)) continue\n seen.add(trimmed)\n out.push(trimmed)\n }\n return out\n}\n\nexport interface MatchedPerson {\n /** customer_entities.id (the anchor for customer_interactions.entity_id). */\n id: string\n /** Lowercased email address. */\n email: string\n}\n\n/**\n * Max person rows scanned by the in-memory fallback match. `primary_email` is\n * GDPR-encrypted with a random IV (see `customers/encryption.ts`), so it cannot\n * be filtered by value in SQL when tenant data encryption is on \u2014 recent rows are\n * decrypted and compared in memory instead. Bounded to keep the inbound path cheap;\n * a `primary_email` blind-index (hash) column is the follow-up if tenants outgrow it.\n */\nconst MATCH_CANDIDATE_LIMIT = 500\n\n/**\n * Batch lookup of CustomerEntity rows (kind='person') whose `primaryEmail`\n * matches any of the given addresses (case-insensitive), scoped to the tenant\n * and organization. Returns at most one MatchedPerson per resolved person.\n *\n * `primary_email` is encrypted with a non-deterministic IV, so a `WHERE\n * primary_email = ?` filter silently matches nothing when encryption is on. We\n * therefore try a direct equality match first (the fast path when encryption is\n * off) and, for any address it leaves unresolved, fall back to scanning recent\n * decrypted person rows and comparing in memory \u2014 the same dual-mode pattern as\n * `inbox-actions.ts`.\n */\nexport async function findPeopleByAddresses(\n em: EntityManager,\n addresses: string[],\n tenantId: string,\n organizationId: string | null = null,\n): Promise<MatchedPerson[]> {\n const normalized = normalizeAddresses(addresses)\n if (normalized.length === 0) return []\n if (!organizationId) return []\n const dscope = { tenantId, organizationId }\n const resolved = new Map<string, string>()\n\n // Fast path: direct equality match. Matches exactly when tenant data encryption\n // is off; returns nothing against encrypted (random-IV) ciphertext, which the\n // in-memory fallback below covers.\n const direct = (await findWithDecryption(\n em,\n CustomerEntity,\n { primaryEmail: { $in: normalized }, kind: 'person', tenantId, organizationId, deletedAt: null },\n undefined,\n dscope,\n )) as Array<{ id: string; primaryEmail?: string | null }>\n for (const row of direct) {\n const rowEmail = row.primaryEmail?.trim().toLowerCase()\n if (rowEmail && !resolved.has(rowEmail)) resolved.set(rowEmail, row.id)\n }\n\n // Fallback for unresolved addresses (the encryption-on path): scan recent person\n // rows and compare decrypted emails in memory.\n if (normalized.some((email) => !resolved.has(email))) {\n const candidates = (await findWithDecryption(\n em,\n CustomerEntity,\n { kind: 'person', tenantId, organizationId, deletedAt: null },\n { limit: MATCH_CANDIDATE_LIMIT, orderBy: { createdAt: 'DESC' } },\n dscope,\n )) as Array<{ id: string; primaryEmail?: string | null }>\n for (const row of candidates) {\n const rowEmail = row.primaryEmail?.trim().toLowerCase()\n if (rowEmail && !resolved.has(rowEmail)) resolved.set(rowEmail, row.id)\n }\n }\n\n const seen = new Set<string>()\n const out: MatchedPerson[] = []\n for (const email of normalized) {\n const id = resolved.get(email)\n if (!id || seen.has(id)) continue\n seen.add(id)\n out.push({ id, email })\n }\n return out\n}\n"],
5
+ "mappings": "AACA,SAAS,0BAA0B;AACnC,SAAS,sBAAsB;AAOxB,SAAS,mBAAmB,OAA0B;AAC3D,MAAI,CAAC,MAAM,QAAQ,KAAK,EAAG,QAAO,CAAC;AACnC,QAAM,OAAO,oBAAI,IAAY;AAC7B,QAAM,MAAgB,CAAC;AACvB,aAAW,SAAS,OAAO;AACzB,QAAI,OAAO,UAAU,SAAU;AAC/B,UAAM,UAAU,MAAM,KAAK,EAAE,YAAY;AACzC,QAAI,CAAC,QAAS;AACd,UAAM,KAAK,QAAQ,QAAQ,GAAG;AAC9B,QAAI,MAAM,KAAK,OAAO,QAAQ,SAAS,KAAK,QAAQ,YAAY,GAAG,MAAM,GAAI;AAC7E,QAAI,KAAK,IAAI,OAAO,EAAG;AACvB,SAAK,IAAI,OAAO;AAChB,QAAI,KAAK,OAAO;AAAA,EAClB;AACA,SAAO;AACT;AAgBA,MAAM,wBAAwB;AAc9B,eAAsB,sBACpB,IACA,WACA,UACA,iBAAgC,MACN;AAC1B,QAAM,aAAa,mBAAmB,SAAS;AAC/C,MAAI,WAAW,WAAW,EAAG,QAAO,CAAC;AACrC,MAAI,CAAC,eAAgB,QAAO,CAAC;AAC7B,QAAM,SAAS,EAAE,UAAU,eAAe;AAC1C,QAAM,WAAW,oBAAI,IAAoB;AAKzC,QAAM,SAAU,MAAM;AAAA,IACpB;AAAA,IACA;AAAA,IACA,EAAE,cAAc,EAAE,KAAK,WAAW,GAAG,MAAM,UAAU,UAAU,gBAAgB,WAAW,KAAK;AAAA,IAC/F;AAAA,IACA;AAAA,EACF;AACA,aAAW,OAAO,QAAQ;AACxB,UAAM,WAAW,IAAI,cAAc,KAAK,EAAE,YAAY;AACtD,QAAI,YAAY,CAAC,SAAS,IAAI,QAAQ,EAAG,UAAS,IAAI,UAAU,IAAI,EAAE;AAAA,EACxE;AAIA,MAAI,WAAW,KAAK,CAAC,UAAU,CAAC,SAAS,IAAI,KAAK,CAAC,GAAG;AACpD,UAAM,aAAc,MAAM;AAAA,MACxB;AAAA,MACA;AAAA,MACA,EAAE,MAAM,UAAU,UAAU,gBAAgB,WAAW,KAAK;AAAA,MAC5D,EAAE,OAAO,uBAAuB,SAAS,EAAE,WAAW,OAAO,EAAE;AAAA,MAC/D;AAAA,IACF;AACA,eAAW,OAAO,YAAY;AAC5B,YAAM,WAAW,IAAI,cAAc,KAAK,EAAE,YAAY;AACtD,UAAI,YAAY,CAAC,SAAS,IAAI,QAAQ,EAAG,UAAS,IAAI,UAAU,IAAI,EAAE;AAAA,IACxE;AAAA,EACF;AAEA,QAAM,OAAO,oBAAI,IAAY;AAC7B,QAAM,MAAuB,CAAC;AAC9B,aAAW,SAAS,YAAY;AAC9B,UAAM,KAAK,SAAS,IAAI,KAAK;AAC7B,QAAI,CAAC,MAAM,KAAK,IAAI,EAAE,EAAG;AACzB,SAAK,IAAI,EAAE;AACX,QAAI,KAAK,EAAE,IAAI,MAAM,CAAC;AAAA,EACxB;AACA,SAAO;AACT;",
6
+ "names": []
7
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/customers/lib/kysely.ts"],
4
- "sourcesContent": ["import type { Kysely } from 'kysely'\n\n/**\n * Narrow Kysely schema for the customers-module tables used by the kanban helpers\n * (`stuckDeals`, `enrichers`). Defining the schema locally keeps the queries strongly\n * typed without forcing a project-wide DB schema declaration. Add columns/tables here\n * when new customer-module queries need typed access.\n */\nexport interface CustomerKyselyDb {\n customer_settings: {\n organization_id: string\n tenant_id: string\n stuck_threshold_days: number | string | null\n }\n customer_deals: {\n id: string\n organization_id: string\n tenant_id: string\n deleted_at: Date | string | null\n created_at: Date | string | null\n }\n customer_deal_stage_transitions: {\n deal_id: string\n organization_id: string\n tenant_id: string\n deleted_at: Date | string | null\n transitioned_at: Date | string | null\n }\n customer_interactions: {\n deal_id: string | null\n organization_id: string\n tenant_id: string\n deleted_at: Date | string | null\n status: string | null\n }\n}\n\nexport type CustomerKysely = Kysely<CustomerKyselyDb>\n\n/**\n * Narrows an EntityManager-like value to a MikroORM v7 PG EntityManager and returns its\n * Kysely client, or `null` when the value doesn't expose `getKysely`.\n *\n * MikroORM v7's `@mikro-orm/postgresql` EntityManager ships `getKysely<TDb>()`, but the\n * shared `EnricherContext` types `em` as `unknown` to keep the contract DB-agnostic. The\n * kanban helpers (`stuckDeals`, `enrichers`) both need the typed client \u2014 this helper\n * centralizes the runtime check so neither call site has to repeat `(em as any).getKysely`.\n *\n * Returns `null` (rather than throwing) on purpose: callers fall back to a sensible default\n * (empty stuck-id list, un-enriched records) when the env doesn't have Kysely available\n * (e.g. unit tests with a stub EntityManager).\n */\nexport function resolveKyselyClient<TDb = CustomerKyselyDb>(em: unknown): Kysely<TDb> | null {\n if (em == null || typeof em !== 'object') return null\n const candidate = (em as { getKysely?: unknown }).getKysely\n if (typeof candidate !== 'function') return null\n const db = (candidate as () => unknown).call(em)\n if (db == null) return null\n return db as Kysely<TDb>\n}\n"],
5
- "mappings": "AAoDO,SAAS,oBAA4C,IAAiC;AAC3F,MAAI,MAAM,QAAQ,OAAO,OAAO,SAAU,QAAO;AACjD,QAAM,YAAa,GAA+B;AAClD,MAAI,OAAO,cAAc,WAAY,QAAO;AAC5C,QAAM,KAAM,UAA4B,KAAK,EAAE;AAC/C,MAAI,MAAM,KAAM,QAAO;AACvB,SAAO;AACT;",
4
+ "sourcesContent": ["import type { Kysely } from 'kysely'\n\n/**\n * Narrow Kysely schema for the customers-module tables used by the kanban helpers\n * (`stuckDeals`, `enrichers`). Defining the schema locally keeps the queries strongly\n * typed without forcing a project-wide DB schema declaration. Add columns/tables here\n * when new customer-module queries need typed access.\n */\nexport interface CustomerKyselyDb {\n customer_settings: {\n organization_id: string\n tenant_id: string\n stuck_threshold_days: number | string | null\n }\n customer_deals: {\n id: string\n organization_id: string\n tenant_id: string\n deleted_at: Date | string | null\n created_at: Date | string | null\n }\n customer_deal_stage_transitions: {\n deal_id: string\n organization_id: string\n tenant_id: string\n deleted_at: Date | string | null\n transitioned_at: Date | string | null\n }\n customer_interactions: {\n deal_id: string | null\n entity_id: string | null\n organization_id: string\n tenant_id: string\n deleted_at: Date | string | null\n status: string | null\n interaction_type: string | null\n visibility: string | null\n author_user_id: string | null\n }\n /**\n * Read-only projection of the communication_channels-owned table. Declared\n * here (rather than coupling to that module's types) so the email-card\n * enricher can resolve `channel_metadata` for linked interactions with full\n * type-safety instead of an `as any` escape hatch.\n */\n message_channel_links: {\n id: string\n channel_metadata: unknown\n organization_id: string | null\n tenant_id: string\n }\n}\n\nexport type CustomerKysely = Kysely<CustomerKyselyDb>\n\n/**\n * Narrows an EntityManager-like value to a MikroORM v7 PG EntityManager and returns its\n * Kysely client, or `null` when the value doesn't expose `getKysely`.\n *\n * MikroORM v7's `@mikro-orm/postgresql` EntityManager ships `getKysely<TDb>()`, but the\n * shared `EnricherContext` types `em` as `unknown` to keep the contract DB-agnostic. The\n * kanban helpers (`stuckDeals`, `enrichers`) both need the typed client \u2014 this helper\n * centralizes the runtime check so neither call site has to repeat `(em as any).getKysely`.\n *\n * Returns `null` (rather than throwing) on purpose: callers fall back to a sensible default\n * (empty stuck-id list, un-enriched records) when the env doesn't have Kysely available\n * (e.g. unit tests with a stub EntityManager).\n */\nexport function resolveKyselyClient<TDb = CustomerKyselyDb>(em: unknown): Kysely<TDb> | null {\n if (em == null || typeof em !== 'object') return null\n const candidate = (em as { getKysely?: unknown }).getKysely\n if (typeof candidate !== 'function') return null\n const db = (candidate as () => unknown).call(em)\n if (db == null) return null\n return db as Kysely<TDb>\n}\n"],
5
+ "mappings": "AAoEO,SAAS,oBAA4C,IAAiC;AAC3F,MAAI,MAAM,QAAQ,OAAO,OAAO,SAAU,QAAO;AACjD,QAAM,YAAa,GAA+B;AAClD,MAAI,OAAO,cAAc,WAAY,QAAO;AAC5C,QAAM,KAAM,UAA4B,KAAK,EAAE;AAC/C,MAAI,MAAM,KAAM,QAAO;AACvB,SAAO;AACT;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,303 @@
1
+ import { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
2
+ import { CustomerEntity, CustomerInteraction } from "../data/entities.js";
3
+ import { findPeopleByAddresses, normalizeAddresses } from "./findPeopleByAddresses.js";
4
+ import { emitCustomersEvent } from "../events.js";
5
+ const POSTGRES_UNIQUE_VIOLATION = "23505";
6
+ async function handler(payload, ctx) {
7
+ const linkId = payload?.channelLinkId ?? payload?.messageChannelLinkId;
8
+ if (typeof linkId !== "string" || !linkId) return;
9
+ if (typeof payload.tenantId !== "string" || !payload.tenantId) return;
10
+ const tenantId = payload.tenantId;
11
+ const organizationId = payload.organizationId ?? null;
12
+ if (!organizationId) return;
13
+ const dscope = { tenantId, organizationId };
14
+ const em = ctx.resolve("em").fork();
15
+ const link = await findOneWithDecryption(
16
+ em,
17
+ "MessageChannelLink",
18
+ { id: linkId, tenantId, organizationId },
19
+ void 0,
20
+ dscope
21
+ );
22
+ if (!link) return;
23
+ const metaJson = link.channelMetadata ?? null;
24
+ const payloadJson = link.channelPayload ?? null;
25
+ let channelUserId = null;
26
+ if (typeof payload.channelId === "string" && payload.channelId) {
27
+ const channel = await findOneWithDecryption(
28
+ em,
29
+ "CommunicationChannel",
30
+ { id: payload.channelId, tenantId, organizationId },
31
+ void 0,
32
+ dscope
33
+ );
34
+ channelUserId = channel?.userId ?? null;
35
+ }
36
+ const rawAddresses = [];
37
+ if (payloadJson) {
38
+ collectAddressField(rawAddresses, payloadJson.from);
39
+ collectAddressField(rawAddresses, payloadJson.to);
40
+ collectAddressField(rawAddresses, payloadJson.cc);
41
+ collectAddressField(rawAddresses, payloadJson.bcc);
42
+ }
43
+ if (rawAddresses.length === 0 && metaJson) {
44
+ collectAddressField(rawAddresses, metaJson.from);
45
+ collectAddressField(rawAddresses, metaJson.to);
46
+ collectAddressField(rawAddresses, metaJson.cc);
47
+ collectAddressField(rawAddresses, metaJson.bcc);
48
+ }
49
+ const normalized = normalizeAddresses(rawAddresses);
50
+ const crmPersonIdHint = typeof metaJson?.crmPersonId === "string" ? metaJson.crmPersonId : null;
51
+ let crmPersonId = null;
52
+ if (crmPersonIdHint) {
53
+ const hintedPerson = await findOneWithDecryption(
54
+ em,
55
+ CustomerEntity,
56
+ { id: crmPersonIdHint, kind: "person", tenantId, organizationId, deletedAt: null },
57
+ void 0,
58
+ dscope
59
+ );
60
+ if (hintedPerson) crmPersonId = crmPersonIdHint;
61
+ }
62
+ if (normalized.length === 0 && !crmPersonId) {
63
+ await handleThreadingInheritance(em, link, linkId, tenantId, organizationId, channelUserId, metaJson, payloadJson);
64
+ return;
65
+ }
66
+ const matched = await findPeopleByAddresses(em, normalized, tenantId, organizationId);
67
+ const personIdSet = new Set(matched.map((m) => m.id));
68
+ if (crmPersonId) personIdSet.add(crmPersonId);
69
+ if (personIdSet.size === 0) {
70
+ await handleThreadingInheritance(em, link, linkId, tenantId, organizationId, channelUserId, metaJson, payloadJson);
71
+ return;
72
+ }
73
+ const linkDirection = link.direction === "inbound" || link.direction === "outbound" ? link.direction : null;
74
+ const visibility = resolveVisibility(metaJson, channelUserId, linkDirection);
75
+ const subject = typeof metaJson?.subject === "string" ? metaJson.subject : typeof payloadJson?.subject === "string" ? payloadJson.subject : null;
76
+ const bodyText = typeof metaJson?.bodyText === "string" ? metaJson.bodyText : typeof payloadJson?.text === "string" ? payloadJson.text : null;
77
+ const occurredAt = link.createdAt instanceof Date ? link.createdAt : /* @__PURE__ */ new Date();
78
+ const providerKey = typeof link.providerKey === "string" ? link.providerKey : null;
79
+ await persistInteractions(
80
+ em,
81
+ personIdSet,
82
+ {
83
+ linkId,
84
+ tenantId,
85
+ organizationId,
86
+ interactionType: "email",
87
+ title: subject,
88
+ body: bodyText,
89
+ authorUserId: channelUserId,
90
+ occurredAt,
91
+ visibility,
92
+ channelProviderKey: providerKey
93
+ }
94
+ );
95
+ }
96
+ async function handleThreadingInheritance(em, _link, linkId, tenantId, organizationId, channelUserId, metaJson, payloadJson) {
97
+ const inboundMessageId = typeof _link.messageId === "string" ? _link.messageId : null;
98
+ if (inboundMessageId && organizationId) {
99
+ const threadPersonRows = await em.getConnection().execute(
100
+ `SELECT DISTINCT ci.entity_id AS entity_id
101
+ FROM messages inbound_m
102
+ JOIN messages thread_m ON thread_m.thread_id = inbound_m.thread_id
103
+ JOIN message_channel_links mcl ON mcl.message_id = thread_m.id
104
+ JOIN customer_interactions ci ON ci.external_message_id = mcl.id
105
+ WHERE inbound_m.id = ?
106
+ AND inbound_m.tenant_id = ?
107
+ AND inbound_m.organization_id = ?
108
+ AND inbound_m.deleted_at IS NULL
109
+ AND inbound_m.thread_id IS NOT NULL
110
+ AND thread_m.tenant_id = ?
111
+ AND thread_m.organization_id = ?
112
+ AND thread_m.deleted_at IS NULL
113
+ AND mcl.tenant_id = ?
114
+ AND mcl.organization_id = ?
115
+ AND ci.tenant_id = ?
116
+ AND ci.organization_id = ?
117
+ AND ci.interaction_type = 'email'
118
+ AND ci.deleted_at IS NULL
119
+ AND ci.entity_id IS NOT NULL
120
+ LIMIT 200`,
121
+ [
122
+ inboundMessageId,
123
+ tenantId,
124
+ organizationId,
125
+ tenantId,
126
+ organizationId,
127
+ tenantId,
128
+ organizationId,
129
+ tenantId,
130
+ organizationId
131
+ ]
132
+ );
133
+ const threadPersonIds = new Set(
134
+ threadPersonRows.map((row) => row.entity_id).filter((id) => !!id)
135
+ );
136
+ if (threadPersonIds.size > 0) {
137
+ const subject2 = typeof metaJson?.subject === "string" ? metaJson.subject : typeof payloadJson?.subject === "string" ? payloadJson.subject : null;
138
+ const bodyText = typeof payloadJson?.text === "string" ? payloadJson.text : typeof metaJson?.bodyText === "string" ? metaJson.bodyText : null;
139
+ const occurredAt2 = _link.createdAt instanceof Date ? _link.createdAt : /* @__PURE__ */ new Date();
140
+ const providerKey2 = typeof _link.providerKey === "string" ? _link.providerKey : null;
141
+ await persistInteractions(em, threadPersonIds, {
142
+ linkId,
143
+ tenantId,
144
+ organizationId,
145
+ interactionType: "email",
146
+ title: subject2,
147
+ body: bodyText,
148
+ authorUserId: channelUserId,
149
+ occurredAt: occurredAt2,
150
+ visibility: channelUserId ? "private" : "shared",
151
+ channelProviderKey: providerKey2
152
+ });
153
+ return;
154
+ }
155
+ }
156
+ const refIds = [];
157
+ const inReplyTo = typeof metaJson?.inReplyTo === "string" ? stripBrackets(metaJson.inReplyTo) : typeof payloadJson?.inReplyTo === "string" ? stripBrackets(payloadJson.inReplyTo) : null;
158
+ if (inReplyTo) refIds.push(inReplyTo);
159
+ const refs = Array.isArray(metaJson?.references) ? metaJson.references : Array.isArray(payloadJson?.references) ? payloadJson.references : [];
160
+ for (const r of refs) {
161
+ if (typeof r === "string") {
162
+ const stripped = stripBrackets(r);
163
+ if (stripped && !refIds.includes(stripped)) refIds.push(stripped);
164
+ }
165
+ }
166
+ if (refIds.length === 0) return;
167
+ const dscope = { tenantId, organizationId };
168
+ const messageIdCandidates = Array.from(
169
+ new Set(refIds.flatMap((ref) => [ref, `<${ref}>`]))
170
+ );
171
+ const placeholders = messageIdCandidates.map(() => "?").join(", ");
172
+ const parentLinkRows = await em.getConnection().execute(
173
+ `SELECT id FROM message_channel_links
174
+ WHERE tenant_id = ?
175
+ AND organization_id = ?
176
+ AND channel_metadata->>'messageId' IN (${placeholders})
177
+ LIMIT 200`,
178
+ [tenantId, organizationId, ...messageIdCandidates]
179
+ );
180
+ const matchedParentIds = parentLinkRows.map((parentLink) => parentLink.id);
181
+ if (matchedParentIds.length === 0) return;
182
+ const parentInteractions = await findWithDecryption(
183
+ em,
184
+ CustomerInteraction,
185
+ {
186
+ externalMessageId: { $in: matchedParentIds },
187
+ tenantId,
188
+ organizationId,
189
+ interactionType: "email",
190
+ deletedAt: null
191
+ },
192
+ void 0,
193
+ dscope
194
+ );
195
+ const inheritedPersonIdSet = new Set(
196
+ parentInteractions.map((pi) => pi.entity?.id).filter(Boolean)
197
+ );
198
+ if (inheritedPersonIdSet.size === 0) return;
199
+ const visibility = channelUserId ? "private" : "shared";
200
+ const link = _link;
201
+ const inheritedMeta = link.channelMetadata ?? null;
202
+ const subject = typeof inheritedMeta?.subject === "string" ? inheritedMeta.subject : null;
203
+ const occurredAt = link.createdAt instanceof Date ? link.createdAt : /* @__PURE__ */ new Date();
204
+ const providerKey = typeof link.providerKey === "string" ? link.providerKey : null;
205
+ await persistInteractions(
206
+ em,
207
+ inheritedPersonIdSet,
208
+ {
209
+ linkId,
210
+ tenantId,
211
+ organizationId,
212
+ interactionType: "email",
213
+ title: subject,
214
+ body: null,
215
+ authorUserId: channelUserId,
216
+ occurredAt,
217
+ visibility,
218
+ channelProviderKey: providerKey
219
+ }
220
+ );
221
+ }
222
+ async function persistInteractions(em, personIdSet, data) {
223
+ for (const personId of personIdSet) {
224
+ const rowEm = em.fork();
225
+ const entityRef = rowEm.getReference(CustomerEntity, personId);
226
+ const interaction = rowEm.create(CustomerInteraction, {
227
+ tenantId: data.tenantId,
228
+ organizationId: data.organizationId,
229
+ entity: entityRef,
230
+ interactionType: data.interactionType,
231
+ title: data.title,
232
+ body: data.body,
233
+ authorUserId: data.authorUserId,
234
+ occurredAt: data.occurredAt,
235
+ // Emails are logged AFTER they're sent/received — they are not
236
+ // scheduled work. Without an explicit status the entity default
237
+ // ('planned') combined with a past `occurredAt` makes the activity
238
+ // timeline render the email as "overdue", which is the wrong UX.
239
+ // The canonical "completed" value is `'done'` per
240
+ // `validators.ts:interactionStatusValues = ['planned', 'done', 'canceled']`
241
+ // — `'completed'` is a legacy spelling that the enricher accepts
242
+ // defensively but the activity timeline `isOverdue` predicate does NOT.
243
+ status: "done",
244
+ externalMessageId: data.linkId,
245
+ visibility: data.visibility,
246
+ channelProviderKey: data.channelProviderKey
247
+ });
248
+ try {
249
+ await rowEm.flush();
250
+ } catch (err) {
251
+ const code = err.code;
252
+ if (code !== POSTGRES_UNIQUE_VIOLATION) throw err;
253
+ continue;
254
+ }
255
+ try {
256
+ await emitCustomersEvent("customers.email.linked", {
257
+ personId,
258
+ interactionId: interaction.id,
259
+ tenantId: data.tenantId,
260
+ organizationId: data.organizationId
261
+ });
262
+ } catch {
263
+ }
264
+ }
265
+ }
266
+ function resolveVisibility(metaJson, channelUserId, direction) {
267
+ if (direction === "outbound") {
268
+ if (metaJson?.crmVisibility === "shared") return "shared";
269
+ if (metaJson?.crmVisibility === "private") return "private";
270
+ }
271
+ return channelUserId ? "private" : "shared";
272
+ }
273
+ function collectAddressField(out, value) {
274
+ if (!value) return;
275
+ if (typeof value === "string") {
276
+ out.push(value);
277
+ return;
278
+ }
279
+ if (Array.isArray(value)) {
280
+ for (const item of value) {
281
+ if (typeof item === "string") {
282
+ out.push(item);
283
+ } else if (item && typeof item === "object" && typeof item.address === "string") {
284
+ out.push(item.address);
285
+ }
286
+ }
287
+ return;
288
+ }
289
+ if (typeof value === "object" && typeof value.address === "string") {
290
+ out.push(value.address);
291
+ }
292
+ }
293
+ function stripBrackets(value) {
294
+ const trimmed = value.trim();
295
+ if (trimmed.startsWith("<") && trimmed.endsWith(">")) {
296
+ return trimmed.slice(1, -1);
297
+ }
298
+ return trimmed;
299
+ }
300
+ export {
301
+ handler as default
302
+ };
303
+ //# sourceMappingURL=link-channel-message-handler.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/customers/lib/link-channel-message-handler.ts"],
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { CustomerEntity, CustomerInteraction } from '../data/entities'\nimport { findPeopleByAddresses, normalizeAddresses } from './findPeopleByAddresses'\nimport { emitCustomersEvent } from '../events'\n\n/**\n * Shared implementation for the link-channel-message subscribers.\n *\n * The auto-discovery scanner requires a single string `event` value on each\n * subscriber file. Because we must handle both `communication_channels.message.received`\n * AND `.sent`, we use TWO thin subscriber files (link-channel-message-received.ts\n * and link-channel-message-sent.ts) that both delegate here.\n */\n\n// \u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Payload emitted by `communication_channels.message.received` and\n * `communication_channels.message.sent`.\n *\n * The canonical field is `channelLinkId` (as emitted by the hub). The alias\n * `messageChannelLinkId` is kept for test stubs and legacy compatibility.\n */\ntype LinkChannelMessagePayload = {\n eventType?: string\n /** UUID of the MessageChannelLink row (canonical hub field name). */\n channelLinkId?: string\n /** Alias used in some older stubs / test payloads. Prefer channelLinkId. */\n messageChannelLinkId?: string\n channelId?: string | null\n tenantId?: string\n organizationId?: string | null\n providerKey?: string | null\n direction?: 'inbound' | 'outbound' | null\n}\n\ntype SubscriberContext = {\n resolve: <T = unknown>(name: string) => T\n}\n\n// \u2500\u2500 Constants \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst POSTGRES_UNIQUE_VIOLATION = '23505'\n\n// \u2500\u2500 Main handler \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport default async function handler(\n payload: LinkChannelMessagePayload,\n ctx: SubscriberContext,\n): Promise<void> {\n // Resolve the link ID from either field name.\n const linkId = payload?.channelLinkId ?? payload?.messageChannelLinkId\n if (typeof linkId !== 'string' || !linkId) return\n\n // Fail-closed when tenantId is missing \u2014 unscoped queries are unsafe.\n if (typeof payload.tenantId !== 'string' || !payload.tenantId) return\n\n const tenantId = payload.tenantId\n const organizationId = payload.organizationId ?? null\n // CustomerEntity and CustomerInteraction are organization-scoped. Without an\n // organization id, fail closed instead of linking tenant-wide by email/thread.\n if (!organizationId) return\n const dscope = { tenantId, organizationId }\n\n // em.fork() gives us an isolated identity map for this event.\n const em = (ctx.resolve('em') as EntityManager).fork()\n\n // \u2500\u2500 (1) Load the MessageChannelLink row \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n //\n // The customers module MUST NOT import MessageChannelLink from the\n // communication_channels module (cross-module ORM boundary rule in AGENTS.md).\n // We use the entity class name as a string so MikroORM's identity map resolves\n // it at runtime \u2014 the generated entity registry includes the hub's entities.\n // Read through findOneWithDecryption so any encrypted columns on the hub\n // entity are transparently decrypted (per the Encryption section in AGENTS.md).\n const link = (await findOneWithDecryption(\n em,\n 'MessageChannelLink' as any,\n { id: linkId, tenantId, organizationId } as any,\n undefined,\n dscope,\n )) as Record<string, unknown> | null\n\n if (!link) return\n\n const metaJson = (link.channelMetadata ?? null) as Record<string, unknown> | null\n const payloadJson = (link.channelPayload ?? null) as Record<string, unknown> | null\n\n // \u2500\u2500 (2) Resolve the channel to get its owner userId \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n //\n // The channel.userId is needed for two purposes:\n // - authorUserId on the CustomerInteraction row\n // - default visibility ('private' for user-scoped, 'shared' for tenant-scoped)\n //\n // We look up the channel only when channelId is provided in the event payload.\n let channelUserId: string | null = null\n if (typeof payload.channelId === 'string' && payload.channelId) {\n const channel = (await findOneWithDecryption(\n em,\n 'CommunicationChannel' as any,\n { id: payload.channelId, tenantId, organizationId } as any,\n undefined,\n dscope,\n )) as { userId?: string | null } | null\n channelUserId = channel?.userId ?? null\n }\n\n // \u2500\u2500 (3) Collect recipient addresses \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n //\n // The channel metadata shape differs by provider and direction:\n // - Outbound (Gmail): subject/to/cc/bcc/from are in channelMetadata\n // (merged from GmailEmailNativeMetadata by deliver-outbound-message.ts)\n // - Inbound (Gmail): from/to/cc/bcc/subject are in channelPayload\n // (from NormalizedInboundMessage.channelPayload)\n //\n // We check channelMetadata first (works for outbound + any provider that\n // writes addresses there), then fall back to channelPayload for inbound.\n const rawAddresses: unknown[] = []\n\n // For inbound IMAP messages we ALWAYS read addresses from `channelPayload`,\n // not `channelMetadata`. The IMAP adapter stores raw provider headers in\n // `channelMetadata` (where `from` is a JSON-stringified string that's\n // useless for address matching), and the structured normalized addresses\n // in `channelPayload.from` / `.to` / `.cc` / `.bcc`. Reading metadata\n // first poisoned `rawAddresses` with stringified-JSON garbage so the\n // payload-fallback never ran.\n //\n // Priority: payloadJson (canonical normalized shape) \u2192 metaJson fallback\n // (for legacy/outbound providers that still write addresses there).\n if (payloadJson) {\n collectAddressField(rawAddresses, payloadJson.from)\n collectAddressField(rawAddresses, payloadJson.to)\n collectAddressField(rawAddresses, payloadJson.cc)\n collectAddressField(rawAddresses, payloadJson.bcc)\n }\n\n if (rawAddresses.length === 0 && metaJson) {\n collectAddressField(rawAddresses, metaJson.from)\n collectAddressField(rawAddresses, metaJson.to)\n collectAddressField(rawAddresses, metaJson.cc)\n collectAddressField(rawAddresses, metaJson.bcc)\n }\n\n const normalized = normalizeAddresses(rawAddresses as string[])\n\n // \u2500\u2500 (4) Optional explicit crmPersonId hint \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n //\n // The outbound compose route stores `crmPersonId` in channelMetadata so the\n // subscriber can link to the intended Person even if their address isn't in\n // the recipient list (e.g. typo in To: field).\n const crmPersonIdHint =\n typeof metaJson?.crmPersonId === 'string' ? (metaJson!.crmPersonId as string) : null\n\n // Defense-in-depth: the crmPersonId hint is written by the compose route\n // (which already verifies tenant ownership), but the subscriber MUST re-verify\n // the hinted Person belongs to THIS tenant before linking. Otherwise a stale or\n // forged hint could attach an interaction to a Person in another tenant.\n let crmPersonId: string | null = null\n if (crmPersonIdHint) {\n const hintedPerson = await findOneWithDecryption(\n em,\n CustomerEntity,\n { id: crmPersonIdHint, kind: 'person', tenantId, organizationId, deletedAt: null } as any,\n undefined,\n dscope,\n )\n if (hintedPerson) crmPersonId = crmPersonIdHint\n }\n\n // Early exit: no addresses AND no hint \u2192 nothing to link.\n if (normalized.length === 0 && !crmPersonId) {\n // Before giving up, try threading-inheritance (TC-CRM-EMAIL-005).\n await handleThreadingInheritance(em, link, linkId, tenantId, organizationId, channelUserId, metaJson, payloadJson)\n return\n }\n\n // \u2500\u2500 (5) Resolve People by address \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const matched = await findPeopleByAddresses(em, normalized, tenantId, organizationId)\n const personIdSet = new Set<string>(matched.map((m) => m.id))\n if (crmPersonId) personIdSet.add(crmPersonId)\n\n if (personIdSet.size === 0) {\n // Try threading-inheritance before giving up.\n await handleThreadingInheritance(em, link, linkId, tenantId, organizationId, channelUserId, metaJson, payloadJson)\n return\n }\n\n // \u2500\u2500 (6) Determine visibility \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n //\n // Priority order:\n // 1. Explicit `crmVisibility` in channelMetadata (set by compose route)\n // 2. Channel owner: 'private' if user-scoped, 'shared' if tenant-scoped\n const linkDirection =\n link.direction === 'inbound' || link.direction === 'outbound' ? link.direction : null\n const visibility: 'private' | 'shared' = resolveVisibility(metaJson, channelUserId, linkDirection)\n\n // \u2500\u2500 (7) Extract subject / body / timestamps \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const subject =\n typeof metaJson?.subject === 'string'\n ? (metaJson!.subject as string)\n : typeof payloadJson?.subject === 'string'\n ? (payloadJson!.subject as string)\n : null\n\n const bodyText =\n typeof metaJson?.bodyText === 'string'\n ? (metaJson!.bodyText as string)\n : typeof payloadJson?.text === 'string'\n ? (payloadJson!.text as string)\n : null\n\n const occurredAt = link.createdAt instanceof Date ? link.createdAt : new Date()\n const providerKey =\n typeof link.providerKey === 'string' ? (link.providerKey as string) : null\n\n // \u2500\u2500 (8) INSERT one CustomerInteraction per matched Person \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n await persistInteractions(\n em,\n personIdSet,\n {\n linkId,\n tenantId,\n organizationId,\n interactionType: 'email',\n title: subject,\n body: bodyText,\n authorUserId: channelUserId,\n occurredAt,\n visibility,\n channelProviderKey: providerKey,\n },\n )\n}\n\n// \u2500\u2500 Threading-inheritance fallback \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * When direct address matching finds 0 people AND crmPersonId is absent, look\n * up parent message references from channelMetadata (`inReplyTo` + `references`).\n *\n * For each reference (an RFC2822 Message-ID), find a prior MessageChannelLink\n * in this tenant whose `channelMetadata.messageId` matches. For each such\n * parent link, find any existing email CustomerInteraction rows where\n * `externalMessageId = parent.id` \u2014 and link THIS message to the same Persons.\n *\n * This enables TC-CRM-EMAIL-005: a reply from an unknown address is still\n * attached to alice's timeline because the original thread was.\n */\nasync function handleThreadingInheritance(\n em: EntityManager,\n _link: Record<string, unknown>,\n linkId: string,\n tenantId: string,\n organizationId: string | null,\n channelUserId: string | null,\n metaJson: Record<string, unknown> | null,\n payloadJson: Record<string, unknown> | null,\n): Promise<void> {\n // \u2500\u2500 Primary: inherit Person(s) from the hub's authoritative thread \u2500\u2500\u2500\u2500\u2500\u2500\n //\n // The hub threads an inbound reply into the same `messages.message.thread_id`\n // as the outbound that started the conversation (via the thread token /\n // subject+participants matcher). That outbound is already linked to the CRM\n // Person (through the `crmPersonId` hint set by the compose route). So a reply\n // inherits the Person of any existing email interaction in the same thread.\n //\n // This is the dependable join where the alternatives are not:\n // - Address matching (`findPeopleByAddresses`) filters the *encrypted*\n // `primary_email` column by a plaintext value, which never matches when\n // tenant data encryption is on (ciphertext != plaintext).\n // - RFC Message-IDs are rewritten by some providers (e.g. Gmail) on send,\n // so the legacy In-Reply-To/References inheritance below also misses.\n // The hub thread id survives both, so we resolve by it first.\n const inboundMessageId = typeof _link.messageId === 'string' ? _link.messageId : null\n if (inboundMessageId && organizationId) {\n const threadPersonRows = (await em.getConnection().execute(\n `SELECT DISTINCT ci.entity_id AS entity_id\n FROM messages inbound_m\n JOIN messages thread_m ON thread_m.thread_id = inbound_m.thread_id\n JOIN message_channel_links mcl ON mcl.message_id = thread_m.id\n JOIN customer_interactions ci ON ci.external_message_id = mcl.id\n WHERE inbound_m.id = ?\n AND inbound_m.tenant_id = ?\n AND inbound_m.organization_id = ?\n AND inbound_m.deleted_at IS NULL\n AND inbound_m.thread_id IS NOT NULL\n AND thread_m.tenant_id = ?\n AND thread_m.organization_id = ?\n AND thread_m.deleted_at IS NULL\n AND mcl.tenant_id = ?\n AND mcl.organization_id = ?\n AND ci.tenant_id = ?\n AND ci.organization_id = ?\n AND ci.interaction_type = 'email'\n AND ci.deleted_at IS NULL\n AND ci.entity_id IS NOT NULL\n LIMIT 200`,\n [\n inboundMessageId,\n tenantId,\n organizationId,\n tenantId,\n organizationId,\n tenantId,\n organizationId,\n tenantId,\n organizationId,\n ],\n )) as Array<{ entity_id: string }>\n const threadPersonIds = new Set<string>(\n threadPersonRows.map((row) => row.entity_id).filter((id): id is string => !!id),\n )\n if (threadPersonIds.size > 0) {\n const subject =\n typeof metaJson?.subject === 'string'\n ? (metaJson.subject as string)\n : typeof payloadJson?.subject === 'string'\n ? (payloadJson.subject as string)\n : null\n const bodyText =\n typeof payloadJson?.text === 'string'\n ? (payloadJson.text as string)\n : typeof metaJson?.bodyText === 'string'\n ? (metaJson.bodyText as string)\n : null\n const occurredAt = _link.createdAt instanceof Date ? (_link.createdAt as Date) : new Date()\n const providerKey = typeof _link.providerKey === 'string' ? (_link.providerKey as string) : null\n await persistInteractions(em, threadPersonIds, {\n linkId,\n tenantId,\n organizationId,\n interactionType: 'email',\n title: subject,\n body: bodyText,\n authorUserId: channelUserId,\n occurredAt,\n visibility: channelUserId ? 'private' : 'shared',\n channelProviderKey: providerKey,\n })\n return\n }\n }\n\n // \u2500\u2500 Fallback: legacy In-Reply-To / References Message-ID inheritance \u2500\u2500\u2500\u2500\u2500\n // Collect reference message-ids from inReplyTo + references\n const refIds: string[] = []\n const inReplyTo =\n typeof metaJson?.inReplyTo === 'string'\n ? stripBrackets(metaJson!.inReplyTo as string)\n : typeof payloadJson?.inReplyTo === 'string'\n ? stripBrackets(payloadJson!.inReplyTo as string)\n : null\n if (inReplyTo) refIds.push(inReplyTo)\n\n const refs =\n Array.isArray(metaJson?.references)\n ? (metaJson!.references as unknown[])\n : Array.isArray(payloadJson?.references)\n ? (payloadJson!.references as unknown[])\n : []\n for (const r of refs) {\n if (typeof r === 'string') {\n const stripped = stripBrackets(r)\n if (stripped && !refIds.includes(stripped)) refIds.push(stripped)\n }\n }\n\n if (refIds.length === 0) return\n\n // Find parent MessageChannelLinks whose channelMetadata.messageId is in refIds.\n // Bounded lookup: narrow to the candidate Message-IDs directly in SQL instead\n // of loading every link in the tenant and filtering in JS (which does not scale\n // on a busy mailbox \u2014 this path runs for every inbound reply that didn't match a\n // Person by address). channel_metadata is NOT an encrypted column, so a\n // parameterized raw query is safe; we match both bracketed (`<id>`) and\n // unbracketed (`id`) Message-ID storage forms, capped to a sane page.\n const dscope = { tenantId, organizationId }\n const messageIdCandidates = Array.from(\n new Set(refIds.flatMap((ref) => [ref, `<${ref}>`])),\n )\n const placeholders = messageIdCandidates.map(() => '?').join(', ')\n const parentLinkRows = (await em.getConnection().execute(\n `SELECT id FROM message_channel_links\n WHERE tenant_id = ?\n AND organization_id = ?\n AND channel_metadata->>'messageId' IN (${placeholders})\n LIMIT 200`,\n [tenantId, organizationId, ...messageIdCandidates],\n )) as Array<{ id: string }>\n\n const matchedParentIds = parentLinkRows.map((parentLink) => parentLink.id)\n\n if (matchedParentIds.length === 0) return\n\n // Find existing email CustomerInteraction rows for those parent links.\n const parentInteractions = (await findWithDecryption(\n em,\n CustomerInteraction,\n {\n externalMessageId: { $in: matchedParentIds },\n tenantId,\n organizationId,\n interactionType: 'email',\n deletedAt: null,\n } as any,\n undefined,\n dscope,\n )) as Array<{ entity: { id: string } }>\n\n const inheritedPersonIdSet = new Set<string>(\n parentInteractions.map((pi) => pi.entity?.id).filter(Boolean) as string[],\n )\n\n if (inheritedPersonIdSet.size === 0) return\n\n const visibility: 'private' | 'shared' = channelUserId ? 'private' : 'shared'\n const link = _link\n const inheritedMeta = (link.channelMetadata ?? null) as Record<string, unknown> | null\n const subject =\n typeof inheritedMeta?.subject === 'string' ? (inheritedMeta.subject as string) : null\n const occurredAt = link.createdAt instanceof Date ? (link.createdAt as Date) : new Date()\n const providerKey =\n typeof link.providerKey === 'string' ? (link.providerKey as string) : null\n\n await persistInteractions(\n em,\n inheritedPersonIdSet,\n {\n linkId,\n tenantId,\n organizationId,\n interactionType: 'email',\n title: subject,\n body: null,\n authorUserId: channelUserId,\n occurredAt,\n visibility,\n channelProviderKey: providerKey,\n },\n )\n}\n\n// \u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ninterface InteractionData {\n linkId: string\n tenantId: string\n organizationId: string | null\n interactionType: string\n title: string | null\n body: string | null\n authorUserId: string | null\n occurredAt: Date\n visibility: 'private' | 'shared'\n channelProviderKey: string | null\n}\n\nasync function persistInteractions(\n em: EntityManager,\n personIdSet: Set<string>,\n data: InteractionData,\n): Promise<void> {\n for (const personId of personIdSet) {\n // Fork per row so a unique-violation on one row doesn't poison the identity\n // map for subsequent rows. The parent em's connection pool is reused.\n const rowEm = em.fork()\n // Use em.getReference so we satisfy the ManyToOne relation without a\n // redundant SELECT \u2014 MikroORM will flush the FK column directly.\n const entityRef = rowEm.getReference(CustomerEntity, personId)\n const interaction = rowEm.create(CustomerInteraction, {\n tenantId: data.tenantId,\n organizationId: data.organizationId,\n entity: entityRef,\n interactionType: data.interactionType,\n title: data.title,\n body: data.body,\n authorUserId: data.authorUserId,\n occurredAt: data.occurredAt,\n // Emails are logged AFTER they're sent/received \u2014 they are not\n // scheduled work. Without an explicit status the entity default\n // ('planned') combined with a past `occurredAt` makes the activity\n // timeline render the email as \"overdue\", which is the wrong UX.\n // The canonical \"completed\" value is `'done'` per\n // `validators.ts:interactionStatusValues = ['planned', 'done', 'canceled']`\n // \u2014 `'completed'` is a legacy spelling that the enricher accepts\n // defensively but the activity timeline `isOverdue` predicate does NOT.\n status: 'done',\n externalMessageId: data.linkId,\n visibility: data.visibility,\n channelProviderKey: data.channelProviderKey,\n } as any)\n try {\n await rowEm.flush()\n } catch (err) {\n // Idempotency: swallow unique-violation on (entity_id, external_message_id).\n // The partial unique index `customer_interactions_email_dedupe_uq` guarantees\n // at-most-once semantics across retries.\n const code = (err as { code?: string }).code\n if (code !== POSTGRES_UNIQUE_VIOLATION) throw err\n // Duplicate (retried delivery) \u2014 already linked, skip the refresh signal.\n continue\n }\n // Live-refresh signal for the CRM Person page (clientBroadcast \u2192 SSE). The\n // interaction is already persisted, so a failed emit must not abort linking\n // or fail this persistent (retried) subscriber.\n try {\n await emitCustomersEvent('customers.email.linked', {\n personId,\n interactionId: interaction.id,\n tenantId: data.tenantId,\n organizationId: data.organizationId,\n })\n } catch {\n /* swallow \u2014 UI refresh signal is non-critical */\n }\n }\n}\n\nfunction resolveVisibility(\n metaJson: Record<string, unknown> | null,\n channelUserId: string | null,\n direction: 'inbound' | 'outbound' | null,\n): 'private' | 'shared' {\n // The explicit `crmVisibility` override is written ONLY by the outbound compose\n // route. Inbound `channelMetadata` is provider-derived (and therefore attacker-\n // influenceable), so it MUST NOT be able to downgrade a user-owned channel's\n // inbound mail from private \u2192 shared. Honor the override on outbound only.\n if (direction === 'outbound') {\n if (metaJson?.crmVisibility === 'shared') return 'shared'\n if (metaJson?.crmVisibility === 'private') return 'private'\n }\n // Tenant-scoped channels (no userId) \u2192 shared; user-scoped \u2192 private.\n return channelUserId ? 'private' : 'shared'\n}\n\n/**\n * Collect email address strings from a field that may be:\n * - a plain string: 'alice@example.com'\n * - an array of strings: ['alice@example.com', 'bob@example.com']\n * - an object with an `address` property: { address: 'alice@example.com', name: 'Alice' }\n * - an array of such objects\n */\nfunction collectAddressField(out: unknown[], value: unknown): void {\n if (!value) return\n if (typeof value === 'string') {\n out.push(value)\n return\n }\n if (Array.isArray(value)) {\n for (const item of value) {\n if (typeof item === 'string') {\n out.push(item)\n } else if (item && typeof item === 'object' && typeof (item as Record<string, unknown>).address === 'string') {\n out.push((item as Record<string, unknown>).address as string)\n }\n }\n return\n }\n if (typeof value === 'object' && typeof (value as Record<string, unknown>).address === 'string') {\n out.push((value as Record<string, unknown>).address as string)\n }\n}\n\nfunction stripBrackets(value: string): string {\n const trimmed = value.trim()\n if (trimmed.startsWith('<') && trimmed.endsWith('>')) {\n return trimmed.slice(1, -1)\n }\n return trimmed\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["subject", "occurredAt", "providerKey"]
7
+ }