@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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/feature_toggles/api/overrides/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { Tenant } from '@open-mercato/core/modules/directory/data/entities'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport { getOverrides } from '../../lib/queries'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { logCrudAccess } from '@open-mercato/shared/lib/crud/factory'\nimport { FeatureToggleOverride } from '../../data/entities'\nimport { buildContext } from '../../lib/utils'\nimport { resolveFeatureCheckContext } from \"@open-mercato/core/modules/directory/utils/organizationScope\"\nimport { ProcessedChangeOverrideStateInput } from '../../data/validators'\nimport {\n changeOverrideStateBaseSchema,\n overrideListQuerySchema,\n featureTogglesTag,\n featureToggleOverrideListResponseSchema,\n featureToggleErrorSchema,\n changeOverrideStateResponseSchema,\n} from '../openapi'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\n\nexport async function GET(req: Request) {\n const { auth, ctx, scope } = await buildContext(req)\n\n const url = new URL(req.url)\n const parsed = overrideListQuerySchema.safeParse(Object.fromEntries(url.searchParams.entries()))\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid query parameters' }, { status: 400 })\n }\n const em = ctx.container.resolve('em') as EntityManager\n\n const tenant = await em.findOne(Tenant, {\n id: scope.tenantId,\n })\n\n if (!tenant) {\n return NextResponse.json({\n error: 'Tenant context required. Please select a tenant.'\n }, { status: 400 })\n }\n\n const result = await getOverrides(em, tenant, parsed.data)\n\n await logCrudAccess({\n container: ctx.container,\n auth,\n request: req,\n items: [...result.raw.toggles, ...result.raw.overrides],\n idField: 'id',\n resourceKind: 'feature_toggles.override_list',\n tenantId: scope.tenantId ?? null,\n query: parsed.data,\n accessType: 'read:list',\n fields: ['id', 'toggleId', 'tenantId', 'identifier', 'name', 'category']\n })\n\n return NextResponse.json({\n items: result.items,\n total: result.total,\n totalPages: result.totalPages,\n page: result.page,\n pageSize: result.pageSize,\n isSuperAdmin: auth.isSuperAdmin ?? false\n })\n}\n\nexport async function PUT(req: Request) {\n try {\n const { ctx } = await buildContext(req)\n\n const parsed = changeOverrideStateBaseSchema.safeParse(await req.json())\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload', details: parsed.error.flatten() }, { status: 400 })\n }\n\n const { scope } = await resolveFeatureCheckContext({\n container: ctx.container,\n auth: ctx.auth,\n request: req\n })\n\n if (!scope.tenantId) {\n return NextResponse.json({\n error: 'Tenant context required. Please select a tenant.'\n }, { status: 400 })\n }\n\n // Optimistic lock: refuse a stale override overwrite when two admins edit the\n // same global toggle override in parallel. Only enforced when an override row\n // already exists (first set has no prior version). Strictly additive.\n const em = ctx.container.resolve('em') as EntityManager\n const existingOverride = await em.findOne(FeatureToggleOverride, {\n tenantId: scope.tenantId,\n toggle: { id: parsed.data.toggleId },\n })\n if (existingOverride) {\n enforceCommandOptimisticLock({\n resourceKind: 'feature_toggles.feature_toggle_override',\n resourceId: existingOverride.id,\n current: existingOverride.updatedAt ?? null,\n request: req,\n })\n }\n\n const commandBus = ctx.container.resolve('commandBus') as CommandBus\n const { result, logEntry } = await commandBus.execute<ProcessedChangeOverrideStateInput, { overrideToggleId: string | null }>('feature_toggles.overrides.changeState', {\n input: {\n toggleId: parsed.data.toggleId,\n tenantId: scope.tenantId,\n isOverride: parsed.data.isOverride,\n overrideValue: parsed.data.overrideValue,\n },\n ctx,\n })\n\n const response = NextResponse.json({\n ok: true,\n overrideToggleId: result?.overrideToggleId ?? null,\n })\n\n if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {\n response.headers.set(\n 'x-om-operation',\n serializeOperationMetadata({\n id: logEntry.id,\n undoToken: logEntry.undoToken,\n commandId: logEntry.commandId,\n actionLabel: logEntry.actionLabel ?? null,\n resourceKind: logEntry.resourceKind ?? 'feature_toggles.override',\n resourceId: logEntry.resourceId ?? result?.overrideToggleId ?? null,\n executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : undefined,\n })\n )\n }\n\n return response\n } catch (error) {\n if (isCrudHttpError(error)) {\n return NextResponse.json(error.body, { status: error.status })\n }\n const message = error instanceof Error ? error.message : 'Unknown error'\n if (message === 'NOT_FOUND') return NextResponse.json({ error: 'Not found' }, { status: 404 })\n if (message === 'INVALID_STATE' || message === 'INVALID_TOGGLE_ID' || message === 'INVALID_TENANT_ID') {\n return NextResponse.json({ error: message }, { status: 400 })\n }\n console.error('feature_toggles.overrides.changeState failed', error)\n return NextResponse.json({ error: 'Failed to update override' }, { status: 500 })\n }\n}\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['feature_toggles.view'] },\n PUT: { requireAuth: true, requireFeatures: ['feature_toggles.manage'] },\n}\n\nexport const metadata = routeMetadata\n\nconst overrideGetDoc: OpenApiMethodDoc = {\n summary: 'List overrides',\n description: 'Returns list of feature toggle overrides.',\n tags: [featureTogglesTag],\n query: overrideListQuerySchema,\n responses: [\n { status: 200, description: 'List of overrides', schema: featureToggleOverrideListResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid query parameters', schema: featureToggleErrorSchema },\n { status: 401, description: 'Authentication required', schema: featureToggleErrorSchema },\n ],\n}\n\nconst overridePutDoc: OpenApiMethodDoc = {\n summary: 'Change override state',\n description: 'Enable, disable or inherit a feature toggle for a specific tenant.',\n tags: [featureTogglesTag],\n requestBody: {\n contentType: 'application/json',\n schema: changeOverrideStateBaseSchema,\n description: 'Override details.',\n },\n responses: [\n { status: 200, description: 'Override updated', schema: changeOverrideStateResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Validation failed', schema: featureToggleErrorSchema },\n { status: 401, description: 'Authentication required', schema: featureToggleErrorSchema },\n { status: 404, description: 'Not found', schema: featureToggleErrorSchema },\n { status: 500, description: 'Internal server error', schema: featureToggleErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: featureTogglesTag,\n summary: 'Manage feature toggle overrides',\n methods: {\n GET: overrideGetDoc,\n PUT: overridePutDoc,\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { Tenant } from '@open-mercato/core/modules/directory/data/entities'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport { getOverrides } from '../../lib/queries'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { logCrudAccess } from '@open-mercato/shared/lib/crud/factory'\nimport { FeatureToggleOverride } from '../../data/entities'\nimport { buildContext } from '../../lib/utils'\nimport { resolveFeatureCheckContext } from \"@open-mercato/core/modules/directory/utils/organizationScope\"\nimport { ProcessedChangeOverrideStateInput } from '../../data/validators'\nimport {\n changeOverrideStateBaseSchema,\n overrideListQuerySchema,\n featureTogglesTag,\n featureToggleOverrideListResponseSchema,\n featureToggleErrorSchema,\n changeOverrideStateResponseSchema,\n} from '../openapi'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\n\nexport async function GET(req: Request) {\n const { auth, ctx, scope } = await buildContext(req)\n\n const url = new URL(req.url)\n const parsed = overrideListQuerySchema.safeParse(Object.fromEntries(url.searchParams.entries()))\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid query parameters' }, { status: 400 })\n }\n const em = ctx.container.resolve('em') as EntityManager\n\n const tenant = await em.findOne(Tenant, {\n id: scope.tenantId,\n })\n\n if (!tenant) {\n return NextResponse.json({\n error: 'Tenant context required. Please select a tenant.'\n }, { status: 400 })\n }\n\n const result = await getOverrides(em, tenant, parsed.data)\n\n await logCrudAccess({\n container: ctx.container,\n auth,\n request: req,\n items: [...result.raw.toggles, ...result.raw.overrides],\n idField: 'id',\n resourceKind: 'feature_toggles.override_list',\n tenantId: scope.tenantId ?? null,\n query: parsed.data,\n accessType: 'read:list',\n fields: ['id', 'toggleId', 'tenantId', 'identifier', 'name', 'category']\n })\n\n return NextResponse.json({\n items: result.items,\n total: result.total,\n totalPages: result.totalPages,\n page: result.page,\n pageSize: result.pageSize,\n isSuperAdmin: auth.isSuperAdmin ?? false\n })\n}\n\nexport async function PUT(req: Request) {\n try {\n const { ctx } = await buildContext(req)\n\n const parsed = changeOverrideStateBaseSchema.safeParse(await req.json())\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload', details: parsed.error.flatten() }, { status: 400 })\n }\n\n const { scope, organizationId } = await resolveFeatureCheckContext({\n container: ctx.container,\n auth: ctx.auth,\n request: req\n })\n\n if (!scope.tenantId) {\n return NextResponse.json({\n error: 'Tenant context required. Please select a tenant.'\n }, { status: 400 })\n }\n\n // Optimistic lock: refuse a stale override overwrite when two admins edit the\n // same global toggle override in parallel. Only enforced when an override row\n // already exists (first set has no prior version). Strictly additive.\n const em = ctx.container.resolve('em') as EntityManager\n const existingOverride = await em.findOne(FeatureToggleOverride, {\n tenantId: scope.tenantId,\n toggle: { id: parsed.data.toggleId },\n })\n if (existingOverride) {\n enforceCommandOptimisticLock({\n resourceKind: 'feature_toggles.feature_toggle_override',\n resourceId: existingOverride.id,\n current: existingOverride.updatedAt ?? null,\n request: req,\n })\n }\n\n // Run the CRUD mutation guard lifecycle so this custom write route honors\n // module-level guard injections (and after-success hooks) like every other\n // mutating endpoint. The override either updates an existing row or creates a\n // new one, so the resource id falls back to the toggle id when no row exists.\n const guardUserId = ctx.auth?.userId ?? ctx.auth?.sub ?? null\n if (!guardUserId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const guardResourceKind = 'feature_toggles.feature_toggle_override'\n const guardResourceId = existingOverride?.id ?? parsed.data.toggleId\n const guardResult = await validateCrudMutationGuard(ctx.container, {\n tenantId: scope.tenantId,\n organizationId: organizationId ?? null,\n userId: guardUserId,\n resourceKind: guardResourceKind,\n resourceId: guardResourceId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: parsed.data,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n const commandBus = ctx.container.resolve('commandBus') as CommandBus\n const { result, logEntry } = await commandBus.execute<ProcessedChangeOverrideStateInput, { overrideToggleId: string | null }>('feature_toggles.overrides.changeState', {\n input: {\n toggleId: parsed.data.toggleId,\n tenantId: scope.tenantId,\n isOverride: parsed.data.isOverride,\n overrideValue: parsed.data.overrideValue,\n },\n ctx,\n })\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(ctx.container, {\n tenantId: scope.tenantId,\n organizationId: organizationId ?? null,\n userId: guardUserId,\n resourceKind: guardResourceKind,\n resourceId: result?.overrideToggleId ?? guardResourceId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n const response = NextResponse.json({\n ok: true,\n overrideToggleId: result?.overrideToggleId ?? null,\n })\n\n if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {\n response.headers.set(\n 'x-om-operation',\n serializeOperationMetadata({\n id: logEntry.id,\n undoToken: logEntry.undoToken,\n commandId: logEntry.commandId,\n actionLabel: logEntry.actionLabel ?? null,\n resourceKind: logEntry.resourceKind ?? 'feature_toggles.override',\n resourceId: logEntry.resourceId ?? result?.overrideToggleId ?? null,\n executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : undefined,\n })\n )\n }\n\n return response\n } catch (error) {\n if (isCrudHttpError(error)) {\n return NextResponse.json(error.body, { status: error.status })\n }\n const message = error instanceof Error ? error.message : 'Unknown error'\n if (message === 'NOT_FOUND') return NextResponse.json({ error: 'Not found' }, { status: 404 })\n if (message === 'INVALID_STATE' || message === 'INVALID_TOGGLE_ID' || message === 'INVALID_TENANT_ID') {\n return NextResponse.json({ error: message }, { status: 400 })\n }\n console.error('feature_toggles.overrides.changeState failed', error)\n return NextResponse.json({ error: 'Failed to update override' }, { status: 500 })\n }\n}\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['feature_toggles.view'] },\n PUT: { requireAuth: true, requireFeatures: ['feature_toggles.manage'] },\n}\n\nexport const metadata = routeMetadata\n\nconst overrideGetDoc: OpenApiMethodDoc = {\n summary: 'List overrides',\n description: 'Returns list of feature toggle overrides.',\n tags: [featureTogglesTag],\n query: overrideListQuerySchema,\n responses: [\n { status: 200, description: 'List of overrides', schema: featureToggleOverrideListResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid query parameters', schema: featureToggleErrorSchema },\n { status: 401, description: 'Authentication required', schema: featureToggleErrorSchema },\n ],\n}\n\nconst overridePutDoc: OpenApiMethodDoc = {\n summary: 'Change override state',\n description: 'Enable, disable or inherit a feature toggle for a specific tenant.',\n tags: [featureTogglesTag],\n requestBody: {\n contentType: 'application/json',\n schema: changeOverrideStateBaseSchema,\n description: 'Override details.',\n },\n responses: [\n { status: 200, description: 'Override updated', schema: changeOverrideStateResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Validation failed', schema: featureToggleErrorSchema },\n { status: 401, description: 'Authentication required', schema: featureToggleErrorSchema },\n { status: 404, description: 'Not found', schema: featureToggleErrorSchema },\n { status: 500, description: 'Internal server error', schema: featureToggleErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: featureTogglesTag,\n summary: 'Manage feature toggle overrides',\n methods: {\n GET: overrideGetDoc,\n PUT: overridePutDoc,\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,5 +1,9 @@
1
1
  import { NextResponse } from "next/server";
