@open-mercato/core 0.6.6-develop.6205.1.109e4b6a84 → 0.6.6-develop.6228.1.cf483c68b5

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 (205) 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/fields/dictionary.js +29 -4
  57. package/dist/modules/dictionaries/fields/dictionary.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/fields/dictionary.tsx +31 -8
  165. package/src/modules/dictionaries/i18n/de.json +1 -0
  166. package/src/modules/dictionaries/i18n/en.json +1 -0
  167. package/src/modules/dictionaries/i18n/es.json +1 -0
  168. package/src/modules/dictionaries/i18n/pl.json +1 -0
  169. package/src/modules/directory/api/organization-branding/route.ts +61 -0
  170. package/src/modules/directory/backend/directory/branding/page.tsx +16 -10
  171. package/src/modules/directory/backend/directory/organizations/page.tsx +35 -8
  172. package/src/modules/directory/backend/directory/tenants/page.tsx +37 -13
  173. package/src/modules/entities/api/definitions.batch.ts +17 -0
  174. package/src/modules/entities/api/definitions.mutation-guard.ts +80 -0
  175. package/src/modules/entities/api/definitions.restore.ts +17 -0
  176. package/src/modules/entities/api/definitions.ts +30 -0
  177. package/src/modules/entities/api/entities.ts +35 -3
  178. package/src/modules/entities/api/records.ts +20 -0
  179. package/src/modules/entities/backend/entities/user/[entityId]/records/page.tsx +33 -10
  180. package/src/modules/feature_toggles/api/overrides/route.ts +44 -1
  181. package/src/modules/inbox_ops/api/emails/[id]/route.ts +32 -0
  182. package/src/modules/inbox_ops/api/proposals/[id]/accept-all/route.ts +32 -0
  183. package/src/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.ts +33 -0
  184. package/src/modules/inbox_ops/api/proposals/[id]/categorize/route.ts +33 -0
  185. package/src/modules/messages/components/message-detail/hooks/useMessageDetailsActions.ts +80 -42
  186. package/src/modules/notifications/frontend/NotificationSettingsPageClient.tsx +21 -4
  187. package/src/modules/payment_gateways/api/cancel/route.ts +39 -0
  188. package/src/modules/payment_gateways/api/capture/route.ts +39 -0
  189. package/src/modules/payment_gateways/api/guards.ts +59 -0
  190. package/src/modules/payment_gateways/api/refund/route.ts +39 -0
  191. package/src/modules/payment_gateways/api/sessions/route.ts +40 -0
  192. package/src/modules/planner/api/availability-date-specific.ts +10 -0
  193. package/src/modules/planner/backend/planner/availability-rulesets/page.tsx +26 -5
  194. package/src/modules/planner/commands/availability-date-specific.ts +24 -0
  195. package/src/modules/planner/components/AvailabilityRulesEditor.tsx +122 -41
  196. package/src/modules/query_index/api/purge.ts +37 -3
  197. package/src/modules/query_index/api/reindex.ts +43 -3
  198. package/src/modules/query_index/components/QueryIndexesTable.tsx +66 -24
  199. package/src/modules/shipping_carriers/api/shipments/route.ts +31 -0
  200. package/src/modules/shipping_carriers/api/tracking/refresh/route.ts +53 -0
  201. package/src/modules/shipping_carriers/data/validators.ts +5 -0
  202. package/src/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.ts +29 -8
  203. package/src/modules/shipping_carriers/lib/shipping-service.ts +43 -7
  204. package/src/modules/shipping_carriers/workers/status-poller.ts +1 -1
  205. package/src/modules/translations/components/TranslationManager.tsx +65 -21
@@ -7,6 +7,10 @@ import type { SyncScheduleService } from '../../lib/sync-schedule-service'
7
7
  import { serializeSchedule } from './serialize'
8
8
  import { readOptimisticLockExpected } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
9
9
  import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
10
+ import {
11
+ runCrudMutationGuardAfterSuccess,
12
+ validateCrudMutationGuard,
13
+ } from '@open-mercato/shared/lib/crud/mutation-guard'
10
14
 
