@open-mercato/core 0.6.6-develop.6205.1.109e4b6a84 → 0.6.6-develop.6227.1.2695efff30
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/currencies/backend/currencies/[id]/page.js +33 -8
- package/dist/modules/currencies/backend/currencies/[id]/page.js.map +2 -2
- package/dist/modules/currencies/backend/currencies/page.js +63 -27
- package/dist/modules/currencies/backend/currencies/page.js.map +2 -2
- package/dist/modules/currencies/backend/exchange-rates/page.js +35 -14
- package/dist/modules/currencies/backend/exchange-rates/page.js.map +2 -2
- package/dist/modules/currencies/components/CurrencyFetchingConfig.js +104 -41
- package/dist/modules/currencies/components/CurrencyFetchingConfig.js.map +2 -2
- package/dist/modules/customer_accounts/api/admin/roles/[id]/acl.js +57 -8
- package/dist/modules/customer_accounts/api/admin/roles/[id]/acl.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.js +5 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/roles/page.js +30 -14
- package/dist/modules/customer_accounts/backend/customer_accounts/roles/page.js.map +2 -2
- package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js +29 -19
- package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js.map +2 -2
- package/dist/modules/customers/backend/config/customers/pipeline-stages/page.js +166 -64
- package/dist/modules/customers/backend/config/customers/pipeline-stages/page.js.map +2 -2
- package/dist/modules/customers/components/AddressFormatSettings.js +31 -16
- package/dist/modules/customers/components/AddressFormatSettings.js.map +2 -2
- package/dist/modules/customers/components/DictionarySettings.js +57 -31
- package/dist/modules/customers/components/DictionarySettings.js.map +2 -2
- package/dist/modules/customers/components/PipelineSettings.js +156 -76
- package/dist/modules/customers/components/PipelineSettings.js.map +2 -2
- package/dist/modules/customers/components/calendar/AgendaList.js +10 -4
- package/dist/modules/customers/components/calendar/AgendaList.js.map +2 -2
- package/dist/modules/customers/components/calendar/CalendarSettingsModal.js +18 -2
- package/dist/modules/customers/components/calendar/CalendarSettingsModal.js.map +2 -2
- package/dist/modules/customers/components/calendar/MonthGrid.js +5 -2
- package/dist/modules/customers/components/calendar/MonthGrid.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/PeopleField.js +6 -0
- package/dist/modules/customers/components/calendar/editor/PeopleField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/RelatedToField.js +2 -0
- package/dist/modules/customers/components/calendar/editor/RelatedToField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/ScheduleSection.js +4 -1
- package/dist/modules/customers/components/calendar/editor/ScheduleSection.js.map +2 -2
- package/dist/modules/customers/lib/calendar/labels.js +33 -0
- package/dist/modules/customers/lib/calendar/labels.js.map +7 -0
- package/dist/modules/dashboards/components/WidgetVisibilityEditor.js +32 -11
- package/dist/modules/dashboards/components/WidgetVisibilityEditor.js.map +2 -2
- package/dist/modules/data_sync/api/mappings/[id]/route.js +59 -0
- package/dist/modules/data_sync/api/mappings/[id]/route.js.map +2 -2
- package/dist/modules/data_sync/api/mappings/route.js +44 -0
- package/dist/modules/data_sync/api/mappings/route.js.map +2 -2
- package/dist/modules/data_sync/api/run.js +31 -0
- package/dist/modules/data_sync/api/run.js.map +2 -2
- package/dist/modules/data_sync/api/runs/[id]/cancel.js +31 -0
- package/dist/modules/data_sync/api/runs/[id]/cancel.js.map +2 -2
- package/dist/modules/data_sync/api/runs/[id]/retry.js +31 -0
- package/dist/modules/data_sync/api/runs/[id]/retry.js.map +2 -2
- package/dist/modules/data_sync/api/schedules/[id]/route.js +59 -3
- package/dist/modules/data_sync/api/schedules/[id]/route.js.map +2 -2
- package/dist/modules/data_sync/api/schedules/route.js +33 -4
- package/dist/modules/data_sync/api/schedules/route.js.map +2 -2
- package/dist/modules/directory/api/organization-branding/route.js +54 -2
- package/dist/modules/directory/api/organization-branding/route.js.map +2 -2
- package/dist/modules/directory/backend/directory/branding/page.js +15 -9
- package/dist/modules/directory/backend/directory/branding/page.js.map +2 -2
- package/dist/modules/directory/backend/directory/organizations/page.js +28 -9
- package/dist/modules/directory/backend/directory/organizations/page.js.map +2 -2
- package/dist/modules/directory/backend/directory/tenants/page.js +29 -13
- package/dist/modules/directory/backend/directory/tenants/page.js.map +2 -2
- package/dist/modules/entities/api/definitions.batch.js +15 -0
- package/dist/modules/entities/api/definitions.batch.js.map +2 -2
- package/dist/modules/entities/api/definitions.js +26 -0
- package/dist/modules/entities/api/definitions.js.map +2 -2
- package/dist/modules/entities/api/definitions.mutation-guard.js +57 -0
- package/dist/modules/entities/api/definitions.mutation-guard.js.map +7 -0
- package/dist/modules/entities/api/definitions.restore.js +15 -0
- package/dist/modules/entities/api/definitions.restore.js.map +2 -2
- package/dist/modules/entities/api/entities.js +31 -3
- package/dist/modules/entities/api/entities.js.map +2 -2
- package/dist/modules/entities/api/records.js +18 -0
- package/dist/modules/entities/api/records.js.map +2 -2
- package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js +28 -10
- package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js.map +2 -2
- package/dist/modules/feature_toggles/api/overrides/route.js +38 -1
- package/dist/modules/feature_toggles/api/overrides/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/emails/[id]/route.js +30 -0
- package/dist/modules/inbox_ops/api/emails/[id]/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/[id]/accept-all/route.js +30 -0
- package/dist/modules/inbox_ops/api/proposals/[id]/accept-all/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.js +31 -0
- package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/[id]/categorize/route.js +31 -0
- package/dist/modules/inbox_ops/api/proposals/[id]/categorize/route.js.map +2 -2
- package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js +68 -39
- package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js.map +2 -2
- package/dist/modules/notifications/frontend/NotificationSettingsPageClient.js +14 -4
- package/dist/modules/notifications/frontend/NotificationSettingsPageClient.js.map +2 -2
- package/dist/modules/payment_gateways/api/cancel/route.js +37 -0
- package/dist/modules/payment_gateways/api/cancel/route.js.map +2 -2
- package/dist/modules/payment_gateways/api/capture/route.js +37 -0
- package/dist/modules/payment_gateways/api/capture/route.js.map +2 -2
- package/dist/modules/payment_gateways/api/guards.js +31 -0
- package/dist/modules/payment_gateways/api/guards.js.map +7 -0
- package/dist/modules/payment_gateways/api/refund/route.js +37 -0
- package/dist/modules/payment_gateways/api/refund/route.js.map +2 -2
- package/dist/modules/payment_gateways/api/sessions/route.js +37 -0
- package/dist/modules/payment_gateways/api/sessions/route.js.map +2 -2
- package/dist/modules/planner/api/availability-date-specific.js +11 -1
- package/dist/modules/planner/api/availability-date-specific.js.map +2 -2
- package/dist/modules/planner/backend/planner/availability-rulesets/page.js +20 -3
- package/dist/modules/planner/backend/planner/availability-rulesets/page.js.map +2 -2
- package/dist/modules/planner/commands/availability-date-specific.js +16 -0
- package/dist/modules/planner/commands/availability-date-specific.js.map +2 -2
- package/dist/modules/planner/components/AvailabilityRulesEditor.js +109 -40
- package/dist/modules/planner/components/AvailabilityRulesEditor.js.map +2 -2
- package/dist/modules/query_index/api/purge.js +35 -3
- package/dist/modules/query_index/api/purge.js.map +2 -2
- package/dist/modules/query_index/api/reindex.js +41 -3
- package/dist/modules/query_index/api/reindex.js.map +2 -2
- package/dist/modules/query_index/components/QueryIndexesTable.js +57 -24
- package/dist/modules/query_index/components/QueryIndexesTable.js.map +2 -2
- package/dist/modules/shipping_carriers/api/shipments/route.js +31 -0
- package/dist/modules/shipping_carriers/api/shipments/route.js.map +2 -2
- package/dist/modules/shipping_carriers/api/tracking/refresh/route.js +55 -0
- package/dist/modules/shipping_carriers/api/tracking/refresh/route.js.map +7 -0
- package/dist/modules/shipping_carriers/data/validators.js +6 -1
- package/dist/modules/shipping_carriers/data/validators.js.map +2 -2
- package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.js +28 -8
- package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.js.map +2 -2
- package/dist/modules/shipping_carriers/lib/shipping-service.js +37 -7
- package/dist/modules/shipping_carriers/lib/shipping-service.js.map +2 -2
- package/dist/modules/shipping_carriers/workers/status-poller.js +1 -1
- package/dist/modules/shipping_carriers/workers/status-poller.js.map +2 -2
- package/dist/modules/translations/components/TranslationManager.js +49 -20
- package/dist/modules/translations/components/TranslationManager.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/currencies/backend/currencies/[id]/page.tsx +40 -10
- package/src/modules/currencies/backend/currencies/page.tsx +68 -29
- package/src/modules/currencies/backend/exchange-rates/page.tsx +40 -15
- package/src/modules/currencies/components/CurrencyFetchingConfig.tsx +110 -41
- package/src/modules/customer_accounts/api/admin/roles/[id]/acl.ts +69 -7
- package/src/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.tsx +16 -8
- package/src/modules/customer_accounts/backend/customer_accounts/roles/page.tsx +32 -14
- package/src/modules/customer_accounts/widgets/injection/account-status/widget.client.tsx +32 -20
- package/src/modules/customers/backend/config/customers/pipeline-stages/page.tsx +171 -64
- package/src/modules/customers/components/AddressFormatSettings.tsx +39 -19
- package/src/modules/customers/components/DictionarySettings.tsx +63 -29
- package/src/modules/customers/components/PipelineSettings.tsx +165 -80
- package/src/modules/customers/components/calendar/AgendaList.tsx +13 -8
- package/src/modules/customers/components/calendar/CalendarSettingsModal.tsx +16 -2
- package/src/modules/customers/components/calendar/MonthGrid.tsx +5 -2
- package/src/modules/customers/components/calendar/editor/PeopleField.tsx +6 -0
- package/src/modules/customers/components/calendar/editor/RelatedToField.tsx +2 -0
- package/src/modules/customers/components/calendar/editor/ScheduleSection.tsx +7 -0
- package/src/modules/customers/i18n/de.json +11 -7
- package/src/modules/customers/i18n/en.json +6 -2
- package/src/modules/customers/i18n/es.json +11 -7
- package/src/modules/customers/i18n/pl.json +11 -7
- package/src/modules/customers/lib/calendar/labels.ts +42 -0
- package/src/modules/dashboards/components/WidgetVisibilityEditor.tsx +39 -11
- package/src/modules/data_sync/api/mappings/[id]/route.ts +63 -0
- package/src/modules/data_sync/api/mappings/route.ts +48 -0
- package/src/modules/data_sync/api/run.ts +33 -0
- package/src/modules/data_sync/api/runs/[id]/cancel.ts +34 -0
- package/src/modules/data_sync/api/runs/[id]/retry.ts +33 -0
- package/src/modules/data_sync/api/schedules/[id]/route.ts +64 -2
- package/src/modules/data_sync/api/schedules/route.ts +36 -4
- package/src/modules/directory/api/organization-branding/route.ts +61 -0
- package/src/modules/directory/backend/directory/branding/page.tsx +16 -10
- package/src/modules/directory/backend/directory/organizations/page.tsx +35 -8
- package/src/modules/directory/backend/directory/tenants/page.tsx +37 -13
- package/src/modules/entities/api/definitions.batch.ts +17 -0
- package/src/modules/entities/api/definitions.mutation-guard.ts +80 -0
- package/src/modules/entities/api/definitions.restore.ts +17 -0
- package/src/modules/entities/api/definitions.ts +30 -0
- package/src/modules/entities/api/entities.ts +35 -3
- package/src/modules/entities/api/records.ts +20 -0
- package/src/modules/entities/backend/entities/user/[entityId]/records/page.tsx +33 -10
- package/src/modules/feature_toggles/api/overrides/route.ts +44 -1
- package/src/modules/inbox_ops/api/emails/[id]/route.ts +32 -0
- package/src/modules/inbox_ops/api/proposals/[id]/accept-all/route.ts +32 -0
- package/src/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.ts +33 -0
- package/src/modules/inbox_ops/api/proposals/[id]/categorize/route.ts +33 -0
- package/src/modules/messages/components/message-detail/hooks/useMessageDetailsActions.ts +80 -42
- package/src/modules/notifications/frontend/NotificationSettingsPageClient.tsx +21 -4
- package/src/modules/payment_gateways/api/cancel/route.ts +39 -0
- package/src/modules/payment_gateways/api/capture/route.ts +39 -0
- package/src/modules/payment_gateways/api/guards.ts +59 -0
- package/src/modules/payment_gateways/api/refund/route.ts +39 -0
- package/src/modules/payment_gateways/api/sessions/route.ts +40 -0
- package/src/modules/planner/api/availability-date-specific.ts +10 -0
- package/src/modules/planner/backend/planner/availability-rulesets/page.tsx +26 -5
- package/src/modules/planner/commands/availability-date-specific.ts +24 -0
- package/src/modules/planner/components/AvailabilityRulesEditor.tsx +122 -41
- package/src/modules/query_index/api/purge.ts +37 -3
- package/src/modules/query_index/api/reindex.ts +43 -3
- package/src/modules/query_index/components/QueryIndexesTable.tsx +66 -24
- package/src/modules/shipping_carriers/api/shipments/route.ts +31 -0
- package/src/modules/shipping_carriers/api/tracking/refresh/route.ts +53 -0
- package/src/modules/shipping_carriers/data/validators.ts +5 -0
- package/src/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.ts +29 -8
- package/src/modules/shipping_carriers/lib/shipping-service.ts +43 -7
- package/src/modules/shipping_carriers/workers/status-poller.ts +1 -1
- package/src/modules/translations/components/TranslationManager.tsx +65 -21
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
import { useState, useEffect, useCallback, useMemo } from 'react'
|
|
4
4
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
5
5
|
import { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
|
|
6
|
-
import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
|
|
6
|
+
import { buildOptimisticLockHeader, extractOptimisticLockConflict } from '@open-mercato/ui/backend/utils/optimisticLock'
|
|
7
|
+
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
7
8
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
8
9
|
import { Spinner } from '@open-mercato/ui/primitives/spinner'
|
|
9
10
|
import { Switch } from '@open-mercato/ui/primitives/switch'
|
|
@@ -31,16 +32,35 @@ export default function CurrencyFetchingConfig() {
|
|
|
31
32
|
// Available providers that should be configured
|
|
32
33
|
const availableProviders = useMemo(() => ['NBP', 'Raiffeisen Bank Polska'], [])
|
|
33
34
|
|
|
35
|
+
const mutationContextId = 'currencies-fetch-configs:mutation'
|
|
36
|
+
const { runMutation, retryLastMutation } = useGuardedMutation<{
|
|
37
|
+
formId: string
|
|
38
|
+
resourceKind: string
|
|
39
|
+
resourceId?: string
|
|
40
|
+
retryLastMutation: () => Promise<boolean>
|
|
41
|
+
}>({
|
|
42
|
+
contextId: mutationContextId,
|
|
43
|
+
blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
|
|
44
|
+
})
|
|
45
|
+
|
|
34
46
|
const createProviderConfig = useCallback(async (provider: string) => {
|
|
35
47
|
try {
|
|
36
|
-
await
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
48
|
+
await runMutation({
|
|
49
|
+
operation: () => apiCall('/api/currencies/fetch-configs', {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers: { 'Content-Type': 'application/json' },
|
|
52
|
+
body: JSON.stringify({
|
|
53
|
+
provider,
|
|
54
|
+
isEnabled: false,
|
|
55
|
+
syncTime: '09:00',
|
|
56
|
+
}),
|
|
43
57
|
}),
|
|
58
|
+
context: {
|
|
59
|
+
formId: mutationContextId,
|
|
60
|
+
resourceKind: 'currencies.fetch_config',
|
|
61
|
+
retryLastMutation,
|
|
62
|
+
},
|
|
63
|
+
mutationPayload: { provider, isEnabled: false, syncTime: '09:00' },
|
|
44
64
|
})
|
|
45
65
|
} catch (err: any) {
|
|
46
66
|
// Ignore errors for duplicate providers
|
|
@@ -48,7 +68,7 @@ export default function CurrencyFetchingConfig() {
|
|
|
48
68
|
throw err
|
|
49
69
|
}
|
|
50
70
|
}
|
|
51
|
-
}, [])
|
|
71
|
+
}, [mutationContextId, retryLastMutation, runMutation])
|
|
52
72
|
|
|
53
73
|
const initializeMissingProviders = useCallback(async (providers: string[]) => {
|
|
54
74
|
setInitializing(true)
|
|
@@ -96,19 +116,38 @@ export default function CurrencyFetchingConfig() {
|
|
|
96
116
|
|
|
97
117
|
const toggleEnabled = useCallback(async (configId: string, currentValue: boolean, updatedAt?: string | null) => {
|
|
98
118
|
try {
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
119
|
+
const call = await runMutation({
|
|
120
|
+
operation: async () => {
|
|
121
|
+
const response = await withScopedApiRequestHeaders(
|
|
122
|
+
buildOptimisticLockHeader(updatedAt),
|
|
123
|
+
() =>
|
|
124
|
+
apiCall('/api/currencies/fetch-configs', {
|
|
125
|
+
method: 'PUT',
|
|
126
|
+
headers: { 'Content-Type': 'application/json' },
|
|
127
|
+
body: JSON.stringify({
|
|
128
|
+
id: configId,
|
|
129
|
+
isEnabled: !currentValue,
|
|
130
|
+
}),
|
|
131
|
+
}),
|
|
132
|
+
)
|
|
133
|
+
if (!response.ok) {
|
|
134
|
+
throw Object.assign(new Error('[internal] currencies.fetchConfig.toggle failed'), {
|
|
135
|
+
status: response.status,
|
|
136
|
+
...((response.result as Record<string, unknown> | null) ?? {}),
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
return response
|
|
140
|
+
},
|
|
141
|
+
context: {
|
|
142
|
+
formId: mutationContextId,
|
|
143
|
+
resourceKind: 'currencies.fetch_config',
|
|
144
|
+
resourceId: configId,
|
|
145
|
+
retryLastMutation,
|
|
146
|
+
},
|
|
147
|
+
mutationPayload: { id: configId, isEnabled: !currentValue },
|
|
148
|
+
})
|
|
111
149
|
|
|
150
|
+
const result = call.result as { config?: FetchConfig } | null
|
|
112
151
|
if (result?.config) {
|
|
113
152
|
setConfigs((prev) =>
|
|
114
153
|
prev.map((c) => (c.id === configId ? (result.config as FetchConfig) : c))
|
|
@@ -121,47 +160,77 @@ export default function CurrencyFetchingConfig() {
|
|
|
121
160
|
)
|
|
122
161
|
}
|
|
123
162
|
} catch (err: any) {
|
|
163
|
+
if (extractOptimisticLockConflict(err)) return
|
|
124
164
|
flash(err.message || t('currencies.fetch.error_update_config'), 'error')
|
|
125
165
|
}
|
|
126
|
-
}, [t])
|
|
166
|
+
}, [mutationContextId, retryLastMutation, runMutation, t])
|
|
127
167
|
|
|
128
168
|
const updateSyncTime = useCallback(async (configId: string, syncTime: string, updatedAt?: string | null) => {
|
|
129
169
|
try {
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
170
|
+
const call = await runMutation({
|
|
171
|
+
operation: async () => {
|
|
172
|
+
const response = await withScopedApiRequestHeaders(
|
|
173
|
+
buildOptimisticLockHeader(updatedAt),
|
|
174
|
+
() =>
|
|
175
|
+
apiCall('/api/currencies/fetch-configs', {
|
|
176
|
+
method: 'PUT',
|
|
177
|
+
headers: { 'Content-Type': 'application/json' },
|
|
178
|
+
body: JSON.stringify({
|
|
179
|
+
id: configId,
|
|
180
|
+
syncTime: syncTime || null,
|
|
181
|
+
}),
|
|
182
|
+
}),
|
|
183
|
+
)
|
|
184
|
+
if (!response.ok) {
|
|
185
|
+
throw Object.assign(new Error('[internal] currencies.fetchConfig.updateSyncTime failed'), {
|
|
186
|
+
status: response.status,
|
|
187
|
+
...((response.result as Record<string, unknown> | null) ?? {}),
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
return response
|
|
191
|
+
},
|
|
192
|
+
context: {
|
|
193
|
+
formId: mutationContextId,
|
|
194
|
+
resourceKind: 'currencies.fetch_config',
|
|
195
|
+
resourceId: configId,
|
|
196
|
+
retryLastMutation,
|
|
197
|
+
},
|
|
198
|
+
mutationPayload: { id: configId, syncTime: syncTime || null },
|
|
199
|
+
})
|
|
142
200
|
|
|
201
|
+
const result = call.result as { config?: FetchConfig } | null
|
|
143
202
|
if (result?.config) {
|
|
144
203
|
setConfigs((prev) =>
|
|
145
204
|
prev.map((c) => (c.id === configId ? (result.config as FetchConfig) : c))
|
|
146
205
|
)
|
|
147
206
|
}
|
|
148
207
|
} catch (err: any) {
|
|
208
|
+
if (extractOptimisticLockConflict(err)) return
|
|
149
209
|
flash(err.message || t('currencies.fetch.error_update_sync_time'), 'error')
|
|
150
210
|
}
|
|
151
|
-
}, [t])
|
|
211
|
+
}, [mutationContextId, retryLastMutation, runMutation, t])
|
|
152
212
|
|
|
153
213
|
const fetchNow = useCallback(async (provider: string) => {
|
|
154
214
|
setFetching(provider)
|
|
155
215
|
|
|
156
216
|
try {
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
217
|
+
const call = await runMutation({
|
|
218
|
+
operation: () => apiCall('/api/currencies/fetch-rates', {
|
|
219
|
+
method: 'POST',
|
|
220
|
+
headers: { 'Content-Type': 'application/json' },
|
|
221
|
+
body: JSON.stringify({
|
|
222
|
+
providers: [provider],
|
|
223
|
+
}),
|
|
162
224
|
}),
|
|
225
|
+
context: {
|
|
226
|
+
formId: mutationContextId,
|
|
227
|
+
resourceKind: 'currencies.fetch_rates',
|
|
228
|
+
retryLastMutation,
|
|
229
|
+
},
|
|
230
|
+
mutationPayload: { providers: [provider] },
|
|
163
231
|
})
|
|
164
232
|
|
|
233
|
+
const result = call.result as { byProvider?: Record<string, { count: number; errors?: string[] }> } | null
|
|
165
234
|
if (result) {
|
|
166
235
|
const byProvider = result.byProvider as Record<string, { count: number; errors?: string[] }>
|
|
167
236
|
const count = byProvider?.[provider]?.count || 0
|
|
@@ -178,7 +247,7 @@ export default function CurrencyFetchingConfig() {
|
|
|
178
247
|
} finally {
|
|
179
248
|
setFetching(null)
|
|
180
249
|
}
|
|
181
|
-
}, [t, loadConfigs])
|
|
250
|
+
}, [mutationContextId, retryLastMutation, runMutation, t, loadConfigs])
|
|
182
251
|
|
|
183
252
|
useEffect(() => {
|
|
184
253
|
loadConfigs()
|
|
@@ -7,18 +7,27 @@ import { RbacService } from '@open-mercato/core/modules/auth/services/rbacServic
|
|
|
7
7
|
import { CustomerRole, CustomerRoleAcl } from '@open-mercato/core/modules/customer_accounts/data/entities'
|
|
8
8
|
import { CustomerRbacService } from '@open-mercato/core/modules/customer_accounts/services/customerRbacService'
|
|
9
9
|
import { updateRoleAclSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'
|
|
10
|
+
import { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
|
|
11
|
+
import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
12
|
+
import {
|
|
13
|
+
runCrudMutationGuardAfterSuccess,
|
|
14
|
+
validateCrudMutationGuard,
|
|
15
|
+
} from '@open-mercato/shared/lib/crud/mutation-guard'
|
|
10
16
|
|
|
11
17
|
export const metadata = {}
|
|
12
18
|
|
|
19
|
+
const ROLE_RESOURCE_KIND = 'customer_accounts.role'
|
|
20
|
+
|
|
13
21
|
export async function PUT(req: Request, { params }: { params: { id: string } }) {
|
|
14
22
|
const auth = await getAuthFromRequest(req)
|
|
15
|
-
if (!auth) {
|
|
23
|
+
if (!auth || !auth.tenantId) {
|
|
16
24
|
return NextResponse.json({ ok: false, error: 'Authentication required' }, { status: 401 })
|
|
17
25
|
}
|
|
26
|
+
const tenantId = auth.tenantId
|
|
18
27
|
|
|
19
28
|
const container = await createRequestContainer()
|
|
20
29
|
const rbacService = container.resolve('rbacService') as RbacService
|
|
21
|
-
const hasAccess = await rbacService.userHasAllFeatures(auth.sub, ['customer_accounts.roles.manage'], { tenantId
|
|
30
|
+
const hasAccess = await rbacService.userHasAllFeatures(auth.sub, ['customer_accounts.roles.manage'], { tenantId, organizationId: auth.orgId })
|
|
22
31
|
if (!hasAccess) {
|
|
23
32
|
return NextResponse.json({ ok: false, error: 'Insufficient permissions' }, { status: 403 })
|
|
24
33
|
}
|
|
@@ -39,16 +48,48 @@ export async function PUT(req: Request, { params }: { params: { id: string } })
|
|
|
39
48
|
|
|
40
49
|
const role = await em.findOne(CustomerRole, {
|
|
41
50
|
id: params.id,
|
|
42
|
-
tenantId
|
|
51
|
+
tenantId,
|
|
43
52
|
deletedAt: null,
|
|
44
53
|
})
|
|
45
54
|
if (!role) {
|
|
46
55
|
return NextResponse.json({ ok: false, error: 'Role not found' }, { status: 404 })
|
|
47
56
|
}
|
|
48
57
|
|
|
58
|
+
// The ACL is part of the role aggregate: the role-edit screen loads and saves
|
|
59
|
+
// the role and its permissions together. Guard the write against the parent
|
|
60
|
+
// role's `updatedAt` so two admins editing the same role cannot silently
|
|
61
|
+
// overwrite each other's permission changes (#3194). Strictly additive — when
|
|
62
|
+
// the client sends no expected-version header the helper is a no-op.
|
|
63
|
+
try {
|
|
64
|
+
enforceCommandOptimisticLock({
|
|
65
|
+
resourceKind: ROLE_RESOURCE_KIND,
|
|
66
|
+
resourceId: role.id,
|
|
67
|
+
current: role.updatedAt ?? null,
|
|
68
|
+
request: req,
|
|
69
|
+
})
|
|
70
|
+
} catch (err) {
|
|
71
|
+
if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })
|
|
72
|
+
throw err
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const guardResult = await validateCrudMutationGuard(container, {
|
|
76
|
+
tenantId,
|
|
77
|
+
organizationId: auth.orgId,
|
|
78
|
+
userId: auth.sub,
|
|
79
|
+
resourceKind: ROLE_RESOURCE_KIND,
|
|
80
|
+
resourceId: role.id,
|
|
81
|
+
operation: 'update',
|
|
82
|
+
requestMethod: req.method,
|
|
83
|
+
requestHeaders: req.headers,
|
|
84
|
+
mutationPayload: parsed.data,
|
|
85
|
+
})
|
|
86
|
+
if (guardResult && !guardResult.ok) {
|
|
87
|
+
return NextResponse.json(guardResult.body, { status: guardResult.status })
|
|
88
|
+
}
|
|
89
|
+
|
|
49
90
|
const acl = await em.findOne(CustomerRoleAcl, {
|
|
50
91
|
role: role.id as any,
|
|
51
|
-
tenantId
|
|
92
|
+
tenantId,
|
|
52
93
|
})
|
|
53
94
|
|
|
54
95
|
if (acl) {
|
|
@@ -59,7 +100,7 @@ export async function PUT(req: Request, { params }: { params: { id: string } })
|
|
|
59
100
|
} else {
|
|
60
101
|
const newAcl = em.create(CustomerRoleAcl, {
|
|
61
102
|
role,
|
|
62
|
-
tenantId
|
|
103
|
+
tenantId,
|
|
63
104
|
featuresJson: parsed.data.features,
|
|
64
105
|
isPortalAdmin: parsed.data.isPortalAdmin ?? false,
|
|
65
106
|
createdAt: new Date(),
|
|
@@ -68,13 +109,33 @@ export async function PUT(req: Request, { params }: { params: { id: string } })
|
|
|
68
109
|
await em.flush()
|
|
69
110
|
}
|
|
70
111
|
|
|
112
|
+
// Bump the aggregate version so a stale role-edit screen (which holds the role's
|
|
113
|
+
// pre-edit `updatedAt`) cannot save old permission arrays afterwards. `nativeUpdate`
|
|
114
|
+
// bypasses MikroORM's `onUpdate` hook, so set it explicitly.
|
|
115
|
+
const nextUpdatedAt = new Date()
|
|
116
|
+
await em.nativeUpdate(CustomerRole, { id: role.id }, { updatedAt: nextUpdatedAt })
|
|
117
|
+
|
|
71
118
|
const customerRbacService = container.resolve('customerRbacService') as CustomerRbacService
|
|
72
119
|
await customerRbacService.invalidateRoleCache(role.id)
|
|
73
120
|
|
|
74
|
-
|
|
121
|
+
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
122
|
+
await runCrudMutationGuardAfterSuccess(container, {
|
|
123
|
+
tenantId,
|
|
124
|
+
organizationId: auth.orgId,
|
|
125
|
+
userId: auth.sub,
|
|
126
|
+
resourceKind: ROLE_RESOURCE_KIND,
|
|
127
|
+
resourceId: role.id,
|
|
128
|
+
operation: 'update',
|
|
129
|
+
requestMethod: req.method,
|
|
130
|
+
requestHeaders: req.headers,
|
|
131
|
+
metadata: guardResult.metadata ?? null,
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return NextResponse.json({ ok: true, updatedAt: nextUpdatedAt.toISOString() })
|
|
75
136
|
}
|
|
76
137
|
|
|
77
|
-
const successSchema = z.object({ ok: z.literal(true) })
|
|
138
|
+
const successSchema = z.object({ ok: z.literal(true), updatedAt: z.string().datetime() })
|
|
78
139
|
const errorSchema = z.object({ ok: z.literal(false), error: z.string() })
|
|
79
140
|
|
|
80
141
|
const methodDoc: OpenApiMethodDoc = {
|
|
@@ -88,6 +149,7 @@ const methodDoc: OpenApiMethodDoc = {
|
|
|
88
149
|
{ status: 401, description: 'Not authenticated', schema: errorSchema },
|
|
89
150
|
{ status: 403, description: 'Insufficient permissions', schema: errorSchema },
|
|
90
151
|
{ status: 404, description: 'Role not found', schema: errorSchema },
|
|
152
|
+
{ status: 409, description: 'Stale ACL write (optimistic-lock conflict)', schema: errorSchema },
|
|
91
153
|
],
|
|
92
154
|
}
|
|
93
155
|
|
|
@@ -273,15 +273,23 @@ export default function CustomerRoleDetailPage({ params }: { params?: { id?: str
|
|
|
273
273
|
return
|
|
274
274
|
}
|
|
275
275
|
const features = Array.isArray(values.features) ? values.features : []
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
276
|
+
// The role PUT just bumped the aggregate's `updatedAt`; the ACL write guards
|
|
277
|
+
// the SAME role aggregate, so send its NEW version (not the stale pre-edit one)
|
|
278
|
+
// to avoid a false 409 while still blocking a concurrent overwrite (#3194).
|
|
279
|
+
const roleResult = (roleCall.result ?? null) as { updatedAt?: string | null } | null
|
|
280
|
+
const aclHeaders = buildOptimisticLockHeader(roleResult?.updatedAt ?? data?.updatedAt ?? data?.updated_at ?? null)
|
|
281
|
+
const aclCall = await withScopedApiRequestHeaders(aclHeaders, () => (
|
|
282
|
+
apiCall(
|
|
283
|
+
`/api/customer_accounts/admin/roles/${encodeURIComponent(id)}/acl`,
|
|
284
|
+
{
|
|
285
|
+
method: 'PUT',
|
|
286
|
+
headers: { 'content-type': 'application/json' },
|
|
287
|
+
body: JSON.stringify({ features }),
|
|
288
|
+
},
|
|
289
|
+
)
|
|
290
|
+
))
|
|
284
291
|
if (!aclCall.ok) {
|
|
292
|
+
if (surfaceRecordConflict({ status: aclCall.status, body: aclCall.result }, t)) return
|
|
285
293
|
flash(t('customer_accounts.admin.roleDetail.error.saveAcl', 'Failed to save permissions'), 'error')
|
|
286
294
|
return
|
|
287
295
|
}
|
|
@@ -14,6 +14,7 @@ import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimi
|
|
|
14
14
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
15
15
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
16
16
|
import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
|
|
17
|
+
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
17
18
|
import { ListEmptyState } from '@open-mercato/ui/backend/filters/ListEmptyState'
|
|
18
19
|
|
|
19
20
|
type RoleRow = {
|
|
@@ -47,6 +48,21 @@ export default function CustomerRolesPage() {
|
|
|
47
48
|
const [isLoading, setIsLoading] = React.useState(true)
|
|
48
49
|
const [reloadToken, setReloadToken] = React.useState(0)
|
|
49
50
|
|
|
51
|
+
const { runMutation } = useGuardedMutation<{ entityType: string }>({
|
|
52
|
+
contextId: 'customer_accounts:roles-list',
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const runMutationWithContext = React.useCallback(
|
|
56
|
+
async <T,>(operation: () => Promise<T>, mutationPayload?: Record<string, unknown>): Promise<T> => {
|
|
57
|
+
return runMutation({
|
|
58
|
+
operation,
|
|
59
|
+
mutationPayload,
|
|
60
|
+
context: { entityType: 'customer_accounts:role' },
|
|
61
|
+
})
|
|
62
|
+
},
|
|
63
|
+
[runMutation],
|
|
64
|
+
)
|
|
65
|
+
|
|
50
66
|
const queryParams = React.useMemo(() => {
|
|
51
67
|
const params = new URLSearchParams()
|
|
52
68
|
params.set('page', String(page))
|
|
@@ -95,24 +111,26 @@ export default function CustomerRolesPage() {
|
|
|
95
111
|
})
|
|
96
112
|
if (!confirmed) return
|
|
97
113
|
try {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
114
|
+
await runMutationWithContext(async () => {
|
|
115
|
+
const call = await withScopedApiRequestHeaders(
|
|
116
|
+
buildOptimisticLockHeader(role.updatedAt),
|
|
117
|
+
() => apiCall(
|
|
118
|
+
`/api/customer_accounts/admin/roles/${encodeURIComponent(role.id)}`,
|
|
119
|
+
{ method: 'DELETE' },
|
|
120
|
+
),
|
|
121
|
+
)
|
|
122
|
+
if (!call.ok) {
|
|
123
|
+
flash(t('customer_accounts.admin.roles.error.delete', 'Failed to delete role'), 'error')
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
flash(t('customer_accounts.admin.roles.flash.deleted', 'Role deleted'), 'success')
|
|
127
|
+
setReloadToken((token) => token + 1)
|
|
128
|
+
}, { id: role.id })
|
|
111
129
|
} catch (err) {
|
|
112
130
|
const message = err instanceof Error ? err.message : t('customer_accounts.admin.roles.error.delete', 'Failed to delete role')
|
|
113
131
|
flash(message, 'error')
|
|
114
132
|
}
|
|
115
|
-
}, [confirm, t])
|
|
133
|
+
}, [confirm, runMutationWithContext, t])
|
|
116
134
|
|
|
117
135
|
const columns = React.useMemo<ColumnDef<RoleRow>[]>(() => [
|
|
118
136
|
{
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import React from 'react'
|
|
4
4
|
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
|
5
5
|
import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
|
|
6
|
+
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
6
7
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
7
8
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
8
9
|
import { Input } from '@open-mercato/ui/primitives/input'
|
|
@@ -50,6 +51,10 @@ function InviteForm({ personEntityId, onSuccess }: { personEntityId: string; onS
|
|
|
50
51
|
const [isLoadingRoles, setIsLoadingRoles] = React.useState(true)
|
|
51
52
|
const [isSubmitting, setIsSubmitting] = React.useState(false)
|
|
52
53
|
|
|
54
|
+
const { runMutation } = useGuardedMutation<{ entityType: string }>({
|
|
55
|
+
contextId: 'customer_accounts:account-status-invite',
|
|
56
|
+
})
|
|
57
|
+
|
|
53
58
|
React.useEffect(() => {
|
|
54
59
|
let cancelled = false
|
|
55
60
|
async function loadPerson() {
|
|
@@ -124,28 +129,35 @@ function InviteForm({ personEntityId, onSuccess }: { personEntityId: string; onS
|
|
|
124
129
|
|
|
125
130
|
setIsSubmitting(true)
|
|
126
131
|
try {
|
|
127
|
-
|
|
128
|
-
'
|
|
129
|
-
{
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
132
|
+
await runMutation({
|
|
133
|
+
context: { entityType: 'customer_accounts:user' },
|
|
134
|
+
mutationPayload: { customerEntityId: personEntityId, roleIds: selectedRoleIds },
|
|
135
|
+
operation: async () => {
|
|
136
|
+
// optimistic-lock-exempt: creates a new portal invitation, not a concurrent record edit
|
|
137
|
+
const call = await apiCall<{ ok: boolean; error?: string }>(
|
|
138
|
+
'/api/customer_accounts/admin/users-invite',
|
|
139
|
+
{
|
|
140
|
+
method: 'POST',
|
|
141
|
+
headers: { 'content-type': 'application/json' },
|
|
142
|
+
body: JSON.stringify({
|
|
143
|
+
email: trimmedEmail,
|
|
144
|
+
roleIds: selectedRoleIds,
|
|
145
|
+
displayName: displayName.trim() || undefined,
|
|
146
|
+
customerEntityId: personEntityId,
|
|
147
|
+
}),
|
|
148
|
+
},
|
|
149
|
+
)
|
|
140
150
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
151
|
+
if (!call.ok) {
|
|
152
|
+
const errorMessage = (call.result as Record<string, unknown> | null)?.error as string | undefined
|
|
153
|
+
flash(errorMessage || t('customer_accounts.widgets.invite.error.failed', 'Failed to send invitation'), 'error')
|
|
154
|
+
return
|
|
155
|
+
}
|
|
146
156
|
|
|
147
|
-
|
|
148
|
-
|
|
157
|
+
flash(t('customer_accounts.widgets.invite.success', 'Invitation sent successfully'), 'success')
|
|
158
|
+
onSuccess()
|
|
159
|
+
},
|
|
160
|
+
})
|
|
149
161
|
} catch {
|
|
150
162
|
flash(t('customer_accounts.widgets.invite.error.failed', 'Failed to send invitation'), 'error')
|
|
151
163
|
} finally {
|