@open-mercato/core 0.6.6-develop.6204.1.30b1f58642 → 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.
Files changed (201) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/currencies/backend/currencies/[id]/page.js +33 -8
  3. package/dist/modules/currencies/backend/currencies/[id]/page.js.map +2 -2
  4. package/dist/modules/currencies/backend/currencies/page.js +63 -27
  5. package/dist/modules/currencies/backend/currencies/page.js.map +2 -2
  6. package/dist/modules/currencies/backend/exchange-rates/page.js +35 -14
  7. package/dist/modules/currencies/backend/exchange-rates/page.js.map +2 -2
  8. package/dist/modules/currencies/components/CurrencyFetchingConfig.js +104 -41
  9. package/dist/modules/currencies/components/CurrencyFetchingConfig.js.map +2 -2
  10. package/dist/modules/customer_accounts/api/admin/roles/[id]/acl.js +57 -8
  11. package/dist/modules/customer_accounts/api/admin/roles/[id]/acl.js.map +2 -2
  12. package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.js +5 -2
  13. package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.js.map +2 -2
  14. package/dist/modules/customer_accounts/backend/customer_accounts/roles/page.js +30 -14
  15. package/dist/modules/customer_accounts/backend/customer_accounts/roles/page.js.map +2 -2
  16. package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js +29 -19
  17. package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js.map +2 -2
  18. package/dist/modules/customers/backend/config/customers/pipeline-stages/page.js +166 -64
  19. package/dist/modules/customers/backend/config/customers/pipeline-stages/page.js.map +2 -2
  20. package/dist/modules/customers/components/AddressFormatSettings.js +31 -16
  21. package/dist/modules/customers/components/AddressFormatSettings.js.map +2 -2
  22. package/dist/modules/customers/components/DictionarySettings.js +57 -31
  23. package/dist/modules/customers/components/DictionarySettings.js.map +2 -2
  24. package/dist/modules/customers/components/PipelineSettings.js +156 -76
  25. package/dist/modules/customers/components/PipelineSettings.js.map +2 -2
  26. package/dist/modules/customers/components/calendar/AgendaList.js +10 -4
  27. package/dist/modules/customers/components/calendar/AgendaList.js.map +2 -2
  28. package/dist/modules/customers/components/calendar/CalendarSettingsModal.js +18 -2
  29. package/dist/modules/customers/components/calendar/CalendarSettingsModal.js.map +2 -2
  30. package/dist/modules/customers/components/calendar/MonthGrid.js +5 -2
  31. package/dist/modules/customers/components/calendar/MonthGrid.js.map +2 -2
  32. package/dist/modules/customers/components/calendar/editor/PeopleField.js +6 -0
  33. package/dist/modules/customers/components/calendar/editor/PeopleField.js.map +2 -2
  34. package/dist/modules/customers/components/calendar/editor/RelatedToField.js +2 -0
  35. package/dist/modules/customers/components/calendar/editor/RelatedToField.js.map +2 -2
  36. package/dist/modules/customers/components/calendar/editor/ScheduleSection.js +4 -1
  37. package/dist/modules/customers/components/calendar/editor/ScheduleSection.js.map +2 -2
  38. package/dist/modules/customers/lib/calendar/labels.js +33 -0
  39. package/dist/modules/customers/lib/calendar/labels.js.map +7 -0
  40. package/dist/modules/dashboards/components/WidgetVisibilityEditor.js +32 -11
  41. package/dist/modules/dashboards/components/WidgetVisibilityEditor.js.map +2 -2
  42. package/dist/modules/data_sync/api/mappings/[id]/route.js +59 -0
  43. package/dist/modules/data_sync/api/mappings/[id]/route.js.map +2 -2
  44. package/dist/modules/data_sync/api/mappings/route.js +44 -0
  45. package/dist/modules/data_sync/api/mappings/route.js.map +2 -2
  46. package/dist/modules/data_sync/api/run.js +31 -0
  47. package/dist/modules/data_sync/api/run.js.map +2 -2
  48. package/dist/modules/data_sync/api/runs/[id]/cancel.js +31 -0
  49. package/dist/modules/data_sync/api/runs/[id]/cancel.js.map +2 -2
  50. package/dist/modules/data_sync/api/runs/[id]/retry.js +31 -0
  51. package/dist/modules/data_sync/api/runs/[id]/retry.js.map +2 -2
  52. package/dist/modules/data_sync/api/schedules/[id]/route.js +59 -3
  53. package/dist/modules/data_sync/api/schedules/[id]/route.js.map +2 -2
  54. package/dist/modules/data_sync/api/schedules/route.js +33 -4
  55. package/dist/modules/data_sync/api/schedules/route.js.map +2 -2
  56. package/dist/modules/dictionaries/components/DictionariesManager.js +7 -4
  57. package/dist/modules/dictionaries/components/DictionariesManager.js.map +2 -2
  58. package/dist/modules/directory/api/organization-branding/route.js +54 -2
  59. package/dist/modules/directory/api/organization-branding/route.js.map +2 -2
  60. package/dist/modules/directory/backend/directory/branding/page.js +15 -9
  61. package/dist/modules/directory/backend/directory/branding/page.js.map +2 -2
  62. package/dist/modules/directory/backend/directory/organizations/page.js +28 -9
  63. package/dist/modules/directory/backend/directory/organizations/page.js.map +2 -2
  64. package/dist/modules/directory/backend/directory/tenants/page.js +29 -13
  65. package/dist/modules/directory/backend/directory/tenants/page.js.map +2 -2
  66. package/dist/modules/entities/api/definitions.batch.js +15 -0
  67. package/dist/modules/entities/api/definitions.batch.js.map +2 -2
  68. package/dist/modules/entities/api/definitions.js +26 -0
  69. package/dist/modules/entities/api/definitions.js.map +2 -2
  70. package/dist/modules/entities/api/definitions.mutation-guard.js +57 -0
  71. package/dist/modules/entities/api/definitions.mutation-guard.js.map +7 -0
  72. package/dist/modules/entities/api/definitions.restore.js +15 -0
  73. package/dist/modules/entities/api/definitions.restore.js.map +2 -2
  74. package/dist/modules/entities/api/entities.js +31 -3
  75. package/dist/modules/entities/api/entities.js.map +2 -2
  76. package/dist/modules/entities/api/records.js +18 -0
  77. package/dist/modules/entities/api/records.js.map +2 -2
  78. package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js +28 -10
  79. package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js.map +2 -2
  80. package/dist/modules/feature_toggles/api/overrides/route.js +38 -1
  81. package/dist/modules/feature_toggles/api/overrides/route.js.map +2 -2
  82. package/dist/modules/inbox_ops/api/emails/[id]/route.js +30 -0
  83. package/dist/modules/inbox_ops/api/emails/[id]/route.js.map +2 -2
  84. package/dist/modules/inbox_ops/api/proposals/[id]/accept-all/route.js +30 -0
  85. package/dist/modules/inbox_ops/api/proposals/[id]/accept-all/route.js.map +2 -2
  86. package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.js +31 -0
  87. package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.js.map +2 -2
  88. package/dist/modules/inbox_ops/api/proposals/[id]/categorize/route.js +31 -0
  89. package/dist/modules/inbox_ops/api/proposals/[id]/categorize/route.js.map +2 -2
  90. package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js +68 -39
  91. package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js.map +2 -2
  92. package/dist/modules/notifications/frontend/NotificationSettingsPageClient.js +14 -4
  93. package/dist/modules/notifications/frontend/NotificationSettingsPageClient.js.map +2 -2
  94. package/dist/modules/payment_gateways/api/cancel/route.js +37 -0
  95. package/dist/modules/payment_gateways/api/cancel/route.js.map +2 -2
  96. package/dist/modules/payment_gateways/api/capture/route.js +37 -0
  97. package/dist/modules/payment_gateways/api/capture/route.js.map +2 -2
  98. package/dist/modules/payment_gateways/api/guards.js +31 -0
  99. package/dist/modules/payment_gateways/api/guards.js.map +7 -0
  100. package/dist/modules/payment_gateways/api/refund/route.js +37 -0
  101. package/dist/modules/payment_gateways/api/refund/route.js.map +2 -2
  102. package/dist/modules/payment_gateways/api/sessions/route.js +37 -0
  103. package/dist/modules/payment_gateways/api/sessions/route.js.map +2 -2
  104. package/dist/modules/planner/api/availability-date-specific.js +11 -1
  105. package/dist/modules/planner/api/availability-date-specific.js.map +2 -2
  106. package/dist/modules/planner/backend/planner/availability-rulesets/page.js +20 -3
  107. package/dist/modules/planner/backend/planner/availability-rulesets/page.js.map +2 -2
  108. package/dist/modules/planner/commands/availability-date-specific.js +16 -0
  109. package/dist/modules/planner/commands/availability-date-specific.js.map +2 -2
  110. package/dist/modules/planner/components/AvailabilityRulesEditor.js +109 -40
  111. package/dist/modules/planner/components/AvailabilityRulesEditor.js.map +2 -2
  112. package/dist/modules/query_index/api/purge.js +35 -3
  113. package/dist/modules/query_index/api/purge.js.map +2 -2
  114. package/dist/modules/query_index/api/reindex.js +41 -3
  115. package/dist/modules/query_index/api/reindex.js.map +2 -2
  116. package/dist/modules/query_index/components/QueryIndexesTable.js +57 -24
  117. package/dist/modules/query_index/components/QueryIndexesTable.js.map +2 -2
  118. package/dist/modules/shipping_carriers/api/shipments/route.js +31 -0
  119. package/dist/modules/shipping_carriers/api/shipments/route.js.map +2 -2
  120. package/dist/modules/shipping_carriers/api/tracking/refresh/route.js +55 -0
  121. package/dist/modules/shipping_carriers/api/tracking/refresh/route.js.map +7 -0
  122. package/dist/modules/shipping_carriers/data/validators.js +6 -1
  123. package/dist/modules/shipping_carriers/data/validators.js.map +2 -2
  124. package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.js +28 -8
  125. package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.js.map +2 -2
  126. package/dist/modules/shipping_carriers/lib/shipping-service.js +37 -7
  127. package/dist/modules/shipping_carriers/lib/shipping-service.js.map +2 -2
  128. package/dist/modules/shipping_carriers/workers/status-poller.js +1 -1
  129. package/dist/modules/shipping_carriers/workers/status-poller.js.map +2 -2
  130. package/dist/modules/translations/components/TranslationManager.js +49 -20
  131. package/dist/modules/translations/components/TranslationManager.js.map +2 -2
  132. package/package.json +7 -7
  133. package/src/modules/currencies/backend/currencies/[id]/page.tsx +40 -10
  134. package/src/modules/currencies/backend/currencies/page.tsx +68 -29
  135. package/src/modules/currencies/backend/exchange-rates/page.tsx +40 -15
  136. package/src/modules/currencies/components/CurrencyFetchingConfig.tsx +110 -41
  137. package/src/modules/customer_accounts/api/admin/roles/[id]/acl.ts +69 -7
  138. package/src/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.tsx +16 -8
  139. package/src/modules/customer_accounts/backend/customer_accounts/roles/page.tsx +32 -14
  140. package/src/modules/customer_accounts/widgets/injection/account-status/widget.client.tsx +32 -20
  141. package/src/modules/customers/backend/config/customers/pipeline-stages/page.tsx +171 -64
  142. package/src/modules/customers/components/AddressFormatSettings.tsx +39 -19
  143. package/src/modules/customers/components/DictionarySettings.tsx +63 -29
  144. package/src/modules/customers/components/PipelineSettings.tsx +165 -80
  145. package/src/modules/customers/components/calendar/AgendaList.tsx +13 -8
  146. package/src/modules/customers/components/calendar/CalendarSettingsModal.tsx +16 -2
  147. package/src/modules/customers/components/calendar/MonthGrid.tsx +5 -2
  148. package/src/modules/customers/components/calendar/editor/PeopleField.tsx +6 -0
  149. package/src/modules/customers/components/calendar/editor/RelatedToField.tsx +2 -0
  150. package/src/modules/customers/components/calendar/editor/ScheduleSection.tsx +7 -0
  151. package/src/modules/customers/i18n/de.json +11 -7
  152. package/src/modules/customers/i18n/en.json +6 -2
  153. package/src/modules/customers/i18n/es.json +11 -7
  154. package/src/modules/customers/i18n/pl.json +11 -7
  155. package/src/modules/customers/lib/calendar/labels.ts +42 -0
  156. package/src/modules/dashboards/components/WidgetVisibilityEditor.tsx +39 -11
  157. package/src/modules/data_sync/api/mappings/[id]/route.ts +63 -0
  158. package/src/modules/data_sync/api/mappings/route.ts +48 -0
  159. package/src/modules/data_sync/api/run.ts +33 -0
  160. package/src/modules/data_sync/api/runs/[id]/cancel.ts +34 -0
  161. package/src/modules/data_sync/api/runs/[id]/retry.ts +33 -0
  162. package/src/modules/data_sync/api/schedules/[id]/route.ts +64 -2
  163. package/src/modules/data_sync/api/schedules/route.ts +36 -4
  164. package/src/modules/dictionaries/components/DictionariesManager.tsx +7 -4
  165. package/src/modules/directory/api/organization-branding/route.ts +61 -0
  166. package/src/modules/directory/backend/directory/branding/page.tsx +16 -10
  167. package/src/modules/directory/backend/directory/organizations/page.tsx +35 -8
  168. package/src/modules/directory/backend/directory/tenants/page.tsx +37 -13
  169. package/src/modules/entities/api/definitions.batch.ts +17 -0
  170. package/src/modules/entities/api/definitions.mutation-guard.ts +80 -0
  171. package/src/modules/entities/api/definitions.restore.ts +17 -0
  172. package/src/modules/entities/api/definitions.ts +30 -0
  173. package/src/modules/entities/api/entities.ts +35 -3
  174. package/src/modules/entities/api/records.ts +20 -0
  175. package/src/modules/entities/backend/entities/user/[entityId]/records/page.tsx +33 -10
  176. package/src/modules/feature_toggles/api/overrides/route.ts +44 -1
  177. package/src/modules/inbox_ops/api/emails/[id]/route.ts +32 -0
  178. package/src/modules/inbox_ops/api/proposals/[id]/accept-all/route.ts +32 -0
  179. package/src/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.ts +33 -0
  180. package/src/modules/inbox_ops/api/proposals/[id]/categorize/route.ts +33 -0
  181. package/src/modules/messages/components/message-detail/hooks/useMessageDetailsActions.ts +80 -42
  182. package/src/modules/notifications/frontend/NotificationSettingsPageClient.tsx +21 -4
  183. package/src/modules/payment_gateways/api/cancel/route.ts +39 -0
  184. package/src/modules/payment_gateways/api/capture/route.ts +39 -0
  185. package/src/modules/payment_gateways/api/guards.ts +59 -0
  186. package/src/modules/payment_gateways/api/refund/route.ts +39 -0
  187. package/src/modules/payment_gateways/api/sessions/route.ts +40 -0
  188. package/src/modules/planner/api/availability-date-specific.ts +10 -0
  189. package/src/modules/planner/backend/planner/availability-rulesets/page.tsx +26 -5
  190. package/src/modules/planner/commands/availability-date-specific.ts +24 -0
  191. package/src/modules/planner/components/AvailabilityRulesEditor.tsx +122 -41
  192. package/src/modules/query_index/api/purge.ts +37 -3
  193. package/src/modules/query_index/api/reindex.ts +43 -3
  194. package/src/modules/query_index/components/QueryIndexesTable.tsx +66 -24
  195. package/src/modules/shipping_carriers/api/shipments/route.ts +31 -0
  196. package/src/modules/shipping_carriers/api/tracking/refresh/route.ts +53 -0
  197. package/src/modules/shipping_carriers/data/validators.ts +5 -0
  198. package/src/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.ts +29 -8
  199. package/src/modules/shipping_carriers/lib/shipping-service.ts +43 -7
  200. package/src/modules/shipping_carriers/workers/status-poller.ts +1 -1
  201. package/src/modules/translations/components/TranslationManager.tsx +65 -21
