@open-mercato/core 0.6.6-develop.6229.1.ebe6706732 → 0.6.6-develop.6241.1.47c01ad4ed
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/modules/auth/commands/users.js +1 -0
- package/dist/modules/auth/commands/users.js.map +2 -2
- package/dist/modules/communication_channels/data/enrichers.js +9 -1
- package/dist/modules/communication_channels/data/enrichers.js.map +2 -2
- package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.js +3 -5
- package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.js.map +2 -2
- package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.js.map +2 -2
- package/dist/modules/customers/api/pipeline-stages/reorder/route.js +39 -1
- package/dist/modules/customers/api/pipeline-stages/reorder/route.js.map +2 -2
- package/dist/modules/customers/api/pipeline-stages/route.js +107 -12
- package/dist/modules/customers/api/pipeline-stages/route.js.map +2 -2
- package/dist/modules/customers/api/pipelines/route.js +107 -12
- package/dist/modules/customers/api/pipelines/route.js.map +2 -2
- package/dist/modules/customers/api/settings/address-format/route.js +33 -1
- package/dist/modules/customers/api/settings/address-format/route.js.map +2 -2
- package/dist/modules/customers/api/tags/assign/route.js +37 -0
- package/dist/modules/customers/api/tags/assign/route.js.map +2 -2
- package/dist/modules/customers/api/tags/unassign/route.js +37 -0
- package/dist/modules/customers/api/tags/unassign/route.js.map +2 -2
- package/dist/modules/entities/api/encryption.js +109 -38
- package/dist/modules/entities/api/encryption.js.map +2 -2
- package/dist/modules/entities/components/EncryptionManager.js +22 -9
- package/dist/modules/entities/components/EncryptionManager.js.map +2 -2
- package/dist/modules/integrations/api/[id]/credentials/route.js +23 -1
- package/dist/modules/integrations/api/[id]/credentials/route.js.map +2 -2
- package/dist/modules/integrations/api/[id]/route.js +10 -3
- package/dist/modules/integrations/api/[id]/route.js.map +2 -2
- package/dist/modules/integrations/api/[id]/state/route.js +20 -5
- package/dist/modules/integrations/api/[id]/state/route.js.map +2 -2
- package/dist/modules/integrations/api/[id]/version/route.js +17 -1
- package/dist/modules/integrations/api/[id]/version/route.js.map +2 -2
- package/dist/modules/integrations/api/route.js +2 -0
- package/dist/modules/integrations/api/route.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/[id]/page.js +36 -19
- package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js +13 -9
- package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/page.js +4 -3
- package/dist/modules/integrations/backend/integrations/page.js.map +2 -2
- package/dist/modules/integrations/data/entities.js +2 -2
- package/dist/modules/integrations/data/entities.js.map +2 -2
- package/dist/modules/integrations/lib/credentials-service.js +32 -0
- package/dist/modules/integrations/lib/credentials-service.js.map +2 -2
- package/dist/modules/integrations/lib/state-service.js +4 -2
- package/dist/modules/integrations/lib/state-service.js.map +2 -2
- package/dist/modules/notifications/lib/notificationService.js +42 -10
- package/dist/modules/notifications/lib/notificationService.js.map +2 -2
- package/dist/modules/payment_gateways/api/status/route.js +91 -6
- package/dist/modules/payment_gateways/api/status/route.js.map +2 -2
- package/dist/modules/payment_gateways/backend/payment-gateways/page.js +25 -11
- package/dist/modules/payment_gateways/backend/payment-gateways/page.js.map +2 -2
- package/dist/modules/sales/backend/sales/channels/offers/page.js +25 -7
- package/dist/modules/sales/backend/sales/channels/offers/page.js.map +2 -2
- package/dist/modules/sales/backend/sales/channels/page.js +25 -7
- package/dist/modules/sales/backend/sales/channels/page.js.map +2 -2
- package/dist/modules/sales/components/AdjustmentKindSettings.js +52 -24
- package/dist/modules/sales/components/AdjustmentKindSettings.js.map +2 -2
- package/dist/modules/sales/components/DocumentNumberSettings.js +26 -9
- package/dist/modules/sales/components/DocumentNumberSettings.js.map +2 -2
- package/dist/modules/sales/components/OrderEditingSettings.js +26 -9
- package/dist/modules/sales/components/OrderEditingSettings.js.map +2 -2
- package/dist/modules/sales/components/StatusSettings.js +66 -31
- package/dist/modules/sales/components/StatusSettings.js.map +2 -2
- package/dist/modules/sales/components/channels/SalesChannelOffersPanel.js +25 -7
- package/dist/modules/sales/components/channels/SalesChannelOffersPanel.js.map +2 -2
- package/dist/modules/shipping_carriers/api/cancel/route.js +38 -0
- package/dist/modules/shipping_carriers/api/cancel/route.js.map +2 -2
- package/dist/modules/shipping_carriers/api/shipments/route.js +12 -5
- package/dist/modules/shipping_carriers/api/shipments/route.js.map +2 -2
- package/dist/modules/staff/lib/timesheets-ui/TimerBar.js +7 -8
- package/dist/modules/staff/lib/timesheets-ui/TimerBar.js.map +2 -2
- package/dist/modules/staff/lib/timesheets-ui/timerErrors.js +12 -0
- package/dist/modules/staff/lib/timesheets-ui/timerErrors.js.map +7 -0
- package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js +4 -3
- package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/page.js +19 -10
- package/dist/modules/workflows/backend/definitions/page.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/auth/commands/users.ts +1 -0
- package/src/modules/communication_channels/data/enrichers.ts +25 -0
- package/src/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.tsx +7 -6
- package/src/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.ts +5 -2
- package/src/modules/customers/api/pipeline-stages/reorder/route.ts +41 -1
- package/src/modules/customers/api/pipeline-stages/route.ts +112 -13
- package/src/modules/customers/api/pipelines/route.ts +112 -13
- package/src/modules/customers/api/settings/address-format/route.ts +36 -1
- package/src/modules/customers/api/tags/assign/route.ts +39 -0
- package/src/modules/customers/api/tags/unassign/route.ts +39 -0
- package/src/modules/customers/i18n/de.json +1 -0
- package/src/modules/customers/i18n/en.json +1 -0
- package/src/modules/customers/i18n/es.json +1 -0
- package/src/modules/customers/i18n/pl.json +1 -0
- package/src/modules/entities/api/encryption.ts +122 -39
- package/src/modules/entities/components/EncryptionManager.tsx +28 -9
- package/src/modules/integrations/api/[id]/credentials/route.ts +23 -0
- package/src/modules/integrations/api/[id]/route.ts +8 -1
- package/src/modules/integrations/api/[id]/state/route.ts +20 -4
- package/src/modules/integrations/api/[id]/version/route.ts +18 -1
- package/src/modules/integrations/api/route.ts +3 -0
- package/src/modules/integrations/backend/integrations/[id]/page.tsx +39 -20
- package/src/modules/integrations/backend/integrations/bundle/[id]/page.tsx +19 -11
- package/src/modules/integrations/backend/integrations/page.tsx +8 -5
- package/src/modules/integrations/data/entities.ts +2 -2
- package/src/modules/integrations/lib/credentials-service.ts +35 -0
- package/src/modules/integrations/lib/state-service.ts +3 -0
- package/src/modules/notifications/lib/notificationService.ts +74 -11
- package/src/modules/payment_gateways/api/status/route.ts +97 -5
- package/src/modules/payment_gateways/backend/payment-gateways/page.tsx +27 -11
- package/src/modules/sales/backend/sales/channels/offers/page.tsx +31 -7
- package/src/modules/sales/backend/sales/channels/page.tsx +31 -7
- package/src/modules/sales/components/AdjustmentKindSettings.tsx +60 -24
- package/src/modules/sales/components/DocumentNumberSettings.tsx +32 -10
- package/src/modules/sales/components/OrderEditingSettings.tsx +32 -10
- package/src/modules/sales/components/StatusSettings.tsx +74 -33
- package/src/modules/sales/components/channels/SalesChannelOffersPanel.tsx +30 -6
- package/src/modules/shipping_carriers/api/cancel/route.ts +46 -0
- package/src/modules/shipping_carriers/api/shipments/route.ts +21 -6
- package/src/modules/staff/i18n/de.json +5 -5
- package/src/modules/staff/i18n/es.json +5 -5
- package/src/modules/staff/i18n/pl.json +5 -5
- package/src/modules/staff/lib/timesheets-ui/TimerBar.tsx +7 -8
- package/src/modules/staff/lib/timesheets-ui/timerErrors.ts +23 -0
- package/src/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.tsx +4 -3
- package/src/modules/workflows/backend/definitions/page.tsx +19 -10
|
@@ -24,7 +24,8 @@ import { PasswordInput } from '@open-mercato/ui/primitives/password-input'
|
|
|
24
24
|
import { Spinner } from '@open-mercato/ui/primitives/spinner'
|
|
25
25
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@open-mercato/ui/primitives/tabs'
|
|
26
26
|
import { JsonDisplay } from '@open-mercato/ui/backend/JsonDisplay'
|
|
27
|
-
import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
|
|
27
|
+
import { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
|
|
28
|
+
import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
|
|
28
29
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
29
30
|
import { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'
|
|
30
31
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
@@ -105,8 +106,10 @@ type IntegrationDetail = {
|
|
|
105
106
|
lastHealthCheckedAt: string | null
|
|
106
107
|
lastHealthLatencyMs: number | null
|
|
107
108
|
enabledAt: string | null
|
|
109
|
+
updatedAt: string | null
|
|
108
110
|
}
|
|
109
111
|
hasCredentials: boolean
|
|
112
|
+
credentialsUpdatedAt?: string | null
|
|
110
113
|
healthStatus: 'healthy' | 'degraded' | 'unhealthy' | 'unconfigured'
|
|
111
114
|
analytics: IntegrationLogAnalytics
|
|
112
115
|
}
|
|
@@ -423,6 +426,7 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
|
|
|
423
426
|
const [isNotFound, setIsNotFound] = React.useState(false)
|
|
424
427
|
|
|
425
428
|
const [credValues, setCredValues] = React.useState<Record<string, unknown>>({})
|
|
429
|
+
const [credentialsUpdatedAt, setCredentialsUpdatedAt] = React.useState<string | null>(null)
|
|
426
430
|
const [credentialsFormKey, setCredentialsFormKey] = React.useState(0)
|
|
427
431
|
const [isSavingCredentials, setIsSavingCredentials] = React.useState(false)
|
|
428
432
|
|
|
@@ -486,11 +490,14 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
|
|
|
486
490
|
const loadCredentials = React.useCallback(async () => {
|
|
487
491
|
const currentIntegrationId = resolveCurrentIntegrationId()
|
|
488
492
|
if (!currentIntegrationId) return
|
|
489
|
-
const call = await apiCall<{ credentials: Record<string, unknown
|
|
493
|
+
const call = await apiCall<{ credentials: Record<string, unknown>; updatedAt?: string | null }>(
|
|
490
494
|
`/api/integrations/${encodeURIComponent(currentIntegrationId)}/credentials`,
|
|
491
495
|
undefined,
|
|
492
496
|
{ fallback: null },
|
|
493
497
|
)
|
|
498
|
+
if (call.ok && call.result) {
|
|
499
|
+
setCredentialsUpdatedAt(call.result.updatedAt ?? null)
|
|
500
|
+
}
|
|
494
501
|
if (call.ok && call.result?.credentials) {
|
|
495
502
|
const next = { ...call.result.credentials }
|
|
496
503
|
if (currentIntegrationId === 'storage_s3') {
|
|
@@ -671,11 +678,14 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
|
|
|
671
678
|
const call = await runMutationWithContext({
|
|
672
679
|
actionId: 'toggle-state',
|
|
673
680
|
mutationPayload: { integrationId: currentIntegrationId, isEnabled: enabled },
|
|
674
|
-
operation: () =>
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
681
|
+
operation: () => withScopedApiRequestHeaders(
|
|
682
|
+
buildOptimisticLockHeader(detail?.state.updatedAt),
|
|
683
|
+
() => apiCall(`/api/integrations/${encodeURIComponent(currentIntegrationId)}/state`, {
|
|
684
|
+
method: 'PUT',
|
|
685
|
+
headers: { 'Content-Type': 'application/json' },
|
|
686
|
+
body: JSON.stringify({ isEnabled: enabled }),
|
|
687
|
+
}, { fallback: null }),
|
|
688
|
+
),
|
|
679
689
|
})
|
|
680
690
|
if (call.ok) {
|
|
681
691
|
setDetail((prev) => prev ? {
|
|
@@ -687,6 +697,7 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
|
|
|
687
697
|
},
|
|
688
698
|
} : prev)
|
|
689
699
|
flash(t('integrations.detail.stateUpdated'), 'success')
|
|
700
|
+
void refreshDetail()
|
|
690
701
|
} else {
|
|
691
702
|
flash(t('integrations.detail.stateError'), 'error')
|
|
692
703
|
}
|
|
@@ -695,7 +706,7 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
|
|
|
695
706
|
} finally {
|
|
696
707
|
setIsTogglingState(false)
|
|
697
708
|
}
|
|
698
|
-
}, [resolveCurrentIntegrationId, runMutationWithContext, t])
|
|
709
|
+
}, [detail?.state.updatedAt, refreshDetail, resolveCurrentIntegrationId, runMutationWithContext, t])
|
|
699
710
|
|
|
700
711
|
const handleSaveCredentials = React.useCallback(async (values: Record<string, unknown>) => {
|
|
701
712
|
const currentIntegrationId = resolveCurrentIntegrationId()
|
|
@@ -719,17 +730,21 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
|
|
|
719
730
|
actionId: 'save-credentials',
|
|
720
731
|
tabId: 'credentials',
|
|
721
732
|
mutationPayload: { integrationId: currentIntegrationId, credentials: sanitizedValues },
|
|
722
|
-
operation: () =>
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
733
|
+
operation: () => withScopedApiRequestHeaders(
|
|
734
|
+
buildOptimisticLockHeader(credentialsUpdatedAt),
|
|
735
|
+
() => apiCall(`/api/integrations/${encodeURIComponent(currentIntegrationId)}/credentials`, {
|
|
736
|
+
method: 'PUT',
|
|
737
|
+
headers: { 'Content-Type': 'application/json' },
|
|
738
|
+
body: JSON.stringify({ credentials: sanitizedValues }),
|
|
739
|
+
}, { fallback: null }),
|
|
740
|
+
),
|
|
727
741
|
})
|
|
728
742
|
|
|
729
743
|
if (call.ok) {
|
|
730
744
|
setCredValues(sanitizedValues)
|
|
731
745
|
setCredentialsFormKey((current) => current + 1)
|
|
732
746
|
flash(t('integrations.detail.credentials.saved'), 'success')
|
|
747
|
+
void loadCredentials()
|
|
733
748
|
return
|
|
734
749
|
}
|
|
735
750
|
|
|
@@ -745,7 +760,7 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
|
|
|
745
760
|
} finally {
|
|
746
761
|
setIsSavingCredentials(false)
|
|
747
762
|
}
|
|
748
|
-
}, [resolveCurrentIntegrationId, runMutationWithContext, t])
|
|
763
|
+
}, [credentialsUpdatedAt, loadCredentials, resolveCurrentIntegrationId, runMutationWithContext, t])
|
|
749
764
|
|
|
750
765
|
const handleVersionChange = React.useCallback(async (version: string) => {
|
|
751
766
|
const currentIntegrationId = resolveCurrentIntegrationId()
|
|
@@ -755,22 +770,26 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
|
|
|
755
770
|
actionId: 'change-version',
|
|
756
771
|
tabId: 'version',
|
|
757
772
|
mutationPayload: { integrationId: currentIntegrationId, apiVersion: version },
|
|
758
|
-
operation: () =>
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
773
|
+
operation: () => withScopedApiRequestHeaders(
|
|
774
|
+
buildOptimisticLockHeader(detail?.state.updatedAt),
|
|
775
|
+
() => apiCall(`/api/integrations/${encodeURIComponent(currentIntegrationId)}/version`, {
|
|
776
|
+
method: 'PUT',
|
|
777
|
+
headers: { 'Content-Type': 'application/json' },
|
|
778
|
+
body: JSON.stringify({ apiVersion: version }),
|
|
779
|
+
}, { fallback: null }),
|
|
780
|
+
),
|
|
763
781
|
})
|
|
764
782
|
if (call.ok) {
|
|
765
783
|
setDetail((prev) => prev ? { ...prev, state: { ...prev.state, apiVersion: version } } : prev)
|
|
766
784
|
flash(t('integrations.detail.version.saved'), 'success')
|
|
785
|
+
void refreshDetail()
|
|
767
786
|
} else {
|
|
768
787
|
flash(t('integrations.detail.version.saveError'), 'error')
|
|
769
788
|
}
|
|
770
789
|
} catch {
|
|
771
790
|
flash(t('integrations.detail.version.saveError'), 'error')
|
|
772
791
|
}
|
|
773
|
-
}, [resolveCurrentIntegrationId, runMutationWithContext, t])
|
|
792
|
+
}, [detail?.state.updatedAt, refreshDetail, resolveCurrentIntegrationId, runMutationWithContext, t])
|
|
774
793
|
|
|
775
794
|
const handleHealthCheck = React.useCallback(async () => {
|
|
776
795
|
const currentIntegrationId = resolveCurrentIntegrationId()
|
|
@@ -38,6 +38,7 @@ type BundleIntegration = {
|
|
|
38
38
|
description?: string
|
|
39
39
|
category?: string
|
|
40
40
|
isEnabled: boolean
|
|
41
|
+
state?: { updatedAt?: string | null }
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
type BundleDetail = {
|
|
@@ -56,6 +57,7 @@ type BundleDetail = {
|
|
|
56
57
|
bundleIntegrations: BundleIntegration[]
|
|
57
58
|
state: { isEnabled: boolean }
|
|
58
59
|
hasCredentials: boolean
|
|
60
|
+
credentialsUpdatedAt?: string | null
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
type BundleConfigPageProps = {
|
|
@@ -86,6 +88,7 @@ export default function BundleConfigPage({ params }: BundleConfigPageProps) {
|
|
|
86
88
|
const [error, setError] = React.useState<string | null>(null)
|
|
87
89
|
const [isNotFound, setIsNotFound] = React.useState(false)
|
|
88
90
|
const [credValues, setCredValues] = React.useState<Record<string, unknown>>({})
|
|
91
|
+
const [credentialsUpdatedAt, setCredentialsUpdatedAt] = React.useState<string | null>(null)
|
|
89
92
|
const [isSavingCreds, setIsSavingCreds] = React.useState(false)
|
|
90
93
|
const [togglingIds, setTogglingIds] = React.useState<Set<string>>(new Set())
|
|
91
94
|
|
|
@@ -131,11 +134,14 @@ export default function BundleConfigPage({ params }: BundleConfigPageProps) {
|
|
|
131
134
|
}
|
|
132
135
|
setDetail(call.result)
|
|
133
136
|
|
|
134
|
-
const credCall = await apiCall<{ credentials: Record<string, unknown
|
|
137
|
+
const credCall = await apiCall<{ credentials: Record<string, unknown>; updatedAt?: string | null }>(
|
|
135
138
|
`/api/integrations/${encodeURIComponent(currentBundleId)}/credentials`,
|
|
136
139
|
undefined,
|
|
137
140
|
{ fallback: null },
|
|
138
141
|
)
|
|
142
|
+
if (credCall.ok && credCall.result) {
|
|
143
|
+
setCredentialsUpdatedAt(credCall.result.updatedAt ?? null)
|
|
144
|
+
}
|
|
139
145
|
if (credCall.ok && credCall.result?.credentials) {
|
|
140
146
|
const next = { ...credCall.result.credentials }
|
|
141
147
|
if (currentBundleId === 'storage_s3') {
|
|
@@ -168,9 +174,8 @@ export default function BundleConfigPage({ params }: BundleConfigPageProps) {
|
|
|
168
174
|
bundleId: currentBundleId,
|
|
169
175
|
retryLastMutation,
|
|
170
176
|
},
|
|
171
|
-
// TODO(#2373-B): thread updatedAt — integration detail/state response does not expose a record version yet
|
|
172
177
|
operation: () => withScopedApiRequestHeaders(
|
|
173
|
-
buildOptimisticLockHeader(
|
|
178
|
+
buildOptimisticLockHeader(credentialsUpdatedAt),
|
|
174
179
|
() => apiCall(`/api/integrations/${encodeURIComponent(currentBundleId)}/credentials`, {
|
|
175
180
|
method: 'PUT',
|
|
176
181
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -180,6 +185,7 @@ export default function BundleConfigPage({ params }: BundleConfigPageProps) {
|
|
|
180
185
|
})
|
|
181
186
|
if (call.ok) {
|
|
182
187
|
flash(t('integrations.detail.credentials.saved'), 'success')
|
|
188
|
+
await load()
|
|
183
189
|
} else {
|
|
184
190
|
flash(t('integrations.detail.credentials.saveError'), 'error')
|
|
185
191
|
}
|
|
@@ -188,9 +194,9 @@ export default function BundleConfigPage({ params }: BundleConfigPageProps) {
|
|
|
188
194
|
} finally {
|
|
189
195
|
setIsSavingCreds(false)
|
|
190
196
|
}
|
|
191
|
-
}, [resolveCurrentBundleId, runMutation, mutationContextId, retryLastMutation, credValues, t])
|
|
197
|
+
}, [resolveCurrentBundleId, runMutation, mutationContextId, retryLastMutation, credValues, credentialsUpdatedAt, load, t])
|
|
192
198
|
|
|
193
|
-
const handleToggle = React.useCallback(async (integrationId: string, enabled: boolean) => {
|
|
199
|
+
const handleToggle = React.useCallback(async (integrationId: string, enabled: boolean, updatedAt?: string | null) => {
|
|
194
200
|
setTogglingIds((prev) => new Set(prev).add(integrationId))
|
|
195
201
|
try {
|
|
196
202
|
const call = await runMutation({
|
|
@@ -204,10 +210,9 @@ export default function BundleConfigPage({ params }: BundleConfigPageProps) {
|
|
|
204
210
|
integrationId,
|
|
205
211
|
retryLastMutation,
|
|
206
212
|
},
|
|
207
|
-
// TODO(#2373-B): thread updatedAt — integration detail/state response does not expose a record version yet
|
|
208
213
|
operation: () => withScopedApiRequestHeaders(
|
|
209
|
-
buildOptimisticLockHeader(
|
|
210
|
-
() => apiCall(`/api/integrations/${encodeURIComponent(integrationId)}/state`, {
|
|
214
|
+
buildOptimisticLockHeader(updatedAt),
|
|
215
|
+
() => apiCall<{ updatedAt?: string | null }>(`/api/integrations/${encodeURIComponent(integrationId)}/state`, {
|
|
211
216
|
method: 'PUT',
|
|
212
217
|
headers: { 'Content-Type': 'application/json' },
|
|
213
218
|
body: JSON.stringify({ isEnabled: enabled }),
|
|
@@ -215,12 +220,15 @@ export default function BundleConfigPage({ params }: BundleConfigPageProps) {
|
|
|
215
220
|
),
|
|
216
221
|
})
|
|
217
222
|
if (call.ok) {
|
|
223
|
+
const nextUpdatedAt = call.result?.updatedAt ?? null
|
|
218
224
|
setDetail((prev) => {
|
|
219
225
|
if (!prev) return prev
|
|
220
226
|
return {
|
|
221
227
|
...prev,
|
|
222
228
|
bundleIntegrations: prev.bundleIntegrations.map((item) =>
|
|
223
|
-
item.id === integrationId
|
|
229
|
+
item.id === integrationId
|
|
230
|
+
? { ...item, isEnabled: enabled, state: { updatedAt: nextUpdatedAt ?? item.state?.updatedAt ?? null } }
|
|
231
|
+
: item,
|
|
224
232
|
),
|
|
225
233
|
}
|
|
226
234
|
})
|
|
@@ -237,7 +245,7 @@ export default function BundleConfigPage({ params }: BundleConfigPageProps) {
|
|
|
237
245
|
const handleBulkToggle = React.useCallback(async (enabled: boolean) => {
|
|
238
246
|
if (!detail) return
|
|
239
247
|
const targets = detail.bundleIntegrations.filter((item) => item.isEnabled !== enabled)
|
|
240
|
-
await Promise.all(targets.map((item) => handleToggle(item.id, enabled)))
|
|
248
|
+
await Promise.all(targets.map((item) => handleToggle(item.id, enabled, item.state?.updatedAt)))
|
|
241
249
|
}, [detail, handleToggle])
|
|
242
250
|
|
|
243
251
|
if (isLoading) return <Page><PageBody><LoadingMessage label={t('integrations.bundle.title')} /></PageBody></Page>
|
|
@@ -368,7 +376,7 @@ export default function BundleConfigPage({ params }: BundleConfigPageProps) {
|
|
|
368
376
|
<Switch
|
|
369
377
|
checked={item.isEnabled}
|
|
370
378
|
disabled={togglingIds.has(item.id)}
|
|
371
|
-
onCheckedChange={(checked) => void handleToggle(item.id, checked)}
|
|
379
|
+
onCheckedChange={(checked) => void handleToggle(item.id, checked, item.state?.updatedAt)}
|
|
372
380
|
/>
|
|
373
381
|
</div>
|
|
374
382
|
</div>
|
|
@@ -45,7 +45,7 @@ type IntegrationItem = {
|
|
|
45
45
|
hasCredentials: boolean
|
|
46
46
|
healthStatus: 'healthy' | 'degraded' | 'unhealthy' | 'unconfigured'
|
|
47
47
|
analytics: IntegrationAnalytics
|
|
48
|
-
|
|
48
|
+
stateUpdatedAt?: string | null
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
type BundleItem = {
|
|
@@ -220,7 +220,7 @@ export default function IntegrationsMarketplacePage() {
|
|
|
220
220
|
},
|
|
221
221
|
operation: () => withScopedApiRequestHeaders(
|
|
222
222
|
buildOptimisticLockHeader(updatedAt),
|
|
223
|
-
() => apiCall(`/api/integrations/${encodeURIComponent(integrationId)}/state`, {
|
|
223
|
+
() => apiCall<{ updatedAt?: string | null }>(`/api/integrations/${encodeURIComponent(integrationId)}/state`, {
|
|
224
224
|
method: 'PUT',
|
|
225
225
|
headers: { 'Content-Type': 'application/json' },
|
|
226
226
|
body: JSON.stringify({ isEnabled: enabled }),
|
|
@@ -231,12 +231,15 @@ export default function IntegrationsMarketplacePage() {
|
|
|
231
231
|
if (!call.ok) {
|
|
232
232
|
flash(t('integrations.detail.stateError'), 'error')
|
|
233
233
|
} else {
|
|
234
|
+
const nextUpdatedAt = call.result?.updatedAt ?? null
|
|
234
235
|
setData((prev) => {
|
|
235
236
|
if (!prev) return prev
|
|
236
237
|
return {
|
|
237
238
|
...prev,
|
|
238
239
|
items: prev.items.map((item) =>
|
|
239
|
-
item.id === integrationId
|
|
240
|
+
item.id === integrationId
|
|
241
|
+
? { ...item, isEnabled: enabled, stateUpdatedAt: nextUpdatedAt ?? item.stateUpdatedAt }
|
|
242
|
+
: item,
|
|
240
243
|
),
|
|
241
244
|
}
|
|
242
245
|
})
|
|
@@ -465,7 +468,7 @@ export default function IntegrationsMarketplacePage() {
|
|
|
465
468
|
<Switch
|
|
466
469
|
checked={item.isEnabled}
|
|
467
470
|
disabled={togglingIds.has(item.id)}
|
|
468
|
-
onCheckedChange={(checked) => void handleToggle(item.id, checked, item.
|
|
471
|
+
onCheckedChange={(checked) => void handleToggle(item.id, checked, item.stateUpdatedAt)}
|
|
469
472
|
className="shrink-0"
|
|
470
473
|
/>
|
|
471
474
|
</div>
|
|
@@ -503,7 +506,7 @@ export default function IntegrationsMarketplacePage() {
|
|
|
503
506
|
<Switch
|
|
504
507
|
checked={item.isEnabled}
|
|
505
508
|
disabled={togglingIds.has(item.id)}
|
|
506
|
-
onCheckedChange={(checked) => void handleToggle(item.id, checked)}
|
|
509
|
+
onCheckedChange={(checked) => void handleToggle(item.id, checked, item.stateUpdatedAt)}
|
|
507
510
|
className="shrink-0"
|
|
508
511
|
/>
|
|
509
512
|
</div>
|
|
@@ -85,7 +85,7 @@ export class IntegrationCredentials {
|
|
|
85
85
|
@Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
|
|
86
86
|
createdAt: Date = new Date()
|
|
87
87
|
|
|
88
|
-
@Property({ name: 'updated_at', type: Date, onUpdate: () => new Date() })
|
|
88
|
+
@Property({ name: 'updated_at', type: Date, onCreate: () => new Date(), onUpdate: () => new Date() })
|
|
89
89
|
updatedAt: Date = new Date()
|
|
90
90
|
|
|
91
91
|
@Property({ name: 'deleted_at', type: Date, nullable: true })
|
|
@@ -132,7 +132,7 @@ export class IntegrationState {
|
|
|
132
132
|
@Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
|
|
133
133
|
createdAt: Date = new Date()
|
|
134
134
|
|
|
135
|
-
@Property({ name: 'updated_at', type: Date, onUpdate: () => new Date() })
|
|
135
|
+
@Property({ name: 'updated_at', type: Date, onCreate: () => new Date(), onUpdate: () => new Date() })
|
|
136
136
|
updatedAt: Date = new Date()
|
|
137
137
|
|
|
138
138
|
@Property({ name: 'deleted_at', type: Date, nullable: true })
|
|
@@ -184,6 +184,41 @@ export function createCredentialsService(em: EntityManager) {
|
|
|
184
184
|
return decryptCredentialsBlob(row.credentials, scope)
|
|
185
185
|
},
|
|
186
186
|
|
|
187
|
+
async getRowUpdatedAt(integrationId: string, scope: IntegrationScope): Promise<Date | null> {
|
|
188
|
+
let row = await findOneWithDecryption(
|
|
189
|
+
em,
|
|
190
|
+
IntegrationCredentials,
|
|
191
|
+
buildCredentialsFilter(integrationId, scope),
|
|
192
|
+
undefined,
|
|
193
|
+
scope,
|
|
194
|
+
)
|
|
195
|
+
if (!row && scope.userId) {
|
|
196
|
+
row = await findOneWithDecryption(
|
|
197
|
+
em,
|
|
198
|
+
IntegrationCredentials,
|
|
199
|
+
buildCredentialsFilter(integrationId, { ...scope, userId: null }),
|
|
200
|
+
undefined,
|
|
201
|
+
scope,
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
return row?.updatedAt ?? null
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Resolve the persisted `updated_at` version for the credentials a caller
|
|
209
|
+
* would read via {@link resolve} (direct row first, then the bundle
|
|
210
|
+
* fallthrough). Returns `null` when no credentials row exists yet — the
|
|
211
|
+
* optimistic-lock guard treats a missing current version as "no conflict".
|
|
212
|
+
*/
|
|
213
|
+
async resolveUpdatedAt(integrationId: string, scope: IntegrationScope): Promise<Date | null> {
|
|
214
|
+
const direct = await this.getRowUpdatedAt(integrationId, scope)
|
|
215
|
+
if (direct) return direct
|
|
216
|
+
|
|
217
|
+
const definition = getIntegration(integrationId)
|
|
218
|
+
if (!definition?.bundleId) return null
|
|
219
|
+
return this.getRowUpdatedAt(definition.bundleId, scope)
|
|
220
|
+
},
|
|
221
|
+
|
|
187
222
|
async resolve(integrationId: string, scope: IntegrationScope): Promise<Record<string, unknown> | null> {
|
|
188
223
|
const direct = await this.getRaw(integrationId, scope)
|
|
189
224
|
if (direct) return direct
|
|
@@ -11,6 +11,7 @@ export type ResolvedIntegrationState = {
|
|
|
11
11
|
lastHealthCheckedAt: Date | null
|
|
12
12
|
lastHealthLatencyMs: number | null
|
|
13
13
|
enabledAt: Date | null
|
|
14
|
+
updatedAt: Date | null
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export function createIntegrationStateService(em: EntityManager) {
|
|
@@ -25,6 +26,7 @@ export function createIntegrationStateService(em: EntityManager) {
|
|
|
25
26
|
lastHealthCheckedAt: null,
|
|
26
27
|
lastHealthLatencyMs: null,
|
|
27
28
|
enabledAt: null,
|
|
29
|
+
updatedAt: null,
|
|
28
30
|
}
|
|
29
31
|
}
|
|
30
32
|
|
|
@@ -56,6 +58,7 @@ export function createIntegrationStateService(em: EntityManager) {
|
|
|
56
58
|
lastHealthCheckedAt: state?.lastHealthCheckedAt ?? null,
|
|
57
59
|
lastHealthLatencyMs: state?.lastHealthLatencyMs ?? null,
|
|
58
60
|
enabledAt: state?.enabledAt ?? null,
|
|
61
|
+
updatedAt: state?.updatedAt ?? null,
|
|
59
62
|
}
|
|
60
63
|
},
|
|
61
64
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
2
|
import { type Kysely, sql } from 'kysely'
|
|
3
|
-
import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
3
|
+
import { CrudHttpError, conflict } from '@open-mercato/shared/lib/crud/errors'
|
|
4
4
|
import { Notification, type NotificationStatus } from '../data/entities'
|
|
5
5
|
import type { CreateNotificationInput, CreateBatchNotificationInput, CreateRoleNotificationInput, CreateFeatureNotificationInput, ExecuteActionInput } from '../data/validators'
|
|
6
6
|
import type { NotificationPollData } from '@open-mercato/shared/modules/notifications/types'
|
|
@@ -448,6 +448,57 @@ export function createNotificationService(deps: NotificationServiceDeps): Notifi
|
|
|
448
448
|
throw new Error('Action not found')
|
|
449
449
|
}
|
|
450
450
|
|
|
451
|
+
// Reject an already-actioned notification before dispatching the command,
|
|
452
|
+
// so a retry or double-click cannot re-run the side effect.
|
|
453
|
+
if (notification.status === 'actioned') {
|
|
454
|
+
throw conflict('Notification action already executed')
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const actionedAt = new Date()
|
|
458
|
+
const previousStatus = notification.status
|
|
459
|
+
const previousActionedAt = notification.actionedAt ?? null
|
|
460
|
+
const previousActionTaken = notification.actionTaken ?? null
|
|
461
|
+
|
|
462
|
+
// Atomically claim the notification so only one concurrent request can run
|
|
463
|
+
// the side-effecting command. The conditional UPDATE matches only while the
|
|
464
|
+
// notification has not been actioned yet; a losing request updates 0 rows.
|
|
465
|
+
const claimResult = (await getDb(em)
|
|
466
|
+
.updateTable('notifications' as any)
|
|
467
|
+
.set({
|
|
468
|
+
status: 'actioned',
|
|
469
|
+
actioned_at: actionedAt,
|
|
470
|
+
action_taken: input.actionId,
|
|
471
|
+
} as any)
|
|
472
|
+
.where('id' as any, '=', notification.id)
|
|
473
|
+
.where('recipient_user_id' as any, '=', ctx.userId as any)
|
|
474
|
+
.where('tenant_id' as any, '=', ctx.tenantId)
|
|
475
|
+
.where('status' as any, '!=', 'actioned')
|
|
476
|
+
.executeTakeFirst()) as { numUpdatedRows?: bigint | number } | undefined
|
|
477
|
+
|
|
478
|
+
if (Number(claimResult?.numUpdatedRows ?? 0) === 0) {
|
|
479
|
+
throw conflict('Notification action already executed')
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// The claim is provisional: if the side-effecting command fails, the action
|
|
483
|
+
// never actually completed, so release the claim to its prior state. This
|
|
484
|
+
// lets the user retry the action instead of the notification being locked as
|
|
485
|
+
// `actioned` forever. Only release while we still own the claim
|
|
486
|
+
// (status = 'actioned'), so a concurrent winner's state is never clobbered.
|
|
487
|
+
const releaseClaim = async () => {
|
|
488
|
+
await getDb(em)
|
|
489
|
+
.updateTable('notifications' as any)
|
|
490
|
+
.set({
|
|
491
|
+
status: previousStatus,
|
|
492
|
+
actioned_at: previousActionedAt,
|
|
493
|
+
action_taken: previousActionTaken,
|
|
494
|
+
} as any)
|
|
495
|
+
.where('id' as any, '=', notification.id)
|
|
496
|
+
.where('recipient_user_id' as any, '=', ctx.userId as any)
|
|
497
|
+
.where('tenant_id' as any, '=', ctx.tenantId)
|
|
498
|
+
.where('status' as any, '=', 'actioned')
|
|
499
|
+
.executeTakeFirst()
|
|
500
|
+
}
|
|
501
|
+
|
|
451
502
|
let result: unknown = null
|
|
452
503
|
|
|
453
504
|
if (action.commandId && commandBus && container) {
|
|
@@ -469,21 +520,33 @@ export function createNotificationService(deps: NotificationServiceDeps): Notifi
|
|
|
469
520
|
organizationIds: ctx.organizationId ? [ctx.organizationId] : null,
|
|
470
521
|
}
|
|
471
522
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
523
|
+
let commandResult: { result: unknown }
|
|
524
|
+
try {
|
|
525
|
+
commandResult = await commandBus.execute(action.commandId, {
|
|
526
|
+
input: commandInput,
|
|
527
|
+
ctx: commandCtx,
|
|
528
|
+
metadata: {
|
|
529
|
+
tenantId: ctx.tenantId,
|
|
530
|
+
organizationId: ctx.organizationId,
|
|
531
|
+
resourceKind: 'notifications',
|
|
532
|
+
},
|
|
533
|
+
})
|
|
534
|
+
} catch (err) {
|
|
535
|
+
// Never let a rollback failure mask the original command error — the
|
|
536
|
+
// caller needs the real failure to decide whether to retry.
|
|
537
|
+
try {
|
|
538
|
+
await releaseClaim()
|
|
539
|
+
} catch (releaseErr) {
|
|
540
|
+
debug('failed to release notification action claim', releaseErr)
|
|
541
|
+
}
|
|
542
|
+
throw err
|
|
543
|
+
}
|
|
481
544
|
|
|
482
545
|
result = commandResult.result
|
|
483
546
|
}
|
|
484
547
|
|
|
485
548
|
notification.status = 'actioned'
|
|
486
|
-
notification.actionedAt =
|
|
549
|
+
notification.actionedAt = actionedAt
|
|
487
550
|
notification.actionTaken = input.actionId
|
|
488
551
|
notification.actionResult = result as Record<string, unknown>
|
|
489
552
|
|
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
3
3
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
4
|
+
import { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'
|
|
5
|
+
import {
|
|
6
|
+
runCrudMutationGuardAfterSuccess,
|
|
7
|
+
validateCrudMutationGuard,
|
|
8
|
+
} from '@open-mercato/shared/lib/crud/mutation-guard'
|
|
4
9
|
import { getStatusSchema } from '../../data/validators'
|
|
5
10
|
import type { PaymentGatewayService } from '../../lib/gateway-service'
|
|
6
11
|
import { paymentGatewaysTag } from '../openapi'
|
|
7
12
|
|
|
13
|
+
const gatewayTransactionResourceKind = 'payment_gateways:gateway_transaction'
|
|
14
|
+
|
|
8
15
|
export const metadata = {
|
|
9
16
|
path: '/payment_gateways/status',
|
|
10
17
|
GET: { requireAuth: true, requireFeatures: ['payment_gateways.view'] },
|
|
18
|
+
POST: { requireAuth: true, requireFeatures: ['payment_gateways.manage'] },
|
|
11
19
|
}
|
|
12
20
|
|
|
13
21
|
export async function GET(req: Request) {
|
|
@@ -36,8 +44,81 @@ export async function GET(req: Request) {
|
|
|
36
44
|
return NextResponse.json({ error: 'Transaction not found' }, { status: 404 })
|
|
37
45
|
}
|
|
38
46
|
|
|
47
|
+
return NextResponse.json({
|
|
48
|
+
transactionId: transaction.id,
|
|
49
|
+
paymentId: transaction.paymentId,
|
|
50
|
+
providerKey: transaction.providerKey,
|
|
51
|
+
sessionId: transaction.providerSessionId,
|
|
52
|
+
status: transaction.unifiedStatus,
|
|
53
|
+
gatewayStatus: transaction.gatewayStatus,
|
|
54
|
+
amount: Number(transaction.amount),
|
|
55
|
+
amountReceived: null,
|
|
56
|
+
currencyCode: transaction.currencyCode,
|
|
57
|
+
redirectUrl: transaction.redirectUrl,
|
|
58
|
+
createdAt: transaction.createdAt.toISOString(),
|
|
59
|
+
updatedAt: transaction.updatedAt.toISOString(),
|
|
60
|
+
})
|
|
61
|
+
} catch (err: unknown) {
|
|
62
|
+
const message = err instanceof Error ? err.message : 'Failed to get status'
|
|
63
|
+
return NextResponse.json({ error: message }, { status: 500 })
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function POST(req: Request) {
|
|
68
|
+
const auth = await getAuthFromRequest(req)
|
|
69
|
+
if (!auth?.tenantId || !auth.orgId) {
|
|
70
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const payload = await readJsonSafe<unknown>(req)
|
|
74
|
+
const parsed = getStatusSchema.safeParse(payload)
|
|
75
|
+
if (!parsed.success) {
|
|
76
|
+
return NextResponse.json({ error: 'Invalid payload', details: parsed.error.flatten() }, { status: 422 })
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const container = await createRequestContainer()
|
|
80
|
+
const service = container.resolve('paymentGatewayService') as PaymentGatewayService
|
|
81
|
+
const scope = { organizationId: auth.orgId as string, tenantId: auth.tenantId }
|
|
82
|
+
const actorUserId = auth.userId ?? auth.sub
|
|
83
|
+
const { transactionId } = parsed.data
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const transaction = await service.findTransaction(transactionId, scope)
|
|
87
|
+
if (!transaction) {
|
|
88
|
+
return NextResponse.json({ error: 'Transaction not found' }, { status: 404 })
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const guardResult = await validateCrudMutationGuard(container, {
|
|
92
|
+
tenantId: auth.tenantId,
|
|
93
|
+
organizationId: scope.organizationId,
|
|
94
|
+
userId: actorUserId,
|
|
95
|
+
resourceKind: gatewayTransactionResourceKind,
|
|
96
|
+
resourceId: transactionId,
|
|
97
|
+
operation: 'custom',
|
|
98
|
+
requestMethod: req.method,
|
|
99
|
+
requestHeaders: req.headers,
|
|
100
|
+
mutationPayload: parsed.data,
|
|
101
|
+
})
|
|
102
|
+
if (guardResult && !guardResult.ok) {
|
|
103
|
+
return NextResponse.json(guardResult.body, { status: guardResult.status })
|
|
104
|
+
}
|
|
105
|
+
|
|
39
106
|
const status = await service.getPaymentStatus(transactionId, scope)
|
|
40
107
|
|
|
108
|
+
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
109
|
+
await runCrudMutationGuardAfterSuccess(container, {
|
|
110
|
+
tenantId: auth.tenantId,
|
|
111
|
+
organizationId: scope.organizationId,
|
|
112
|
+
userId: actorUserId,
|
|
113
|
+
resourceKind: gatewayTransactionResourceKind,
|
|
114
|
+
resourceId: transactionId,
|
|
115
|
+
operation: 'custom',
|
|
116
|
+
requestMethod: req.method,
|
|
117
|
+
requestHeaders: req.headers,
|
|
118
|
+
metadata: guardResult.metadata ?? null,
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
|
|
41
122
|
return NextResponse.json({
|
|
42
123
|
transactionId: transaction.id,
|
|
43
124
|
paymentId: transaction.paymentId,
|
|
@@ -53,23 +134,34 @@ export async function GET(req: Request) {
|
|
|
53
134
|
updatedAt: transaction.updatedAt.toISOString(),
|
|
54
135
|
})
|
|
55
136
|
} catch (err: unknown) {
|
|
56
|
-
const message = err instanceof Error ? err.message : 'Failed to
|
|
57
|
-
return NextResponse.json({ error: message }, { status:
|
|
137
|
+
const message = err instanceof Error ? err.message : 'Failed to refresh status'
|
|
138
|
+
return NextResponse.json({ error: message }, { status: 502 })
|
|
58
139
|
}
|
|
59
140
|
}
|
|
60
141
|
|
|
61
142
|
export const openApi = {
|
|
62
143
|
tags: [paymentGatewaysTag],
|
|
63
|
-
summary: '
|
|
144
|
+
summary: 'Payment transaction status',
|
|
64
145
|
methods: {
|
|
65
146
|
GET: {
|
|
66
|
-
summary: 'Get transaction status',
|
|
147
|
+
summary: 'Get stored transaction status',
|
|
67
148
|
tags: [paymentGatewaysTag],
|
|
68
149
|
responses: [
|
|
69
|
-
{ status: 200, description: '
|
|
150
|
+
{ status: 200, description: 'Stored transaction status' },
|
|
70
151
|
{ status: 400, description: 'Missing or malformed transactionId' },
|
|
71
152
|
{ status: 404, description: 'Transaction not found' },
|
|
72
153
|
],
|
|
73
154
|
},
|
|
155
|
+
POST: {
|
|
156
|
+
summary: 'Refresh transaction status from the provider',
|
|
157
|
+
tags: [paymentGatewaysTag],
|
|
158
|
+
responses: [
|
|
159
|
+
{ status: 200, description: 'Refreshed transaction status' },
|
|
160
|
+
{ status: 401, description: 'Unauthorized' },
|
|
161
|
+
{ status: 404, description: 'Transaction not found' },
|
|
162
|
+
{ status: 422, description: 'Missing or malformed payload' },
|
|
163
|
+
{ status: 502, description: 'Gateway provider error' },
|
|
164
|
+
],
|
|
165
|
+
},
|
|
74
166
|
},
|
|
75
167
|
}
|