11
15
  export const metadata = {
12
16
  GET: { requireAuth: true, requireFeatures: ['data_sync.configure'] },
@@ -65,15 +69,43 @@ export async function POST(req: Request) {
65
69
 
66
70
  const container = await createRequestContainer()
67
71
  const scheduleService = container.resolve('dataSyncScheduleService') as SyncScheduleService
72
+ const scope = { organizationId: auth.orgId as string, tenantId: auth.tenantId }
73
+
74
+ const guardResult = await validateCrudMutationGuard(container, {
75
+ tenantId: auth.tenantId,
76
+ organizationId: scope.organizationId,
77
+ userId: auth.sub,
78
+ resourceKind: 'data_sync.schedule',
79
+ resourceId: scope.organizationId,
80
+ operation: 'create',
81
+ requestMethod: req.method,
82
+ requestHeaders: req.headers,
83
+ mutationPayload: parsed.data,
84
+ })
85
+ if (guardResult && !guardResult.ok) {
86
+ return NextResponse.json(guardResult.body, { status: guardResult.status })
87
+ }
68
88
 
69
89
  try {
70
90
  const schedule = await scheduleService.saveSchedule({
71
91
  ...parsed.data,
72
92
  expectedUpdatedAt: readOptimisticLockExpected(req),
73
- }, {
74
- organizationId: auth.orgId as string,
75
- tenantId: auth.tenantId,
76
- })
93
+ }, scope)
94
+
95
+ if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
96
+ await runCrudMutationGuardAfterSuccess(container, {
97
+ tenantId: auth.tenantId,
98
+ organizationId: scope.organizationId,
99
+ userId: auth.sub,
100
+ resourceKind: 'data_sync.schedule',
101
+ resourceId: schedule.id,
102
+ operation: 'create',
103
+ requestMethod: req.method,
104
+ requestHeaders: req.headers,
105
+ metadata: guardResult.metadata ?? null,
106
+ })
107
+ }
108
+
77
109
  return NextResponse.json(serializeSchedule(schedule), { status: 201 })