@@ -3,6 +3,7 @@
3
3
  import * as React from 'react'
4
4
  import { useT } from '@open-mercato/shared/lib/i18n/context'
5
5
  import { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
6
+ import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
6
7
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
7
8
  import { Button } from '@open-mercato/ui/primitives/button'
8
9
  import { Input } from '@open-mercato/ui/primitives/input'
@@ -35,6 +36,8 @@ const emptySettings: NotificationDeliveryConfig = {
35
36
  },
36
37
  }
37
38
 
39
+ const SETTINGS_CONTEXT_ID = 'notifications-settings'
40
+
38
41
  export function NotificationSettingsPageClient() {
39
42
  const t = useT()
40
43
  const [settings, setSettings] = React.useState<NotificationDeliveryConfig | null>(null)
@@ -42,6 +45,15 @@ export function NotificationSettingsPageClient() {
42
45
  const [saving, setSaving] = React.useState(false)
43
46
  const [error, setError] = React.useState<string | null>(null)
44
47
 
48
+ const { runMutation, retryLastMutation } = useGuardedMutation<{
49
+ formId: string
50
+ resourceKind: string
51
+ retryLastMutation: () => Promise<boolean>
52
+ }>({
53
+ contextId: SETTINGS_CONTEXT_ID,
54
+ blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
55
+ })
56
+
45
57
  const fetchSettings = React.useCallback(async () => {
46
58
  setLoading(true)
47
59
  setError(null)
@@ -96,10 +108,15 @@ export function NotificationSettingsPageClient() {
96
108
  if (!settings) return
97
109
  setSaving(true)
98
110
  try {
99
- const response = await apiCall<SettingsResponse>('/api/notifications/settings', {
100
- method: 'POST',
101
- headers: { 'Content-Type': 'application/json' },
102
- body: JSON.stringify(settings),
111
+ const response = await runMutation({
112
+ operation: () =>
113
+ apiCall<SettingsResponse>('/api/notifications/settings', {
114
+ method: 'POST',
115
+ headers: { 'Content-Type': 'application/json' },
116
+ body: JSON.stringify(settings),
117
+ }),
118
+ context: { formId: SETTINGS_CONTEXT_ID, resourceKind: 'notifications.settings', retryLastMutation },
119
+ mutationPayload: { settings },
103
120
  })
104
121
  if (!response.ok) {
105
122
  const message = response.result?.error || t('notifications.settings.saveError', 'Failed to save notification settings')
@@ -5,6 +5,13 @@ import { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'
5
5
  import { cancelSchema } from '../../data/validators'
6
6
  import type { PaymentGatewayService } from '../../lib/gateway-service'
7
7
  import { paymentGatewaysTag } from '../openapi'
8
+ import {
9
+ resolveUserFeatures,
10
+ runPaymentGatewayMutationGuardAfterSuccess,
11
+ runPaymentGatewayMutationGuards,
12
+ } from '../guards'
13
+
14
+ const gatewayTransactionResourceKind = 'payment_gateways.gateway_transaction'
8
15
 
9
16
  export const metadata = {
10
17
  path: '/payment_gateways/cancel',
@@ -24,6 +31,28 @@ export async function POST(req: Request) {
24
31
  }
25
32
 
26
33
  const container = await createRequestContainer()
34
+ const guardResult = await runPaymentGatewayMutationGuards(
35
+ container,
36
+ {
37
+ tenantId: auth.tenantId,
38
+ organizationId: auth.orgId,
39
+ userId: auth.sub ?? '',
40
+ resourceKind: gatewayTransactionResourceKind,
41
+ resourceId: parsed.data.transactionId,
42
+ operation: 'update',
43
+ requestMethod: req.method,
44
+ requestHeaders: req.headers,
45
+ mutationPayload: parsed.data as Record<string, unknown>,
46
+ },
47
+ resolveUserFeatures(auth),
48
+ )
49
+ if (!guardResult.ok) {
50
+ return NextResponse.json(
51
+ guardResult.errorBody ?? { error: 'Operation blocked by guard' },
52
+ { status: guardResult.errorStatus ?? 422 },
53
+ )
54
+ }
55
+
27
56
  const service = container.resolve('paymentGatewayService') as PaymentGatewayService
28
57
 
29
58
  try {
@@ -32,6 +61,16 @@ export async function POST(req: Request) {
32
61
  parsed.data.reason,
33
62
  { organizationId: auth.orgId as string, tenantId: auth.tenantId },
34
63
  )
64
+ await runPaymentGatewayMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {
65
+ tenantId: auth.tenantId,
66
+ organizationId: auth.orgId,
67
+ userId: auth.sub ?? '',
68
+ resourceKind: gatewayTransactionResourceKind,
69
+ resourceId: parsed.data.transactionId,
70
+ operation: 'update',
71
+ requestMethod: req.method,
72
+ requestHeaders: req.headers,
73
+ })
35
74
  return NextResponse.json(result)
36
75
  } catch (err: unknown) {
37
76
  const message = err instanceof Error ? err.message : 'Cancel failed'
@@ -5,6 +5,13 @@ import { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'
5
5
  import { captureSchema } from '../../data/validators'
6
6
  import type { PaymentGatewayService } from '../../lib/gateway-service'
7
7
  import { paymentGatewaysTag } from '../openapi'
8
+ import {
9
+ resolveUserFeatures,
10
+ runPaymentGatewayMutationGuardAfterSuccess,
11
+ runPaymentGatewayMutationGuards,
12
+ } from '../guards'
13
+
14
+ const gatewayTransactionResourceKind = 'payment_gateways.gateway_transaction'
8
15
 
9
16
  export const metadata = {
10
17
  path: '/payment_gateways/capture',
@@ -24,6 +31,28 @@ export async function POST(req: Request) {
24
31
  }
25
32
 
26
33
  const container = await createRequestContainer()
34
+ const guardResult = await runPaymentGatewayMutationGuards(
35
+ container,
36
+ {
37
+ tenantId: auth.tenantId,
38
+ organizationId: auth.orgId,
39
+ userId: auth.sub ?? '',
40
+ resourceKind: gatewayTransactionResourceKind,
41
+ resourceId: parsed.data.transactionId,
42
+ operation: 'update',
43
+ requestMethod: req.method,
44
+ requestHeaders: req.headers,
45
+ mutationPayload: parsed.data as Record<string, unknown>,
46
+ },
47
+ resolveUserFeatures(auth),
48
+ )
49
+ if (!guardResult.ok) {
50
+ return NextResponse.json(
51
+ guardResult.errorBody ?? { error: 'Operation blocked by guard' },
52
+ { status: guardResult.errorStatus ?? 422 },
53
+ )
54
+ }
55
+
27
56
  const service = container.resolve('paymentGatewayService') as PaymentGatewayService
28
57
 
29
58
  try {
@@ -32,6 +61,16 @@ export async function POST(req: Request) {
32
61
  parsed.data.amount,
33
62
  { organizationId: auth.orgId as string, tenantId: auth.tenantId },
34
63
  )
64
+ await runPaymentGatewayMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {
65
+ tenantId: auth.tenantId,
66
+ organizationId: auth.orgId,
67
+ userId: auth.sub ?? '',
68
+ resourceKind: gatewayTransactionResourceKind,
69
+ resourceId: parsed.data.transactionId,
70
+ operation: 'update',
71
+ requestMethod: req.method,
72
+ requestHeaders: req.headers,
73
+ })
35
74
  return NextResponse.json(result)
36
75
  } catch (err: unknown) {
37
76
  const message = err instanceof Error ? err.message : 'Capture failed'
@@ -0,0 +1,59 @@
1
+ import type { AwilixContainer } from 'awilix'
2
+ import {
3
+ bridgeLegacyGuard,
4
+ runMutationGuards,
5
+ type MutationGuard,
6
+ type MutationGuardInput,
7
+ } from '@open-mercato/shared/lib/crud/mutation-guard-registry'
8
+
9
+ type GuardAfterCallback = {
10
+ guard: MutationGuard
11
+ metadata: Record<string, unknown> | null
12
+ }
13
+
14
+ export function resolveUserFeatures(auth: unknown): string[] {
15
+ const features = (auth as { features?: unknown })?.features
16
+ if (!Array.isArray(features)) return []
17
+ return features.filter((value): value is string => typeof value === 'string')
18
+ }
19
+
20
+ export async function runPaymentGatewayMutationGuards(
21
+ container: AwilixContainer,
22
+ input: MutationGuardInput,
23
+ userFeatures: string[],
24
+ ): Promise<{
25
+ ok: boolean
26
+ errorBody?: Record<string, unknown>
27
+ errorStatus?: number
28
+ modifiedPayload?: Record<string, unknown>
29
+ afterSuccessCallbacks: GuardAfterCallback[]
30
+ }> {
31
+ const legacyGuard = bridgeLegacyGuard(container)
32
+ if (!legacyGuard) {
33
+ return { ok: true, afterSuccessCallbacks: [] }
34
+ }
35
+
36
+ return runMutationGuards([legacyGuard], input, { userFeatures })
37
+ }
38
+
39
+ export async function runPaymentGatewayMutationGuardAfterSuccess(
40
+ callbacks: GuardAfterCallback[],
41
+ input: {
42
+ tenantId: string
43
+ organizationId: string | null
44
+ userId: string
45
+ resourceKind: string
46
+ resourceId: string
47
+ operation: 'create' | 'update' | 'delete'
48
+ requestMethod: string
49
+ requestHeaders: Headers
50
+ },
51
+ ): Promise<void> {
52
+ for (const callback of callbacks) {
53
+ if (!callback.guard.afterSuccess) continue
54
+ await callback.guard.afterSuccess({
55
+ ...input,
56
+ metadata: callback.metadata ?? null,
57
+ })
58
+ }
59
+ }
@@ -5,6 +5,13 @@ import { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'
5
5
  import { refundSchema } from '../../data/validators'
6
6
  import type { PaymentGatewayService } from '../../lib/gateway-service'
7
7
  import { paymentGatewaysTag } from '../openapi'
8
+ import {
9
+ resolveUserFeatures,
10
+ runPaymentGatewayMutationGuardAfterSuccess,
11
+ runPaymentGatewayMutationGuards,
12
+ } from '../guards'
13
+
14
+ const gatewayTransactionResourceKind = 'payment_gateways.gateway_transaction'
8
15
 
9
16
  export const metadata = {
10
17
  path: '/payment_gateways/refund',
@@ -24,6 +31,28 @@ export async function POST(req: Request) {
24
31
  }
25
32
 
26
33
  const container = await createRequestContainer()
34
+ const guardResult = await runPaymentGatewayMutationGuards(
35
+ container,
36
+ {
37
+ tenantId: auth.tenantId,
38
+ organizationId: auth.orgId,
39
+ userId: auth.sub ?? '',
40
+ resourceKind: gatewayTransactionResourceKind,
41
+ resourceId: parsed.data.transactionId,
42
+ operation: 'update',
43
+ requestMethod: req.method,
44
+ requestHeaders: req.headers,
45
+ mutationPayload: parsed.data as Record<string, unknown>,
46
+ },
47
+ resolveUserFeatures(auth),
48
+ )
49
+ if (!guardResult.ok) {
50
+ return NextResponse.json(
51
+ guardResult.errorBody ?? { error: 'Operation blocked by guard' },
52
+ { status: guardResult.errorStatus ?? 422 },
53
+ )
54
+ }
55
+
27
56
  const service = container.resolve('paymentGatewayService') as PaymentGatewayService
28
57
 
29
58
  try {
@@ -33,6 +62,16 @@ export async function POST(req: Request) {
33
62
  parsed.data.reason,
34
63
  { organizationId: auth.orgId as string, tenantId: auth.tenantId },
35
64
  )
65
+ await runPaymentGatewayMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {
66
+ tenantId: auth.tenantId,
67
+ organizationId: auth.orgId,
68
+ userId: auth.sub ?? '',
69
+ resourceKind: gatewayTransactionResourceKind,
70
+ resourceId: parsed.data.transactionId,
71
+ operation: 'update',
72
+ requestMethod: req.method,
73
+ requestHeaders: req.headers,
74
+ })
36
75
  return NextResponse.json(result)
37
76
  } catch (err: unknown) {
38
77
  const message = err instanceof Error ? err.message : 'Refund failed'
@@ -5,6 +5,13 @@ import { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'
5
5
  import { createSessionSchema } from '../../data/validators'
6
6
  import type { PaymentGatewayService } from '../../lib/gateway-service'
7
7
  import { paymentGatewaysTag } from '../openapi'
8
+ import {
9
+ resolveUserFeatures,
10
+ runPaymentGatewayMutationGuardAfterSuccess,
11
+ runPaymentGatewayMutationGuards,
12
+ } from '../guards'
13
+
14
+ const gatewayTransactionResourceKind = 'payment_gateways.gateway_transaction'
8
15
 
9
16
  export const metadata = {
10
17
  path: '/payment_gateways/sessions',
@@ -24,6 +31,28 @@ export async function POST(req: Request) {
24
31
  }
25
32
 
26
33
  const container = await createRequestContainer()
34
+ const guardResult = await runPaymentGatewayMutationGuards(
35
+ container,
36
+ {
37
+ tenantId: auth.tenantId,
38
+ organizationId: auth.orgId,
39
+ userId: auth.sub ?? '',
40
+ resourceKind: gatewayTransactionResourceKind,
41
+ resourceId: null,
42
+ operation: 'create',
43
+ requestMethod: req.method,
44
+ requestHeaders: req.headers,
45
+ mutationPayload: parsed.data as Record<string, unknown>,
46
+ },
47
+ resolveUserFeatures(auth),
48
+ )
49
+ if (!guardResult.ok) {
50
+ return NextResponse.json(
51
+ guardResult.errorBody ?? { error: 'Operation blocked by guard' },
52
+ { status: guardResult.errorStatus ?? 422 },
53
+ )
54
+ }
55
+
27
56
  const service = container.resolve('paymentGatewayService') as PaymentGatewayService
28
57
 
29
58
  try {
@@ -46,6 +75,17 @@ export async function POST(req: Request) {
46
75
  const redirectUrl = session.redirectUrl
47
76
  ?? (session.clientSession?.type === 'redirect' ? session.clientSession.redirectUrl : null)
48
77
 
78
+ await runPaymentGatewayMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {
79
+ tenantId: auth.tenantId,
80
+ organizationId: auth.orgId,
81
+ userId: auth.sub ?? '',
82
+ resourceKind: gatewayTransactionResourceKind,
83
+ resourceId: transaction.id,
84
+ operation: 'create',
85
+ requestMethod: req.method,
86
+ requestHeaders: req.headers,
87
+ })
88
+
49
89
  return NextResponse.json({
50
90
  transactionId: transaction.id,
51
91
  sessionId: session.sessionId,
@@ -161,6 +161,16 @@ export const openApi = {
161
161
  { status: 400, description: 'Invalid payload', schema: z.object({ error: z.string() }) },
162
162
  { status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },
163
163
  { status: 403, description: 'Forbidden', schema: z.object({ error: z.string() }) },
164
+ {
165
+ status: 409,
166
+ description: 'Conflict — the subject/date availability was modified by another edit',
167
+ schema: z.object({
168
+ error: z.string(),
169
+ code: z.string(),
170
+ currentUpdatedAt: z.string(),
171
+ expectedUpdatedAt: z.string(),
172
+ }),
173
+ },
164
174
  ],
165
175
  },
166
176
  },
@@ -11,6 +11,7 @@ import { RowActions } from '@open-mercato/ui/backend/RowActions'
11
11
  import { Button } from '@open-mercato/ui/primitives/button'
12
12
  import { readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
13
13
  import { deleteCrud } from '@open-mercato/ui/backend/utils/crud'
14
+ import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
14
15
  import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
15
16
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
16
17
  import { normalizeCrudServerError } from '@open-mercato/ui/backend/utils/serverErrors'
@@ -41,6 +42,23 @@ export default function PlannerAvailabilityRuleSetsPage() {
41
42
  const { confirm, ConfirmDialogElement } = useConfirmDialog()
42
43
  const router = useRouter()
43
44
  const scopeVersion = useOrganizationScopeVersion()
45
+ const mutationContextId = 'planner-availability-rule-sets'
46
+ const { runMutation, retryLastMutation } = useGuardedMutation<{
47
+ formId: string
48
+ resourceKind: string
49
+ retryLastMutation: () => Promise<boolean>
50
+ }>({
51
+ contextId: mutationContextId,
52
+ blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
53
+ })
54
+ const mutationContext = React.useMemo(
55
+ () => ({
56
+ formId: mutationContextId,
57
+ resourceKind: 'planner.availability_rule_set',
58
+ retryLastMutation,
59
+ }),
60
+ [mutationContextId, retryLastMutation],
61
+ )
44
62
  const [rows, setRows] = React.useState<RuleSetRow[]>([])
45
63
  const [page, setPage] = React.useState(1)
46
64
  const [total, setTotal] = React.useState(0)
@@ -127,10 +145,13 @@ export default function PlannerAvailabilityRuleSetsPage() {
127
145
  })
128
146
  if (!confirmed) return
129
147
  try {
130
- const headers = buildOptimisticLockHeader(entry.updatedAt)
131
- await withScopedApiRequestHeaders(headers, () => (
132
- deleteCrud('planner/availability-rule-sets', entry.id, { errorMessage: labels.errors.delete })
133
- ))
148
+ await runMutation({
149
+ operation: () => withScopedApiRequestHeaders(buildOptimisticLockHeader(entry.updatedAt), () => (
150
+ deleteCrud('planner/availability-rule-sets', entry.id, { errorMessage: labels.errors.delete })
151
+ )),
152
+ context: mutationContext,
153
+ mutationPayload: { action: 'delete', id: entry.id },
154
+ })
134
155
  flash(labels.messages.deleted, 'success')
135
156
  handleRefresh()
136
157
  } catch (error) {
@@ -138,7 +159,7 @@ export default function PlannerAvailabilityRuleSetsPage() {
138
159
  const normalized = normalizeCrudServerError(error)
139
160
  flash(normalized.message ?? labels.errors.delete, 'error')
140
161
  }
141
- }, [confirm, handleRefresh, labels.actions.deleteConfirm, labels.errors.delete, labels.messages.deleted])
162
+ }, [confirm, handleRefresh, labels.actions.deleteConfirm, labels.errors.delete, labels.messages.deleted, mutationContext, runMutation])
142
163
 
143
164
  const columns = React.useMemo<ColumnDef<RuleSetRow>[]>(() => [
144
165
  {
@@ -9,10 +9,28 @@ import {
9
9
  type PlannerAvailabilityDateSpecificReplaceInput,
10
10
  } from '../data/validators'
11
11
  import { ensureOrganizationScope, ensureTenantScope, extractUndoPayload } from './shared'
12
+ import { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
12
13
  import type { PlannerAvailabilityKind, PlannerAvailabilitySubjectType } from '../data/entities'
13
14
 
14
15
  const AVAILABILITY_RULE_RESOURCE_KIND = 'planner.availability.rule'
15
16
 
17
+ /**
18
+ * The date-specific aggregate (the one-off rules for a subject/date) has no
19
+ * single parent row, so its optimistic-lock version is the latest `updated_at`
20
+ * of the rules being replaced. The editor sends that token via the extension
21
+ * header; an empty set means "nothing to clobber" → no current version → the
22
+ * guard is a no-op (strictly additive, fail-open).
23
+ */
24
+ function resolveAggregateLockVersion(rules: PlannerAvailabilityRule[]): Date | null {
25
+ let latest: Date | null = null
26
+ for (const rule of rules) {
27
+ const value = rule.updatedAt
28
+ if (!value || Number.isNaN(value.getTime())) continue
29
+ if (!latest || value.getTime() > latest.getTime()) latest = value
30
+ }
31
+ return latest
32
+ }
33
+
16
34
  type AvailabilityRuleSnapshot = {
17
35
  id: string
18
36
  tenantId: string
@@ -207,6 +225,12 @@ const replaceDateSpecificAvailabilityCommand: CommandHandler<PlannerAvailability
207
225
  if (window.repeat !== 'once') return false
208
226
  return dates.has(formatDateKey(window.startAt))
209
227
  })
228
+ enforceCommandOptimisticLock({
229
+ resourceKind: AVAILABILITY_RULE_RESOURCE_KIND,
230
+ resourceId: `${parsed.subjectType}:${parsed.subjectId}`,
231
+ current: resolveAggregateLockVersion(toDelete),
232
+ request: ctx.request ?? null,
233
+ })
210
234
  toDelete.forEach((rule) => {
211
235
  rule.deletedAt = now
212
236
  rule.updatedAt = now