@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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/generated/entities/customer_dictionary_entry/index.js +2 -0
- package/dist/generated/entities/customer_dictionary_entry/index.js.map +2 -2
- package/dist/generated/entities/customer_interaction/index.js +2 -0
- package/dist/generated/entities/customer_interaction/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +2 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/catalog/commands/categories.js +1 -1
- package/dist/modules/catalog/commands/categories.js.map +2 -2
- package/dist/modules/catalog/commands/productDeleteChildren.js +389 -0
- package/dist/modules/catalog/commands/productDeleteChildren.js.map +7 -0
- package/dist/modules/catalog/commands/products.js +52 -17
- package/dist/modules/catalog/commands/products.js.map +2 -2
- package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js +4 -6
- package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js.map +2 -2
- package/dist/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.js +4 -6
- package/dist/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.js.map +2 -2
- package/dist/modules/communication_channels/api/post/oauth/[provider]/initiate/route.js +47 -11
- package/dist/modules/communication_channels/api/post/oauth/[provider]/initiate/route.js.map +3 -3
- package/dist/modules/communication_channels/commands/deliver-outbound-message.js +5 -1
- package/dist/modules/communication_channels/commands/deliver-outbound-message.js.map +2 -2
- package/dist/modules/communication_channels/lib/inbound-message-origin.js +1 -1
- package/dist/modules/communication_channels/lib/inbound-message-origin.js.map +2 -2
- package/dist/modules/communication_channels/lib/oauth-state.js +4 -1
- package/dist/modules/communication_channels/lib/oauth-state.js.map +2 -2
- package/dist/modules/communication_channels/lib/use-connect-channel.js +10 -0
- package/dist/modules/communication_channels/lib/use-connect-channel.js.map +2 -2
- package/dist/modules/customers/api/activity-types/route.js +151 -0
- package/dist/modules/customers/api/activity-types/route.js.map +7 -0
- package/dist/modules/customers/api/deals/[id]/people/route.js +58 -5
- package/dist/modules/customers/api/deals/[id]/people/route.js.map +2 -2
- package/dist/modules/customers/api/dictionaries/[kind]/[id]/route.js +13 -5
- package/dist/modules/customers/api/dictionaries/[kind]/[id]/route.js.map +2 -2
- package/dist/modules/customers/api/dictionaries/[kind]/route.js +20 -5
- package/dist/modules/customers/api/dictionaries/[kind]/route.js.map +2 -2
- package/dist/modules/customers/api/interactions/route.js +3 -0
- package/dist/modules/customers/api/interactions/route.js.map +2 -2
- package/dist/modules/customers/api/interactions/tasks/route.js +1 -0
- package/dist/modules/customers/api/interactions/tasks/route.js.map +2 -2
- package/dist/modules/customers/backend/config/customers/pipeline-stages/page.meta.js +18 -0
- package/dist/modules/customers/backend/config/customers/pipeline-stages/page.meta.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js +0 -23
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/page.js +15 -17
- package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
- package/dist/modules/customers/calendar-event-types.js +548 -0
- package/dist/modules/customers/calendar-event-types.js.map +7 -0
- package/dist/modules/customers/commands/dictionaries.js +70 -10
- package/dist/modules/customers/commands/dictionaries.js.map +2 -2
- package/dist/modules/customers/commands/interactions.js +187 -22
- package/dist/modules/customers/commands/interactions.js.map +3 -3
- package/dist/modules/customers/components/ActivityTypeEditor.js +288 -0
- package/dist/modules/customers/components/ActivityTypeEditor.js.map +7 -0
- package/dist/modules/customers/components/DictionarySettings.js +3 -1
- package/dist/modules/customers/components/DictionarySettings.js.map +2 -2
- package/dist/modules/customers/components/calendar/CalendarEventEditor.js +391 -297
- package/dist/modules/customers/components/calendar/CalendarEventEditor.js.map +3 -3
- package/dist/modules/customers/components/calendar/CalendarSettingsModal.js +9 -74
- package/dist/modules/customers/components/calendar/CalendarSettingsModal.js.map +2 -2
- package/dist/modules/customers/components/calendar/TimeGrid.js +20 -16
- package/dist/modules/customers/components/calendar/TimeGrid.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/EventTypePanel.js +218 -0
- package/dist/modules/customers/components/calendar/editor/EventTypePanel.js.map +7 -0
- package/dist/modules/customers/components/calendar/editor/LocationField.js +7 -3
- package/dist/modules/customers/components/calendar/editor/LocationField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/PeopleField.js +5 -2
- package/dist/modules/customers/components/calendar/editor/PeopleField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/PriorityField.js +3 -1
- package/dist/modules/customers/components/calendar/editor/PriorityField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/RepeatField.js +9 -1
- package/dist/modules/customers/components/calendar/editor/RepeatField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/ResourcesField.js +4 -1
- package/dist/modules/customers/components/calendar/editor/ResourcesField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/ScheduleSection.js +15 -7
- package/dist/modules/customers/components/calendar/editor/ScheduleSection.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/SegmentGroup.js +3 -1
- package/dist/modules/customers/components/calendar/editor/SegmentGroup.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/hooks.js +28 -13
- package/dist/modules/customers/components/calendar/editor/hooks.js.map +3 -3
- package/dist/modules/customers/components/calendar/editor/inputs.js +14 -6
- package/dist/modules/customers/components/calendar/editor/inputs.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/useCalendarCustomFields.js +54 -0
- package/dist/modules/customers/components/calendar/editor/useCalendarCustomFields.js.map +7 -0
- package/dist/modules/customers/components/calendar/editor/useEventTypeCatalog.js +82 -0
- package/dist/modules/customers/components/calendar/editor/useEventTypeCatalog.js.map +7 -0
- package/dist/modules/customers/components/calendar/types.js +1 -0
- package/dist/modules/customers/components/calendar/types.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyPeopleSection.js +78 -343
- package/dist/modules/customers/components/detail/CompanyPeopleSection.js.map +2 -2
- package/dist/modules/customers/components/detail/CreatePersonDialog.js +12 -11
- package/dist/modules/customers/components/detail/CreatePersonDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/DealForm.js +7 -3
- package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
- package/dist/modules/customers/components/detail/DealPeopleSection.js +206 -0
- package/dist/modules/customers/components/detail/DealPeopleSection.js.map +7 -0
- package/dist/modules/customers/components/detail/LinkedPeopleSection.js +342 -0
- package/dist/modules/customers/components/detail/LinkedPeopleSection.js.map +7 -0
- package/dist/modules/customers/components/detail/PersonCard.js +5 -2
- package/dist/modules/customers/components/detail/PersonCard.js.map +2 -2
- package/dist/modules/customers/components/linking/adapters/personAdapter.js +3 -0
- package/dist/modules/customers/components/linking/adapters/personAdapter.js.map +2 -2
- package/dist/modules/customers/data/entities.js +6 -0
- package/dist/modules/customers/data/entities.js.map +2 -2
- package/dist/modules/customers/data/validators.js +39 -10
- package/dist/modules/customers/data/validators.js.map +2 -2
- package/dist/modules/customers/di.js +20 -1
- package/dist/modules/customers/di.js.map +2 -2
- package/dist/modules/customers/extension-points.js +13 -0
- package/dist/modules/customers/extension-points.js.map +2 -2
- package/dist/modules/customers/lib/calendar/conflicts.js +59 -16
- package/dist/modules/customers/lib/calendar/conflicts.js.map +2 -2
- package/dist/modules/customers/lib/calendar/editorPayload.js +34 -16
- package/dist/modules/customers/lib/calendar/editorPayload.js.map +2 -2
- package/dist/modules/customers/lib/calendar/eventTypeResolver.js +197 -0
- package/dist/modules/customers/lib/calendar/eventTypeResolver.js.map +7 -0
- package/dist/modules/customers/lib/calendar/interactionApplicability.js +67 -0
- package/dist/modules/customers/lib/calendar/interactionApplicability.js.map +7 -0
- package/dist/modules/customers/lib/calendar/mapItem.js +5 -2
- package/dist/modules/customers/lib/calendar/mapItem.js.map +2 -2
- package/dist/modules/customers/lib/calendar/recurrence.js +62 -1
- package/dist/modules/customers/lib/calendar/recurrence.js.map +2 -2
- package/dist/modules/customers/lib/calendar/timezone.js +111 -0
- package/dist/modules/customers/lib/calendar/timezone.js.map +7 -0
- package/dist/modules/customers/lib/interactionReadModel.js +1 -0
- package/dist/modules/customers/lib/interactionReadModel.js.map +2 -2
- package/dist/modules/customers/lib/todoCompatibility.js +1 -0
- package/dist/modules/customers/lib/todoCompatibility.js.map +2 -2
- package/dist/modules/customers/migrations/Migration20260928175058_customers.js +17 -0
- package/dist/modules/customers/migrations/Migration20260928175058_customers.js.map +7 -0
- package/dist/modules/customers/migrations/Migration20260930110235_customers.js +17 -0
- package/dist/modules/customers/migrations/Migration20260930110235_customers.js.map +7 -0
- package/dist/modules/dictionaries/components/DictionaryTable.js +60 -51
- package/dist/modules/dictionaries/components/DictionaryTable.js.map +2 -2
- package/dist/modules/messages/subscribers/message-notification.js +13 -1
- package/dist/modules/messages/subscribers/message-notification.js.map +2 -2
- package/dist/modules/payment_gateways/api/webhook/[provider]/route.js +201 -53
- package/dist/modules/payment_gateways/api/webhook/[provider]/route.js.map +2 -2
- package/dist/modules/payment_gateways/lib/gateway-service.js +37 -11
- package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
- package/dist/modules/planner/lib/availabilityMerge.js +101 -38
- package/dist/modules/planner/lib/availabilityMerge.js.map +3 -3
- package/dist/modules/planner/services/plannerAvailabilityService.js.map +2 -2
- package/dist/modules/sales/components/documents/LineItemDialog.js +159 -81
- package/dist/modules/sales/components/documents/LineItemDialog.js.map +2 -2
- package/dist/modules/sales/components/documents/lineItemUtils.js +7 -0
- package/dist/modules/sales/components/documents/lineItemUtils.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js +12 -7
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js.map +2 -2
- package/dist/modules/sync_excel/lib/adapters/customers.js +28 -25
- package/dist/modules/sync_excel/lib/adapters/customers.js.map +2 -2
- package/generated/entities/customer_dictionary_entry/index.ts +1 -0
- package/generated/entities/customer_interaction/index.ts +1 -0
- package/generated/entity-fields-registry.ts +2 -0
- package/package.json +9 -8
- package/src/modules/auth/i18n/de.json +3 -0
- package/src/modules/auth/i18n/en.json +3 -0
- package/src/modules/auth/i18n/es.json +3 -0
- package/src/modules/auth/i18n/ko.json +3 -0
- package/src/modules/auth/i18n/pl.json +3 -0
- package/src/modules/catalog/commands/categories.ts +1 -1
- package/src/modules/catalog/commands/productDeleteChildren.ts +557 -0
- package/src/modules/catalog/commands/products.ts +58 -17
- package/src/modules/communication_channels/api/post/channels/connect/credentials/route.ts +4 -6
- package/src/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.ts +4 -6
- package/src/modules/communication_channels/api/post/oauth/[provider]/initiate/route.ts +46 -10
- package/src/modules/communication_channels/commands/deliver-outbound-message.ts +6 -1
- package/src/modules/communication_channels/i18n/de.json +4 -0
- package/src/modules/communication_channels/i18n/en.json +4 -0
- package/src/modules/communication_channels/i18n/es.json +4 -0
- package/src/modules/communication_channels/i18n/ko.json +4 -0
- package/src/modules/communication_channels/i18n/pl.json +4 -0
- package/src/modules/communication_channels/lib/inbound-message-origin.ts +2 -6
- package/src/modules/communication_channels/lib/oauth-state.ts +4 -1
- package/src/modules/communication_channels/lib/use-connect-channel.ts +10 -0
- package/src/modules/customers/api/activity-types/route.ts +154 -0
- package/src/modules/customers/api/deals/[id]/people/route.ts +82 -3
- package/src/modules/customers/api/dictionaries/[kind]/[id]/route.ts +9 -1
- package/src/modules/customers/api/dictionaries/[kind]/route.ts +18 -2
- package/src/modules/customers/api/interactions/route.ts +4 -0
- package/src/modules/customers/api/interactions/tasks/route.ts +1 -0
- package/src/modules/customers/backend/config/customers/pipeline-stages/page.meta.ts +20 -0
- package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.ts +0 -29
- package/src/modules/customers/backend/customers/deals/[id]/page.tsx +15 -17
- package/src/modules/customers/calendar-event-types.ts +674 -0
- package/src/modules/customers/commands/dictionaries.ts +84 -8
- package/src/modules/customers/commands/interactions.ts +244 -21
- package/src/modules/customers/components/ActivityTypeEditor.tsx +239 -0
- package/src/modules/customers/components/DictionarySettings.tsx +5 -3
- package/src/modules/customers/components/calendar/CalendarEventEditor.tsx +347 -218
- package/src/modules/customers/components/calendar/CalendarSettingsModal.tsx +18 -96
- package/src/modules/customers/components/calendar/TimeGrid.tsx +20 -18
- package/src/modules/customers/components/calendar/editor/EventTypePanel.tsx +230 -0
- package/src/modules/customers/components/calendar/editor/LocationField.tsx +7 -1
- package/src/modules/customers/components/calendar/editor/PeopleField.tsx +4 -0
- package/src/modules/customers/components/calendar/editor/PriorityField.tsx +3 -0
- package/src/modules/customers/components/calendar/editor/RepeatField.tsx +10 -0
- package/src/modules/customers/components/calendar/editor/ResourcesField.tsx +4 -0
- package/src/modules/customers/components/calendar/editor/ScheduleSection.tsx +16 -4
- package/src/modules/customers/components/calendar/editor/SegmentGroup.tsx +3 -0
- package/src/modules/customers/components/calendar/editor/hooks.ts +29 -13
- package/src/modules/customers/components/calendar/editor/inputs.tsx +17 -5
- package/src/modules/customers/components/calendar/editor/useCalendarCustomFields.ts +49 -0
- package/src/modules/customers/components/calendar/editor/useEventTypeCatalog.ts +100 -0
- package/src/modules/customers/components/calendar/types.ts +1 -0
- package/src/modules/customers/components/detail/CompanyPeopleSection.tsx +94 -388
- package/src/modules/customers/components/detail/CreatePersonDialog.tsx +39 -19
- package/src/modules/customers/components/detail/DealForm.tsx +7 -3
- package/src/modules/customers/components/detail/DealPeopleSection.tsx +256 -0
- package/src/modules/customers/components/detail/LinkedPeopleSection.tsx +474 -0
- package/src/modules/customers/components/detail/PersonCard.tsx +12 -4
- package/src/modules/customers/components/detail/types.ts +1 -0
- package/src/modules/customers/components/linking/adapters/personAdapter.tsx +4 -0
- package/src/modules/customers/data/entities.ts +8 -1
- package/src/modules/customers/data/validators.ts +42 -7
- package/src/modules/customers/di.ts +21 -0
- package/src/modules/customers/extension-points.ts +13 -0
- package/src/modules/customers/i18n/de.json +104 -8
- package/src/modules/customers/i18n/en.json +104 -8
- package/src/modules/customers/i18n/es.json +104 -8
- package/src/modules/customers/i18n/ko.json +104 -8
- package/src/modules/customers/i18n/pl.json +104 -8
- package/src/modules/customers/lib/calendar/conflicts.ts +106 -27
- package/src/modules/customers/lib/calendar/editorPayload.ts +39 -19
- package/src/modules/customers/lib/calendar/eventTypeResolver.ts +258 -0
- package/src/modules/customers/lib/calendar/interactionApplicability.ts +99 -0
- package/src/modules/customers/lib/calendar/mapItem.ts +6 -2
- package/src/modules/customers/lib/calendar/recurrence.ts +65 -1
- package/src/modules/customers/lib/calendar/timezone.ts +117 -0
- package/src/modules/customers/lib/interactionReadModel.ts +1 -0
- package/src/modules/customers/lib/todoCompatibility.ts +2 -0
- package/src/modules/customers/migrations/.snapshot-open-mercato.json +34 -0
- package/src/modules/customers/migrations/Migration20260928175058_customers.ts +15 -0
- package/src/modules/customers/migrations/Migration20260930110235_customers.ts +15 -0
- package/src/modules/dictionaries/components/DictionaryTable.tsx +63 -56
- package/src/modules/messages/subscribers/message-notification.ts +13 -3
- package/src/modules/payment_gateways/api/webhook/[provider]/route.ts +243 -62
- package/src/modules/payment_gateways/lib/gateway-service.ts +39 -12
- package/src/modules/planner/lib/availabilityMerge.ts +106 -41
- package/src/modules/planner/services/plannerAvailabilityService.ts +4 -0
- package/src/modules/sales/components/documents/LineItemDialog.tsx +194 -111
- package/src/modules/sales/components/documents/lineItemUtils.ts +18 -0
- package/src/modules/staff/backend/staff/time-tracking/projects/[id]/page.tsx +25 -13
- 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
|
-
<
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
140
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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>>(
|
|
68
|
-
const
|
|
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
|
|
75
|
-
// credentials can verify the signature, we fail closed
|
|
76
|
-
// forged webhooks (e.g. mock gateway PoC) from mutating another tenant's
|
|
77
|
-
// state via `event.data.metadata.{organizationId,tenantId}`.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
181
|
+
if (error instanceof WebhookVerificationUnavailableError) verificationUnavailable = true
|
|
106
182
|
}
|
|
107
183
|
}
|
|
108
184
|
|
|
109
|
-
if (
|
|
110
|
-
|
|
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
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
|
136
|
-
} catch (
|
|
137
|
-
|
|
138
|
-
return
|
|
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:
|
|
175
|
-
{ status:
|
|
176
|
-
{ status:
|
|
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
|
-
|
|
689
|
-
|
|
690
|
-
{
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
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
|
-
|
|
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 =
|
|
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: {
|
|
876
|
+
orderBy: { lastPolledAt: QueryOrder.ASC_NULLS_FIRST, createdAt: QueryOrder.ASC },
|
|
850
877
|
limit: scope?.limit ?? 100,
|
|
851
878
|
},
|
|
852
879
|
scope,
|