@open-mercato/core 0.9.0 → 0.9.1-develop.7459.1.99e3accd30

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 (243) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/generated/entities/customer_dictionary_entry/index.js +2 -0
  3. package/dist/generated/entities/customer_dictionary_entry/index.js.map +2 -2
  4. package/dist/generated/entities/customer_interaction/index.js +2 -0
  5. package/dist/generated/entities/customer_interaction/index.js.map +2 -2
  6. package/dist/generated/entity-fields-registry.js +2 -0
  7. package/dist/generated/entity-fields-registry.js.map +2 -2
  8. package/dist/modules/catalog/commands/categories.js +1 -1
  9. package/dist/modules/catalog/commands/categories.js.map +2 -2
  10. package/dist/modules/catalog/commands/productDeleteChildren.js +389 -0
  11. package/dist/modules/catalog/commands/productDeleteChildren.js.map +7 -0
  12. package/dist/modules/catalog/commands/products.js +52 -17
  13. package/dist/modules/catalog/commands/products.js.map +2 -2
  14. package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js +4 -6
  15. package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js.map +2 -2
  16. package/dist/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.js +4 -6
  17. package/dist/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.js.map +2 -2
  18. package/dist/modules/communication_channels/api/post/oauth/[provider]/initiate/route.js +47 -11
  19. package/dist/modules/communication_channels/api/post/oauth/[provider]/initiate/route.js.map +3 -3
  20. package/dist/modules/communication_channels/commands/deliver-outbound-message.js +5 -1
  21. package/dist/modules/communication_channels/commands/deliver-outbound-message.js.map +2 -2
  22. package/dist/modules/communication_channels/lib/inbound-message-origin.js +1 -1
  23. package/dist/modules/communication_channels/lib/inbound-message-origin.js.map +2 -2
  24. package/dist/modules/communication_channels/lib/oauth-state.js +4 -1
  25. package/dist/modules/communication_channels/lib/oauth-state.js.map +2 -2
  26. package/dist/modules/communication_channels/lib/use-connect-channel.js +10 -0
  27. package/dist/modules/communication_channels/lib/use-connect-channel.js.map +2 -2
  28. package/dist/modules/customers/api/activity-types/route.js +151 -0
  29. package/dist/modules/customers/api/activity-types/route.js.map +7 -0
  30. package/dist/modules/customers/api/deals/[id]/people/route.js +58 -5
  31. package/dist/modules/customers/api/deals/[id]/people/route.js.map +2 -2
  32. package/dist/modules/customers/api/dictionaries/[kind]/[id]/route.js +13 -5
  33. package/dist/modules/customers/api/dictionaries/[kind]/[id]/route.js.map +2 -2
  34. package/dist/modules/customers/api/dictionaries/[kind]/route.js +20 -5
  35. package/dist/modules/customers/api/dictionaries/[kind]/route.js.map +2 -2
  36. package/dist/modules/customers/api/interactions/route.js +3 -0
  37. package/dist/modules/customers/api/interactions/route.js.map +2 -2
  38. package/dist/modules/customers/api/interactions/tasks/route.js +1 -0
  39. package/dist/modules/customers/api/interactions/tasks/route.js.map +2 -2
  40. package/dist/modules/customers/backend/config/customers/pipeline-stages/page.meta.js +18 -0
  41. package/dist/modules/customers/backend/config/customers/pipeline-stages/page.meta.js.map +2 -2
  42. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js +0 -23
  43. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js.map +2 -2
  44. package/dist/modules/customers/backend/customers/deals/[id]/page.js +15 -17
  45. package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
  46. package/dist/modules/customers/calendar-event-types.js +548 -0
  47. package/dist/modules/customers/calendar-event-types.js.map +7 -0
  48. package/dist/modules/customers/commands/dictionaries.js +70 -10
  49. package/dist/modules/customers/commands/dictionaries.js.map +2 -2
  50. package/dist/modules/customers/commands/interactions.js +187 -22
  51. package/dist/modules/customers/commands/interactions.js.map +3 -3
  52. package/dist/modules/customers/components/ActivityTypeEditor.js +288 -0
  53. package/dist/modules/customers/components/ActivityTypeEditor.js.map +7 -0
  54. package/dist/modules/customers/components/DictionarySettings.js +3 -1
  55. package/dist/modules/customers/components/DictionarySettings.js.map +2 -2
  56. package/dist/modules/customers/components/calendar/CalendarEventEditor.js +391 -297
  57. package/dist/modules/customers/components/calendar/CalendarEventEditor.js.map +3 -3
  58. package/dist/modules/customers/components/calendar/CalendarSettingsModal.js +9 -74
  59. package/dist/modules/customers/components/calendar/CalendarSettingsModal.js.map +2 -2
  60. package/dist/modules/customers/components/calendar/TimeGrid.js +20 -16
  61. package/dist/modules/customers/components/calendar/TimeGrid.js.map +2 -2
  62. package/dist/modules/customers/components/calendar/editor/EventTypePanel.js +218 -0
  63. package/dist/modules/customers/components/calendar/editor/EventTypePanel.js.map +7 -0
  64. package/dist/modules/customers/components/calendar/editor/LocationField.js +7 -3
  65. package/dist/modules/customers/components/calendar/editor/LocationField.js.map +2 -2
  66. package/dist/modules/customers/components/calendar/editor/PeopleField.js +5 -2
  67. package/dist/modules/customers/components/calendar/editor/PeopleField.js.map +2 -2
  68. package/dist/modules/customers/components/calendar/editor/PriorityField.js +3 -1
  69. package/dist/modules/customers/components/calendar/editor/PriorityField.js.map +2 -2
  70. package/dist/modules/customers/components/calendar/editor/RepeatField.js +9 -1
  71. package/dist/modules/customers/components/calendar/editor/RepeatField.js.map +2 -2
  72. package/dist/modules/customers/components/calendar/editor/ResourcesField.js +4 -1
  73. package/dist/modules/customers/components/calendar/editor/ResourcesField.js.map +2 -2
  74. package/dist/modules/customers/components/calendar/editor/ScheduleSection.js +15 -7
  75. package/dist/modules/customers/components/calendar/editor/ScheduleSection.js.map +2 -2
  76. package/dist/modules/customers/components/calendar/editor/SegmentGroup.js +3 -1
  77. package/dist/modules/customers/components/calendar/editor/SegmentGroup.js.map +2 -2
  78. package/dist/modules/customers/components/calendar/editor/hooks.js +28 -13
  79. package/dist/modules/customers/components/calendar/editor/hooks.js.map +3 -3
  80. package/dist/modules/customers/components/calendar/editor/inputs.js +14 -6
  81. package/dist/modules/customers/components/calendar/editor/inputs.js.map +2 -2
  82. package/dist/modules/customers/components/calendar/editor/useCalendarCustomFields.js +54 -0
  83. package/dist/modules/customers/components/calendar/editor/useCalendarCustomFields.js.map +7 -0
  84. package/dist/modules/customers/components/calendar/editor/useEventTypeCatalog.js +82 -0
  85. package/dist/modules/customers/components/calendar/editor/useEventTypeCatalog.js.map +7 -0
  86. package/dist/modules/customers/components/calendar/types.js +1 -0
  87. package/dist/modules/customers/components/calendar/types.js.map +2 -2
  88. package/dist/modules/customers/components/detail/CompanyPeopleSection.js +78 -343
  89. package/dist/modules/customers/components/detail/CompanyPeopleSection.js.map +2 -2
  90. package/dist/modules/customers/components/detail/CreatePersonDialog.js +12 -11
  91. package/dist/modules/customers/components/detail/CreatePersonDialog.js.map +2 -2
  92. package/dist/modules/customers/components/detail/DealForm.js +7 -3
  93. package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
  94. package/dist/modules/customers/components/detail/DealPeopleSection.js +206 -0
  95. package/dist/modules/customers/components/detail/DealPeopleSection.js.map +7 -0
  96. package/dist/modules/customers/components/detail/LinkedPeopleSection.js +342 -0
  97. package/dist/modules/customers/components/detail/LinkedPeopleSection.js.map +7 -0
  98. package/dist/modules/customers/components/detail/PersonCard.js +5 -2
  99. package/dist/modules/customers/components/detail/PersonCard.js.map +2 -2
  100. package/dist/modules/customers/components/linking/adapters/personAdapter.js +3 -0
  101. package/dist/modules/customers/components/linking/adapters/personAdapter.js.map +2 -2
  102. package/dist/modules/customers/data/entities.js +6 -0
  103. package/dist/modules/customers/data/entities.js.map +2 -2
  104. package/dist/modules/customers/data/validators.js +39 -10
  105. package/dist/modules/customers/data/validators.js.map +2 -2
  106. package/dist/modules/customers/di.js +20 -1
  107. package/dist/modules/customers/di.js.map +2 -2
  108. package/dist/modules/customers/extension-points.js +13 -0
  109. package/dist/modules/customers/extension-points.js.map +2 -2
  110. package/dist/modules/customers/lib/calendar/conflicts.js +59 -16
  111. package/dist/modules/customers/lib/calendar/conflicts.js.map +2 -2
  112. package/dist/modules/customers/lib/calendar/editorPayload.js +34 -16
  113. package/dist/modules/customers/lib/calendar/editorPayload.js.map +2 -2
  114. package/dist/modules/customers/lib/calendar/eventTypeResolver.js +197 -0
  115. package/dist/modules/customers/lib/calendar/eventTypeResolver.js.map +7 -0
  116. package/dist/modules/customers/lib/calendar/interactionApplicability.js +67 -0
  117. package/dist/modules/customers/lib/calendar/interactionApplicability.js.map +7 -0
  118. package/dist/modules/customers/lib/calendar/mapItem.js +5 -2
  119. package/dist/modules/customers/lib/calendar/mapItem.js.map +2 -2
  120. package/dist/modules/customers/lib/calendar/recurrence.js +62 -1
  121. package/dist/modules/customers/lib/calendar/recurrence.js.map +2 -2
  122. package/dist/modules/customers/lib/calendar/timezone.js +111 -0
  123. package/dist/modules/customers/lib/calendar/timezone.js.map +7 -0
  124. package/dist/modules/customers/lib/interactionReadModel.js +1 -0
  125. package/dist/modules/customers/lib/interactionReadModel.js.map +2 -2
  126. package/dist/modules/customers/lib/todoCompatibility.js +1 -0
  127. package/dist/modules/customers/lib/todoCompatibility.js.map +2 -2
  128. package/dist/modules/customers/migrations/Migration20260928175058_customers.js +17 -0
  129. package/dist/modules/customers/migrations/Migration20260928175058_customers.js.map +7 -0
  130. package/dist/modules/customers/migrations/Migration20260930110235_customers.js +17 -0
  131. package/dist/modules/customers/migrations/Migration20260930110235_customers.js.map +7 -0
  132. package/dist/modules/dictionaries/components/DictionaryTable.js +60 -51
  133. package/dist/modules/dictionaries/components/DictionaryTable.js.map +2 -2
  134. package/dist/modules/messages/subscribers/message-notification.js +13 -1
  135. package/dist/modules/messages/subscribers/message-notification.js.map +2 -2
  136. package/dist/modules/payment_gateways/api/webhook/[provider]/route.js +201 -53
  137. package/dist/modules/payment_gateways/api/webhook/[provider]/route.js.map +2 -2
  138. package/dist/modules/payment_gateways/lib/gateway-service.js +37 -11
  139. package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
  140. package/dist/modules/planner/lib/availabilityMerge.js +101 -38
  141. package/dist/modules/planner/lib/availabilityMerge.js.map +3 -3
  142. package/dist/modules/planner/services/plannerAvailabilityService.js.map +2 -2
  143. package/dist/modules/sales/components/documents/LineItemDialog.js +159 -81
  144. package/dist/modules/sales/components/documents/LineItemDialog.js.map +2 -2
  145. package/dist/modules/sales/components/documents/lineItemUtils.js +7 -0
  146. package/dist/modules/sales/components/documents/lineItemUtils.js.map +2 -2
  147. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js +12 -7
  148. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js.map +2 -2
  149. package/dist/modules/sync_excel/lib/adapters/customers.js +28 -25
  150. package/dist/modules/sync_excel/lib/adapters/customers.js.map +2 -2
  151. package/generated/entities/customer_dictionary_entry/index.ts +1 -0
  152. package/generated/entities/customer_interaction/index.ts +1 -0
  153. package/generated/entity-fields-registry.ts +2 -0
  154. package/package.json +9 -8
  155. package/src/modules/auth/i18n/de.json +3 -0
  156. package/src/modules/auth/i18n/en.json +3 -0
  157. package/src/modules/auth/i18n/es.json +3 -0
  158. package/src/modules/auth/i18n/ko.json +3 -0
  159. package/src/modules/auth/i18n/pl.json +3 -0
  160. package/src/modules/catalog/commands/categories.ts +1 -1
  161. package/src/modules/catalog/commands/productDeleteChildren.ts +557 -0
  162. package/src/modules/catalog/commands/products.ts +58 -17
  163. package/src/modules/communication_channels/api/post/channels/connect/credentials/route.ts +4 -6
  164. package/src/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.ts +4 -6
  165. package/src/modules/communication_channels/api/post/oauth/[provider]/initiate/route.ts +46 -10
  166. package/src/modules/communication_channels/commands/deliver-outbound-message.ts +6 -1
  167. package/src/modules/communication_channels/i18n/de.json +4 -0
  168. package/src/modules/communication_channels/i18n/en.json +4 -0
  169. package/src/modules/communication_channels/i18n/es.json +4 -0
  170. package/src/modules/communication_channels/i18n/ko.json +4 -0
  171. package/src/modules/communication_channels/i18n/pl.json +4 -0
  172. package/src/modules/communication_channels/lib/inbound-message-origin.ts +2 -6
  173. package/src/modules/communication_channels/lib/oauth-state.ts +4 -1
  174. package/src/modules/communication_channels/lib/use-connect-channel.ts +10 -0
  175. package/src/modules/customers/api/activity-types/route.ts +154 -0
  176. package/src/modules/customers/api/deals/[id]/people/route.ts +82 -3
  177. package/src/modules/customers/api/dictionaries/[kind]/[id]/route.ts +9 -1
  178. package/src/modules/customers/api/dictionaries/[kind]/route.ts +18 -2
  179. package/src/modules/customers/api/interactions/route.ts +4 -0
  180. package/src/modules/customers/api/interactions/tasks/route.ts +1 -0
  181. package/src/modules/customers/backend/config/customers/pipeline-stages/page.meta.ts +20 -0
  182. package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.ts +0 -29
  183. package/src/modules/customers/backend/customers/deals/[id]/page.tsx +15 -17
  184. package/src/modules/customers/calendar-event-types.ts +674 -0
  185. package/src/modules/customers/commands/dictionaries.ts +84 -8
  186. package/src/modules/customers/commands/interactions.ts +244 -21
  187. package/src/modules/customers/components/ActivityTypeEditor.tsx +239 -0
  188. package/src/modules/customers/components/DictionarySettings.tsx +5 -3
  189. package/src/modules/customers/components/calendar/CalendarEventEditor.tsx +347 -218
  190. package/src/modules/customers/components/calendar/CalendarSettingsModal.tsx +18 -96
  191. package/src/modules/customers/components/calendar/TimeGrid.tsx +20 -18
  192. package/src/modules/customers/components/calendar/editor/EventTypePanel.tsx +230 -0
  193. package/src/modules/customers/components/calendar/editor/LocationField.tsx +7 -1
  194. package/src/modules/customers/components/calendar/editor/PeopleField.tsx +4 -0
  195. package/src/modules/customers/components/calendar/editor/PriorityField.tsx +3 -0
  196. package/src/modules/customers/components/calendar/editor/RepeatField.tsx +10 -0
  197. package/src/modules/customers/components/calendar/editor/ResourcesField.tsx +4 -0
  198. package/src/modules/customers/components/calendar/editor/ScheduleSection.tsx +16 -4
  199. package/src/modules/customers/components/calendar/editor/SegmentGroup.tsx +3 -0
  200. package/src/modules/customers/components/calendar/editor/hooks.ts +29 -13
  201. package/src/modules/customers/components/calendar/editor/inputs.tsx +17 -5
  202. package/src/modules/customers/components/calendar/editor/useCalendarCustomFields.ts +49 -0
  203. package/src/modules/customers/components/calendar/editor/useEventTypeCatalog.ts +100 -0
  204. package/src/modules/customers/components/calendar/types.ts +1 -0
  205. package/src/modules/customers/components/detail/CompanyPeopleSection.tsx +94 -388
  206. package/src/modules/customers/components/detail/CreatePersonDialog.tsx +39 -19
  207. package/src/modules/customers/components/detail/DealForm.tsx +7 -3
  208. package/src/modules/customers/components/detail/DealPeopleSection.tsx +256 -0
  209. package/src/modules/customers/components/detail/LinkedPeopleSection.tsx +474 -0
  210. package/src/modules/customers/components/detail/PersonCard.tsx +12 -4
  211. package/src/modules/customers/components/detail/types.ts +1 -0
  212. package/src/modules/customers/components/linking/adapters/personAdapter.tsx +4 -0
  213. package/src/modules/customers/data/entities.ts +8 -1
  214. package/src/modules/customers/data/validators.ts +42 -7
  215. package/src/modules/customers/di.ts +21 -0
  216. package/src/modules/customers/extension-points.ts +13 -0
  217. package/src/modules/customers/i18n/de.json +104 -8
  218. package/src/modules/customers/i18n/en.json +104 -8
  219. package/src/modules/customers/i18n/es.json +104 -8
  220. package/src/modules/customers/i18n/ko.json +104 -8
  221. package/src/modules/customers/i18n/pl.json +104 -8
  222. package/src/modules/customers/lib/calendar/conflicts.ts +106 -27
  223. package/src/modules/customers/lib/calendar/editorPayload.ts +39 -19
  224. package/src/modules/customers/lib/calendar/eventTypeResolver.ts +258 -0
  225. package/src/modules/customers/lib/calendar/interactionApplicability.ts +99 -0
  226. package/src/modules/customers/lib/calendar/mapItem.ts +6 -2
  227. package/src/modules/customers/lib/calendar/recurrence.ts +65 -1
  228. package/src/modules/customers/lib/calendar/timezone.ts +117 -0
  229. package/src/modules/customers/lib/interactionReadModel.ts +1 -0
  230. package/src/modules/customers/lib/todoCompatibility.ts +2 -0
  231. package/src/modules/customers/migrations/.snapshot-open-mercato.json +34 -0
  232. package/src/modules/customers/migrations/Migration20260928175058_customers.ts +15 -0
  233. package/src/modules/customers/migrations/Migration20260930110235_customers.ts +15 -0
  234. package/src/modules/dictionaries/components/DictionaryTable.tsx +63 -56
  235. package/src/modules/messages/subscribers/message-notification.ts +13 -3
  236. package/src/modules/payment_gateways/api/webhook/[provider]/route.ts +243 -62
  237. package/src/modules/payment_gateways/lib/gateway-service.ts +39 -12
  238. package/src/modules/planner/lib/availabilityMerge.ts +106 -41
  239. package/src/modules/planner/services/plannerAvailabilityService.ts +4 -0
  240. package/src/modules/sales/components/documents/LineItemDialog.tsx +194 -111
  241. package/src/modules/sales/components/documents/lineItemUtils.ts +18 -0
  242. package/src/modules/staff/backend/staff/time-tracking/projects/[id]/page.tsx +25 -13
  243. package/src/modules/sync_excel/lib/adapters/customers.ts +33 -27
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/customers/migrations/Migration20260928175058_customers.ts"],
4
+ "sourcesContent": ["import { Migration } from '@mikro-orm/migrations';\n\nexport class Migration20260928175058_customers extends Migration {\n\n override name = 'Migration20260928175058_customers';\n\n override up(): void | Promise<void> {\n this.addSql(`alter table \"customer_dictionary_entries\" add \"activity_type_behavior\" jsonb null;`);\n }\n\n override down(): void | Promise<void> {\n this.addSql(`alter table \"customer_dictionary_entries\" drop column \"activity_type_behavior\";`);\n }\n\n}\n"],
5
+ "mappings": "AAAA,SAAS,iBAAiB;AAEnB,MAAM,0CAA0C,UAAU;AAAA,EAA1D;AAAA;AAEL,SAAS,OAAO;AAAA;AAAA,EAEP,KAA2B;AAClC,SAAK,OAAO,oFAAoF;AAAA,EAClG;AAAA,EAES,OAA6B;AACpC,SAAK,OAAO,iFAAiF;AAAA,EAC/F;AAEF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,17 @@
1
+ import { Migration } from "@mikro-orm/migrations";
2
+ class Migration20260930110235_customers extends Migration {
3
+ constructor() {
4
+ super(...arguments);
5
+ this.name = "Migration20260930110235_customers";
6
+ }
7
+ up() {
8
+ this.addSql(`alter table "customer_interactions" add "timezone" text null;`);
9
+ }
10
+ down() {
11
+ this.addSql(`alter table "customer_interactions" drop column "timezone";`);
12
+ }
13
+ }
14
+ export {
15
+ Migration20260930110235_customers
16
+ };
17
+ //# sourceMappingURL=Migration20260930110235_customers.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/customers/migrations/Migration20260930110235_customers.ts"],
4
+ "sourcesContent": ["import { Migration } from '@mikro-orm/migrations';\n\nexport class Migration20260930110235_customers extends Migration {\n\n override name = 'Migration20260930110235_customers';\n\n override up(): void | Promise<void> {\n this.addSql(`alter table \"customer_interactions\" add \"timezone\" text null;`);\n }\n\n override down(): void | Promise<void> {\n this.addSql(`alter table \"customer_interactions\" drop column \"timezone\";`);\n }\n\n}\n"],
5
+ "mappings": "AAAA,SAAS,iBAAiB;AAEnB,MAAM,0CAA0C,UAAU;AAAA,EAA1D;AAAA;AAEL,SAAS,OAAO;AAAA;AAAA,EAEP,KAA2B;AAClC,SAAK,OAAO,+DAA+D;AAAA,EAC7E;AAAA,EAES,OAA6B;AACpC,SAAK,OAAO,6DAA6D;AAAA,EAC3E;AAEF;",
6
+ "names": []
7
+ }
@@ -72,59 +72,68 @@ function DictionaryTable({
72
72
  onEdit(entry);
73
73
  } : void 0;
74
74
  return /* @__PURE__ */ jsx(
75
- DataTable,
75
+ "div",
76
76
  {
77
- title: translations.title,
78
- titleHeadingLevel: 2,
79
- actions,
80
- columns,
81
- data: paginated,
82
- embedded: true,
83
- sortable: true,
84
- sorting,
85
- onSortingChange: setSorting,
86
- searchValue: search,
87
- onSearchChange: (value) => {
88
- setSearch(value);
89
- setPage(1);
90
- },
91
- searchPlaceholder: translations.searchPlaceholder,
92
- isLoading: loading,
93
- emptyState: /* @__PURE__ */ jsx("p", { className: "py-10 text-center text-sm text-muted-foreground", children: translations.emptyLabel }),
94
- pagination: {
95
- page,
96
- pageSize,
97
- total: filtered.length,
98
- totalPages,
99
- onPageChange: setPage
100
- },
101
- refreshButton: onRefresh ? {
102
- label: translations.refreshLabel,
103
- onRefresh,
104
- isRefreshing: loading
105
- } : void 0,
106
- onRowClick: handleRowClick,
107
- rowActions: canManage ? (entry) => {
108
- if (!entry) return null;
109
- if (entry.isInherited) return null;
110
- const items = [];
111
- if (onEdit) {
112
- items.push({
113
- id: "edit",
114
- label: translations.editLabel,
115
- onSelect: () => onEdit(entry)
116
- });
77
+ role: "region",
78
+ "aria-label": translations.title,
79
+ tabIndex: 0,
80
+ className: "w-full min-w-0 max-w-full overflow-x-auto focus-visible:outline-none focus-visible:shadow-focus",
81
+ children: /* @__PURE__ */ jsx(
82
+ DataTable,
83
+ {
84
+ title: translations.title,
85
+ titleHeadingLevel: 2,
86
+ actions,
87
+ columns,
88
+ data: paginated,
89
+ embedded: true,
90
+ sortable: true,
91
+ sorting,
92
+ onSortingChange: setSorting,
93
+ searchValue: search,
94
+ onSearchChange: (value) => {
95
+ setSearch(value);
96
+ setPage(1);
97
+ },
98
+ searchPlaceholder: translations.searchPlaceholder,
99
+ isLoading: loading,
100
+ emptyState: /* @__PURE__ */ jsx("p", { className: "py-10 text-center text-sm text-muted-foreground", children: translations.emptyLabel }),
101
+ pagination: {
102
+ page,
103
+ pageSize,
104
+ total: filtered.length,
105
+ totalPages,
106
+ onPageChange: setPage
107
+ },
108
+ refreshButton: onRefresh ? {
109
+ label: translations.refreshLabel,
110
+ onRefresh,
111
+ isRefreshing: loading
112
+ } : void 0,
113
+ onRowClick: handleRowClick,
114
+ rowActions: canManage ? (entry) => {
115
+ if (!entry) return null;
116
+ if (entry.isInherited) return null;
117
+ const items = [];
118
+ if (onEdit) {
119
+ items.push({
120
+ id: "edit",
121
+ label: translations.editLabel,
122
+ onSelect: () => onEdit(entry)
123
+ });
124
+ }
125
+ if (onDelete) {
126
+ items.push({
127
+ id: "delete",
128
+ label: translations.deleteLabel,
129
+ onSelect: () => onDelete(entry),
130
+ destructive: true
131
+ });
132
+ }
133
+ return items.length ? /* @__PURE__ */ jsx(RowActions, { items }) : null;
134
+ } : void 0
117
135
  }
118
- if (onDelete) {
119
- items.push({
120
- id: "delete",
121
- label: translations.deleteLabel,
122
- onSelect: () => onDelete(entry),
123
- destructive: true
124
- });
125
- }
126
- return items.length ? /* @__PURE__ */ jsx(RowActions, { items }) : null;
127
- } : void 0
136
+ )
128
137
  }
129
138
  );
130
139
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/dictionaries/components/DictionaryTable.tsx"],
4
- "sourcesContent": ["import * as React from 'react'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport type { SortingState } from '@tanstack/react-table'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { RowActions, type RowActionItem } from '@open-mercato/ui/backend/RowActions'\nimport { renderDictionaryColor, renderDictionaryIcon } from './dictionaryAppearance'\n\nexport type DictionaryTableEntry = {\n id: string\n value: string\n label: string\n color: string | null\n icon: string | null\n usageCount?: number\n organizationId?: string | null\n tenantId?: string | null\n isInherited?: boolean\n createdAt?: string | null\n updatedAt?: string | null\n}\n\nexport type DictionaryTableTranslations = {\n title: string\n valueColumn: string\n labelColumn: string\n appearanceColumn: string\n addLabel: string\n editLabel: string\n deleteLabel: string\n refreshLabel: string\n inheritedLabel: string\n inheritedTooltip: string\n emptyLabel: string\n usageCountLabel?: string\n searchPlaceholder?: string\n}\n\ntype DictionaryTableProps = {\n entries: DictionaryTableEntry[]\n loading?: boolean\n canManage?: boolean\n onCreate?: () => void\n onEdit?: (entry: DictionaryTableEntry) => void\n onDelete?: (entry: DictionaryTableEntry) => void\n onRefresh?: () => void\n translations: DictionaryTableTranslations\n}\n\nexport function DictionaryTable({\n entries,\n loading = false,\n canManage = false,\n onCreate,\n onEdit,\n onDelete,\n onRefresh,\n translations,\n}: DictionaryTableProps) {\n const [search, setSearch] = React.useState('')\n const [sorting, setSorting] = React.useState<SortingState>([{ id: 'value', desc: false }])\n const [page, setPage] = React.useState(1)\n const pageSize = 50\n\n const filtered = React.useMemo(() => {\n if (!search.trim()) return entries\n const term = search.trim().toLowerCase()\n return entries.filter((entry) => {\n return (\n entry.value.toLowerCase().includes(term) ||\n (entry.label ?? '').toLowerCase().includes(term)\n )\n })\n }, [entries, search])\n\n const paginated = React.useMemo(() => {\n const start = (page - 1) * pageSize\n return filtered.slice(start, start + pageSize)\n }, [filtered, page])\n\n const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize))\n\n const columns = React.useMemo<ColumnDef<DictionaryTableEntry>[]>(() => [\n {\n accessorKey: 'value',\n header: translations.valueColumn,\n meta: { priority: 1 },\n cell: ({ getValue, row }) => (\n <div className=\"space-y-1\">\n <div className=\"flex items-center gap-2\">\n <span className=\"font-medium\">{String(getValue())}</span>\n {row.original.isInherited ? (\n <span className=\"rounded bg-muted px-2 py-0.5 text-xs text-muted-foreground\" title={translations.inheritedTooltip}>\n {translations.inheritedLabel}\n </span>\n ) : null}\n </div>\n {typeof row.original.usageCount === 'number' && row.original.usageCount > 0 && translations.usageCountLabel ? (\n <div className=\"text-xs text-muted-foreground\">\n {translations.usageCountLabel.replace('{{count}}', String(row.original.usageCount))}\n </div>\n ) : null}\n </div>\n ),\n },\n {\n accessorKey: 'label',\n header: translations.labelColumn,\n meta: { priority: 2 },\n cell: ({ getValue }) => <span>{String(getValue() ?? '')}</span>,\n },\n {\n id: 'appearance',\n header: translations.appearanceColumn,\n meta: { priority: 3 },\n cell: ({ row }) => {\n const { color, icon } = row.original\n if (!color && !icon) return <span className=\"text-muted-foreground\">\u2014</span>\n return (\n <div className=\"flex items-center gap-2\">\n {color ? renderDictionaryColor(color, 'h-4 w-4 rounded-full border border-border') : null}\n {icon ? renderDictionaryIcon(icon, 'h-4 w-4') : null}\n </div>\n )\n },\n },\n ], [translations.appearanceColumn, translations.inheritedLabel, translations.inheritedTooltip, translations.labelColumn, translations.valueColumn])\n\n const actions = React.useMemo(() => {\n if (!canManage || !onCreate) return null\n return (\n <Button size=\"sm\" onClick={onCreate}>\n {translations.addLabel}\n </Button>\n )\n }, [canManage, onCreate, translations.addLabel])\n\n const handleRowClick = canManage && onEdit\n ? (entry: DictionaryTableEntry) => {\n if (entry.isInherited) return\n onEdit(entry)\n }\n : undefined\n\n return (\n <DataTable<DictionaryTableEntry>\n title={translations.title}\n titleHeadingLevel={2}\n actions={actions}\n columns={columns}\n data={paginated}\n embedded\n sortable\n sorting={sorting}\n onSortingChange={setSorting}\n searchValue={search}\n onSearchChange={(value) => {\n setSearch(value)\n setPage(1)\n }}\n searchPlaceholder={translations.searchPlaceholder}\n isLoading={loading}\n emptyState={<p className=\"py-10 text-center text-sm text-muted-foreground\">{translations.emptyLabel}</p>}\n pagination={{\n page,\n pageSize,\n total: filtered.length,\n totalPages,\n onPageChange: setPage,\n }}\n refreshButton={onRefresh ? {\n label: translations.refreshLabel,\n onRefresh,\n isRefreshing: loading,\n } : undefined}\n onRowClick={handleRowClick}\n rowActions={\n canManage\n ? (entry) => {\n if (!entry) return null\n if (entry.isInherited) return null\n const items: RowActionItem[] = []\n if (onEdit) {\n items.push({\n id: 'edit',\n label: translations.editLabel,\n onSelect: () => onEdit(entry),\n })\n }\n if (onDelete) {\n items.push({\n id: 'delete',\n label: translations.deleteLabel,\n onSelect: () => onDelete(entry),\n destructive: true,\n })\n }\n return items.length ? <RowActions items={items} /> : null\n }\n : undefined\n }\n />\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import * as React from 'react'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport type { SortingState } from '@tanstack/react-table'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { RowActions, type RowActionItem } from '@open-mercato/ui/backend/RowActions'\nimport { renderDictionaryColor, renderDictionaryIcon } from './dictionaryAppearance'\n\nexport type DictionaryTableEntry = {\n id: string\n value: string\n label: string\n color: string | null\n icon: string | null\n usageCount?: number\n organizationId?: string | null\n tenantId?: string | null\n isInherited?: boolean\n createdAt?: string | null\n updatedAt?: string | null\n}\n\nexport type DictionaryTableTranslations = {\n title: string\n valueColumn: string\n labelColumn: string\n appearanceColumn: string\n addLabel: string\n editLabel: string\n deleteLabel: string\n refreshLabel: string\n inheritedLabel: string\n inheritedTooltip: string\n emptyLabel: string\n usageCountLabel?: string\n searchPlaceholder?: string\n}\n\ntype DictionaryTableProps = {\n entries: DictionaryTableEntry[]\n loading?: boolean\n canManage?: boolean\n onCreate?: () => void\n onEdit?: (entry: DictionaryTableEntry) => void\n onDelete?: (entry: DictionaryTableEntry) => void\n onRefresh?: () => void\n translations: DictionaryTableTranslations\n}\n\nexport function DictionaryTable({\n entries,\n loading = false,\n canManage = false,\n onCreate,\n onEdit,\n onDelete,\n onRefresh,\n translations,\n}: DictionaryTableProps) {\n const [search, setSearch] = React.useState('')\n const [sorting, setSorting] = React.useState<SortingState>([{ id: 'value', desc: false }])\n const [page, setPage] = React.useState(1)\n const pageSize = 50\n\n const filtered = React.useMemo(() => {\n if (!search.trim()) return entries\n const term = search.trim().toLowerCase()\n return entries.filter((entry) => {\n return (\n entry.value.toLowerCase().includes(term) ||\n (entry.label ?? '').toLowerCase().includes(term)\n )\n })\n }, [entries, search])\n\n const paginated = React.useMemo(() => {\n const start = (page - 1) * pageSize\n return filtered.slice(start, start + pageSize)\n }, [filtered, page])\n\n const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize))\n\n const columns = React.useMemo<ColumnDef<DictionaryTableEntry>[]>(() => [\n {\n accessorKey: 'value',\n header: translations.valueColumn,\n meta: { priority: 1 },\n cell: ({ getValue, row }) => (\n <div className=\"space-y-1\">\n <div className=\"flex items-center gap-2\">\n <span className=\"font-medium\">{String(getValue())}</span>\n {row.original.isInherited ? (\n <span className=\"rounded bg-muted px-2 py-0.5 text-xs text-muted-foreground\" title={translations.inheritedTooltip}>\n {translations.inheritedLabel}\n </span>\n ) : null}\n </div>\n {typeof row.original.usageCount === 'number' && row.original.usageCount > 0 && translations.usageCountLabel ? (\n <div className=\"text-xs text-muted-foreground\">\n {translations.usageCountLabel.replace('{{count}}', String(row.original.usageCount))}\n </div>\n ) : null}\n </div>\n ),\n },\n {\n accessorKey: 'label',\n header: translations.labelColumn,\n meta: { priority: 2 },\n cell: ({ getValue }) => <span>{String(getValue() ?? '')}</span>,\n },\n {\n id: 'appearance',\n header: translations.appearanceColumn,\n meta: { priority: 3 },\n cell: ({ row }) => {\n const { color, icon } = row.original\n if (!color && !icon) return <span className=\"text-muted-foreground\">\u2014</span>\n return (\n <div className=\"flex items-center gap-2\">\n {color ? renderDictionaryColor(color, 'h-4 w-4 rounded-full border border-border') : null}\n {icon ? renderDictionaryIcon(icon, 'h-4 w-4') : null}\n </div>\n )\n },\n },\n ], [translations.appearanceColumn, translations.inheritedLabel, translations.inheritedTooltip, translations.labelColumn, translations.valueColumn])\n\n const actions = React.useMemo(() => {\n if (!canManage || !onCreate) return null\n return (\n <Button size=\"sm\" onClick={onCreate}>\n {translations.addLabel}\n </Button>\n )\n }, [canManage, onCreate, translations.addLabel])\n\n const handleRowClick = canManage && onEdit\n ? (entry: DictionaryTableEntry) => {\n if (entry.isInherited) return\n onEdit(entry)\n }\n : undefined\n\n return (\n <div\n role=\"region\"\n aria-label={translations.title}\n tabIndex={0}\n className=\"w-full min-w-0 max-w-full overflow-x-auto focus-visible:outline-none focus-visible:shadow-focus\"\n >\n <DataTable<DictionaryTableEntry>\n title={translations.title}\n titleHeadingLevel={2}\n actions={actions}\n columns={columns}\n data={paginated}\n embedded\n sortable\n sorting={sorting}\n onSortingChange={setSorting}\n searchValue={search}\n onSearchChange={(value) => {\n setSearch(value)\n setPage(1)\n }}\n searchPlaceholder={translations.searchPlaceholder}\n isLoading={loading}\n emptyState={<p className=\"py-10 text-center text-sm text-muted-foreground\">{translations.emptyLabel}</p>}\n pagination={{\n page,\n pageSize,\n total: filtered.length,\n totalPages,\n onPageChange: setPage,\n }}\n refreshButton={onRefresh ? {\n label: translations.refreshLabel,\n onRefresh,\n isRefreshing: loading,\n } : undefined}\n onRowClick={handleRowClick}\n rowActions={\n canManage\n ? (entry) => {\n if (!entry) return null\n if (entry.isInherited) return null\n const items: RowActionItem[] = []\n if (onEdit) {\n items.push({\n id: 'edit',\n label: translations.editLabel,\n onSelect: () => onEdit(entry),\n })\n }\n if (onDelete) {\n items.push({\n id: 'delete',\n label: translations.deleteLabel,\n onSelect: () => onDelete(entry),\n destructive: true,\n })\n }\n return items.length ? <RowActions items={items} /> : null\n }\n : undefined\n }\n />\n </div>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -8,6 +8,7 @@ import {
8
8
  import { User } from "../../auth/data/entities.js";
9
9
  import { Message } from "../data/entities.js";
10
10
  import { notificationTypes } from "../notifications.js";
11
+ import { resolveMessageChannelThreadAccess } from "../lib/channelThreadAccess.js";
11
12
  import {
12
13
  EXTERNAL_CONVERSATION_SOURCE_ENTITY_TYPE,
13
14
  SEND_AS_USER_SOURCE_ENTITY_TYPE
@@ -84,7 +85,18 @@ async function isChannelRoutedMessage(payload, ctx) {
84
85
  }
85
86
  );
86
87
  if (!message) return false;
87
- return message.sourceEntityType === SEND_AS_USER_SOURCE_ENTITY_TYPE || message.sourceEntityType === EXTERNAL_CONVERSATION_SOURCE_ENTITY_TYPE;
88
+ if (message.sourceEntityType === SEND_AS_USER_SOURCE_ENTITY_TYPE) return true;
89
+ if (message.sourceEntityType !== EXTERNAL_CONVERSATION_SOURCE_ENTITY_TYPE) return false;
90
+ const channelThreadAccess = await resolveMessageChannelThreadAccess(
91
+ ctx,
92
+ {
93
+ tenantId: payload.tenantId,
94
+ organizationId: payload.organizationId ?? null
95
+ },
96
+ { messageThreadId: message.threadId },
97
+ { userId: null, features: [] }
98
+ );
99
+ return channelThreadAccess !== null;
88
100
  } catch {
89
101
  return false;
90
102
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/messages/subscribers/message-notification.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { createQueue } from '@open-mercato/queue'\nimport { buildBatchNotificationFromType } from '../../notifications/lib/notificationBuilder'\nimport { resolveNotificationService } from '../../notifications/lib/notificationService'\nimport {\n MESSAGES_EMAIL_QUEUE_NAME,\n type SendMessageEmailJob,\n} from '../workers/send-email.worker'\nimport { User } from '../../auth/data/entities'\nimport { Message } from '../data/entities'\nimport { notificationTypes } from '../notifications'\nimport {\n EXTERNAL_CONVERSATION_SOURCE_ENTITY_TYPE,\n SEND_AS_USER_SOURCE_ENTITY_TYPE,\n} from '../lib/messageSourceEntityTypes'\n\nexport const metadata = {\n event: 'messages.message.sent',\n persistent: true,\n id: 'messages:queue-email-delivery',\n}\n\ntype MessageSentPayload = {\n messageId: string\n senderUserId: string\n recipientUserIds: string[]\n sendViaEmail: boolean\n /** Set by compose for channel-ingested messages (#6093); see `resolveNotificationVariables`. */\n inboundFromChannel?: boolean\n externalEmail?: string | null\n forwardedFrom?: string\n replyTo?: string\n tenantId: string\n organizationId?: string | null\n}\n\ntype ResolverContext = {\n resolve: <T = unknown>(name: string) => T\n}\n\nasync function resolveNotificationVariables(payload: MessageSentPayload, ctx: ResolverContext): Promise<{ title: string; from: string }> {\n try {\n const em = ctx.resolve<EntityManager>('em')?.fork()\n if (!em) return { title: '', from: '' }\n\n const scope = {\n tenantId: payload.tenantId,\n organizationId: payload.organizationId ?? null,\n }\n const whereUser: { id: string; tenantId: string; deletedAt: null; organizationId?: string | null } = {\n id: payload.senderUserId,\n tenantId: payload.tenantId,\n deletedAt: null,\n }\n if (payload.organizationId !== undefined) {\n whereUser.organizationId = payload.organizationId\n }\n\n const [message, sender] = await Promise.all([\n findOneWithDecryption(\n em,\n Message,\n {\n id: payload.messageId,\n tenantId: payload.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n ),\n findOneWithDecryption(\n em,\n User,\n whereUser,\n undefined,\n scope,\n ),\n ])\n\n // #6093: a channel-ingested message is composed on behalf of the channel's\n // system user (or, without one, the assignee), so `senderUserId` is not who\n // wrote it. The correspondent is on the message itself.\n const externalFrom = payload.inboundFromChannel\n ? [message?.externalName, message?.externalEmail].find(\n (value): value is string => typeof value === 'string' && value.trim().length > 0,\n )\n : undefined\n\n return {\n title: typeof message?.subject === 'string' ? message.subject : '',\n from: externalFrom\n ?? (typeof sender?.name === 'string' && sender.name.trim().length > 0\n ? sender.name\n : typeof sender?.email === 'string'\n ? sender.email\n : ''),\n }\n } catch {\n return { title: '', from: '' }\n }\n}\n\n\n/**\n * Whether this Message is already being delivered through a connected\n * channel (`sendAsUser`, or the platform copy of an inbound channel message),\n * so the external-email job below would duplicate \u2014 or, for the inbound\n * case, misdirect \u2014 the send. Mirrors `outbound-bridge.ts`'s re-fetch: the\n * `messages.message.sent` payload doesn't carry `sourceEntityType` and never\n * should, to keep this subscriber decoupled from `communication_channels`.\n *\n * Fails open (`false`, i.e. today's behavior) when the Message can't be\n * found \u2014 we can only skip the external send when we can affirmatively show\n * it is channel-routed.\n */\nasync function isChannelRoutedMessage(payload: MessageSentPayload, ctx: ResolverContext): Promise<boolean> {\n try {\n const em = ctx.resolve<EntityManager>('em')?.fork()\n if (!em) return false\n\n const message = await findOneWithDecryption(\n em,\n Message,\n {\n id: payload.messageId,\n tenantId: payload.tenantId,\n organizationId: payload.organizationId ?? null,\n deletedAt: null,\n },\n undefined,\n {\n tenantId: payload.tenantId,\n organizationId: payload.organizationId ?? null,\n },\n )\n if (!message) return false\n return (\n message.sourceEntityType === SEND_AS_USER_SOURCE_ENTITY_TYPE ||\n message.sourceEntityType === EXTERNAL_CONVERSATION_SOURCE_ENTITY_TYPE\n )\n } catch {\n return false\n }\n}\n\nexport default async function handle(payload: MessageSentPayload, ctx: ResolverContext): Promise<void> {\n const uniqueRecipientUserIds = Array.from(new Set(payload.recipientUserIds))\n\n const typeDef = notificationTypes.find((type) => type.type === 'messages.new')\n if (typeDef && uniqueRecipientUserIds.length > 0) {\n const variables = await resolveNotificationVariables(payload, ctx)\n const notificationService = resolveNotificationService(ctx)\n const notificationInput = buildBatchNotificationFromType(typeDef, {\n recipientUserIds: uniqueRecipientUserIds,\n titleVariables: variables,\n bodyVariables: variables,\n sourceEntityType: 'message',\n sourceEntityId: payload.messageId,\n linkHref: `/backend/messages/${payload.messageId}`,\n })\n await notificationService.createBatch(notificationInput, {\n tenantId: payload.tenantId,\n organizationId: payload.organizationId ?? null,\n })\n }\n\n if (!payload.sendViaEmail) {\n return\n }\n\n const strategy = process.env.QUEUE_STRATEGY === 'async' ? 'async' : 'local'\n\n const emailQueue = createQueue<SendMessageEmailJob>(MESSAGES_EMAIL_QUEUE_NAME, strategy)\n\n for (const recipientUserId of uniqueRecipientUserIds) {\n await emailQueue.enqueue({\n type: 'recipient',\n messageId: payload.messageId,\n recipientUserId,\n tenantId: payload.tenantId,\n organizationId: payload.organizationId ?? null,\n })\n }\n\n const externalEmail = payload.externalEmail?.trim()\n if (externalEmail && !(await isChannelRoutedMessage(payload, ctx))) {\n await emailQueue.enqueue({\n type: 'external',\n messageId: payload.messageId,\n email: externalEmail,\n tenantId: payload.tenantId,\n organizationId: payload.organizationId ?? null,\n })\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { createQueue } from '@open-mercato/queue'\nimport { buildBatchNotificationFromType } from '../../notifications/lib/notificationBuilder'\nimport { resolveNotificationService } from '../../notifications/lib/notificationService'\nimport {\n MESSAGES_EMAIL_QUEUE_NAME,\n type SendMessageEmailJob,\n} from '../workers/send-email.worker'\nimport { User } from '../../auth/data/entities'\nimport { Message } from '../data/entities'\nimport { notificationTypes } from '../notifications'\nimport { resolveMessageChannelThreadAccess } from '../lib/channelThreadAccess'\nimport {\n EXTERNAL_CONVERSATION_SOURCE_ENTITY_TYPE,\n SEND_AS_USER_SOURCE_ENTITY_TYPE,\n} from '../lib/messageSourceEntityTypes'\n\nexport const metadata = {\n event: 'messages.message.sent',\n persistent: true,\n id: 'messages:queue-email-delivery',\n}\n\ntype MessageSentPayload = {\n messageId: string\n senderUserId: string\n recipientUserIds: string[]\n sendViaEmail: boolean\n /** Set by compose for channel-ingested messages (#6093); see `resolveNotificationVariables`. */\n inboundFromChannel?: boolean\n externalEmail?: string | null\n forwardedFrom?: string\n replyTo?: string\n tenantId: string\n organizationId?: string | null\n}\n\ntype ResolverContext = {\n resolve: <T = unknown>(name: string) => T\n}\n\nasync function resolveNotificationVariables(payload: MessageSentPayload, ctx: ResolverContext): Promise<{ title: string; from: string }> {\n try {\n const em = ctx.resolve<EntityManager>('em')?.fork()\n if (!em) return { title: '', from: '' }\n\n const scope = {\n tenantId: payload.tenantId,\n organizationId: payload.organizationId ?? null,\n }\n const whereUser: { id: string; tenantId: string; deletedAt: null; organizationId?: string | null } = {\n id: payload.senderUserId,\n tenantId: payload.tenantId,\n deletedAt: null,\n }\n if (payload.organizationId !== undefined) {\n whereUser.organizationId = payload.organizationId\n }\n\n const [message, sender] = await Promise.all([\n findOneWithDecryption(\n em,\n Message,\n {\n id: payload.messageId,\n tenantId: payload.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n ),\n findOneWithDecryption(\n em,\n User,\n whereUser,\n undefined,\n scope,\n ),\n ])\n\n // #6093: a channel-ingested message is composed on behalf of the channel's\n // system user (or, without one, the assignee), so `senderUserId` is not who\n // wrote it. The correspondent is on the message itself.\n const externalFrom = payload.inboundFromChannel\n ? [message?.externalName, message?.externalEmail].find(\n (value): value is string => typeof value === 'string' && value.trim().length > 0,\n )\n : undefined\n\n return {\n title: typeof message?.subject === 'string' ? message.subject : '',\n from: externalFrom\n ?? (typeof sender?.name === 'string' && sender.name.trim().length > 0\n ? sender.name\n : typeof sender?.email === 'string'\n ? sender.email\n : ''),\n }\n } catch {\n return { title: '', from: '' }\n }\n}\n\n\n/**\n * Whether this Message is already being delivered through a connected\n * channel (`sendAsUser`, or the platform copy of an inbound channel message),\n * so the external-email job below would duplicate \u2014 or, for the inbound\n * case, misdirect \u2014 the send. Mirrors `outbound-bridge.ts`'s re-fetch: the\n * `messages.message.sent` payload doesn't carry `sourceEntityType` and never\n * should, to keep this subscriber decoupled from `communication_channels`.\n *\n * Fails open (`false`, i.e. today's behavior) when the Message can't be\n * found \u2014 we can only skip the external send when we can affirmatively show\n * it is channel-routed.\n */\nasync function isChannelRoutedMessage(payload: MessageSentPayload, ctx: ResolverContext): Promise<boolean> {\n try {\n const em = ctx.resolve<EntityManager>('em')?.fork()\n if (!em) return false\n\n const message = await findOneWithDecryption(\n em,\n Message,\n {\n id: payload.messageId,\n tenantId: payload.tenantId,\n organizationId: payload.organizationId ?? null,\n deletedAt: null,\n },\n undefined,\n {\n tenantId: payload.tenantId,\n organizationId: payload.organizationId ?? null,\n },\n )\n if (!message) return false\n if (message.sourceEntityType === SEND_AS_USER_SOURCE_ENTITY_TYPE) return true\n if (message.sourceEntityType !== EXTERNAL_CONVERSATION_SOURCE_ENTITY_TYPE) return false\n\n const channelThreadAccess = await resolveMessageChannelThreadAccess(\n ctx,\n {\n tenantId: payload.tenantId,\n organizationId: payload.organizationId ?? null,\n },\n { messageThreadId: message.threadId },\n { userId: null, features: [] },\n )\n return channelThreadAccess !== null\n } catch {\n return false\n }\n}\n\nexport default async function handle(payload: MessageSentPayload, ctx: ResolverContext): Promise<void> {\n const uniqueRecipientUserIds = Array.from(new Set(payload.recipientUserIds))\n\n const typeDef = notificationTypes.find((type) => type.type === 'messages.new')\n if (typeDef && uniqueRecipientUserIds.length > 0) {\n const variables = await resolveNotificationVariables(payload, ctx)\n const notificationService = resolveNotificationService(ctx)\n const notificationInput = buildBatchNotificationFromType(typeDef, {\n recipientUserIds: uniqueRecipientUserIds,\n titleVariables: variables,\n bodyVariables: variables,\n sourceEntityType: 'message',\n sourceEntityId: payload.messageId,\n linkHref: `/backend/messages/${payload.messageId}`,\n })\n await notificationService.createBatch(notificationInput, {\n tenantId: payload.tenantId,\n organizationId: payload.organizationId ?? null,\n })\n }\n\n if (!payload.sendViaEmail) {\n return\n }\n\n const strategy = process.env.QUEUE_STRATEGY === 'async' ? 'async' : 'local'\n\n const emailQueue = createQueue<SendMessageEmailJob>(MESSAGES_EMAIL_QUEUE_NAME, strategy)\n\n for (const recipientUserId of uniqueRecipientUserIds) {\n await emailQueue.enqueue({\n type: 'recipient',\n messageId: payload.messageId,\n recipientUserId,\n tenantId: payload.tenantId,\n organizationId: payload.organizationId ?? null,\n })\n }\n\n const externalEmail = payload.externalEmail?.trim()\n if (externalEmail && !(await isChannelRoutedMessage(payload, ctx))) {\n await emailQueue.enqueue({\n type: 'external',\n messageId: payload.messageId,\n email: externalEmail,\n tenantId: payload.tenantId,\n organizationId: payload.organizationId ?? null,\n })\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,22 +1,33 @@
1
1
  import { NextResponse } from "next/server";
2
+ import { z } from "zod";
2
3
  import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
3
4
  import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
4
5
  import { readJsonSafe } from "@open-mercato/shared/lib/http/readJsonSafe";
5
6
  import { checkRateLimit, getClientIp, RATE_LIMIT_ERROR_FALLBACK } from "@open-mercato/shared/lib/ratelimit/helpers";
6
- import { getWebhookHandler } from "@open-mercato/shared/modules/payment_gateways/types";
7
+ import {
8
+ getWebhookHandler,
9
+ WebhookVerificationUnavailableError
10
+ } from "@open-mercato/shared/modules/payment_gateways/types";
7
11
  import { markQueueJobOrigin } from "@open-mercato/shared/lib/queue/dispatchOrigin";
8
12
  import { GatewayTransaction } from "../../../data/entities.js";
9
13
  import { getPaymentGatewayQueue } from "../../../lib/queue.js";
10
14
  import { processPaymentGatewayWebhookJob } from "../../../lib/webhook-processor.js";
11
15
  import { paymentGatewaysTag } from "../../openapi.js";
12
16
  import { createLogger } from "@open-mercato/shared/lib/logger";
13
- import { readBoundedRequestBody, WebhookBodyTooLargeError } from "@open-mercato/shared/lib/webhooks";
17
+ import { readBoundedRequestBody, readBoundedRequestBytes, WebhookBodyTooLargeError } from "@open-mercato/shared/lib/webhooks";
18
+ import { getTelemetryRuntime } from "@open-mercato/shared/lib/telemetry/runtime";
14
19
  const logger = createLogger("payment_gateways").child({ component: "webhook" });
15
20
  const metadata = {
16
21
  path: "/payment_gateways/webhook/[provider]",
17
22
  POST: { requireAuth: false }
18
23
  };
19
24
  const WEBHOOK_VERIFICATION_FAILED = "Webhook verification failed";
25
+ const WEBHOOK_PAYLOAD_TOO_LARGE = "Webhook payload too large";
26
+ const INTERNAL_SERVER_ERROR = "Internal server error";
27
+ const DEFAULT_TEXT_CONTENT_TYPE = "text/plain; charset=utf-8";
28
+ const CONTROL_CHARACTERS = /[\u0000-\u001F\u007F]/;
29
+ const JSON_CONTENT_TYPE = /^application\/([\w.+-]+\+)?json(\s*;|$)/;
30
+ const paymentIdHintSchema = z.guid();
20
31
  const paymentGatewayWebhookRateLimitConfig = {
21
32
  points: 60,
22
33
  duration: 60,
@@ -31,14 +42,27 @@ async function POST(req, { params }) {
31
42
  return NextResponse.json({ error: `No webhook handler for provider: ${providerKey}` }, { status: 404 });
32
43
  }
33
44
  const rateLimitResponse = await checkProviderWebhookRateLimit(container, req, providerKey);
34
- if (rateLimitResponse) return rateLimitResponse;
45
+ const outcome = rateLimitResponse ? "rate_limited" : await receiveWebhook(req, providerKey, registration, container);
46
+ const response = respond(registration, outcome, rateLimitResponse);
47
+ const logFields = { providerKey, outcome, status: response.status };
48
+ if (outcome === "accepted") logger.info("Webhook handled", logFields);
49
+ else logger.warn("Webhook handled", logFields);
50
+ return response;
51
+ }
52
+ async function receiveWebhook(req, providerKey, registration, container) {
35
53
  let rawBody;
54
+ let bodyText;
36
55
  try {
37
- rawBody = registration.maxBodyBytes === void 0 ? await req.text() : await readBoundedRequestBody(req, { maxBytes: registration.maxBodyBytes });
38
- } catch (error) {
39
- if (error instanceof WebhookBodyTooLargeError) {
40
- return NextResponse.json({ error: "Webhook payload too large" }, { status: 413 });
56
+ if (registration.rawBody === "bytes") {
57
+ const bytes = registration.maxBodyBytes === void 0 ? new Uint8Array(await req.arrayBuffer()) : await readBoundedRequestBytes(req, { maxBytes: registration.maxBodyBytes });
58
+ rawBody = Buffer.from(bytes);
59
+ bodyText = new TextDecoder().decode(rawBody);
60
+ } else {
61
+ bodyText = registration.maxBodyBytes === void 0 ? await req.text() : await readBoundedRequestBody(req, { maxBytes: registration.maxBodyBytes });
62
+ rawBody = bodyText;
41
63
  }
64
+ } catch (error) {
65
+ if (error instanceof WebhookBodyTooLargeError) return "payload_too_large";
42
66
  throw error;
43
67
  }
44
68
  const headers = {};
@@ -49,62 +73,181 @@ async function POST(req, { params }) {
49
73
  const em = container.resolve("em");
50
74
  const integrationCredentialsService = container.resolve("integrationCredentialsService");
51
75
  const queue = getPaymentGatewayQueue(registration.queue ?? "payment-gateways-webhook");
52
- const payload = await readJsonSafe(rawBody);
53
- const sessionIdHint = registration.readSessionIdHint?.(payload) ?? null;
76
+ const payload = await readJsonSafe(bodyText);
77
+ const locatorContext = { rawBody, headers };
78
+ let sessionIdHint;
79
+ let paymentIdHint;
54
80
  try {
55
- const candidates = sessionIdHint ? await findWithDecryption(
56
- em,
57
- GatewayTransaction,
58
- {
59
- providerKey,
60
- providerSessionId: sessionIdHint,
61
- deletedAt: null
62
- },
63
- { limit: 10, orderBy: { createdAt: "desc" } }
64
- ) : [];
65
- let transaction = null;
66
- let matchedScope = null;
67
- let event = null;
68
- let lastVerificationError = null;
81
+ sessionIdHint = normalizeSessionIdHint(registration.readSessionIdHint?.(payload, locatorContext));
82
+ paymentIdHint = normalizePaymentIdHint(registration.readPaymentIdHint?.(payload, locatorContext));
83
+ } catch (error) {
84
+ reportWebhookError(error, "payment_gateways.webhook_locator_failed");
85
+ return "no_candidate";
86
+ }
87
+ try {
88
+ let candidates;
89
+ try {
90
+ candidates = sessionIdHint || paymentIdHint ? await findWithDecryption(
91
+ em,
92
+ GatewayTransaction,
93
+ {
94
+ providerKey,
95
+ ...sessionIdHint ? { providerSessionId: sessionIdHint } : {},
96
+ ...paymentIdHint ? { paymentId: paymentIdHint } : {},
97
+ deletedAt: null
98
+ },
99
+ { limit: 10, orderBy: { createdAt: "desc" } }
100
+ ) : [];
101
+ } catch (error) {
102
+ reportWebhookError(error, "payment_gateways.webhook_lookup_failed");
103
+ return "processing_failed";
104
+ }
105
+ if (candidates.length === 0) return "no_candidate";
106
+ const verifiedMatches = [];
107
+ let verificationUnavailable = false;
69
108
  for (const candidate of candidates) {
70
109
  const candidateScope = { organizationId: candidate.organizationId, tenantId: candidate.tenantId };
71
- const credentials = await integrationCredentialsService.resolve(`gateway_${providerKey}`, candidateScope) ?? {};
110
+ let credentials;
72
111
  try {
73
- event = await registration.handler({ rawBody, headers, credentials });
74
- transaction = candidate;
75
- matchedScope = candidateScope;
76
- break;
112
+ credentials = await integrationCredentialsService.resolve(`gateway_${providerKey}`, candidateScope) ?? {};
77
113
  } catch (error) {
78
- lastVerificationError = error;
114
+ reportWebhookError(error, "payment_gateways.webhook_credentials_failed");
115
+ verificationUnavailable = true;
116
+ continue;
79
117
  }
118
+ try {
119
+ const candidateEvent = await registration.handler({
120
+ rawBody,
121
+ headers,
122
+ credentials,
123
+ candidate: {
124
+ transactionId: candidate.id,
125
+ paymentId: candidate.paymentId,
126
+ providerSessionId: candidate.providerSessionId ?? null,
127
+ amount: candidate.amount,
128
+ currencyCode: candidate.currencyCode
129
+ }
130
+ });
131
+ verifiedMatches.push({ transaction: candidate, scope: candidateScope, event: candidateEvent });
132
+ } catch (error) {
133
+ if (error instanceof WebhookVerificationUnavailableError) verificationUnavailable = true;
134
+ }
135
+ }
136
+ if (verifiedMatches.length > 1) {
137
+ logger.error("Webhook matched multiple verified candidates", {
138
+ providerKey,
139
+ candidateCount: verifiedMatches.length
140
+ });
141
+ getTelemetryRuntime()?.reportError(
142
+ new Error("[internal] Webhook matched multiple verified candidates"),
143
+ { module: "payment_gateways", code: "payment_gateways.webhook_ambiguous_candidates" }
144
+ );
145
+ return "verification_failed";
80
146
  }
81
- if (!event || !transaction || !matchedScope) {
82
- throw lastVerificationError ?? new Error("Webhook verification failed: no matching transaction");
147
+ if (verificationUnavailable && verifiedMatches.length === 1) {
148
+ logger.warn("Webhook candidate left unverified; ambiguity cannot be ruled out", {
149
+ providerKey,
150
+ candidateCount: candidates.length
151
+ });
152
+ return "verification_unavailable";
83
153
  }
84
- const scope = matchedScope;
154
+ const [match] = verifiedMatches;
155
+ if (!match) return verificationUnavailable ? "verification_unavailable" : "verification_failed";
85
156
  const jobPayload = markQueueJobOrigin({
86
157
  providerKey,
87
- event,
88
- transactionId: transaction.id,
89
- scope
158
+ event: match.event,
159
+ transactionId: match.transaction.id,
160
+ scope: match.scope
90
161
  }, "inbound-webhook");
91
- if (process.env.QUEUE_STRATEGY === "async") {
92
- await queue.enqueue(jobPayload);
93
- } else {
94
- await processPaymentGatewayWebhookJob(
95
- {
96
- em: container.resolve("em"),
97
- paymentGatewayService: service,
98
- integrationLogService: container.resolve("integrationLogService")
99
- },
100
- jobPayload
101
- );
162
+ try {
163
+ if (process.env.QUEUE_STRATEGY === "async") {
164
+ await queue.enqueue(jobPayload);
165
+ } else {
166
+ await processPaymentGatewayWebhookJob(
167
+ {
168
+ em: container.resolve("em"),
169
+ paymentGatewayService: service,
170
+ integrationLogService: container.resolve("integrationLogService")
171
+ },
172
+ jobPayload
173
+ );
174
+ }
175
+ } catch (error) {
176
+ getTelemetryRuntime()?.reportError(error, {
177
+ module: "payment_gateways",
178
+ code: "payment_gateways.webhook_processing_failed"
179
+ });
180
+ return "processing_failed";
181
+ }
182
+ return "accepted";
183
+ } catch (error) {
184
+ reportWebhookError(error, "payment_gateways.webhook_unexpected_failure");
185
+ return "verification_failed";
186
+ }
187
+ }
188
+ function respond(registration, outcome, rateLimitResponse) {
189
+ if (!registration.formatResponse) return legacyResponse(outcome, rateLimitResponse);
190
+ try {
191
+ const response = toValidatedResponse(registration.formatResponse(outcome));
192
+ if (outcome === "rate_limited" && rateLimitResponse) {
193
+ rateLimitResponse.headers.forEach((value, key) => {
194
+ if (key !== "content-type" && key !== "content-length") response.headers.set(key, value);
195
+ });
196
+ }
197
+ return response;
198
+ } catch (error) {
199
+ getTelemetryRuntime()?.reportError(error, {
200
+ module: "payment_gateways",
201
+ code: "payment_gateways.webhook_formatter_invalid"
202
+ });
203
+ return NextResponse.json({ error: INTERNAL_SERVER_ERROR }, { status: 500 });
204
+ }
205
+ }
206
+ function legacyResponse(outcome, rateLimitResponse) {
207
+ if (outcome === "rate_limited" && rateLimitResponse) return rateLimitResponse;
208
+ if (outcome === "accepted") return NextResponse.json({ received: true, queued: true }, { status: 202 });
209
+ if (outcome === "payload_too_large") return NextResponse.json({ error: WEBHOOK_PAYLOAD_TOO_LARGE }, { status: 413 });
210
+ return NextResponse.json({ error: WEBHOOK_VERIFICATION_FAILED }, { status: 401 });
211
+ }
212
+ function toValidatedResponse(formatted) {
213
+ const { status, body, contentType } = formatted;
214
+ if (!Number.isInteger(status) || status < 200 || status > 599) {
215
+ throw new Error("[internal] Webhook formatter returned an invalid status");
216
+ }
217
+ if (contentType !== void 0 && (typeof contentType !== "string" || CONTROL_CHARACTERS.test(contentType))) {
218
+ throw new Error("[internal] Webhook formatter returned an invalid content type");
219
+ }
220
+ const normalizedContentType = contentType?.toLowerCase();
221
+ if (typeof body === "string") {
222
+ if (normalizedContentType !== void 0 && !normalizedContentType.startsWith("text/")) {
223
+ throw new Error("[internal] Webhook formatter returned a non-text content type for a text body");
102
224
  }
103
- return NextResponse.json({ received: true, queued: true }, { status: 202 });
104
- } catch (err) {
105
- logger.warn("Webhook verification failed", { providerKey, err });
106
- return NextResponse.json({ error: WEBHOOK_VERIFICATION_FAILED }, { status: 401 });
225
+ return new NextResponse(body, { status, headers: { "content-type": contentType ?? DEFAULT_TEXT_CONTENT_TYPE } });
226
+ }
227
+ if (!isPlainObject(body)) {
228
+ throw new Error("[internal] Webhook formatter returned an invalid body");
107
229
  }
230
+ if (normalizedContentType !== void 0 && !JSON_CONTENT_TYPE.test(normalizedContentType)) {
231
+ throw new Error("[internal] Webhook formatter returned a non-JSON content type for an object body");
232
+ }
233
+ const response = NextResponse.json(body, { status });
234
+ if (contentType !== void 0) response.headers.set("content-type", contentType);
235
+ return response;
236
+ }
237
+ function isPlainObject(value) {
238
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
239
+ const prototype = Object.getPrototypeOf(value);
240
+ return prototype === Object.prototype || prototype === null;
241
+ }
242
+ function reportWebhookError(error, code) {
243
+ getTelemetryRuntime()?.reportError(error, { module: "payment_gateways", code });
244
+ }
245
+ function normalizeSessionIdHint(value) {
246
+ return typeof value === "string" && value.length > 0 ? value : null;
247
+ }
248
+ function normalizePaymentIdHint(value) {
249
+ const parsed = paymentIdHintSchema.safeParse(value);
250
+ return parsed.success ? parsed.data : null;
108
251
  }
109
252
  async function checkProviderWebhookRateLimit(container, req, providerKey) {
110
253
  const rateLimiterService = tryResolve(container, "rateLimiterService");
@@ -131,10 +274,15 @@ const openApi = {
131
274
  summary: "Process inbound webhook from payment provider",
132
275
  tags: [paymentGatewaysTag],
133
276
  responses: [
134
- { status: 202, description: "Webhook accepted for async processing" },
135
- { status: 401, description: "Signature verification failed" },
277
+ { status: 200, description: "Webhook accepted (provider-defined acknowledgement may apply)" },
278
+ { status: 202, description: "Webhook accepted for processing (default acknowledgement)" },
279
+ { status: 400, description: "Webhook rejected (provider-defined response may apply)" },
280
+ { status: 401, description: "Webhook could not be verified or processed (default failure response)" },
281
+ { status: 404, description: "Unknown provider" },
136
282
  { status: 413, description: "Webhook payload too large" },
137
- { status: 404, description: "Unknown provider" }
283
+ { status: 429, description: "Too many webhook requests" },
284
+ { status: 500, description: "Internal server error (provider-defined response may apply)" },
285
+ { status: 503, description: "Webhook temporarily unavailable (provider-defined response may apply)" }
138
286
  ]
139
287
  }
140
288
  }