@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.
Files changed (198) 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/directory/api/organization-branding/route.js +54 -2
  57. package/dist/modules/directory/api/organization-branding/route.js.map +2 -2
  58. package/dist/modules/directory/backend/directory/branding/page.js +15 -9
  59. package/dist/modules/directory/backend/directory/branding/page.js.map +2 -2
  60. package/dist/modules/directory/backend/directory/organizations/page.js +28 -9
  61. package/dist/modules/directory/backend/directory/organizations/page.js.map +2 -2
  62. package/dist/modules/directory/backend/directory/tenants/page.js +29 -13
  63. package/dist/modules/directory/backend/directory/tenants/page.js.map +2 -2
  64. package/dist/modules/entities/api/definitions.batch.js +15 -0
  65. package/dist/modules/entities/api/definitions.batch.js.map +2 -2
  66. package/dist/modules/entities/api/definitions.js +26 -0
  67. package/dist/modules/entities/api/definitions.js.map +2 -2
  68. package/dist/modules/entities/api/definitions.mutation-guard.js +57 -0
  69. package/dist/modules/entities/api/definitions.mutation-guard.js.map +7 -0
  70. package/dist/modules/entities/api/definitions.restore.js +15 -0
  71. package/dist/modules/entities/api/definitions.restore.js.map +2 -2
  72. package/dist/modules/entities/api/entities.js +31 -3
  73. package/dist/modules/entities/api/entities.js.map +2 -2
  74. package/dist/modules/entities/api/records.js +18 -0
  75. package/dist/modules/entities/api/records.js.map +2 -2
  76. package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js +28 -10
  77. package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js.map +2 -2
  78. package/dist/modules/feature_toggles/api/overrides/route.js +38 -1
  79. package/dist/modules/feature_toggles/api/overrides/route.js.map +2 -2
  80. package/dist/modules/inbox_ops/api/emails/[id]/route.js +30 -0
  81. package/dist/modules/inbox_ops/api/emails/[id]/route.js.map +2 -2
  82. package/dist/modules/inbox_ops/api/proposals/[id]/accept-all/route.js +30 -0
  83. package/dist/modules/inbox_ops/api/proposals/[id]/accept-all/route.js.map +2 -2
  84. package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.js +31 -0
  85. package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.js.map +2 -2
  86. package/dist/modules/inbox_ops/api/proposals/[id]/categorize/route.js +31 -0
  87. package/dist/modules/inbox_ops/api/proposals/[id]/categorize/route.js.map +2 -2
  88. package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js +68 -39
  89. package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js.map +2 -2
  90. package/dist/modules/notifications/frontend/NotificationSettingsPageClient.js +14 -4
  91. package/dist/modules/notifications/frontend/NotificationSettingsPageClient.js.map +2 -2
  92. package/dist/modules/payment_gateways/api/cancel/route.js +37 -0
  93. package/dist/modules/payment_gateways/api/cancel/route.js.map +2 -2
  94. package/dist/modules/payment_gateways/api/capture/route.js +37 -0
  95. package/dist/modules/payment_gateways/api/capture/route.js.map +2 -2
  96. package/dist/modules/payment_gateways/api/guards.js +31 -0
  97. package/dist/modules/payment_gateways/api/guards.js.map +7 -0
  98. package/dist/modules/payment_gateways/api/refund/route.js +37 -0
  99. package/dist/modules/payment_gateways/api/refund/route.js.map +2 -2
  100. package/dist/modules/payment_gateways/api/sessions/route.js +37 -0
  101. package/dist/modules/payment_gateways/api/sessions/route.js.map +2 -2
  102. package/dist/modules/planner/api/availability-date-specific.js +11 -1
  103. package/dist/modules/planner/api/availability-date-specific.js.map +2 -2
  104. package/dist/modules/planner/backend/planner/availability-rulesets/page.js +20 -3
  105. package/dist/modules/planner/backend/planner/availability-rulesets/page.js.map +2 -2
  106. package/dist/modules/planner/commands/availability-date-specific.js +16 -0
  107. package/dist/modules/planner/commands/availability-date-specific.js.map +2 -2
  108. package/dist/modules/planner/components/AvailabilityRulesEditor.js +109 -40
  109. package/dist/modules/planner/components/AvailabilityRulesEditor.js.map +2 -2
  110. package/dist/modules/query_index/api/purge.js +35 -3
  111. package/dist/modules/query_index/api/purge.js.map +2 -2
  112. package/dist/modules/query_index/api/reindex.js +41 -3
  113. package/dist/modules/query_index/api/reindex.js.map +2 -2
  114. package/dist/modules/query_index/components/QueryIndexesTable.js +57 -24
  115. package/dist/modules/query_index/components/QueryIndexesTable.js.map +2 -2
  116. package/dist/modules/shipping_carriers/api/shipments/route.js +31 -0
  117. package/dist/modules/shipping_carriers/api/shipments/route.js.map +2 -2
  118. package/dist/modules/shipping_carriers/api/tracking/refresh/route.js +55 -0
  119. package/dist/modules/shipping_carriers/api/tracking/refresh/route.js.map +7 -0
  120. package/dist/modules/shipping_carriers/data/validators.js +6 -1
  121. package/dist/modules/shipping_carriers/data/validators.js.map +2 -2
  122. package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.js +28 -8
  123. package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.js.map +2 -2
  124. package/dist/modules/shipping_carriers/lib/shipping-service.js +37 -7
  125. package/dist/modules/shipping_carriers/lib/shipping-service.js.map +2 -2
  126. package/dist/modules/shipping_carriers/workers/status-poller.js +1 -1
  127. package/dist/modules/shipping_carriers/workers/status-poller.js.map +2 -2
  128. package/dist/modules/translations/components/TranslationManager.js +49 -20
  129. package/dist/modules/translations/components/TranslationManager.js.map +2 -2
  130. package/package.json +7 -7
  131. package/src/modules/currencies/backend/currencies/[id]/page.tsx +40 -10
  132. package/src/modules/currencies/backend/currencies/page.tsx +68 -29
  133. package/src/modules/currencies/backend/exchange-rates/page.tsx +40 -15
  134. package/src/modules/currencies/components/CurrencyFetchingConfig.tsx +110 -41
  135. package/src/modules/customer_accounts/api/admin/roles/[id]/acl.ts +69 -7
  136. package/src/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.tsx +16 -8
  137. package/src/modules/customer_accounts/backend/customer_accounts/roles/page.tsx +32 -14
  138. package/src/modules/customer_accounts/widgets/injection/account-status/widget.client.tsx +32 -20
  139. package/src/modules/customers/backend/config/customers/pipeline-stages/page.tsx +171 -64
  140. package/src/modules/customers/components/AddressFormatSettings.tsx +39 -19
  141. package/src/modules/customers/components/DictionarySettings.tsx +63 -29
  142. package/src/modules/customers/components/PipelineSettings.tsx +165 -80
  143. package/src/modules/customers/components/calendar/AgendaList.tsx +13 -8
  144. package/src/modules/customers/components/calendar/CalendarSettingsModal.tsx +16 -2
  145. package/src/modules/customers/components/calendar/MonthGrid.tsx +5 -2
  146. package/src/modules/customers/components/calendar/editor/PeopleField.tsx +6 -0
  147. package/src/modules/customers/components/calendar/editor/RelatedToField.tsx +2 -0
  148. package/src/modules/customers/components/calendar/editor/ScheduleSection.tsx +7 -0
  149. package/src/modules/customers/i18n/de.json +11 -7
  150. package/src/modules/customers/i18n/en.json +6 -2
  151. package/src/modules/customers/i18n/es.json +11 -7
  152. package/src/modules/customers/i18n/pl.json +11 -7
  153. package/src/modules/customers/lib/calendar/labels.ts +42 -0
  154. package/src/modules/dashboards/components/WidgetVisibilityEditor.tsx +39 -11
  155. package/src/modules/data_sync/api/mappings/[id]/route.ts +63 -0
  156. package/src/modules/data_sync/api/mappings/route.ts +48 -0
  157. package/src/modules/data_sync/api/run.ts +33 -0
  158. package/src/modules/data_sync/api/runs/[id]/cancel.ts +34 -0
  159. package/src/modules/data_sync/api/runs/[id]/retry.ts +33 -0
  160. package/src/modules/data_sync/api/schedules/[id]/route.ts +64 -2
  161. package/src/modules/data_sync/api/schedules/route.ts +36 -4
  162. package/src/modules/directory/api/organization-branding/route.ts +61 -0
  163. package/src/modules/directory/backend/directory/branding/page.tsx +16 -10
  164. package/src/modules/directory/backend/directory/organizations/page.tsx +35 -8
  165. package/src/modules/directory/backend/directory/tenants/page.tsx +37 -13
  166. package/src/modules/entities/api/definitions.batch.ts +17 -0
  167. package/src/modules/entities/api/definitions.mutation-guard.ts +80 -0
  168. package/src/modules/entities/api/definitions.restore.ts +17 -0
  169. package/src/modules/entities/api/definitions.ts +30 -0
  170. package/src/modules/entities/api/entities.ts +35 -3
  171. package/src/modules/entities/api/records.ts +20 -0
  172. package/src/modules/entities/backend/entities/user/[entityId]/records/page.tsx +33 -10
  173. package/src/modules/feature_toggles/api/overrides/route.ts +44 -1
  174. package/src/modules/inbox_ops/api/emails/[id]/route.ts +32 -0
  175. package/src/modules/inbox_ops/api/proposals/[id]/accept-all/route.ts +32 -0
  176. package/src/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.ts +33 -0
  177. package/src/modules/inbox_ops/api/proposals/[id]/categorize/route.ts +33 -0
  178. package/src/modules/messages/components/message-detail/hooks/useMessageDetailsActions.ts +80 -42
  179. package/src/modules/notifications/frontend/NotificationSettingsPageClient.tsx +21 -4
  180. package/src/modules/payment_gateways/api/cancel/route.ts +39 -0
  181. package/src/modules/payment_gateways/api/capture/route.ts +39 -0
  182. package/src/modules/payment_gateways/api/guards.ts +59 -0
  183. package/src/modules/payment_gateways/api/refund/route.ts +39 -0
  184. package/src/modules/payment_gateways/api/sessions/route.ts +40 -0
  185. package/src/modules/planner/api/availability-date-specific.ts +10 -0
  186. package/src/modules/planner/backend/planner/availability-rulesets/page.tsx +26 -5
  187. package/src/modules/planner/commands/availability-date-specific.ts +24 -0
  188. package/src/modules/planner/components/AvailabilityRulesEditor.tsx +122 -41
  189. package/src/modules/query_index/api/purge.ts +37 -3
  190. package/src/modules/query_index/api/reindex.ts +43 -3
  191. package/src/modules/query_index/components/QueryIndexesTable.tsx +66 -24
  192. package/src/modules/shipping_carriers/api/shipments/route.ts +31 -0
  193. package/src/modules/shipping_carriers/api/tracking/refresh/route.ts +53 -0
  194. package/src/modules/shipping_carriers/data/validators.ts +5 -0
  195. package/src/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.ts +29 -8
  196. package/src/modules/shipping_carriers/lib/shipping-service.ts +43 -7
  197. package/src/modules/shipping_carriers/workers/status-poller.ts +1 -1
  198. 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)) {
@@ -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,
@@ -20,6 +20,10 @@ import { installCustomEntitiesFromModules } from '../lib/install-from-ce'
20
20
  import { normalizeCustomFieldOptions } from '@open-mercato/shared/modules/entities/options'
21
21
  import { CURRENCY_OPTIONS_URL } from '@open-mercato/shared/modules/entities/kinds'
22
22
  import type { EntityManager } from '@mikro-orm/postgresql'
23
+ import {
24
+ beginEntitiesMutationGuard,
25
+ FIELD_DEFINITION_RESOURCE_KIND,
26
+ } from './definitions.mutation-guard'
23
27
 
24
28
  /**
25
29
  * Validate defaultValue against the field kind. Returns an error message string
@@ -473,6 +477,18 @@ export async function POST(req: Request) {
473
477
 
474
478
  const where: any = { entityId: input.entityId, key: input.key, organizationId: auth.orgId ?? null, tenantId: auth.tenantId ?? null }
475
479
  let def = await em.findOne(CustomFieldDef, where)
480
+
481
+ const guard = await beginEntitiesMutationGuard({
482
+ container,
483
+ auth,
484
+ req,
485
+ resourceKind: FIELD_DEFINITION_RESOURCE_KIND,
486
+ resourceId: def ? def.id : `${input.entityId}:${input.key}`,
487
+ operation: def ? 'update' : 'create',
488
+ mutationPayload: input as unknown as Record<string, unknown>,
489
+ })
490
+ if (guard.blockedResponse) return guard.blockedResponse
491
+
476
492
  if (!def) def = em.create(CustomFieldDef, { ...where, createdAt: new Date() })
477
493
  def.kind = input.kind
478
494
  const inCfg = (input as any).configJson ?? {}
@@ -508,6 +524,7 @@ export async function POST(req: Request) {
508
524
  def.updatedAt = new Date()
509
525
  em.persist(def)
510
526
  await em.flush()
527
+ await guard.runAfterSuccess()
511
528
  await invalidateDefinitionsCache(cache, {
512
529
  tenantId: auth.tenantId ?? null,
513
530
  organizationId: auth.orgId ?? null,
@@ -535,11 +552,24 @@ export async function DELETE(req: Request) {
535
552
  const where: any = { entityId, key, organizationId: auth.orgId ?? null, tenantId: auth.tenantId ?? null }
536
553
  const def = await em.findOne(CustomFieldDef, where)
537
554
  if (!def) return NextResponse.json({ error: 'Not found' }, { status: 404 })
555
+
556
+ const guard = await beginEntitiesMutationGuard({
557
+ container,
558
+ auth,
559
+ req,
560
+ resourceKind: FIELD_DEFINITION_RESOURCE_KIND,
561
+ resourceId: def.id,
562
+ operation: 'delete',
563
+ mutationPayload: { entityId, key },
564
+ })
565
+ if (guard.blockedResponse) return guard.blockedResponse
566
+
538
567
  def.isActive = false
539
568
  def.updatedAt = new Date()
540
569
  def.deletedAt = def.deletedAt ?? new Date()
541
570
  em.persist(def)
542
571
  await em.flush()
572
+ await guard.runAfterSuccess()
543
573
  await invalidateDefinitionsCache(cache, {
544
574
  tenantId: auth.tenantId ?? null,
545
575
  organizationId: auth.orgId ?? null,
@@ -10,8 +10,12 @@ import { isSystemEntitySelectable } from '@open-mercato/shared/lib/entities/syst
10
10
  import { SYSTEM_ENTITY_RECORDS_BLOCKED_CODE, isOrmBackedSystemEntityId } from '@open-mercato/shared/lib/data/engine'
11
11
  import { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
12
12
  import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
13
+ import {
14
+ beginEntitiesMutationGuard,
15
+ ENTITY_DEFINITION_RESOURCE_KIND,
16
+ } from './definitions.mutation-guard'
13
17
 
14
- const CUSTOM_ENTITY_DEFINITION_RESOURCE_KIND = 'entities.entity'
18
+ const CUSTOM_ENTITY_DEFINITION_RESOURCE_KIND = ENTITY_DEFINITION_RESOURCE_KIND
15
19
 
16
20
  export const metadata = {
17
21
  GET: { requireAuth: true },
@@ -110,7 +114,8 @@ export async function POST(req: Request) {
110
114
  }
111
115
  const input = parsed.data
112
116
 
113
- const { resolve } = await createRequestContainer()
117
+ const container = await createRequestContainer()
118
+ const { resolve } = container
114
119
  const em = resolve('em') as any
115
120
 
116
121
  // A registration for a module-declared, table-backed system entity would flip
@@ -140,6 +145,18 @@ export async function POST(req: Request) {
140
145
  throw lockError
141
146
  }
142
147
  }
148
+
149
+ const guard = await beginEntitiesMutationGuard({
150
+ container,
151
+ auth,
152
+ req,
153
+ resourceKind: ENTITY_DEFINITION_RESOURCE_KIND,
154
+ resourceId: ent ? ent.id : input.entityId,
155
+ operation: ent ? 'update' : 'create',
156
+ mutationPayload: input as unknown as Record<string, unknown>,
157
+ })
158
+ if (guard.blockedResponse) return guard.blockedResponse
159
+
143
160
  if (!ent) ent = em.create(CustomEntity, { ...where, createdAt: new Date() })
144
161
  ent.label = input.label
145
162
  ent.description = input.description ?? null
@@ -150,6 +167,7 @@ export async function POST(req: Request) {
150
167
  ent.updatedAt = new Date()
151
168
  em.persist(ent)
152
169
  await em.flush()
170
+ await guard.runAfterSuccess()
153
171
  // Invalidate sidebar/nav cache for tenant scope (also when tenantId is null)
154
172
  try {
155
173
  const cache = (await createRequestContainer()).resolve('cache') as any
@@ -168,17 +186,31 @@ export async function DELETE(req: Request) {
168
186
  const entityId = body?.entityId
169
187
  if (!entityId) return NextResponse.json({ error: 'entityId is required' }, { status: 400 })
170
188
 
171
- const { resolve } = await createRequestContainer()
189
+ const container = await createRequestContainer()
190
+ const { resolve } = container
172
191
  const em = resolve('em') as any
173
192
 
174
193
  const where: any = { entityId, organizationId: auth.orgId ?? null, tenantId: auth.tenantId ?? null }
175
194
  const ent = await em.findOne(CustomEntity, where)
176
195
  if (!ent) return NextResponse.json({ error: 'Not found' }, { status: 404 })
196
+
197
+ const guard = await beginEntitiesMutationGuard({
198
+ container,
199
+ auth,
200
+ req,
201
+ resourceKind: ENTITY_DEFINITION_RESOURCE_KIND,
202
+ resourceId: ent.id,
203
+ operation: 'delete',
204
+ mutationPayload: { entityId },
205
+ })
206
+ if (guard.blockedResponse) return guard.blockedResponse
207
+
177
208
  ent.isActive = false
178
209
  ent.updatedAt = new Date()
179
210
  ent.deletedAt = ent.deletedAt ?? new Date()
180
211
  em.persist(ent)
181
212
  await em.flush()
213
+ await guard.runAfterSuccess()
182
214
  // Invalidate sidebar/nav cache for tenant scope (also when tenantId is null)
183
215
  try {
184
216
  const cache = (await createRequestContainer()).resolve('cache') as any