78
110
  } catch (error) {
79
111
  if (isCrudHttpError(error)) {
@@ -19,6 +19,7 @@ import { useDictionaryEntries } from '../components/hooks/useDictionaryEntries'
19
19
  type DictionaryFieldDefinition = {
20
20
  dictionaryId?: string
21
21
  dictionaryInlineCreate?: boolean
22
+ multi?: boolean
22
23
  defaultValue?: string
23
24
  }
24
25
 
@@ -84,9 +85,12 @@ function DictionaryFieldDefEditor({ def, onChange }: { def: { configJson?: Dicti
84
85
  const [items, setItems] = React.useState<DictionarySummary[]>([])
85
86
  const [loading, setLoading] = React.useState(false)
86
87
  const [error, setError] = React.useState<string | null>(null)
88
+ const multiId = React.useId()
87
89
  const inlineCreateId = React.useId()
88
90
  const selectedId = typeof def?.configJson?.dictionaryId === 'string' ? def?.configJson?.dictionaryId : ''
89
91
  const inlineCreate = def?.configJson?.dictionaryInlineCreate !== false
92
+ const isMulti = def?.configJson?.multi === true
93
+ const errorLoadLabel = t('dictionaries.customFields.errorLoad', 'Failed to load dictionaries.')
90
94
 
91
95
  React.useEffect(() => {
92
96
  let cancelled = false
@@ -116,7 +120,7 @@ function DictionaryFieldDefEditor({ def, onChange }: { def: { configJson?: Dicti
116
120
  } catch (err) {
117
121
  if (!cancelled) {
118
122
  console.error('Failed to load dictionaries list', err)
119
- setError(t('dictionaries.customFields.errorLoad', 'Failed to load dictionaries.'))
123
+ setError(errorLoadLabel)
120
124
  }
121
125
  } finally {
122
126
  if (!cancelled) {
@@ -128,7 +132,7 @@ function DictionaryFieldDefEditor({ def, onChange }: { def: { configJson?: Dicti
128
132
  return () => {
129
133
  cancelled = true
130
134
  }
131
- }, [t])
135
+ }, [errorLoadLabel])
132
136
 
133
137
  const manageHref = '/backend/config/dictionaries'
134
138
 
@@ -176,19 +180,38 @@ function DictionaryFieldDefEditor({ def, onChange }: { def: { configJson?: Dicti
176
180
  ) : null}
177
181
  <div className="inline-flex items-center gap-2 text-xs">
178
182
  <Checkbox
179
- id={inlineCreateId}
180
- checked={inlineCreate}
181
- onCheckedChange={(checked) => onChange({ dictionaryInlineCreate: checked === true })}
183
+ id={multiId}
184
+ checked={isMulti}
185
+ onCheckedChange={(checked) => {
186
+ const enabled = checked === true
187
+ onChange({ multi: enabled, defaultValue: enabled ? undefined : def?.configJson?.defaultValue })
188
+ }}
182
189
  disabled={!selectedId}
183
190
  />
184
191
  <label
185
- htmlFor={inlineCreateId}
192
+ htmlFor={multiId}
186
193
  className={selectedId ? 'cursor-pointer select-none' : 'cursor-not-allowed opacity-60'}
187
194
  >
188
- {t('dictionaries.customFields.allowInlineCreate', 'Allow inline creation inside forms')}
195
+ {t('dictionaries.customFields.allowMultiple', 'Allow selecting multiple entries')}
189
196
  </label>
190
197
  </div>
191
- {selectedId ? (
198
+ {!isMulti ? (
199
+ <div className="inline-flex items-center gap-2 text-xs">
200
+ <Checkbox
201
+ id={inlineCreateId}
202
+ checked={inlineCreate}
203
+ onCheckedChange={(checked) => onChange({ dictionaryInlineCreate: checked === true })}
204
+ disabled={!selectedId}
205
+ />
206
+ <label
207
+ htmlFor={inlineCreateId}
208
+ className={selectedId ? 'cursor-pointer select-none' : 'cursor-not-allowed opacity-60'}
209
+ >
210
+ {t('dictionaries.customFields.allowInlineCreate', 'Allow inline creation inside forms')}
211
+ </label>
212
+ </div>
213
+ ) : null}
214
+ {selectedId && !isMulti ? (
192
215
  <DictionaryDefaultSelector
193
216
  dictionaryId={selectedId}
194
217
  defaultValue={typeof def?.configJson?.defaultValue === 'string' ? def.configJson.defaultValue : ''}
@@ -80,6 +80,7 @@
80
80
  "dictionaries.config.success.delete": "Wörterbuch gelöscht.",
81
81
  "dictionaries.config.success.update": "Wörterbuch aktualisiert.",
82
82
  "dictionaries.customFields.allowInlineCreate": "Inline-Erstellung in Formularen erlauben",
83
+ "dictionaries.customFields.allowMultiple": "Mehrere Einträge auswählen erlauben",
83
84
  "dictionaries.customFields.defaultValue": "Standardwert",
84
85
  "dictionaries.customFields.defaultValueNone": "Kein Standardwert",
85
86
  "dictionaries.customFields.defaultValueStale": "Standardeintrag nicht gefunden — er wurde möglicherweise gelöscht oder umbenannt.",
@@ -80,6 +80,7 @@
80
80
  "dictionaries.config.success.delete": "Dictionary deleted.",
81
81
  "dictionaries.config.success.update": "Dictionary updated.",
82
82
  "dictionaries.customFields.allowInlineCreate": "Allow inline creation inside forms",
83
+ "dictionaries.customFields.allowMultiple": "Allow selecting multiple entries",
83
84
  "dictionaries.customFields.defaultValue": "Default value",
84
85
  "dictionaries.customFields.defaultValueNone": "No default",
85
86
  "dictionaries.customFields.defaultValueStale": "Default entry not found — it may have been deleted or renamed.",
@@ -80,6 +80,7 @@
80
80
  "dictionaries.config.success.delete": "Diccionario eliminado.",
81
81
  "dictionaries.config.success.update": "Diccionario actualizado.",
82
82
  "dictionaries.customFields.allowInlineCreate": "Permitir creación en línea dentro de los formularios",
83
+ "dictionaries.customFields.allowMultiple": "Permitir seleccionar varias entradas",
83
84
  "dictionaries.customFields.defaultValue": "Valor predeterminado",
84
85
  "dictionaries.customFields.defaultValueNone": "Sin valor predeterminado",
85
86
  "dictionaries.customFields.defaultValueStale": "No se encontró la entrada predeterminada; puede haber sido eliminada o renombrada.",
@@ -80,6 +80,7 @@
80
80
  "dictionaries.config.success.delete": "Słownik został usunięty.",
81
81
  "dictionaries.config.success.update": "Słownik został zaktualizowany.",
82
82
  "dictionaries.customFields.allowInlineCreate": "Zezwól na tworzenie w formularzach",
83
+ "dictionaries.customFields.allowMultiple": "Zezwól na wybór wielu wpisów",
83
84
  "dictionaries.customFields.defaultValue": "Wartość domyślna",
84
85
  "dictionaries.customFields.defaultValueNone": "Brak wartości domyślnej",
85
86
  "dictionaries.customFields.defaultValueStale": "Nie znaleziono domyślnego wpisu — mógł zostać usunięty albo przemianowany.",
@@ -7,6 +7,11 @@ import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
7
7
  import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
8
8
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
9
9
  import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
10
+ import {
11
+ runCrudMutationGuardAfterSuccess,
12
+ validateCrudMutationGuard,
13
+ } from '@open-mercato/shared/lib/crud/mutation-guard'
14
+ import { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
10
15
  import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
11
16
  import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
12
17
  import { Organization } from '@open-mercato/core/modules/directory/data/entities'
@@ -24,6 +29,7 @@ const brandingResponseSchema = z.object({
24
29
  organizationName: z.string(),
25
30
  tenantId: z.string().uuid(),
26
31
  logoUrl: z.string().nullable(),
32
+ updatedAt: z.string().nullable(),
27
33
  })
28
34
 
29
35
  const brandingUpdateSchema = z.object({
@@ -105,12 +111,25 @@ async function resolveCurrentOrganization(req: Request) {
105
111
  return { auth, container, organization, organizationId, tenantId, translate }
106
112
  }
107
113
 
114
+ function toIsoOrNull(value: Date | string | null | undefined): string | null {
115
+ if (value == null) return null
116
+ if (value instanceof Date) {
117
+ const ms = value.getTime()
118
+ return Number.isFinite(ms) ? new Date(ms).toISOString() : null
119
+ }
120
+ const trimmed = String(value).trim()
121
+ if (!trimmed) return null
122
+ const parsed = new Date(trimmed)
123
+ return Number.isFinite(parsed.getTime()) ? parsed.toISOString() : null
124
+ }
125
+
108
126
  function toResponsePayload(organization: Organization, tenantId: string) {
109
127
  return {
110
128
  organizationId: String(organization.id),
111
129
  organizationName: organization.name,
112
130
  tenantId,
113
131
  logoUrl: organization.logoUrl ?? null,
132
+ updatedAt: toIsoOrNull(organization.updatedAt),
114
133
  }
115
134
  }
116
135
 
@@ -169,6 +188,32 @@ export async function PUT(req: Request) {
169
188
  }
170
189
 
171
190
  try {
191
+ // Refuse a stale overwrite when two tabs edit the same organization branding.
192
+ // Strictly additive — a no-op when the client omits the expected-version header.
193
+ enforceCommandOptimisticLock({
194
+ resourceKind: 'directory.organization',
195
+ resourceId: resolved.organizationId,
196
+ current: resolved.organization.updatedAt ?? null,
197
+ request: req,
198
+ })
199
+
200
+ // Mutation-guard contract for custom write routes: validate before the write,
201
+ // then run the after-success hook once the command persists.
202
+ const guardResult = await validateCrudMutationGuard(resolved.container, {
203
+ tenantId: resolved.tenantId,
204
+ organizationId: resolved.organizationId,
205
+ userId: resolved.auth.sub,
206
+ resourceKind: 'directory.organization',
207
+ resourceId: resolved.organizationId,
208
+ operation: 'update',
209
+ requestMethod: req.method,
210
+ requestHeaders: req.headers,
211
+ mutationPayload: { logoUrl: parsed.data.logoUrl ?? null },
212
+ })
213
+ if (guardResult && !guardResult.ok) {
214
+ return NextResponse.json(guardResult.body, { status: guardResult.status })
215
+ }
216
+
172
217
  const commandBus = resolved.container.resolve('commandBus') as CommandBus
173
218
  const ctx = buildCommandContext(
174
219
  resolved.container,
@@ -188,6 +233,21 @@ export async function PUT(req: Request) {
188
233
  ctx,
189
234
  },
190
235
  )
236
+
237
+ if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
238
+ await runCrudMutationGuardAfterSuccess(resolved.container, {
239
+ tenantId: resolved.tenantId,
240
+ organizationId: resolved.organizationId,
241
+ userId: resolved.auth.sub,
242
+ resourceKind: 'directory.organization',
243
+ resourceId: resolved.organizationId,
244
+ operation: 'update',
245
+ requestMethod: req.method,
246
+ requestHeaders: req.headers,
247
+ metadata: guardResult.metadata ?? null,
248
+ })
249
+ }
250
+
191
251
  await invalidateSidebarBrandingCache(resolved.container, resolved.organizationId, resolved.tenantId)
192
252
  return NextResponse.json(toResponsePayload(result, resolved.tenantId))
193
253
  } catch (err) {
@@ -231,6 +291,7 @@ export const openApi: OpenApiRouteDoc = {
231
291
  errors: [
232
292
  { status: 400, description: 'Save failed', schema: errorSchema },
233
293
  { status: 401, description: 'Unauthorized', schema: errorSchema },
294
+ { status: 409, description: 'Organization branding changed since it was loaded', schema: errorSchema },
234
295
  { status: 422, description: 'Invalid logo URL', schema: errorSchema },
235
296
  ],
236
297
  },
@@ -7,7 +7,9 @@ import { Page, PageBody, PageHeader } from '@open-mercato/ui/backend/Page'
7
7
  import { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'
8
8
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
9
9
  import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
10
- import { apiCallOrThrow, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
10
+ import { apiCallOrThrow, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
11
+ import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
12
+ import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
11
13
  import { Button } from '@open-mercato/ui/primitives/button'
12
14
  import { Input } from '@open-mercato/ui/primitives/input'
13
15
  import { useT } from '@open-mercato/shared/lib/i18n/context'
@@ -17,6 +19,7 @@ type BrandingPayload = {
17
19
  organizationName: string
18
20
  tenantId: string
19
21
  logoUrl: string | null
22
+ updatedAt: string | null
20
23
  }
21
24
 
22
25
  type UploadPayload = {
@@ -98,15 +101,17 @@ export default function OrganizationBrandingPage() {
98
101
  operation: async () => {
99
102
  const uploadedLogoUrl = shouldUpload ? await uploadLogo(data.organizationId) : null
100
103
  const resolvedLogoUrl = uploadedLogoUrl ?? nextLogoUrl ?? logoUrl.trim()
101
- // optimistic-lock-exempt: selected organization branding uses a scoped command endpoint without an exposed updatedAt token.
102
- const response = await apiCallOrThrow<BrandingPayload>(
103
- BRANDING_API,
104
- {
105
- method: 'PUT',
106
- headers: { 'content-type': 'application/json' },
107
- body: JSON.stringify({ logoUrl: resolvedLogoUrl || null }),
108
- },
109
- { errorMessage: t('directory.branding.errors.save', 'Failed to update organization branding') },
104
+ const response = await withScopedApiRequestHeaders(
105
+ buildOptimisticLockHeader(data.updatedAt),
106
+ () => apiCallOrThrow<BrandingPayload>(
107
+ BRANDING_API,
108
+ {
109
+ method: 'PUT',
110
+ headers: { 'content-type': 'application/json' },
111
+ body: JSON.stringify({ logoUrl: resolvedLogoUrl || null }),
112
+ },
113
+ { errorMessage: t('directory.branding.errors.save', 'Failed to update organization branding') },
114
+ ),
110
115
  )
111
116
  return response.result
112
117
  },
@@ -127,6 +132,7 @@ export default function OrganizationBrandingPage() {
127
132
  if (fileInputRef.current) fileInputRef.current.value = ''
128
133
  flash(t('directory.branding.flash.saved', 'Organization branding updated'), 'success')
129
134
  } catch (err: unknown) {
135
+ if (surfaceRecordConflict(err, t)) return
130
136
  const fallback = t('directory.branding.errors.save', 'Failed to update organization branding')
131
137
  const message = err instanceof Error ? err.message : fallback
132
138
  flash(message, 'error')
@@ -13,6 +13,8 @@ import { Button } from '@open-mercato/ui/primitives/button'
13
13
  import { apiCall, apiCallOrThrow, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
14
14
  import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
15
15
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
16
+ import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
17
+ import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
16
18
  import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
17
19
  import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
18
20
  import { useT } from '@open-mercato/shared/lib/i18n/context'
@@ -177,6 +179,17 @@ export default function DirectoryOrganizationsPage() {
177
179
  const total = data?.total ?? 0
178
180
  const totalPages = data?.totalPages ?? 0
179
181
 
182
+ const deleteMutationContextId = 'directory-organizations-list:single-delete'
183
+ const { runMutation: runDeleteMutation, retryLastMutation: retryDeleteMutation } = useGuardedMutation<{
184
+ formId: string
185
+ resourceKind: string
186
+ resourceId: string
187
+ retryLastMutation: () => Promise<boolean>
188
+ }>({
189
+ contextId: deleteMutationContextId,
190
+ blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
191
+ })
192
+
180
193
  const handleDelete = React.useCallback(async (org: OrganizationRow) => {
181
194
  const confirmLabel = t('directory.organizations.list.confirmDelete', 'Archive organization "{{name}}"?', { name: org.name })
182
195
  const confirmed = await confirm({
@@ -187,22 +200,36 @@ export default function DirectoryOrganizationsPage() {
187
200
  if (!confirmed) return
188
201
 
189
202
  try {
190
- await withScopedApiRequestHeaders(
191
- buildOptimisticLockHeader(org.updatedAt),
192
- () => apiCallOrThrow(
193
- `/api/directory/organizations?id=${encodeURIComponent(org.id)}`,
194
- { method: 'DELETE' },
195
- { errorMessage: t('directory.organizations.list.error.delete', 'Failed to delete organization') },
203
+ await runDeleteMutation({
204
+ operation: () => withScopedApiRequestHeaders(
205
+ buildOptimisticLockHeader(org.updatedAt),
206
+ () => apiCallOrThrow(
207
+ `/api/directory/organizations?id=${encodeURIComponent(org.id)}`,
208
+ { method: 'DELETE' },
209
+ { errorMessage: t('directory.organizations.list.error.delete', 'Failed to delete organization') },
210
+ ),
196
211
  ),
197
- )
212
+ context: {
213
+ formId: deleteMutationContextId,
214
+ resourceKind: 'directory.organization',
215
+ resourceId: org.id,
216
+ retryLastMutation: retryDeleteMutation,
217
+ },
218
+ })
198
219
  await queryClient.invalidateQueries({ queryKey: ['directory-organizations'] })
199
220
  flash(t('directory.organizations.flash.deleted', 'Organization deleted'), 'success')
200
221
  } catch (err: unknown) {
222
+ // A stale delete surfaces the unified conflict bar (via the guarded
223
+ // mutation) — skip the generic error flash to avoid a double message.
224
+ if (surfaceRecordConflict(err, t)) {
225
+ await queryClient.invalidateQueries({ queryKey: ['directory-organizations'] })
226
+ return
227
+ }
201
228
  const fallback = t('directory.organizations.list.error.delete', 'Failed to delete organization')
202
229
  const message = err instanceof Error ? err.message : fallback
203
230
  flash(message, 'error')
204
231
  }
205
- }, [confirm, queryClient, t])
232
+ }, [confirm, deleteMutationContextId, queryClient, retryDeleteMutation, runDeleteMutation, t])
206
233
 
207
234
  return (
208
235
  <Page>
@@ -10,10 +10,11 @@ import { RowActions } from '@open-mercato/ui/backend/RowActions'
10
10
  import { BooleanIcon } from '@open-mercato/ui/backend/ValueIcons'
11
11
  import type { FilterValues } from '@open-mercato/ui/backend/FilterBar'
12
12
  import { Button } from '@open-mercato/ui/primitives/button'
13
- import { apiCall, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
13
+ import { apiCall, apiCallOrThrow, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
14
14
  import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
15
15
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
16
- import { raiseCrudError } from '@open-mercato/ui/backend/utils/serverErrors'
16
+ import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
17
+ import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
17
18
  import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
18
19
  import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
19
20
  import { useT } from '@open-mercato/shared/lib/i18n/context'
@@ -117,6 +118,17 @@ export default function DirectoryTenantsPage() {
117
118
  const total = data?.total ?? 0
118
119
  const totalPages = data?.totalPages ?? 0
119
120
 
121
+ const deleteMutationContextId = 'directory-tenants-list:single-delete'
122
+ const { runMutation: runDeleteMutation, retryLastMutation: retryDeleteMutation } = useGuardedMutation<{
123
+ formId: string
124
+ resourceKind: string
125
+ resourceId: string
126
+ retryLastMutation: () => Promise<boolean>
127
+ }>({
128
+ contextId: deleteMutationContextId,
129
+ blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
130
+ })
131
+
120
132
  const handleDelete = React.useCallback(async (tenant: TenantRow) => {
121
133
  const confirmMessage = t('directory.tenants.list.confirmDelete', 'Delete tenant "{{name}}"? This will archive it.').replace('{{name}}', tenant.name)
122
134
  const confirmed = await confirm({
@@ -127,23 +139,35 @@ export default function DirectoryTenantsPage() {
127
139
  if (!confirmed) return
128
140
 
129
141
  try {
130
- const call = await withScopedApiRequestHeaders(
131
- buildOptimisticLockHeader(tenant.updatedAt),
132
- () => apiCall(
133
- `/api/directory/tenants?id=${encodeURIComponent(tenant.id)}`,
134
- { method: 'DELETE' },
142
+ await runDeleteMutation({
143
+ operation: () => withScopedApiRequestHeaders(
144
+ buildOptimisticLockHeader(tenant.updatedAt),
145
+ () => apiCallOrThrow(
146
+ `/api/directory/tenants?id=${encodeURIComponent(tenant.id)}`,
147
+ { method: 'DELETE' },
148
+ { errorMessage: t('directory.tenants.list.error.delete', 'Failed to delete tenant') },
149
+ ),
135
150
  ),
136
- )
137
- if (!call.ok) {
138
- await raiseCrudError(call.response, t('directory.tenants.list.error.delete', 'Failed to delete tenant'))
139
- }
151
+ context: {
152
+ formId: deleteMutationContextId,
153
+ resourceKind: 'directory.tenant',
154
+ resourceId: tenant.id,
155
+ retryLastMutation: retryDeleteMutation,
156
+ },
157
+ })
140
158
  await queryClient.invalidateQueries({ queryKey: ['directory-tenants'] })
141
159
  flash(t('directory.tenants.list.success.delete', 'Tenant deleted'), 'success')
142
- } catch (err: any) {
160
+ } catch (err: unknown) {
161
+ // A stale delete surfaces the unified conflict bar (via the guarded
162
+ // mutation) — skip the generic error flash to avoid a double message.
163
+ if (surfaceRecordConflict(err, t)) {
164
+ await queryClient.invalidateQueries({ queryKey: ['directory-tenants'] })
165
+ return
166
+ }
143
167
  const message = err instanceof Error ? err.message : t('directory.tenants.list.error.delete', 'Failed to delete tenant')
144
168
  flash(message, 'error')
145
169
  }
146
- }, [confirm, queryClient, t])
170
+ }, [confirm, deleteMutationContextId, queryClient, retryDeleteMutation, runDeleteMutation, t])
147
171
 
148
172
  return (
149
173
  <Page>
@@ -8,6 +8,10 @@ import { z } from 'zod'
8
8
  import { invalidateDefinitionsCache } from './definitions.cache'
9
9
  import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
10
10
  import { mergeEntityFieldsetConfig, normalizeEntityFieldsetConfig } from '../lib/fieldsets'
11
+ import {
12
+ beginEntitiesMutationGuard,
13
+ FIELD_DEFINITION_RESOURCE_KIND,
14
+ } from './definitions.mutation-guard'
11
15
 
12
16
  export const metadata = {
13
17
  POST: { requireAuth: true, requireFeatures: ['entities.definitions.manage'] },
@@ -67,6 +71,17 @@ export async function POST(req: Request) {
67
71
  cache = resolve('cache') as CacheStrategy
68
72
  } catch {}
69
73
 
74
+ const guard = await beginEntitiesMutationGuard({
75
+ container,
76
+ auth,
77
+ req,
78
+ resourceKind: FIELD_DEFINITION_RESOURCE_KIND,
79
+ resourceId: entityId,
80
+ operation: 'custom',
81
+ mutationPayload: { entityId, definitionCount: definitions.length },
82
+ })
83
+ if (guard.blockedResponse) return guard.blockedResponse
84
+
70
85
  await em.begin()
71
86
  try {
72
87
  // Prefetch every existing definition for this entity in a single query, then index
@@ -134,6 +149,8 @@ export async function POST(req: Request) {
134
149
  return NextResponse.json({ error: 'Failed to save definitions batch' }, { status: 500 })
135
150
  }
136
151
 
152
+ await guard.runAfterSuccess()
153
+
137
154
  await invalidateDefinitionsCache(cache, {
138
155
  tenantId: auth.tenantId ?? null,
139
156
  organizationId: auth.orgId ?? null,
@@ -0,0 +1,80 @@
1
+ import { NextResponse } from 'next/server'
2
+ import type { AwilixContainer } from 'awilix'
3
+ import type { AuthContext } from '@open-mercato/shared/lib/auth/server'
4
+ import {
5
+ runCrudMutationGuardAfterSuccess,
6
+ validateCrudMutationGuard,
7
+ } from '@open-mercato/shared/lib/crud/mutation-guard'
8
+
9
+ export const ENTITY_DEFINITION_RESOURCE_KIND = 'entities.entity'
10
+ export const FIELD_DEFINITION_RESOURCE_KIND = 'entities.field_definition'
11
+
12
+ type AuthenticatedContext = NonNullable<AuthContext>
13
+
14
+ type GuardOperation = 'create' | 'update' | 'delete' | 'custom'
15
+
16
+ type EntitiesMutationGuardContext = {
17
+ container: AwilixContainer
18
+ auth: AuthenticatedContext
19
+ req: Request
20
+ resourceKind: string
21
+ resourceId: string
22
+ operation: GuardOperation
23
+ mutationPayload?: Record<string, unknown> | null
24
+ }
25
+
26
+ type EntitiesMutationGuardHandle = {
27
+ blockedResponse: NextResponse | null
28
+ runAfterSuccess: () => Promise<void>
29
+ }
30
+
31
+ function resolveActorUserId(auth: AuthenticatedContext): string {
32
+ return auth.userId ?? auth.sub
33
+ }
34
+
35
+ export async function beginEntitiesMutationGuard(
36
+ ctx: EntitiesMutationGuardContext,
37
+ ): Promise<EntitiesMutationGuardHandle> {
38
+ const tenantId = ctx.auth.tenantId
39
+ const userId = resolveActorUserId(ctx.auth)
40
+ if (!tenantId) {
41
+ return { blockedResponse: null, runAfterSuccess: async () => undefined }
42
+ }
43
+
44
+ const guardResult = await validateCrudMutationGuard(ctx.container, {
45
+ tenantId,
46
+ organizationId: ctx.auth.orgId ?? null,
47
+ userId,
48
+ resourceKind: ctx.resourceKind,
49
+ resourceId: ctx.resourceId,
50
+ operation: ctx.operation,
51
+ requestMethod: ctx.req.method,
52
+ requestHeaders: ctx.req.headers,
53
+ mutationPayload: ctx.mutationPayload ?? null,
54
+ })
55
+
56
+ if (guardResult && !guardResult.ok) {
57
+ return {
58
+ blockedResponse: NextResponse.json(guardResult.body, { status: guardResult.status }),
59
+ runAfterSuccess: async () => undefined,
60
+ }
61
+ }
62
+
63
+ return {
64
+ blockedResponse: null,
65
+ runAfterSuccess: async () => {
66
+ if (!guardResult?.ok || !guardResult.shouldRunAfterSuccess) return
67
+ await runCrudMutationGuardAfterSuccess(ctx.container, {
68
+ tenantId,
69
+ organizationId: ctx.auth.orgId ?? null,
70
+ userId,
71
+ resourceKind: ctx.resourceKind,
72
+ resourceId: ctx.resourceId,
73
+ operation: ctx.operation,
74
+ requestMethod: ctx.req.method,
75
+ requestHeaders: ctx.req.headers,
76
+ metadata: guardResult.metadata ?? null,
77
+ })
78
+ },
79
+ }
80
+ }
@@ -6,6 +6,10 @@ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
6
6
  import { CustomFieldDef } from '@open-mercato/core/modules/entities/data/entities'
7
7
  import { invalidateDefinitionsCache } from './definitions.cache'
8
8
  import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
9
+ import {
10
+ beginEntitiesMutationGuard,
11
+ FIELD_DEFINITION_RESOURCE_KIND,
12
+ } from './definitions.mutation-guard'
9
13
 
10
14
  export const metadata = {
11
15
  POST: { requireAuth: true, requireFeatures: ['entities.definitions.manage'] },
@@ -30,11 +34,24 @@ export async function POST(req: Request) {
30
34
  const where: any = { entityId, key, organizationId: auth.orgId ?? null, tenantId: auth.tenantId ?? null }
31
35
  const def = await em.findOne(CustomFieldDef, where)
32
36
  if (!def) return NextResponse.json({ error: 'Not found' }, { status: 404 })
37
+
38
+ const guard = await beginEntitiesMutationGuard({
39
+ container,
40
+ auth,
41
+ req,
42
+ resourceKind: FIELD_DEFINITION_RESOURCE_KIND,
43
+ resourceId: def.id,
44
+ operation: 'custom',
45
+ mutationPayload: { entityId, key },
46
+ })
47
+ if (guard.blockedResponse) return guard.blockedResponse
48
+
33
49
  ;(def as any).deletedAt = null
34
50
  ;(def as any).isActive = true
35
51
  ;(def as any).updatedAt = new Date()
36
52
  em.persist(def)
37
53
  await em.flush()
54
+ await guard.runAfterSuccess()
38
55
  await invalidateDefinitionsCache(cache, {
39
56
  tenantId: auth.tenantId ?? null,
40
57
  organizationId: auth.orgId ?? null,