@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,15 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+
3
+ export class Migration20260930110235_customers extends Migration {
4
+
5
+ override name = 'Migration20260930110235_customers';
6
+
7
+ override up(): void | Promise<void> {
8
+ this.addSql(`alter table "customer_interactions" add "timezone" text null;`);
9
+ }
10
+
11
+ override down(): void | Promise<void> {
12
+ this.addSql(`alter table "customer_interactions" drop column "timezone";`);
13
+ }
14
+
15
+ }
@@ -143,62 +143,69 @@ export function DictionaryTable({
143
143
  : undefined
144
144
 
145
145
  return (
146
- <DataTable<DictionaryTableEntry>
147
- title={translations.title}
148
- titleHeadingLevel={2}
149
- actions={actions}
150
- columns={columns}
151
- data={paginated}
152
- embedded
153
- sortable
154
- sorting={sorting}
155
- onSortingChange={setSorting}
156
- searchValue={search}
157
- onSearchChange={(value) => {
158
- setSearch(value)
159
- setPage(1)
160
- }}
161
- searchPlaceholder={translations.searchPlaceholder}
162
- isLoading={loading}
163
- emptyState={<p className="py-10 text-center text-sm text-muted-foreground">{translations.emptyLabel}</p>}
164
- pagination={{
165
- page,
166
- pageSize,
167
- total: filtered.length,
168
- totalPages,
169
- onPageChange: setPage,
170
- }}
171
- refreshButton={onRefresh ? {
172
- label: translations.refreshLabel,
173
- onRefresh,
174
- isRefreshing: loading,
175
- } : undefined}
176
- onRowClick={handleRowClick}
177
- rowActions={
178
- canManage
179
- ? (entry) => {
180
- if (!entry) return null
181
- if (entry.isInherited) return null
182
- const items: RowActionItem[] = []
183
- if (onEdit) {
184
- items.push({
185
- id: 'edit',
186
- label: translations.editLabel,
187
- onSelect: () => onEdit(entry),
188
- })
146
+ <div
147
+ role="region"
148
+ aria-label={translations.title}
149
+ tabIndex={0}
150
+ className="w-full min-w-0 max-w-full overflow-x-auto focus-visible:outline-none focus-visible:shadow-focus"
151
+ >
152
+ <DataTable<DictionaryTableEntry>
153
+ title={translations.title}
154
+ titleHeadingLevel={2}
155
+ actions={actions}
156
+ columns={columns}
157
+ data={paginated}
158
+ embedded
159
+ sortable
160
+ sorting={sorting}
161
+ onSortingChange={setSorting}
162
+ searchValue={search}
163
+ onSearchChange={(value) => {
164
+ setSearch(value)
165
+ setPage(1)
166
+ }}
167
+ searchPlaceholder={translations.searchPlaceholder}
168
+ isLoading={loading}
169
+ emptyState={<p className="py-10 text-center text-sm text-muted-foreground">{translations.emptyLabel}</p>}
170
+ pagination={{
171
+ page,
172
+ pageSize,
173
+ total: filtered.length,
174
+ totalPages,
175
+ onPageChange: setPage,
176
+ }}
177
+ refreshButton={onRefresh ? {
178
+ label: translations.refreshLabel,
179
+ onRefresh,
180
+ isRefreshing: loading,
181
+ } : undefined}
182
+ onRowClick={handleRowClick}
183
+ rowActions={
184
+ canManage
185
+ ? (entry) => {
186
+ if (!entry) return null
187
+ if (entry.isInherited) return null
188
+ const items: RowActionItem[] = []
189
+ if (onEdit) {
190
+ items.push({
191
+ id: 'edit',
192
+ label: translations.editLabel,
193
+ onSelect: () => onEdit(entry),
194
+ })
195
+ }
196
+ if (onDelete) {
197
+ items.push({
198
+ id: 'delete',
199
+ label: translations.deleteLabel,
200
+ onSelect: () => onDelete(entry),
201
+ destructive: true,
202
+ })
203
+ }
204
+ return items.length ? <RowActions items={items} /> : null
189
205
  }
190
- if (onDelete) {
191
- items.push({
192
- id: 'delete',
193
- label: translations.deleteLabel,
194
- onSelect: () => onDelete(entry),
195
- destructive: true,
196
- })
197
- }
198
- return items.length ? <RowActions items={items} /> : null
199
- }
200
- : undefined
201
- }
202
- />
206
+ : undefined
207
+ }
208
+ />
209
+ </div>
203
210
  )
204
211
  }
@@ -10,6 +10,7 @@ import {
10
10
  import { User } from '../../auth/data/entities'
11
11
  import { Message } from '../data/entities'
12
12
  import { notificationTypes } from '../notifications'
13
+ import { resolveMessageChannelThreadAccess } from '../lib/channelThreadAccess'
13
14
  import {
14
15
  EXTERNAL_CONVERSATION_SOURCE_ENTITY_TYPE,
15
16
  SEND_AS_USER_SOURCE_ENTITY_TYPE,
@@ -135,10 +136,19 @@ async function isChannelRoutedMessage(payload: MessageSentPayload, ctx: Resolver
135
136
  },
136
137
  )
137
138
  if (!message) return false
138
- return (
139
- message.sourceEntityType === SEND_AS_USER_SOURCE_ENTITY_TYPE ||
140
- message.sourceEntityType === EXTERNAL_CONVERSATION_SOURCE_ENTITY_TYPE
139
+ if (message.sourceEntityType === SEND_AS_USER_SOURCE_ENTITY_TYPE) return true
140
+ if (message.sourceEntityType !== EXTERNAL_CONVERSATION_SOURCE_ENTITY_TYPE) return false
141
+
142
+ const channelThreadAccess = await resolveMessageChannelThreadAccess(
143
+ ctx,
144
+ {
145
+ tenantId: payload.tenantId,
146
+ organizationId: payload.organizationId ?? null,
147
+ },
148
+ { messageThreadId: message.threadId },
149
+ { userId: null, features: [] },
141
150
  )
151
+ return channelThreadAccess !== null
142
152
  } catch {
143
153
  return false
144
154
  }
@@ -1,11 +1,18 @@
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
7
  import type { RateLimiterService } from '@open-mercato/shared/lib/ratelimit/service'
7
8
  import type { EntityManager } from '@mikro-orm/postgresql'
8
- import { getWebhookHandler } from '@open-mercato/shared/modules/payment_gateways/types'
9
+ import {
10
+ getWebhookHandler,
11
+ WebhookVerificationUnavailableError,
12
+ type WebhookHandlerRegistration,
13
+ type WebhookHttpResponse,
14
+ type WebhookResponseOutcome,
15
+ } from '@open-mercato/shared/modules/payment_gateways/types'
9
16
  import { markQueueJobOrigin } from '@open-mercato/shared/lib/queue/dispatchOrigin'
10
17
  import type { IntegrationLogService } from '../../../../integrations/lib/log-service'
11
18
  import type { PaymentGatewayService } from '../../../lib/gateway-service'
@@ -15,7 +22,8 @@ import { getPaymentGatewayQueue } from '../../../lib/queue'
15
22
  import { processPaymentGatewayWebhookJob } from '../../../lib/webhook-processor'
16
23
  import { paymentGatewaysTag } from '../../openapi'
17
24
  import { createLogger } from '@open-mercato/shared/lib/logger'
18
- import { readBoundedRequestBody, WebhookBodyTooLargeError } from '@open-mercato/shared/lib/webhooks'
25
+ import { readBoundedRequestBody, readBoundedRequestBytes, WebhookBodyTooLargeError } from '@open-mercato/shared/lib/webhooks'
26
+ import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'
19
27
 
20
28
  const logger = createLogger('payment_gateways').child({ component: 'webhook' })
21
29
 
@@ -25,6 +33,13 @@ export const metadata = {
25
33
  }
26
34
 
27
35
  const WEBHOOK_VERIFICATION_FAILED = 'Webhook verification failed'
36
+ const WEBHOOK_PAYLOAD_TOO_LARGE = 'Webhook payload too large'
37
+ const INTERNAL_SERVER_ERROR = 'Internal server error'
38
+ const DEFAULT_TEXT_CONTENT_TYPE = 'text/plain; charset=utf-8'
39
+ const CONTROL_CHARACTERS = /[\u0000-\u001F\u007F]/
40
+ const JSON_CONTENT_TYPE = /^application\/([\w.+-]+\+)?json(\s*;|$)/
41
+
42
+ const paymentIdHintSchema = z.guid()
28
43
 
29
44
  const paymentGatewayWebhookRateLimitConfig = {
30
45
  points: 60,
@@ -32,6 +47,8 @@ const paymentGatewayWebhookRateLimitConfig = {
32
47
  keyPrefix: 'payment_gateways:webhook',
33
48
  }
34
49
 
50
+ type RequestContainer = Awaited<ReturnType<typeof createRequestContainer>>
51
+
35
52
  export async function POST(req: Request, { params }: { params: Promise<{ provider: string }> | { provider: string } }) {
36
53
  const resolvedParams = await params
37
54
  const providerKey = resolvedParams.provider
@@ -42,17 +59,39 @@ export async function POST(req: Request, { params }: { params: Promise<{ provide
42
59
  }
43
60
 
44
61
  const rateLimitResponse = await checkProviderWebhookRateLimit(container, req, providerKey)
45
- if (rateLimitResponse) return rateLimitResponse
62
+ const outcome: WebhookResponseOutcome = rateLimitResponse
63
+ ? 'rate_limited'
64
+ : await receiveWebhook(req, providerKey, registration, container)
65
+ const response = respond(registration, outcome, rateLimitResponse)
66
+ const logFields = { providerKey, outcome, status: response.status }
67
+ if (outcome === 'accepted') logger.info('Webhook handled', logFields)
68
+ else logger.warn('Webhook handled', logFields)
69
+ return response
70
+ }
46
71
 
47
- let rawBody: string
72
+ async function receiveWebhook(
73
+ req: Request,
74
+ providerKey: string,
75
+ registration: WebhookHandlerRegistration,
76
+ container: RequestContainer,
77
+ ): Promise<WebhookResponseOutcome> {
78
+ let rawBody: string | Buffer
79
+ let bodyText: string
48
80
  try {
49
- rawBody = registration.maxBodyBytes === undefined
50
- ? await req.text()
51
- : await readBoundedRequestBody(req, { maxBytes: registration.maxBodyBytes })
52
- } catch (error) {
53
- if (error instanceof WebhookBodyTooLargeError) {
54
- return NextResponse.json({ error: 'Webhook payload too large' }, { status: 413 })
81
+ if (registration.rawBody === 'bytes') {
82
+ const bytes = registration.maxBodyBytes === undefined
83
+ ? new Uint8Array(await req.arrayBuffer())
84
+ : await readBoundedRequestBytes(req, { maxBytes: registration.maxBodyBytes })
85
+ rawBody = Buffer.from(bytes)
86
+ bodyText = new TextDecoder().decode(rawBody)
87
+ } else {
88
+ bodyText = registration.maxBodyBytes === undefined
89
+ ? await req.text()
90
+ : await readBoundedRequestBody(req, { maxBytes: registration.maxBodyBytes })
91
+ rawBody = bodyText
55
92
  }
93
+ } catch (error) {
94
+ if (error instanceof WebhookBodyTooLargeError) return 'payload_too_large'
56
95
  throw error
57
96
  }
58
97
  const headers: Record<string, string> = {}
@@ -64,81 +103,218 @@ export async function POST(req: Request, { params }: { params: Promise<{ provide
64
103
  const em = container.resolve('em') as EntityManager
65
104
  const integrationCredentialsService = container.resolve('integrationCredentialsService') as CredentialsService
66
105
  const queue = getPaymentGatewayQueue(registration.queue ?? 'payment-gateways-webhook')
67
- const payload = await readJsonSafe<Record<string, unknown>>(rawBody)
68
- const sessionIdHint = registration.readSessionIdHint?.(payload) ?? null
106
+ const payload = await readJsonSafe<Record<string, unknown>>(bodyText)
107
+ const locatorContext = { rawBody, headers }
108
+ let sessionIdHint: string | null
109
+ let paymentIdHint: string | null
110
+ try {
111
+ sessionIdHint = normalizeSessionIdHint(registration.readSessionIdHint?.(payload, locatorContext))
112
+ paymentIdHint = normalizePaymentIdHint(registration.readPaymentIdHint?.(payload, locatorContext))
113
+ } catch (error: unknown) {
114
+ reportWebhookError(error, 'payment_gateways.webhook_locator_failed')
115
+ return 'no_candidate'
116
+ }
69
117
 
70
118
  try {
71
119
  // The webhook endpoint is unauthenticated. Tenant/organization scope MUST come from a
72
120
  // GatewayTransaction whose per-tenant credentials successfully verify the inbound
73
121
  // signature — NEVER from attacker-controlled payload metadata. If no candidate
74
- // transaction can be located by the provider-reported session id, or no candidate's
75
- // credentials can verify the signature, we fail closed with 401. This prevents
76
- // forged webhooks (e.g. mock gateway PoC) from mutating another tenant's payment
77
- // state via `event.data.metadata.{organizationId,tenantId}`.
78
- const candidates = sessionIdHint
79
- ? await findWithDecryption(
80
- em,
81
- GatewayTransaction,
82
- {
83
- providerKey,
84
- providerSessionId: sessionIdHint,
85
- deletedAt: null,
86
- },
87
- { limit: 10, orderBy: { createdAt: 'desc' } },
88
- )
89
- : []
122
+ // transaction can be located by the provider-reported session or payment id, or no
123
+ // candidate's credentials can verify the signature, we fail closed. This
124
+ // prevents forged webhooks (e.g. mock gateway PoC) from mutating another tenant's
125
+ // payment state via `event.data.metadata.{organizationId,tenantId}`. When more than one
126
+ // candidate verifies, the scope is ambiguous and we also fail closed.
127
+ let candidates: GatewayTransaction[]
128
+ try {
129
+ candidates = sessionIdHint || paymentIdHint
130
+ ? await findWithDecryption(
131
+ em,
132
+ GatewayTransaction,
133
+ {
134
+ providerKey,
135
+ ...(sessionIdHint ? { providerSessionId: sessionIdHint } : {}),
136
+ ...(paymentIdHint ? { paymentId: paymentIdHint } : {}),
137
+ deletedAt: null,
138
+ },
139
+ { limit: 10, orderBy: { createdAt: 'desc' } },
140
+ )
141
+ : []
142
+ } catch (error: unknown) {
143
+ reportWebhookError(error, 'payment_gateways.webhook_lookup_failed')
144
+ return 'processing_failed'
145
+ }
90
146
 
91
- let transaction: GatewayTransaction | null = null
92
- let matchedScope: { organizationId: string; tenantId: string } | null = null
93
- let event: Awaited<ReturnType<typeof registration.handler>> | null = null
94
- let lastVerificationError: unknown = null
147
+ if (candidates.length === 0) return 'no_candidate'
148
+
149
+ const verifiedMatches: Array<{
150
+ transaction: GatewayTransaction
151
+ scope: { organizationId: string; tenantId: string }
152
+ event: Awaited<ReturnType<typeof registration.handler>>
153
+ }> = []
154
+ let verificationUnavailable = false
95
155
 
96
156
  for (const candidate of candidates) {
97
157
  const candidateScope = { organizationId: candidate.organizationId, tenantId: candidate.tenantId }
98
- const credentials = await integrationCredentialsService.resolve(`gateway_${providerKey}`, candidateScope) ?? {}
158
+ let credentials: Record<string, unknown>
159
+ try {
160
+ credentials = await integrationCredentialsService.resolve(`gateway_${providerKey}`, candidateScope) ?? {}
161
+ } catch (error: unknown) {
162
+ reportWebhookError(error, 'payment_gateways.webhook_credentials_failed')
163
+ verificationUnavailable = true
164
+ continue
165
+ }
99
166
  try {
100
- event = await registration.handler({ rawBody, headers, credentials })
101
- transaction = candidate
102
- matchedScope = candidateScope
103
- break
167
+ const candidateEvent = await registration.handler({
168
+ rawBody,
169
+ headers,
170
+ credentials,
171
+ candidate: {
172
+ transactionId: candidate.id,
173
+ paymentId: candidate.paymentId,
174
+ providerSessionId: candidate.providerSessionId ?? null,
175
+ amount: candidate.amount,
176
+ currencyCode: candidate.currencyCode,
177
+ },
178
+ })
179
+ verifiedMatches.push({ transaction: candidate, scope: candidateScope, event: candidateEvent })
104
180
  } catch (error: unknown) {
105
- lastVerificationError = error
181
+ if (error instanceof WebhookVerificationUnavailableError) verificationUnavailable = true
106
182
  }
107
183
  }
108
184
 
109
- if (!event || !transaction || !matchedScope) {
110
- throw lastVerificationError ?? new Error('Webhook verification failed: no matching transaction')
185
+ if (verifiedMatches.length > 1) {
186
+ logger.error('Webhook matched multiple verified candidates', {
187
+ providerKey,
188
+ candidateCount: verifiedMatches.length,
189
+ })
190
+ getTelemetryRuntime()?.reportError(
191
+ new Error('[internal] Webhook matched multiple verified candidates'),
192
+ { module: 'payment_gateways', code: 'payment_gateways.webhook_ambiguous_candidates' },
193
+ )
194
+ return 'verification_failed'
195
+ }
196
+
197
+ if (verificationUnavailable && verifiedMatches.length === 1) {
198
+ logger.warn('Webhook candidate left unverified; ambiguity cannot be ruled out', {
199
+ providerKey,
200
+ candidateCount: candidates.length,
201
+ })
202
+ return 'verification_unavailable'
111
203
  }
112
204
 
113
- const scope = matchedScope
205
+ const [match] = verifiedMatches
206
+ if (!match) return verificationUnavailable ? 'verification_unavailable' : 'verification_failed'
114
207
 
115
208
  const jobPayload = markQueueJobOrigin({
116
209
  providerKey,
117
- event,
118
- transactionId: transaction.id,
119
- scope,
210
+ event: match.event,
211
+ transactionId: match.transaction.id,
212
+ scope: match.scope,
120
213
  }, 'inbound-webhook')
121
214
 
122
- if (process.env.QUEUE_STRATEGY === 'async') {
123
- await queue.enqueue(jobPayload)
124
- } else {
125
- await processPaymentGatewayWebhookJob(
126
- {
127
- em: container.resolve('em') as EntityManager,
128
- paymentGatewayService: service,
129
- integrationLogService: container.resolve('integrationLogService') as IntegrationLogService,
130
- },
131
- jobPayload,
132
- )
215
+ try {
216
+ if (process.env.QUEUE_STRATEGY === 'async') {
217
+ await queue.enqueue(jobPayload)
218
+ } else {
219
+ await processPaymentGatewayWebhookJob(
220
+ {
221
+ em: container.resolve('em') as EntityManager,
222
+ paymentGatewayService: service,
223
+ integrationLogService: container.resolve('integrationLogService') as IntegrationLogService,
224
+ },
225
+ jobPayload,
226
+ )
227
+ }
228
+ } catch (error: unknown) {
229
+ getTelemetryRuntime()?.reportError(error, {
230
+ module: 'payment_gateways',
231
+ code: 'payment_gateways.webhook_processing_failed',
232
+ })
233
+ return 'processing_failed'
133
234
  }
134
235
 
135
- return NextResponse.json({ received: true, queued: true }, { status: 202 })
136
- } catch (err: unknown) {
137
- logger.warn('Webhook verification failed', { providerKey, err })
138
- return NextResponse.json({ error: WEBHOOK_VERIFICATION_FAILED }, { status: 401 })
236
+ return 'accepted'
237
+ } catch (error: unknown) {
238
+ reportWebhookError(error, 'payment_gateways.webhook_unexpected_failure')
239
+ return 'verification_failed'
139
240
  }
140
241
  }
141
242
 
243
+ function respond(
244
+ registration: WebhookHandlerRegistration,
245
+ outcome: WebhookResponseOutcome,
246
+ rateLimitResponse: NextResponse | null,
247
+ ): NextResponse {
248
+ if (!registration.formatResponse) return legacyResponse(outcome, rateLimitResponse)
249
+ try {
250
+ const response = toValidatedResponse(registration.formatResponse(outcome))
251
+ if (outcome === 'rate_limited' && rateLimitResponse) {
252
+ rateLimitResponse.headers.forEach((value, key) => {
253
+ if (key !== 'content-type' && key !== 'content-length') response.headers.set(key, value)
254
+ })
255
+ }
256
+ return response
257
+ } catch (error: unknown) {
258
+ getTelemetryRuntime()?.reportError(error, {
259
+ module: 'payment_gateways',
260
+ code: 'payment_gateways.webhook_formatter_invalid',
261
+ })
262
+ return NextResponse.json({ error: INTERNAL_SERVER_ERROR }, { status: 500 })
263
+ }
264
+ }
265
+
266
+ function legacyResponse(outcome: WebhookResponseOutcome, rateLimitResponse: NextResponse | null): NextResponse {
267
+ if (outcome === 'rate_limited' && rateLimitResponse) return rateLimitResponse
268
+ if (outcome === 'accepted') return NextResponse.json({ received: true, queued: true }, { status: 202 })
269
+ if (outcome === 'payload_too_large') return NextResponse.json({ error: WEBHOOK_PAYLOAD_TOO_LARGE }, { status: 413 })
270
+ return NextResponse.json({ error: WEBHOOK_VERIFICATION_FAILED }, { status: 401 })
271
+ }
272
+
273
+ function toValidatedResponse(formatted: WebhookHttpResponse): NextResponse {
274
+ const { status, body, contentType } = formatted
275
+ if (!Number.isInteger(status) || status < 200 || status > 599) {
276
+ throw new Error('[internal] Webhook formatter returned an invalid status')
277
+ }
278
+ if (contentType !== undefined && (typeof contentType !== 'string' || CONTROL_CHARACTERS.test(contentType))) {
279
+ throw new Error('[internal] Webhook formatter returned an invalid content type')
280
+ }
281
+ const normalizedContentType = contentType?.toLowerCase()
282
+ if (typeof body === 'string') {
283
+ if (normalizedContentType !== undefined && !normalizedContentType.startsWith('text/')) {
284
+ throw new Error('[internal] Webhook formatter returned a non-text content type for a text body')
285
+ }
286
+ return new NextResponse(body, { status, headers: { 'content-type': contentType ?? DEFAULT_TEXT_CONTENT_TYPE } })
287
+ }
288
+ if (!isPlainObject(body)) {
289
+ throw new Error('[internal] Webhook formatter returned an invalid body')
290
+ }
291
+ if (normalizedContentType !== undefined && !JSON_CONTENT_TYPE.test(normalizedContentType)) {
292
+ throw new Error('[internal] Webhook formatter returned a non-JSON content type for an object body')
293
+ }
294
+ const response = NextResponse.json(body, { status })
295
+ if (contentType !== undefined) response.headers.set('content-type', contentType)
296
+ return response
297
+ }
298
+
299
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
300
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) return false
301
+ const prototype = Object.getPrototypeOf(value)
302
+ return prototype === Object.prototype || prototype === null
303
+ }
304
+
305
+ function reportWebhookError(error: unknown, code: string): void {
306
+ getTelemetryRuntime()?.reportError(error, { module: 'payment_gateways', code })
307
+ }
308
+
309
+ function normalizeSessionIdHint(value: unknown): string | null {
310
+ return typeof value === 'string' && value.length > 0 ? value : null
311
+ }
312
+
313
+ function normalizePaymentIdHint(value: unknown): string | null {
314
+ const parsed = paymentIdHintSchema.safeParse(value)
315
+ return parsed.success ? parsed.data : null
316
+ }
317
+
142
318
  async function checkProviderWebhookRateLimit(
143
319
  container: { resolve: (name: string) => unknown },
144
320
  req: Request,
@@ -171,10 +347,15 @@ export const openApi = {
171
347
  summary: 'Process inbound webhook from payment provider',
172
348
  tags: [paymentGatewaysTag],
173
349
  responses: [
174
- { status: 202, description: 'Webhook accepted for async processing' },
175
- { status: 401, description: 'Signature verification failed' },
176
- { status: 413, description: 'Webhook payload too large' },
350
+ { status: 200, description: 'Webhook accepted (provider-defined acknowledgement may apply)' },
351
+ { status: 202, description: 'Webhook accepted for processing (default acknowledgement)' },
352
+ { status: 400, description: 'Webhook rejected (provider-defined response may apply)' },
353
+ { status: 401, description: 'Webhook could not be verified or processed (default failure response)' },
177
354
  { status: 404, description: 'Unknown provider' },
355
+ { status: 413, description: 'Webhook payload too large' },
356
+ { status: 429, description: 'Too many webhook requests' },
357
+ { status: 500, description: 'Internal server error (provider-defined response may apply)' },
358
+ { status: 503, description: 'Webhook temporarily unavailable (provider-defined response may apply)' },
178
359
  ],
179
360
  },
180
361
  },
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto'
2
+ import { QueryOrder } from '@mikro-orm/core'
2
3
  import type { EntityManager } from '@mikro-orm/postgresql'
3
4
  import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
4
5
  import { createLogger } from '@open-mercato/shared/lib/logger'
@@ -184,6 +185,14 @@ export function createPaymentGatewayService(deps: PaymentGatewayServiceDeps) {
184
185
  }, scope)
185
186
  }
186
187
 
188
+ async function stampLastPolledAt(transaction: GatewayTransaction, polledAt: Date): Promise<void> {
189
+ await em.nativeUpdate(
190
+ GatewayTransaction,
191
+ { id: transaction.id, organizationId: transaction.organizationId, tenantId: transaction.tenantId },
192
+ { lastPolledAt: polledAt },
193
+ )
194
+ }
195
+
187
196
  async function resolveAdapterAndCredentials(providerKey: string, scope: { organizationId: string; tenantId: string }) {
188
197
  const integrationId = `gateway_${providerKey}`
189
198
  const selectedVersion = deps.integrationStateService
@@ -685,23 +694,39 @@ export function createPaymentGatewayService(deps: PaymentGatewayServiceDeps) {
685
694
 
686
695
  async getPaymentStatus(transactionId: string, scope: { organizationId: string; tenantId: string }): Promise<GatewayPaymentStatus> {
687
696
  const transaction = await findTransactionOrThrow(transactionId, scope)
688
- const { adapter, credentials } = await resolveAdapterAndCredentials(
689
- transaction.providerKey,
690
- { organizationId: transaction.organizationId, tenantId: transaction.tenantId },
691
- )
692
-
693
- const status = await adapter.getStatus({
694
- sessionId: readProviderSessionId(transaction),
695
- credentials,
696
- })
697
+ let status: GatewayPaymentStatus
698
+ try {
699
+ const { adapter, credentials } = await resolveAdapterAndCredentials(
700
+ transaction.providerKey,
701
+ { organizationId: transaction.organizationId, tenantId: transaction.tenantId },
702
+ )
703
+ status = await adapter.getStatus({
704
+ sessionId: readProviderSessionId(transaction),
705
+ credentials,
706
+ })
707
+ } catch (error) {
708
+ try {
709
+ await stampLastPolledAt(transaction, new Date())
710
+ } catch (stampError) {
711
+ logger.warn('Failed to record last poll time after a failed status read', {
712
+ transactionId: transaction.id,
713
+ providerKey: transaction.providerKey,
714
+ err: stampError,
715
+ })
716
+ }
717
+ throw error
718
+ }
697
719
 
698
- if (status.status !== transaction.unifiedStatus && isValidTransition(transaction.unifiedStatus as UnifiedPaymentStatus, status.status)) {
720
+ const polledAt = new Date()
721
+ const shouldApplyStatus = status.status !== transaction.unifiedStatus
722
+ && isValidTransition(transaction.unifiedStatus as UnifiedPaymentStatus, status.status)
723
+ if (shouldApplyStatus) {
699
724
  const previousStatus = transaction.unifiedStatus
700
725
  transaction.unifiedStatus = status.status
701
726
  alignCapturedAmountWithStatus(transaction, status.status)
702
727
  transaction.gatewayStatus = status.status
703
728
  transaction.gatewayMetadata = { ...readGatewayMetadata(transaction.gatewayMetadata), statusResult: status.providerData ?? null }
704
- transaction.lastPolledAt = new Date()
729
+ transaction.lastPolledAt = polledAt
705
730
  await em.flush()
706
731
  await emitStatusEvent(status.status, {
707
732
  transactionId: transaction.id,
@@ -722,6 +747,8 @@ export function createPaymentGatewayService(deps: PaymentGatewayServiceDeps) {
722
747
  nextStatus: status.status,
723
748
  },
724
749
  )
750
+ } else {
751
+ await stampLastPolledAt(transaction, polledAt)
725
752
  }
726
753
 
727
754
  return status
@@ -846,7 +873,7 @@ export function createPaymentGatewayService(deps: PaymentGatewayServiceDeps) {
846
873
  GatewayTransaction,
847
874
  where,
848
875
  {
849
- orderBy: { updatedAt: 'asc' },
876
+ orderBy: { lastPolledAt: QueryOrder.ASC_NULLS_FIRST, createdAt: QueryOrder.ASC },
850
877
  limit: scope?.limit ?? 100,
851
878
  },
852
879
  scope,