2
2
  import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
3
+ import {
4
+ runCrudMutationGuardAfterSuccess,
5
+ validateCrudMutationGuard
6
+ } from "@open-mercato/shared/lib/crud/mutation-guard";
3
7
  import { InboxEmail } from "../../../data/entities.js";
4
8
  import {
5
9
  resolveRequestContext,
@@ -50,6 +54,19 @@ async function DELETE(req) {
50
54
  return NextResponse.json({ error: "Missing email ID" }, { status: 400 });
51
55
  }
52
56
  const ctx = await resolveRequestContext(req);
57
+ const guardResult = await validateCrudMutationGuard(ctx.container, {
58
+ tenantId: ctx.tenantId,
59
+ organizationId: ctx.organizationId,
60
+ userId: ctx.userId,
61
+ resourceKind: "inbox_ops:inbox_email",
62
+ resourceId: id,
63
+ operation: "delete",
64
+ requestMethod: req.method,
65
+ requestHeaders: req.headers
66
+ });
67
+ if (guardResult && !guardResult.ok) {
68
+ return NextResponse.json(guardResult.body, { status: guardResult.status });
69
+ }
53
70
  const updated = await ctx.em.nativeUpdate(
54
71
  InboxEmail,
55
72
  {
@@ -63,6 +80,19 @@ async function DELETE(req) {
63
80
  if (updated === 0) {
64
81
  return NextResponse.json({ error: "Email not found" }, { status: 404 });
65
82
  }
83
+ if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
84
+ await runCrudMutationGuardAfterSuccess(ctx.container, {
85
+ tenantId: ctx.tenantId,
86
+ organizationId: ctx.organizationId,
87
+ userId: ctx.userId,
88
+ resourceKind: "inbox_ops:inbox_email",
89
+ resourceId: id,
90
+ operation: "delete",
91
+ requestMethod: req.method,
92
+ requestHeaders: req.headers,
93
+ metadata: guardResult.metadata ?? null
94
+ });
95
+ }
66
96
  return NextResponse.json({ ok: true });
67
97
  } catch (err) {
68
98
  if (err instanceof UnauthorizedError) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/inbox_ops/api/emails/%5Bid%5D/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { InboxEmail } from '../../../data/entities'\nimport {\n resolveRequestContext,\n extractPathSegment,\n UnauthorizedError,\n} from '../../routeHelpers'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['inbox_ops.log.view'] },\n DELETE: { requireAuth: true, requireFeatures: ['inbox_ops.proposals.manage'] },\n}\n\nexport async function GET(req: Request) {\n try {\n const url = new URL(req.url)\n const id = extractPathSegment(url, 'emails')\n\n if (!id) {\n return NextResponse.json({ error: 'Missing email ID' }, { status: 400 })\n }\n\n const ctx = await resolveRequestContext(req)\n\n const email = await findOneWithDecryption(\n ctx.em,\n InboxEmail,\n {\n id,\n organizationId: ctx.organizationId,\n tenantId: ctx.tenantId,\n deletedAt: null,\n },\n undefined,\n ctx.scope,\n )\n\n if (!email) {\n return NextResponse.json({ error: 'Email not found' }, { status: 404 })\n }\n\n return NextResponse.json({ email })\n } catch (err) {\n if (err instanceof UnauthorizedError) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n console.error('[inbox_ops:emails:detail] Error:', err)\n return NextResponse.json({ error: 'Failed to load email' }, { status: 500 })\n }\n}\n\nexport async function DELETE(req: Request) {\n try {\n const url = new URL(req.url)\n const id = extractPathSegment(url, 'emails')\n\n if (!id) {\n return NextResponse.json({ error: 'Missing email ID' }, { status: 400 })\n }\n\n const ctx = await resolveRequestContext(req)\n\n const updated = await ctx.em.nativeUpdate(\n InboxEmail,\n {\n id,\n organizationId: ctx.organizationId,\n tenantId: ctx.tenantId,\n deletedAt: null,\n },\n { deletedAt: new Date() },\n )\n\n if (updated === 0) {\n return NextResponse.json({ error: 'Email not found' }, { status: 404 })\n }\n\n return NextResponse.json({ ok: true })\n } catch (err) {\n if (err instanceof UnauthorizedError) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n console.error('[inbox_ops:emails:delete] Error:', err)\n return NextResponse.json({ error: 'Failed to delete email' }, { status: 500 })\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'InboxOps',\n summary: 'Email detail',\n methods: {\n GET: {\n summary: 'Get email detail with parsed thread',\n responses: [\n { status: 200, description: 'Email detail' },\n { status: 404, description: 'Email not found' },\n ],\n },\n DELETE: {\n summary: 'Soft-delete an inbox email',\n responses: [\n { status: 200, description: 'Email deleted' },\n { status: 404, description: 'Email not found' },\n ],\n },\n },\n}\n"],
5
- "mappings": "AAAA,SAAS,oBAAoB;AAE7B,SAAS,6BAA6B;AACtC,SAAS,kBAAkB;AAC3B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEA,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,oBAAoB,EAAE;AAAA,EAClE,QAAQ,EAAE,aAAa,MAAM,iBAAiB,CAAC,4BAA4B,EAAE;AAC/E;AAEA,eAAsB,IAAI,KAAc;AACtC,MAAI;AACF,UAAM,MAAM,IAAI,IAAI,IAAI,GAAG;AAC3B,UAAM,KAAK,mBAAmB,KAAK,QAAQ;AAE3C,QAAI,CAAC,IAAI;AACP,aAAO,aAAa,KAAK,EAAE,OAAO,mBAAmB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACzE;AAEA,UAAM,MAAM,MAAM,sBAAsB,GAAG;AAE3C,UAAM,QAAQ,MAAM;AAAA,MAClB,IAAI;AAAA,MACJ;AAAA,MACA;AAAA,QACE;AAAA,QACA,gBAAgB,IAAI;AAAA,QACpB,UAAU,IAAI;AAAA,QACd,WAAW;AAAA,MACb;AAAA,MACA;AAAA,MACA,IAAI;AAAA,IACN;AAEA,QAAI,CAAC,OAAO;AACV,aAAO,aAAa,KAAK,EAAE,OAAO,kBAAkB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACxE;AAEA,WAAO,aAAa,KAAK,EAAE,MAAM,CAAC;AAAA,EACpC,SAAS,KAAK;AACZ,QAAI,eAAe,mBAAmB;AACpC,aAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACrE;AACA,YAAQ,MAAM,oCAAoC,GAAG;AACrD,WAAO,aAAa,KAAK,EAAE,OAAO,uBAAuB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC7E;AACF;AAEA,eAAsB,OAAO,KAAc;AACzC,MAAI;AACF,UAAM,MAAM,IAAI,IAAI,IAAI,GAAG;AAC3B,UAAM,KAAK,mBAAmB,KAAK,QAAQ;AAE3C,QAAI,CAAC,IAAI;AACP,aAAO,aAAa,KAAK,EAAE,OAAO,mBAAmB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACzE;AAEA,UAAM,MAAM,MAAM,sBAAsB,GAAG;AAE3C,UAAM,UAAU,MAAM,IAAI,GAAG;AAAA,MAC3B;AAAA,MACA;AAAA,QACE;AAAA,QACA,gBAAgB,IAAI;AAAA,QACpB,UAAU,IAAI;AAAA,QACd,WAAW;AAAA,MACb;AAAA,MACA,EAAE,WAAW,oBAAI,KAAK,EAAE;AAAA,IAC1B;AAEA,QAAI,YAAY,GAAG;AACjB,aAAO,aAAa,KAAK,EAAE,OAAO,kBAAkB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACxE;AAEA,WAAO,aAAa,KAAK,EAAE,IAAI,KAAK,CAAC;AAAA,EACvC,SAAS,KAAK;AACZ,QAAI,eAAe,mBAAmB;AACpC,aAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACrE;AACA,YAAQ,MAAM,oCAAoC,GAAG;AACrD,WAAO,aAAa,KAAK,EAAE,OAAO,yBAAyB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC/E;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK;AAAA,MACH,SAAS;AAAA,MACT,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,eAAe;AAAA,QAC3C,EAAE,QAAQ,KAAK,aAAa,kBAAkB;AAAA,MAChD;AAAA,IACF;AAAA,IACA,QAAQ;AAAA,MACN,SAAS;AAAA,MACT,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,gBAAgB;AAAA,QAC5C,EAAE,QAAQ,KAAK,aAAa,kBAAkB;AAAA,MAChD;AAAA,IACF;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { InboxEmail } from '../../../data/entities'\nimport {\n resolveRequestContext,\n extractPathSegment,\n UnauthorizedError,\n} from '../../routeHelpers'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['inbox_ops.log.view'] },\n DELETE: { requireAuth: true, requireFeatures: ['inbox_ops.proposals.manage'] },\n}\n\nexport async function GET(req: Request) {\n try {\n const url = new URL(req.url)\n const id = extractPathSegment(url, 'emails')\n\n if (!id) {\n return NextResponse.json({ error: 'Missing email ID' }, { status: 400 })\n }\n\n const ctx = await resolveRequestContext(req)\n\n const email = await findOneWithDecryption(\n ctx.em,\n InboxEmail,\n {\n id,\n organizationId: ctx.organizationId,\n tenantId: ctx.tenantId,\n deletedAt: null,\n },\n undefined,\n ctx.scope,\n )\n\n if (!email) {\n return NextResponse.json({ error: 'Email not found' }, { status: 404 })\n }\n\n return NextResponse.json({ email })\n } catch (err) {\n if (err instanceof UnauthorizedError) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n console.error('[inbox_ops:emails:detail] Error:', err)\n return NextResponse.json({ error: 'Failed to load email' }, { status: 500 })\n }\n}\n\nexport async function DELETE(req: Request) {\n try {\n const url = new URL(req.url)\n const id = extractPathSegment(url, 'emails')\n\n if (!id) {\n return NextResponse.json({ error: 'Missing email ID' }, { status: 400 })\n }\n\n const ctx = await resolveRequestContext(req)\n\n const guardResult = await validateCrudMutationGuard(ctx.container, {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n userId: ctx.userId,\n resourceKind: 'inbox_ops:inbox_email',\n resourceId: id,\n operation: 'delete',\n requestMethod: req.method,\n requestHeaders: req.headers,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n const updated = await ctx.em.nativeUpdate(\n InboxEmail,\n {\n id,\n organizationId: ctx.organizationId,\n tenantId: ctx.tenantId,\n deletedAt: null,\n },\n { deletedAt: new Date() },\n )\n\n if (updated === 0) {\n return NextResponse.json({ error: 'Email not found' }, { status: 404 })\n }\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(ctx.container, {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n userId: ctx.userId,\n resourceKind: 'inbox_ops:inbox_email',\n resourceId: id,\n operation: 'delete',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return NextResponse.json({ ok: true })\n } catch (err) {\n if (err instanceof UnauthorizedError) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n console.error('[inbox_ops:emails:delete] Error:', err)\n return NextResponse.json({ error: 'Failed to delete email' }, { status: 500 })\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'InboxOps',\n summary: 'Email detail',\n methods: {\n GET: {\n summary: 'Get email detail with parsed thread',\n responses: [\n { status: 200, description: 'Email detail' },\n { status: 404, description: 'Email not found' },\n ],\n },\n DELETE: {\n summary: 'Soft-delete an inbox email',\n responses: [\n { status: 200, description: 'Email deleted' },\n { status: 404, description: 'Email not found' },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,5 +1,9 @@
1
1
  import { NextResponse } from "next/server";
2
2
  import { runWithCacheTenant } from "@open-mercato/cache";
3
+ import {
4
+ runCrudMutationGuardAfterSuccess,
5
+ validateCrudMutationGuard
6
+ } from "@open-mercato/shared/lib/crud/mutation-guard";
3
7
  import { acceptAllActions } from "../../../../lib/executionEngine.js";
4
8
  import { resolveCache, invalidateCountsCache } from "../../../../lib/cache.js";
5
9
  import {
@@ -18,6 +22,19 @@ async function POST(req) {
18
22
  const ctx = await resolveRequestContext(req);
19
23
  const proposal = await resolveProposal(new URL(req.url), ctx);
20
24
  if (isErrorResponse(proposal)) return proposal;
25
+ const guardResult = await validateCrudMutationGuard(ctx.container, {
26
+ tenantId: ctx.tenantId,
27
+ organizationId: ctx.organizationId,
28
+ userId: ctx.userId,
29
+ resourceKind: "inbox_ops:inbox_proposal",
30
+ resourceId: proposal.id,
31
+ operation: "custom",
32
+ requestMethod: req.method,
33
+ requestHeaders: req.headers
34
+ });
35
+ if (guardResult && !guardResult.ok) {
36
+ return NextResponse.json(guardResult.body, { status: guardResult.status });
37
+ }
21
38
  const entities = resolveCrossModuleEntities(ctx.container);
22
39
  const { results, stoppedOnFailure } = await acceptAllActions(
23
40
  proposal.id,
@@ -25,6 +42,19 @@ async function POST(req) {
25
42
  );
26
43
  const succeeded = results.filter((r) => r.success).length;
27
44
  const failed = results.filter((r) => !r.success).length;
45
+ if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
46
+ await runCrudMutationGuardAfterSuccess(ctx.container, {
47
+ tenantId: ctx.tenantId,
48
+ organizationId: ctx.organizationId,
49
+ userId: ctx.userId,
50
+ resourceKind: "inbox_ops:inbox_proposal",
51
+ resourceId: proposal.id,
52
+ operation: "custom",
53
+ requestMethod: req.method,
54
+ requestHeaders: req.headers,
55
+ metadata: guardResult.metadata ?? null
56
+ });
57
+ }
28
58
  const cache = resolveCache(ctx.container);
29
59
  await runWithCacheTenant(ctx.tenantId, () => invalidateCountsCache(cache, ctx.tenantId));
30
60
  return NextResponse.json({ ok: !stoppedOnFailure, succeeded, failed, stoppedOnFailure, results });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../src/modules/inbox_ops/api/proposals/%5Bid%5D/accept-all/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { runWithCacheTenant } from '@open-mercato/cache'\nimport { acceptAllActions } from '../../../../lib/executionEngine'\nimport { resolveCache, invalidateCountsCache } from '../../../../lib/cache'\nimport {\n resolveRequestContext,\n resolveProposal,\n resolveCrossModuleEntities,\n toExecutionContext,\n handleRouteError,\n isErrorResponse,\n} from '../../../routeHelpers'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['inbox_ops.proposals.manage'] },\n}\n\nexport async function POST(req: Request) {\n try {\n const ctx = await resolveRequestContext(req)\n const proposal = await resolveProposal(new URL(req.url), ctx)\n if (isErrorResponse(proposal)) return proposal\n\n const entities = resolveCrossModuleEntities(ctx.container)\n const { results, stoppedOnFailure } = await acceptAllActions(\n proposal.id,\n toExecutionContext(ctx, entities),\n )\n\n const succeeded = results.filter((r) => r.success).length\n const failed = results.filter((r) => !r.success).length\n\n const cache = resolveCache(ctx.container)\n await runWithCacheTenant(ctx.tenantId, () => invalidateCountsCache(cache, ctx.tenantId))\n\n return NextResponse.json({ ok: !stoppedOnFailure, succeeded, failed, stoppedOnFailure, results })\n } catch (err) {\n return handleRouteError(err, 'accept all actions')\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'InboxOps',\n summary: 'Accept all actions',\n methods: {\n POST: {\n summary: 'Accept and execute all pending actions in a proposal',\n responses: [\n { status: 200, description: 'All actions processed' },\n { status: 404, description: 'Proposal not found' },\n ],\n },\n },\n}\n"],
5
- "mappings": "AAAA,SAAS,oBAAoB;AAE7B,SAAS,0BAA0B;AACnC,SAAS,wBAAwB;AACjC,SAAS,cAAc,6BAA6B;AACpD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEA,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,4BAA4B,EAAE;AAC7E;AAEA,eAAsB,KAAK,KAAc;AACvC,MAAI;AACF,UAAM,MAAM,MAAM,sBAAsB,GAAG;AAC3C,UAAM,WAAW,MAAM,gBAAgB,IAAI,IAAI,IAAI,GAAG,GAAG,GAAG;AAC5D,QAAI,gBAAgB,QAAQ,EAAG,QAAO;AAEtC,UAAM,WAAW,2BAA2B,IAAI,SAAS;AACzD,UAAM,EAAE,SAAS,iBAAiB,IAAI,MAAM;AAAA,MAC1C,SAAS;AAAA,MACT,mBAAmB,KAAK,QAAQ;AAAA,IAClC;AAEA,UAAM,YAAY,QAAQ,OAAO,CAAC,MAAM,EAAE,OAAO,EAAE;AACnD,UAAM,SAAS,QAAQ,OAAO,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE;AAEjD,UAAM,QAAQ,aAAa,IAAI,SAAS;AACxC,UAAM,mBAAmB,IAAI,UAAU,MAAM,sBAAsB,OAAO,IAAI,QAAQ,CAAC;AAEvF,WAAO,aAAa,KAAK,EAAE,IAAI,CAAC,kBAAkB,WAAW,QAAQ,kBAAkB,QAAQ,CAAC;AAAA,EAClG,SAAS,KAAK;AACZ,WAAO,iBAAiB,KAAK,oBAAoB;AAAA,EACnD;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,wBAAwB;AAAA,QACpD,EAAE,QAAQ,KAAK,aAAa,qBAAqB;AAAA,MACnD;AAAA,IACF;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { runWithCacheTenant } from '@open-mercato/cache'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { acceptAllActions } from '../../../../lib/executionEngine'\nimport { resolveCache, invalidateCountsCache } from '../../../../lib/cache'\nimport {\n resolveRequestContext,\n resolveProposal,\n resolveCrossModuleEntities,\n toExecutionContext,\n handleRouteError,\n isErrorResponse,\n} from '../../../routeHelpers'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['inbox_ops.proposals.manage'] },\n}\n\nexport async function POST(req: Request) {\n try {\n const ctx = await resolveRequestContext(req)\n const proposal = await resolveProposal(new URL(req.url), ctx)\n if (isErrorResponse(proposal)) return proposal\n\n const guardResult = await validateCrudMutationGuard(ctx.container, {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n userId: ctx.userId,\n resourceKind: 'inbox_ops:inbox_proposal',\n resourceId: proposal.id,\n operation: 'custom',\n requestMethod: req.method,\n requestHeaders: req.headers,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n const entities = resolveCrossModuleEntities(ctx.container)\n const { results, stoppedOnFailure } = await acceptAllActions(\n proposal.id,\n toExecutionContext(ctx, entities),\n )\n\n const succeeded = results.filter((r) => r.success).length\n const failed = results.filter((r) => !r.success).length\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(ctx.container, {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n userId: ctx.userId,\n resourceKind: 'inbox_ops:inbox_proposal',\n resourceId: proposal.id,\n operation: 'custom',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n const cache = resolveCache(ctx.container)\n await runWithCacheTenant(ctx.tenantId, () => invalidateCountsCache(cache, ctx.tenantId))\n\n return NextResponse.json({ ok: !stoppedOnFailure, succeeded, failed, stoppedOnFailure, results })\n } catch (err) {\n return handleRouteError(err, 'accept all actions')\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'InboxOps',\n summary: 'Accept all actions',\n methods: {\n POST: {\n summary: 'Accept and execute all pending actions in a proposal',\n responses: [\n { status: 200, description: 'All actions processed' },\n { status: 404, description: 'Proposal not found' },\n ],\n },\n },\n}\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAE7B,SAAS,0BAA0B;AACnC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,wBAAwB;AACjC,SAAS,cAAc,6BAA6B;AACpD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEA,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,4BAA4B,EAAE;AAC7E;AAEA,eAAsB,KAAK,KAAc;AACvC,MAAI;AACF,UAAM,MAAM,MAAM,sBAAsB,GAAG;AAC3C,UAAM,WAAW,MAAM,gBAAgB,IAAI,IAAI,IAAI,GAAG,GAAG,GAAG;AAC5D,QAAI,gBAAgB,QAAQ,EAAG,QAAO;AAEtC,UAAM,cAAc,MAAM,0BAA0B,IAAI,WAAW;AAAA,MACjE,UAAU,IAAI;AAAA,MACd,gBAAgB,IAAI;AAAA,MACpB,QAAQ,IAAI;AAAA,MACZ,cAAc;AAAA,MACd,YAAY,SAAS;AAAA,MACrB,WAAW;AAAA,MACX,eAAe,IAAI;AAAA,MACnB,gBAAgB,IAAI;AAAA,IACtB,CAAC;AACD,QAAI,eAAe,CAAC,YAAY,IAAI;AAClC,aAAO,aAAa,KAAK,YAAY,MAAM,EAAE,QAAQ,YAAY,OAAO,CAAC;AAAA,IAC3E;AAEA,UAAM,WAAW,2BAA2B,IAAI,SAAS;AACzD,UAAM,EAAE,SAAS,iBAAiB,IAAI,MAAM;AAAA,MAC1C,SAAS;AAAA,MACT,mBAAmB,KAAK,QAAQ;AAAA,IAClC;AAEA,UAAM,YAAY,QAAQ,OAAO,CAAC,MAAM,EAAE,OAAO,EAAE;AACnD,UAAM,SAAS,QAAQ,OAAO,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE;AAEjD,QAAI,aAAa,MAAM,YAAY,uBAAuB;AACxD,YAAM,iCAAiC,IAAI,WAAW;AAAA,QACpD,UAAU,IAAI;AAAA,QACd,gBAAgB,IAAI;AAAA,QACpB,QAAQ,IAAI;AAAA,QACZ,cAAc;AAAA,QACd,YAAY,SAAS;AAAA,QACrB,WAAW;AAAA,QACX,eAAe,IAAI;AAAA,QACnB,gBAAgB,IAAI;AAAA,QACpB,UAAU,YAAY,YAAY;AAAA,MACpC,CAAC;AAAA,IACH;AAEA,UAAM,QAAQ,aAAa,IAAI,SAAS;AACxC,UAAM,mBAAmB,IAAI,UAAU,MAAM,sBAAsB,OAAO,IAAI,QAAQ,CAAC;AAEvF,WAAO,aAAa,KAAK,EAAE,IAAI,CAAC,kBAAkB,WAAW,QAAQ,kBAAkB,QAAQ,CAAC;AAAA,EAClG,SAAS,KAAK;AACZ,WAAO,iBAAiB,KAAK,oBAAoB;AAAA,EACnD;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,wBAAwB;AAAA,QACpD,EAAE,QAAQ,KAAK,aAAa,qBAAqB;AAAA,MACnD;AAAA,IACF;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -1,5 +1,9 @@
1
1
  import { NextResponse } from "next/server";
2
2
  import { runWithCacheTenant } from "@open-mercato/cache";
3
+ import {
4
+ runCrudMutationGuardAfterSuccess,
5
+ validateCrudMutationGuard
6
+ } from "@open-mercato/shared/lib/crud/mutation-guard";
3
7
  import { actionEditSchema, validateActionPayloadForType } from "../../../../../data/validators.js";
4
8
  import { emitInboxOpsEvent } from "../../../../../events.js";
5
9
  import { resolveCache, invalidateCountsCache } from "../../../../../lib/cache.js";
@@ -31,8 +35,35 @@ async function PATCH(req) {
31
35
  if (!payloadValidation.success) {
32
36
  return NextResponse.json({ error: payloadValidation.error }, { status: 400 });
33
37
  }
38
+ const guardResult = await validateCrudMutationGuard(ctx.container, {
39
+ tenantId: ctx.tenantId,
40
+ organizationId: ctx.organizationId,
41
+ userId: ctx.userId,
42
+ resourceKind: "inbox_ops:inbox_proposal_action",
43
+ resourceId: action.id,
44
+ operation: "update",
45
+ requestMethod: req.method,
46
+ requestHeaders: req.headers,
47
+ mutationPayload: parsed.data
48
+ });
49
+ if (guardResult && !guardResult.ok) {
50
+ return NextResponse.json(guardResult.body, { status: guardResult.status });
51
+ }
34
52
  action.payload = mergedPayload;
35
53
  await ctx.em.flush();
54
+ if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
55
+ await runCrudMutationGuardAfterSuccess(ctx.container, {
56
+ tenantId: ctx.tenantId,
57
+ organizationId: ctx.organizationId,
58
+ userId: ctx.userId,
59
+ resourceKind: "inbox_ops:inbox_proposal_action",
60
+ resourceId: action.id,
61
+ operation: "update",
62
+ requestMethod: req.method,
63
+ requestHeaders: req.headers,
64
+ metadata: guardResult.metadata ?? null
65
+ });
66
+ }
36
67
  const cache = resolveCache(ctx.container);
37
68
  await runWithCacheTenant(ctx.tenantId, () => invalidateCountsCache(cache, ctx.tenantId));
38
69
  try {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../../src/modules/inbox_ops/api/proposals/%5Bid%5D/actions/%5BactionId%5D/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { runWithCacheTenant } from '@open-mercato/cache'\nimport { actionEditSchema, validateActionPayloadForType } from '../../../../../data/validators'\nimport { emitInboxOpsEvent } from '../../../../../events'\nimport { resolveCache, invalidateCountsCache } from '../../../../../lib/cache'\nimport {\n resolveRequestContext,\n resolveActionAndProposal,\n handleRouteError,\n isErrorResponse,\n} from '../../../../routeHelpers'\n\nexport const metadata = {\n PATCH: { requireAuth: true, requireFeatures: ['inbox_ops.proposals.manage'] },\n}\n\nexport async function PATCH(req: Request) {\n try {\n const body = await req.json()\n const parsed = actionEditSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload', details: parsed.error.issues }, { status: 400 })\n }\n\n const ctx = await resolveRequestContext(req)\n const resolved = await resolveActionAndProposal(new URL(req.url), ctx)\n if (isErrorResponse(resolved)) return resolved\n\n const { action } = resolved\n\n if (action.status !== 'pending' && action.status !== 'failed') {\n return NextResponse.json({ error: 'Action already processed' }, { status: 409 })\n }\n\n const mergedPayload = { ...action.payload as Record<string, unknown>, ...parsed.data.payload }\n const payloadValidation = validateActionPayloadForType(action.actionType, mergedPayload)\n if (!payloadValidation.success) {\n return NextResponse.json({ error: payloadValidation.error }, { status: 400 })\n }\n\n action.payload = mergedPayload\n await ctx.em.flush()\n\n const cache = resolveCache(ctx.container)\n await runWithCacheTenant(ctx.tenantId, () => invalidateCountsCache(cache, ctx.tenantId))\n\n try {\n await emitInboxOpsEvent('inbox_ops.action.edited', {\n actionId: action.id,\n proposalId: action.proposalId,\n actionType: action.actionType,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n })\n } catch (eventError) {\n console.error('[inbox_ops:action:edit] Failed to emit event:', eventError)\n }\n\n return NextResponse.json({ ok: true, action })\n } catch (err) {\n return handleRouteError(err, 'edit action')\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'InboxOps',\n summary: 'Edit action',\n methods: {\n PATCH: {\n summary: 'Edit action payload before accepting',\n responses: [\n { status: 200, description: 'Action updated' },\n { status: 404, description: 'Action not found' },\n { status: 409, description: 'Action already processed' },\n ],\n },\n },\n}\n"],
5
- "mappings": "AAAA,SAAS,oBAAoB;AAE7B,SAAS,0BAA0B;AACnC,SAAS,kBAAkB,oCAAoC;AAC/D,SAAS,yBAAyB;AAClC,SAAS,cAAc,6BAA6B;AACpD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEA,MAAM,WAAW;AAAA,EACtB,OAAO,EAAE,aAAa,MAAM,iBAAiB,CAAC,4BAA4B,EAAE;AAC9E;AAEA,eAAsB,MAAM,KAAc;AACxC,MAAI;AACF,UAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,UAAM,SAAS,iBAAiB,UAAU,IAAI;AAC9C,QAAI,CAAC,OAAO,SAAS;AACnB,aAAO,aAAa,KAAK,EAAE,OAAO,mBAAmB,SAAS,OAAO,MAAM,OAAO,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACtG;AAEA,UAAM,MAAM,MAAM,sBAAsB,GAAG;AAC3C,UAAM,WAAW,MAAM,yBAAyB,IAAI,IAAI,IAAI,GAAG,GAAG,GAAG;AACrE,QAAI,gBAAgB,QAAQ,EAAG,QAAO;AAEtC,UAAM,EAAE,OAAO,IAAI;AAEnB,QAAI,OAAO,WAAW,aAAa,OAAO,WAAW,UAAU;AAC7D,aAAO,aAAa,KAAK,EAAE,OAAO,2BAA2B,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACjF;AAEA,UAAM,gBAAgB,EAAE,GAAG,OAAO,SAAoC,GAAG,OAAO,KAAK,QAAQ;AAC7F,UAAM,oBAAoB,6BAA6B,OAAO,YAAY,aAAa;AACvF,QAAI,CAAC,kBAAkB,SAAS;AAC9B,aAAO,aAAa,KAAK,EAAE,OAAO,kBAAkB,MAAM,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC9E;AAEA,WAAO,UAAU;AACjB,UAAM,IAAI,GAAG,MAAM;AAEnB,UAAM,QAAQ,aAAa,IAAI,SAAS;AACxC,UAAM,mBAAmB,IAAI,UAAU,MAAM,sBAAsB,OAAO,IAAI,QAAQ,CAAC;AAEvF,QAAI;AACF,YAAM,kBAAkB,2BAA2B;AAAA,QACjD,UAAU,OAAO;AAAA,QACjB,YAAY,OAAO;AAAA,QACnB,YAAY,OAAO;AAAA,QACnB,UAAU,IAAI;AAAA,QACd,gBAAgB,IAAI;AAAA,MACtB,CAAC;AAAA,IACH,SAAS,YAAY;AACnB,cAAQ,MAAM,iDAAiD,UAAU;AAAA,IAC3E;AAEA,WAAO,aAAa,KAAK,EAAE,IAAI,MAAM,OAAO,CAAC;AAAA,EAC/C,SAAS,KAAK;AACZ,WAAO,iBAAiB,KAAK,aAAa;AAAA,EAC5C;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,OAAO;AAAA,MACL,SAAS;AAAA,MACT,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,iBAAiB;AAAA,QAC7C,EAAE,QAAQ,KAAK,aAAa,mBAAmB;AAAA,QAC/C,EAAE,QAAQ,KAAK,aAAa,2BAA2B;AAAA,MACzD;AAAA,IACF;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { runWithCacheTenant } from '@open-mercato/cache'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { actionEditSchema, validateActionPayloadForType } from '../../../../../data/validators'\nimport { emitInboxOpsEvent } from '../../../../../events'\nimport { resolveCache, invalidateCountsCache } from '../../../../../lib/cache'\nimport {\n resolveRequestContext,\n resolveActionAndProposal,\n handleRouteError,\n isErrorResponse,\n} from '../../../../routeHelpers'\n\nexport const metadata = {\n PATCH: { requireAuth: true, requireFeatures: ['inbox_ops.proposals.manage'] },\n}\n\nexport async function PATCH(req: Request) {\n try {\n const body = await req.json()\n const parsed = actionEditSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload', details: parsed.error.issues }, { status: 400 })\n }\n\n const ctx = await resolveRequestContext(req)\n const resolved = await resolveActionAndProposal(new URL(req.url), ctx)\n if (isErrorResponse(resolved)) return resolved\n\n const { action } = resolved\n\n if (action.status !== 'pending' && action.status !== 'failed') {\n return NextResponse.json({ error: 'Action already processed' }, { status: 409 })\n }\n\n const mergedPayload = { ...action.payload as Record<string, unknown>, ...parsed.data.payload }\n const payloadValidation = validateActionPayloadForType(action.actionType, mergedPayload)\n if (!payloadValidation.success) {\n return NextResponse.json({ error: payloadValidation.error }, { status: 400 })\n }\n\n const guardResult = await validateCrudMutationGuard(ctx.container, {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n userId: ctx.userId,\n resourceKind: 'inbox_ops:inbox_proposal_action',\n resourceId: action.id,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: parsed.data,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n action.payload = mergedPayload\n await ctx.em.flush()\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(ctx.container, {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n userId: ctx.userId,\n resourceKind: 'inbox_ops:inbox_proposal_action',\n resourceId: action.id,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n const cache = resolveCache(ctx.container)\n await runWithCacheTenant(ctx.tenantId, () => invalidateCountsCache(cache, ctx.tenantId))\n\n try {\n await emitInboxOpsEvent('inbox_ops.action.edited', {\n actionId: action.id,\n proposalId: action.proposalId,\n actionType: action.actionType,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n })\n } catch (eventError) {\n console.error('[inbox_ops:action:edit] Failed to emit event:', eventError)\n }\n\n return NextResponse.json({ ok: true, action })\n } catch (err) {\n return handleRouteError(err, 'edit action')\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'InboxOps',\n summary: 'Edit action',\n methods: {\n PATCH: {\n summary: 'Edit action payload before accepting',\n responses: [\n { status: 200, description: 'Action updated' },\n { status: 404, description: 'Action not found' },\n { status: 409, description: 'Action already processed' },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,5 +1,9 @@
1
1
  import { NextResponse } from "next/server";
2
2
  import { runWithCacheTenant } from "@open-mercato/cache";
3
+ import {
4
+ runCrudMutationGuardAfterSuccess,
5
+ validateCrudMutationGuard
6
+ } from "@open-mercato/shared/lib/crud/mutation-guard";
3
7
  import { categorizeProposalSchema } from "../../../../data/validators.js";
4
8
  import { resolveCache, invalidateCountsCache } from "../../../../lib/cache.js";
5
9
  import {
@@ -22,9 +26,36 @@ async function POST(req) {
22
26
  const issues = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
23
27
  return NextResponse.json({ error: `Invalid category: ${issues}` }, { status: 400 });
24
28
  }
29
+ const guardResult = await validateCrudMutationGuard(ctx.container, {
30
+ tenantId: ctx.tenantId,
31
+ organizationId: ctx.organizationId,
32
+ userId: ctx.userId,
33
+ resourceKind: "inbox_ops:inbox_proposal",
34
+ resourceId: proposal.id,
35
+ operation: "update",
36
+ requestMethod: req.method,
37
+ requestHeaders: req.headers,
38
+ mutationPayload: parsed.data
39
+ });
40
+ if (guardResult && !guardResult.ok) {
41
+ return NextResponse.json(guardResult.body, { status: guardResult.status });
42
+ }
25
43
  const previousCategory = proposal.category || null;
26
44
  proposal.category = parsed.data.category;
27
45
  await ctx.em.flush();
46
+ if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
47
+ await runCrudMutationGuardAfterSuccess(ctx.container, {
48
+ tenantId: ctx.tenantId,
49
+ organizationId: ctx.organizationId,
50
+ userId: ctx.userId,
51
+ resourceKind: "inbox_ops:inbox_proposal",
52
+ resourceId: proposal.id,
53
+ operation: "update",
54
+ requestMethod: req.method,
55
+ requestHeaders: req.headers,
56
+ metadata: guardResult.metadata ?? null
57
+ });
58
+ }
28
59
  const cache = resolveCache(ctx.container);
29
60
  await runWithCacheTenant(ctx.tenantId, () => invalidateCountsCache(cache, ctx.tenantId));
30
61
  return NextResponse.json({
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../src/modules/inbox_ops/api/proposals/%5Bid%5D/categorize/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { runWithCacheTenant } from '@open-mercato/cache'\nimport { categorizeProposalSchema } from '../../../../data/validators'\nimport { resolveCache, invalidateCountsCache } from '../../../../lib/cache'\nimport {\n resolveRequestContext,\n resolveProposal,\n handleRouteError,\n isErrorResponse,\n} from '../../../routeHelpers'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['inbox_ops.proposals.manage'] },\n}\n\nexport async function POST(req: Request) {\n try {\n const ctx = await resolveRequestContext(req)\n const proposal = await resolveProposal(new URL(req.url), ctx)\n if (isErrorResponse(proposal)) return proposal\n\n const body = await req.json()\n const parsed = categorizeProposalSchema.safeParse(body)\n if (!parsed.success) {\n const issues = parsed.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ')\n return NextResponse.json({ error: `Invalid category: ${issues}` }, { status: 400 })\n }\n\n const previousCategory = proposal.category || null\n proposal.category = parsed.data.category\n await ctx.em.flush()\n\n const cache = resolveCache(ctx.container)\n await runWithCacheTenant(ctx.tenantId, () => invalidateCountsCache(cache, ctx.tenantId))\n\n return NextResponse.json({\n ok: true,\n category: parsed.data.category,\n previousCategory,\n })\n } catch (err) {\n return handleRouteError(err, 'categorize proposal')\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'InboxOps',\n summary: 'Categorize proposal',\n methods: {\n POST: {\n summary: 'Set or change the category of a proposal',\n description: 'Assigns a category to a proposal. Returns the new and previous category for undo support.',\n responses: [\n { status: 200, description: 'Category updated' },\n { status: 400, description: 'Invalid category value' },\n { status: 404, description: 'Proposal not found' },\n ],\n },\n },\n}\n"],
5
- "mappings": "AAAA,SAAS,oBAAoB;AAE7B,SAAS,0BAA0B;AACnC,SAAS,gCAAgC;AACzC,SAAS,cAAc,6BAA6B;AACpD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEA,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,4BAA4B,EAAE;AAC7E;AAEA,eAAsB,KAAK,KAAc;AACvC,MAAI;AACF,UAAM,MAAM,MAAM,sBAAsB,GAAG;AAC3C,UAAM,WAAW,MAAM,gBAAgB,IAAI,IAAI,IAAI,GAAG,GAAG,GAAG;AAC5D,QAAI,gBAAgB,QAAQ,EAAG,QAAO;AAEtC,UAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,UAAM,SAAS,yBAAyB,UAAU,IAAI;AACtD,QAAI,CAAC,OAAO,SAAS;AACnB,YAAM,SAAS,OAAO,MAAM,OAAO,IAAI,CAAC,MAAM,GAAG,EAAE,KAAK,KAAK,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,KAAK,IAAI;AAC5F,aAAO,aAAa,KAAK,EAAE,OAAO,qBAAqB,MAAM,GAAG,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACpF;AAEA,UAAM,mBAAmB,SAAS,YAAY;AAC9C,aAAS,WAAW,OAAO,KAAK;AAChC,UAAM,IAAI,GAAG,MAAM;AAEnB,UAAM,QAAQ,aAAa,IAAI,SAAS;AACxC,UAAM,mBAAmB,IAAI,UAAU,MAAM,sBAAsB,OAAO,IAAI,QAAQ,CAAC;AAEvF,WAAO,aAAa,KAAK;AAAA,MACvB,IAAI;AAAA,MACJ,UAAU,OAAO,KAAK;AAAA,MACtB;AAAA,IACF,CAAC;AAAA,EACH,SAAS,KAAK;AACZ,WAAO,iBAAiB,KAAK,qBAAqB;AAAA,EACpD;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,aAAa;AAAA,MACb,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,mBAAmB;AAAA,QAC/C,EAAE,QAAQ,KAAK,aAAa,yBAAyB;AAAA,QACrD,EAAE,QAAQ,KAAK,aAAa,qBAAqB;AAAA,MACnD;AAAA,IACF;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { runWithCacheTenant } from '@open-mercato/cache'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { categorizeProposalSchema } from '../../../../data/validators'\nimport { resolveCache, invalidateCountsCache } from '../../../../lib/cache'\nimport {\n resolveRequestContext,\n resolveProposal,\n handleRouteError,\n isErrorResponse,\n} from '../../../routeHelpers'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['inbox_ops.proposals.manage'] },\n}\n\nexport async function POST(req: Request) {\n try {\n const ctx = await resolveRequestContext(req)\n const proposal = await resolveProposal(new URL(req.url), ctx)\n if (isErrorResponse(proposal)) return proposal\n\n const body = await req.json()\n const parsed = categorizeProposalSchema.safeParse(body)\n if (!parsed.success) {\n const issues = parsed.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ')\n return NextResponse.json({ error: `Invalid category: ${issues}` }, { status: 400 })\n }\n\n const guardResult = await validateCrudMutationGuard(ctx.container, {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n userId: ctx.userId,\n resourceKind: 'inbox_ops:inbox_proposal',\n resourceId: proposal.id,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: parsed.data,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n const previousCategory = proposal.category || null\n proposal.category = parsed.data.category\n await ctx.em.flush()\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(ctx.container, {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n userId: ctx.userId,\n resourceKind: 'inbox_ops:inbox_proposal',\n resourceId: proposal.id,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n const cache = resolveCache(ctx.container)\n await runWithCacheTenant(ctx.tenantId, () => invalidateCountsCache(cache, ctx.tenantId))\n\n return NextResponse.json({\n ok: true,\n category: parsed.data.category,\n previousCategory,\n })\n } catch (err) {\n return handleRouteError(err, 'categorize proposal')\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'InboxOps',\n summary: 'Categorize proposal',\n methods: {\n POST: {\n summary: 'Set or change the category of a proposal',\n description: 'Assigns a category to a proposal. Returns the new and previous category for undo support.',\n responses: [\n { status: 200, description: 'Category updated' },\n { status: 400, description: 'Invalid category value' },\n { status: 404, description: 'Proposal not found' },\n ],\n },\n },\n}\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAE7B,SAAS,0BAA0B;AACnC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,gCAAgC;AACzC,SAAS,cAAc,6BAA6B;AACpD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEA,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,4BAA4B,EAAE;AAC7E;AAEA,eAAsB,KAAK,KAAc;AACvC,MAAI;AACF,UAAM,MAAM,MAAM,sBAAsB,GAAG;AAC3C,UAAM,WAAW,MAAM,gBAAgB,IAAI,IAAI,IAAI,GAAG,GAAG,GAAG;AAC5D,QAAI,gBAAgB,QAAQ,EAAG,QAAO;AAEtC,UAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,UAAM,SAAS,yBAAyB,UAAU,IAAI;AACtD,QAAI,CAAC,OAAO,SAAS;AACnB,YAAM,SAAS,OAAO,MAAM,OAAO,IAAI,CAAC,MAAM,GAAG,EAAE,KAAK,KAAK,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,KAAK,IAAI;AAC5F,aAAO,aAAa,KAAK,EAAE,OAAO,qBAAqB,MAAM,GAAG,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACpF;AAEA,UAAM,cAAc,MAAM,0BAA0B,IAAI,WAAW;AAAA,MACjE,UAAU,IAAI;AAAA,MACd,gBAAgB,IAAI;AAAA,MACpB,QAAQ,IAAI;AAAA,MACZ,cAAc;AAAA,MACd,YAAY,SAAS;AAAA,MACrB,WAAW;AAAA,MACX,eAAe,IAAI;AAAA,MACnB,gBAAgB,IAAI;AAAA,MACpB,iBAAiB,OAAO;AAAA,IAC1B,CAAC;AACD,QAAI,eAAe,CAAC,YAAY,IAAI;AAClC,aAAO,aAAa,KAAK,YAAY,MAAM,EAAE,QAAQ,YAAY,OAAO,CAAC;AAAA,IAC3E;AAEA,UAAM,mBAAmB,SAAS,YAAY;AAC9C,aAAS,WAAW,OAAO,KAAK;AAChC,UAAM,IAAI,GAAG,MAAM;AAEnB,QAAI,aAAa,MAAM,YAAY,uBAAuB;AACxD,YAAM,iCAAiC,IAAI,WAAW;AAAA,QACpD,UAAU,IAAI;AAAA,QACd,gBAAgB,IAAI;AAAA,QACpB,QAAQ,IAAI;AAAA,QACZ,cAAc;AAAA,QACd,YAAY,SAAS;AAAA,QACrB,WAAW;AAAA,QACX,eAAe,IAAI;AAAA,QACnB,gBAAgB,IAAI;AAAA,QACpB,UAAU,YAAY,YAAY;AAAA,MACpC,CAAC;AAAA,IACH;AAEA,UAAM,QAAQ,aAAa,IAAI,SAAS;AACxC,UAAM,mBAAmB,IAAI,UAAU,MAAM,sBAAsB,OAAO,IAAI,QAAQ,CAAC;AAEvF,WAAO,aAAa,KAAK;AAAA,MACvB,IAAI;AAAA,MACJ,UAAU,OAAO,KAAK;AAAA,MACtB;AAAA,IACF,CAAC;AAAA,EACH,SAAS,KAAK;AACZ,WAAO,iBAAiB,KAAK,qBAAqB;AAAA,EACpD;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,aAAa;AAAA,MACb,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,mBAAmB;AAAA,QAC/C,EAAE,QAAQ,KAAK,aAAa,yBAAyB;AAAA,QACrD,EAAE,QAAQ,KAAK,aAAa,qBAAqB;AAAA,MACnD;AAAA,IACF;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import * as React from "react";
3
3
  import { flash } from "@open-mercato/ui/backend/FlashMessages";
4
+ import { useGuardedMutation } from "@open-mercato/ui/backend/injection/useGuardedMutation";
4
5
  import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
5
6
  import { isSafeNavigationHref, parseObjectActionId, toErrorMessage } from "../utils.js";
6
7
  function useMessageDetailsActions({
@@ -21,20 +22,29 @@ function useMessageDetailsActions({
21
22
  const [pendingActionConfirmation, setPendingActionConfirmation] = React.useState(null);
22
23
  const [deleteConfirmationOpen, setDeleteConfirmationOpen] = React.useState(false);
23
24
  const [activeConversationAction, setActiveConversationAction] = React.useState(null);
25
+ const { runMutation, retryLastMutation } = useGuardedMutation({
26
+ contextId: "messages-detail-actions"
27
+ });
24
28
  const requestAndRefresh = React.useCallback(async (url, method, options) => {
25
29
  setUpdatingState(true);
26
30
  try {
27
- const call = await apiCall(url, { method });
28
- if (!call.ok) {
29
- throw new Error(
30
- toErrorMessage(call.result) ?? t("messages.errors.stateChangeFailed", "Failed to update message state.")
31
- );
32
- }
33
- if (options?.skipDetailAutoMarkRead) {
34
- await refreshDetailWithoutAutoMarkRead();
35
- } else {
36
- await detailQuery.refetch();
37
- }
31
+ await runMutation({
32
+ operation: async () => {
33
+ const call = await apiCall(url, { method });
34
+ if (!call.ok) {
35
+ throw new Error(
36
+ toErrorMessage(call.result) ?? t("messages.errors.stateChangeFailed", "Failed to update message state.")
37
+ );
38
+ }
39
+ if (options?.skipDetailAutoMarkRead) {
40
+ await refreshDetailWithoutAutoMarkRead();
41
+ } else {
42
+ await detailQuery.refetch();
43
+ }
44
+ },
45
+ context: { resourceKind: "message", messageId: id, action: "state-change", retryLastMutation },
46
+ mutationPayload: { messageId: id, action: "state-change", url, method }
47
+ });
38
48
  } catch (err) {
39
49
  flash(
40
50
  err instanceof Error ? err.message : t("messages.errors.stateChangeFailed", "Failed to update message state."),
@@ -43,16 +53,22 @@ function useMessageDetailsActions({
43
53
  } finally {
44
54
  setUpdatingState(false);
45
55
  }
46
- }, [detailQuery, refreshDetailWithoutAutoMarkRead, t]);
56
+ }, [detailQuery, id, refreshDetailWithoutAutoMarkRead, retryLastMutation, runMutation, t]);
47
57
  const handleDelete = React.useCallback(async () => {
48
58
  setUpdatingState(true);
49
59
  try {
50
- const call = await apiCall(`/api/messages/${encodeURIComponent(id)}`, {
51
- method: "DELETE"
60
+ await runMutation({
61
+ operation: async () => {
62
+ const call = await apiCall(`/api/messages/${encodeURIComponent(id)}`, {
63
+ method: "DELETE"
64
+ });
65
+ if (!call.ok) {
66
+ throw new Error(toErrorMessage(call.result) ?? t("messages.errors.deleteFailed", "Failed to delete message."));
67
+ }
68
+ },
69
+ context: { resourceKind: "message", messageId: id, action: "delete", retryLastMutation },
70
+ mutationPayload: { messageId: id, action: "delete" }
52
71
  });
53
- if (!call.ok) {
54
- throw new Error(toErrorMessage(call.result) ?? t("messages.errors.deleteFailed", "Failed to delete message."));
55
- }
56
72
  flash(t("messages.flash.deleted", "Message deleted."), "success");
57
73
  onDeleted();
58
74
  } catch (err) {
@@ -63,16 +79,22 @@ function useMessageDetailsActions({
63
79
  } finally {
64
80
  setUpdatingState(false);
65
81
  }
66
- }, [id, onDeleted, t]);
82
+ }, [id, onDeleted, retryLastMutation, runMutation, t]);
67
83
  const runConversationAction = React.useCallback(async (action, options) => {
68
84
  setActiveConversationAction(action);
69
85
  try {
70
- const call = await apiCall(options.url, { method: options.method });
71
- if (!call.ok) {
72
- throw new Error(
73
- toErrorMessage(call.result) ?? t("messages.errors.conversationActionFailed", "Failed to update conversation.")
74
- );
75
- }
86
+ await runMutation({
87
+ operation: async () => {
88
+ const call = await apiCall(options.url, { method: options.method });
89
+ if (!call.ok) {
90
+ throw new Error(
91
+ toErrorMessage(call.result) ?? t("messages.errors.conversationActionFailed", "Failed to update conversation.")
92
+ );
93
+ }
94
+ },
95
+ context: { resourceKind: "conversation", messageId: id, action, retryLastMutation },
96
+ mutationPayload: { messageId: id, action, url: options.url, method: options.method }
97
+ });
76
98
  flash(options.successMessage, "success");
77
99
  if (options.onSuccess) {
78
100
  options.onSuccess();
@@ -91,7 +113,7 @@ function useMessageDetailsActions({
91
113
  } finally {
92
114
  setActiveConversationAction(null);
93
115
  }
94
- }, [detailQuery, refreshDetailWithoutAutoMarkRead, t]);
116
+ }, [detailQuery, id, refreshDetailWithoutAutoMarkRead, retryLastMutation, runMutation, t]);
95
117
  const archiveConversation = React.useCallback(async (messageId) => {
96
118
  const targetMessageId = messageId ?? id;
97
119
  await runConversationAction("archiveConversation", {
@@ -132,19 +154,26 @@ function useMessageDetailsActions({
132
154
  const executeAction = React.useCallback(async (action, payload) => {
133
155
  setExecutingActionId(action.id);
134
156
  try {
135
- const call = await apiCall(
136
- `/api/messages/${encodeURIComponent(id)}/actions/${encodeURIComponent(action.id)}`,
137
- {
138
- method: "POST",
139
- headers: { "Content-Type": "application/json" },
140
- body: JSON.stringify(payload ?? {})
141
- }
142
- );
143
- if (!call.ok) {
144
- throw new Error(
145
- toErrorMessage(call.result) ?? t("messages.errors.actionFailed", "Failed to execute action.")
146
- );
147
- }
157
+ const call = await runMutation({
158
+ operation: async () => {
159
+ const result = await apiCall(
160
+ `/api/messages/${encodeURIComponent(id)}/actions/${encodeURIComponent(action.id)}`,
161
+ {
162
+ method: "POST",
163
+ headers: { "Content-Type": "application/json" },
164
+ body: JSON.stringify(payload ?? {})
165
+ }
166
+ );
167
+ if (!result.ok) {
168
+ throw new Error(
169
+ toErrorMessage(result.result) ?? t("messages.errors.actionFailed", "Failed to execute action.")
170
+ );
171
+ }
172
+ return result;
173
+ },
174
+ context: { resourceKind: "message", messageId: id, action: "execute-action", retryLastMutation },
175
+ mutationPayload: { messageId: id, action: "execute-action", actionId: action.id }
176
+ });
148
177
  flash(t("messages.flash.actionSuccess", "Action completed."), "success");
149
178
  const redirectTarget = call.result?.result?.redirect;
150
179
  if (typeof redirectTarget === "string" && redirectTarget.trim().length > 0 && isSafeNavigationHref(redirectTarget)) {
@@ -160,7 +189,7 @@ function useMessageDetailsActions({
160
189
  } finally {
161
190
  setExecutingActionId(null);
162
191
  }
163
- }, [detailQuery, id, t]);
192
+ }, [detailQuery, id, retryLastMutation, runMutation, t]);
164
193
  const handleExecuteAction = React.useCallback(async (action, payload) => {
165
194
  if (executingActionId || detail?.actionTaken) return;
166
195
  if (action.confirmRequired) {