@open-mercato/core 0.6.6-develop.6229.1.ebe6706732 → 0.6.6-develop.6241.1.47c01ad4ed

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 (125) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/auth/commands/users.js +1 -0
  3. package/dist/modules/auth/commands/users.js.map +2 -2
  4. package/dist/modules/communication_channels/data/enrichers.js +9 -1
  5. package/dist/modules/communication_channels/data/enrichers.js.map +2 -2
  6. package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.js +3 -5
  7. package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.js.map +2 -2
  8. package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.js.map +2 -2
  9. package/dist/modules/customers/api/pipeline-stages/reorder/route.js +39 -1
  10. package/dist/modules/customers/api/pipeline-stages/reorder/route.js.map +2 -2
  11. package/dist/modules/customers/api/pipeline-stages/route.js +107 -12
  12. package/dist/modules/customers/api/pipeline-stages/route.js.map +2 -2
  13. package/dist/modules/customers/api/pipelines/route.js +107 -12
  14. package/dist/modules/customers/api/pipelines/route.js.map +2 -2
  15. package/dist/modules/customers/api/settings/address-format/route.js +33 -1
  16. package/dist/modules/customers/api/settings/address-format/route.js.map +2 -2
  17. package/dist/modules/customers/api/tags/assign/route.js +37 -0
  18. package/dist/modules/customers/api/tags/assign/route.js.map +2 -2
  19. package/dist/modules/customers/api/tags/unassign/route.js +37 -0
  20. package/dist/modules/customers/api/tags/unassign/route.js.map +2 -2
  21. package/dist/modules/entities/api/encryption.js +109 -38
  22. package/dist/modules/entities/api/encryption.js.map +2 -2
  23. package/dist/modules/entities/components/EncryptionManager.js +22 -9
  24. package/dist/modules/entities/components/EncryptionManager.js.map +2 -2
  25. package/dist/modules/integrations/api/[id]/credentials/route.js +23 -1
  26. package/dist/modules/integrations/api/[id]/credentials/route.js.map +2 -2
  27. package/dist/modules/integrations/api/[id]/route.js +10 -3
  28. package/dist/modules/integrations/api/[id]/route.js.map +2 -2
  29. package/dist/modules/integrations/api/[id]/state/route.js +20 -5
  30. package/dist/modules/integrations/api/[id]/state/route.js.map +2 -2
  31. package/dist/modules/integrations/api/[id]/version/route.js +17 -1
  32. package/dist/modules/integrations/api/[id]/version/route.js.map +2 -2
  33. package/dist/modules/integrations/api/route.js +2 -0
  34. package/dist/modules/integrations/api/route.js.map +2 -2
  35. package/dist/modules/integrations/backend/integrations/[id]/page.js +36 -19
  36. package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
  37. package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js +13 -9
  38. package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js.map +2 -2
  39. package/dist/modules/integrations/backend/integrations/page.js +4 -3
  40. package/dist/modules/integrations/backend/integrations/page.js.map +2 -2
  41. package/dist/modules/integrations/data/entities.js +2 -2
  42. package/dist/modules/integrations/data/entities.js.map +2 -2
  43. package/dist/modules/integrations/lib/credentials-service.js +32 -0
  44. package/dist/modules/integrations/lib/credentials-service.js.map +2 -2
  45. package/dist/modules/integrations/lib/state-service.js +4 -2
  46. package/dist/modules/integrations/lib/state-service.js.map +2 -2
  47. package/dist/modules/notifications/lib/notificationService.js +42 -10
  48. package/dist/modules/notifications/lib/notificationService.js.map +2 -2
  49. package/dist/modules/payment_gateways/api/status/route.js +91 -6
  50. package/dist/modules/payment_gateways/api/status/route.js.map +2 -2
  51. package/dist/modules/payment_gateways/backend/payment-gateways/page.js +25 -11
  52. package/dist/modules/payment_gateways/backend/payment-gateways/page.js.map +2 -2
  53. package/dist/modules/sales/backend/sales/channels/offers/page.js +25 -7
  54. package/dist/modules/sales/backend/sales/channels/offers/page.js.map +2 -2
  55. package/dist/modules/sales/backend/sales/channels/page.js +25 -7
  56. package/dist/modules/sales/backend/sales/channels/page.js.map +2 -2
  57. package/dist/modules/sales/components/AdjustmentKindSettings.js +52 -24
  58. package/dist/modules/sales/components/AdjustmentKindSettings.js.map +2 -2
  59. package/dist/modules/sales/components/DocumentNumberSettings.js +26 -9
  60. package/dist/modules/sales/components/DocumentNumberSettings.js.map +2 -2
  61. package/dist/modules/sales/components/OrderEditingSettings.js +26 -9
  62. package/dist/modules/sales/components/OrderEditingSettings.js.map +2 -2
  63. package/dist/modules/sales/components/StatusSettings.js +66 -31
  64. package/dist/modules/sales/components/StatusSettings.js.map +2 -2
  65. package/dist/modules/sales/components/channels/SalesChannelOffersPanel.js +25 -7
  66. package/dist/modules/sales/components/channels/SalesChannelOffersPanel.js.map +2 -2
  67. package/dist/modules/shipping_carriers/api/cancel/route.js +38 -0
  68. package/dist/modules/shipping_carriers/api/cancel/route.js.map +2 -2
  69. package/dist/modules/shipping_carriers/api/shipments/route.js +12 -5
  70. package/dist/modules/shipping_carriers/api/shipments/route.js.map +2 -2
  71. package/dist/modules/staff/lib/timesheets-ui/TimerBar.js +7 -8
  72. package/dist/modules/staff/lib/timesheets-ui/TimerBar.js.map +2 -2
  73. package/dist/modules/staff/lib/timesheets-ui/timerErrors.js +12 -0
  74. package/dist/modules/staff/lib/timesheets-ui/timerErrors.js.map +7 -0
  75. package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js +4 -3
  76. package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js.map +2 -2
  77. package/dist/modules/workflows/backend/definitions/page.js +19 -10
  78. package/dist/modules/workflows/backend/definitions/page.js.map +2 -2
  79. package/package.json +7 -7
  80. package/src/modules/auth/commands/users.ts +1 -0
  81. package/src/modules/communication_channels/data/enrichers.ts +25 -0
  82. package/src/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.tsx +7 -6
  83. package/src/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.ts +5 -2
  84. package/src/modules/customers/api/pipeline-stages/reorder/route.ts +41 -1
  85. package/src/modules/customers/api/pipeline-stages/route.ts +112 -13
  86. package/src/modules/customers/api/pipelines/route.ts +112 -13
  87. package/src/modules/customers/api/settings/address-format/route.ts +36 -1
  88. package/src/modules/customers/api/tags/assign/route.ts +39 -0
  89. package/src/modules/customers/api/tags/unassign/route.ts +39 -0
  90. package/src/modules/customers/i18n/de.json +1 -0
  91. package/src/modules/customers/i18n/en.json +1 -0
  92. package/src/modules/customers/i18n/es.json +1 -0
  93. package/src/modules/customers/i18n/pl.json +1 -0
  94. package/src/modules/entities/api/encryption.ts +122 -39
  95. package/src/modules/entities/components/EncryptionManager.tsx +28 -9
  96. package/src/modules/integrations/api/[id]/credentials/route.ts +23 -0
  97. package/src/modules/integrations/api/[id]/route.ts +8 -1
  98. package/src/modules/integrations/api/[id]/state/route.ts +20 -4
  99. package/src/modules/integrations/api/[id]/version/route.ts +18 -1
  100. package/src/modules/integrations/api/route.ts +3 -0
  101. package/src/modules/integrations/backend/integrations/[id]/page.tsx +39 -20
  102. package/src/modules/integrations/backend/integrations/bundle/[id]/page.tsx +19 -11
  103. package/src/modules/integrations/backend/integrations/page.tsx +8 -5
  104. package/src/modules/integrations/data/entities.ts +2 -2
  105. package/src/modules/integrations/lib/credentials-service.ts +35 -0
  106. package/src/modules/integrations/lib/state-service.ts +3 -0
  107. package/src/modules/notifications/lib/notificationService.ts +74 -11
  108. package/src/modules/payment_gateways/api/status/route.ts +97 -5
  109. package/src/modules/payment_gateways/backend/payment-gateways/page.tsx +27 -11
  110. package/src/modules/sales/backend/sales/channels/offers/page.tsx +31 -7
  111. package/src/modules/sales/backend/sales/channels/page.tsx +31 -7
  112. package/src/modules/sales/components/AdjustmentKindSettings.tsx +60 -24
  113. package/src/modules/sales/components/DocumentNumberSettings.tsx +32 -10
  114. package/src/modules/sales/components/OrderEditingSettings.tsx +32 -10
  115. package/src/modules/sales/components/StatusSettings.tsx +74 -33
  116. package/src/modules/sales/components/channels/SalesChannelOffersPanel.tsx +30 -6
  117. package/src/modules/shipping_carriers/api/cancel/route.ts +46 -0
  118. package/src/modules/shipping_carriers/api/shipments/route.ts +21 -6
  119. package/src/modules/staff/i18n/de.json +5 -5
  120. package/src/modules/staff/i18n/es.json +5 -5
  121. package/src/modules/staff/i18n/pl.json +5 -5
  122. package/src/modules/staff/lib/timesheets-ui/TimerBar.tsx +7 -8
  123. package/src/modules/staff/lib/timesheets-ui/timerErrors.ts +23 -0
  124. package/src/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.tsx +4 -3
  125. package/src/modules/workflows/backend/definitions/page.tsx +19 -10
@@ -3,13 +3,15 @@
3
3
  import * as React from 'react'
4
4
  import type { InjectionWidgetComponentProps } from '@open-mercato/shared/modules/widgets/injection'
5
5
  import { useT } from '@open-mercato/shared/lib/i18n/context'
6
- import { sanitizeChannelHtml } from '../../../lib/sanitize-channel-html'
7
6
 
8
7
  type ChannelPayloadEnrichment = {
9
8
  channelContentType: string | null
10
9
  channelPayload: Record<string, unknown> | null
11
10
  interactiveState: Record<string, unknown> | null
12
11
  channelMetadata: Record<string, unknown> | null
12
+ // Email HTML is sanitized server-side by the channel-payload enricher and
13
+ // delivered here ready to render — the client never imports `sanitize-html`.
14
+ sanitizedHtml: string | null
13
15
  }
14
16
 
15
17
  type MessageWithPayload = Record<string, unknown> & {
@@ -24,11 +26,10 @@ export default function ChannelPayloadRendererWidget({
24
26
  const payload = data?._channelPayload ?? null
25
27
  if (!payload || !payload.channelContentType) return null
26
28
 
27
- const { channelContentType, channelPayload } = payload
29
+ const { channelContentType, channelPayload, sanitizedHtml } = payload
28
30
 
29
- // Email / HTML — sanitize, then render.
30
- if (channelContentType.startsWith('email/') && typeof channelPayload?.html === 'string') {
31
- const sanitized = sanitizeChannelHtml(channelPayload.html as string)
31
+ // Email / HTML — render the HTML the server already sanitized.
32
+ if (channelContentType.startsWith('email/') && typeof sanitizedHtml === 'string') {
32
33
  return (
33
34
  <section
34
35
  className="rounded-md border bg-card p-4 text-sm"
@@ -37,7 +38,7 @@ export default function ChannelPayloadRendererWidget({
37
38
  'Channel payload — email',
38
39
  )}
39
40
  >
40
- <div dangerouslySetInnerHTML={{ __html: sanitized }} />
41
+ <div dangerouslySetInnerHTML={{ __html: sanitizedHtml }} />
41
42
  </section>
42
43
  )
43
44
  }
@@ -6,8 +6,11 @@ import ChannelPayloadRendererWidget from './widget.client'
6
6
  * (Slack Block Kit, WhatsApp interactive, email MIME, etc.) below the body in the
7
7
  * messages detail page (`detail:messages:message:body:after`).
8
8
  *
9
- * For `email/*` content types, HTML is sanitized via `sanitizeChannelHtml` before
10
- * being passed to `dangerouslySetInnerHTML` — see SPEC-045d §4.6.
9
+ * For `email/*` content types, HTML is sanitized server-side by the
10
+ * channel-payload enricher (`data/enrichers.ts`, via `sanitizeChannelHtml`) and
11
+ * delivered as `_channelPayload.sanitizedHtml`, which this widget renders with
12
+ * `dangerouslySetInnerHTML` — keeping `sanitize-html` off the client bundle.
13
+ * See SPEC-045d §4.6.
11
14
  *
12
15
  * Provider packages override this via UMES component replacement (handle
13
16
  * `widget:communication_channels.injection.channel-payload-renderer`) to render
@@ -8,8 +8,14 @@ import { pipelineStageReorderSchema, type PipelineStageReorderInput } from '../.
8
8
  import { withScopedPayload } from '../../utils'
9
9
  import { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
10
10
  import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
11
+ import {
12
+ runCrudMutationGuardAfterSuccess,
13
+ validateCrudMutationGuard,
14
+ } from '@open-mercato/shared/lib/crud/mutation-guard'
11
15
  import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
12
16
 
17
+ const PIPELINE_STAGE_RESOURCE_KIND = 'customers.pipelineStage'
18
+
13
19
  export const metadata = {
14
20
  POST: { requireAuth: true, requireFeatures: ['customers.pipelines.manage'] },
15
21
  }
@@ -29,15 +35,49 @@ export async function POST(req: Request) {
29
35
  organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),
30
36
  request: req,
31
37
  }
38
+ const organizationId = scope?.selectedId ?? auth.orgId ?? null
39
+ const tenantId = auth.tenantId ?? null
40
+ if (!organizationId || !tenantId) {
41
+ return NextResponse.json({ error: translate('customers.errors.context_required', 'Organization and tenant context required') }, { status: 400 })
42
+ }
32
43
 
33
44
  const body = await req.json().catch(() => ({}))
34
45
  const scoped = withScopedPayload(body, ctx, translate)
46
+ const input = pipelineStageReorderSchema.parse(scoped)
47
+
48
+ const guardResult = await validateCrudMutationGuard(ctx.container, {
49
+ tenantId,
50
+ organizationId,
51
+ userId: auth.sub,
52
+ resourceKind: PIPELINE_STAGE_RESOURCE_KIND,
53
+ resourceId: organizationId,
54
+ operation: 'custom',
55
+ requestMethod: req.method,
56
+ requestHeaders: req.headers,
57
+ mutationPayload: input,
58
+ })
59
+ if (guardResult && !guardResult.ok) {
60
+ return NextResponse.json(guardResult.body, { status: guardResult.status })
61
+ }
35
62
 
36
63
  const commandBus = (ctx.container.resolve('commandBus') as CommandBus)
37
64
  await commandBus.execute<PipelineStageReorderInput, void>(
38
65
  'customers.pipeline-stages.reorder',
39
- { input: pipelineStageReorderSchema.parse(scoped), ctx },
66
+ { input, ctx },
40
67
  )
68
+ if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
69
+ await runCrudMutationGuardAfterSuccess(ctx.container, {
70
+ tenantId,
71
+ organizationId,
72
+ userId: auth.sub,
73
+ resourceKind: PIPELINE_STAGE_RESOURCE_KIND,
74
+ resourceId: organizationId,
75
+ operation: 'custom',
76
+ requestMethod: req.method,
77
+ requestHeaders: req.headers,
78
+ metadata: guardResult.metadata ?? null,
79
+ })
80
+ }
41
81
  return NextResponse.json({ ok: true })
42
82
  } catch (err) {
43
83
  if (isCrudHttpError(err)) {
@@ -18,8 +18,14 @@ import { withScopedPayload } from '../utils'
18
18
  import { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
19
19
  import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
20
20
  import { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'
21
+ import {
22
+ runCrudMutationGuardAfterSuccess,
23
+ validateCrudMutationGuard,
24
+ } from '@open-mercato/shared/lib/crud/mutation-guard'
21
25
  import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
22
26
 
27
+ const PIPELINE_STAGE_RESOURCE_KIND = 'customers.pipelineStage'
28
+
23
29
  export const metadata = {
24
30
  GET: { requireAuth: true, requireFeatures: ['customers.pipelines.view'] },
25
31
  POST: { requireAuth: true, requireFeatures: ['customers.pipelines.manage'] },
@@ -29,7 +35,7 @@ export const metadata = {
29
35
 
30
36
  async function buildContext(
31
37
  req: Request
32
- ): Promise<{ ctx: CommandRuntimeContext; organizationId: string | null; tenantId: string | null }> {
38
+ ): Promise<{ ctx: CommandRuntimeContext; organizationId: string | null; tenantId: string | null; translate: (key: string, fallback?: string) => string }> {
33
39
  const container = await createRequestContainer()
34
40
  const auth = await getAuthFromRequest(req)
35
41
  const { translate } = await resolveTranslations()
@@ -45,14 +51,14 @@ async function buildContext(
45
51
  }
46
52
  const organizationId = scope?.selectedId ?? auth.orgId ?? null
47
53
  const tenantId = auth.tenantId ?? null
48
- return { ctx, organizationId, tenantId }
54
+ return { ctx, organizationId, tenantId, translate }
49
55
  }
50
56
 
51
57
  export async function GET(req: Request) {
52
58
  try {
53
- const { ctx, organizationId, tenantId } = await buildContext(req)
59
+ const { ctx, organizationId, tenantId, translate } = await buildContext(req)
54
60
  if (!organizationId || !tenantId) {
55
- return NextResponse.json({ error: 'Organization and tenant context required' }, { status: 400 })
61
+ return NextResponse.json({ error: translate('customers.errors.context_required', 'Organization and tenant context required') }, { status: 400 })
56
62
  }
57
63
  const url = new URL(req.url)
58
64
  const pipelineId = url.searchParams.get('pipelineId')
@@ -102,16 +108,47 @@ export async function GET(req: Request) {
102
108
 
103
109
  export async function POST(req: Request) {
104
110
  try {
105
- const { ctx } = await buildContext(req)
111
+ const { ctx, organizationId, tenantId, translate } = await buildContext(req)
112
+ if (!organizationId || !tenantId) {
113
+ return NextResponse.json({ error: translate('customers.errors.context_required', 'Organization and tenant context required') }, { status: 400 })
114
+ }
106
115
  const body = await req.json().catch(() => ({}))
107
- const { translate } = await resolveTranslations()
108
116
  const scoped = withScopedPayload(body, ctx, translate)
117
+ const input = pipelineStageCreateSchema.parse(scoped)
118
+
119
+ const guardResult = await validateCrudMutationGuard(ctx.container, {
120
+ tenantId,
121
+ organizationId,
122
+ userId: ctx.auth!.sub,
123
+ resourceKind: PIPELINE_STAGE_RESOURCE_KIND,
124
+ resourceId: organizationId,
125
+ operation: 'create',
126
+ requestMethod: req.method,
127
+ requestHeaders: req.headers,
128
+ mutationPayload: input,
129
+ })
130
+ if (guardResult && !guardResult.ok) {
131
+ return NextResponse.json(guardResult.body, { status: guardResult.status })
132
+ }
109
133
 
110
134
  const commandBus = (ctx.container.resolve('commandBus') as CommandBus)
111
135
  const { result, logEntry } = await commandBus.execute<PipelineStageCreateInput, { stageId: string }>(
112
136
  'customers.pipeline-stages.create',
113
- { input: pipelineStageCreateSchema.parse(scoped), ctx },
137
+ { input, ctx },
114
138
  )
139
+ if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
140
+ await runCrudMutationGuardAfterSuccess(ctx.container, {
141
+ tenantId,
142
+ organizationId,
143
+ userId: ctx.auth!.sub,
144
+ resourceKind: PIPELINE_STAGE_RESOURCE_KIND,
145
+ resourceId: result?.stageId ?? organizationId,
146
+ operation: 'create',
147
+ requestMethod: req.method,
148
+ requestHeaders: req.headers,
149
+ metadata: guardResult.metadata ?? null,
150
+ })
151
+ }
115
152
  const response = NextResponse.json({ id: result?.stageId ?? null }, { status: 201 })
116
153
  if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {
117
154
  response.headers.set(
@@ -139,16 +176,47 @@ export async function POST(req: Request) {
139
176
 
140
177
  export async function PUT(req: Request) {
141
178
  try {
142
- const { ctx } = await buildContext(req)
179
+ const { ctx, organizationId, tenantId, translate } = await buildContext(req)
180
+ if (!organizationId || !tenantId) {
181
+ return NextResponse.json({ error: translate('customers.errors.context_required', 'Organization and tenant context required') }, { status: 400 })
182
+ }
143
183
  const body = await req.json().catch(() => ({}))
144
- const { translate } = await resolveTranslations()
145
184
  const scoped = withScopedPayload(body, ctx, translate)
185
+ const input = pipelineStageUpdateSchema.parse(scoped)
186
+
187
+ const guardResult = await validateCrudMutationGuard(ctx.container, {
188
+ tenantId,
189
+ organizationId,
190
+ userId: ctx.auth!.sub,
191
+ resourceKind: PIPELINE_STAGE_RESOURCE_KIND,
192
+ resourceId: input.id,
193
+ operation: 'update',
194
+ requestMethod: req.method,
195
+ requestHeaders: req.headers,
196
+ mutationPayload: input,
197
+ })
198
+ if (guardResult && !guardResult.ok) {
199
+ return NextResponse.json(guardResult.body, { status: guardResult.status })
200
+ }
146
201
 
147
202
  const commandBus = (ctx.container.resolve('commandBus') as CommandBus)
148
203
  const { logEntry } = await commandBus.execute<PipelineStageUpdateInput, void>(
149
204
  'customers.pipeline-stages.update',
150
- { input: pipelineStageUpdateSchema.parse(scoped), ctx },
205
+ { input, ctx },
151
206
  )
207
+ if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
208
+ await runCrudMutationGuardAfterSuccess(ctx.container, {
209
+ tenantId,
210
+ organizationId,
211
+ userId: ctx.auth!.sub,
212
+ resourceKind: PIPELINE_STAGE_RESOURCE_KIND,
213
+ resourceId: input.id,
214
+ operation: 'update',
215
+ requestMethod: req.method,
216
+ requestHeaders: req.headers,
217
+ metadata: guardResult.metadata ?? null,
218
+ })
219
+ }
152
220
  const response = NextResponse.json({ ok: true })
153
221
  if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {
154
222
  response.headers.set(
@@ -176,16 +244,47 @@ export async function PUT(req: Request) {
176
244
 
177
245
  export async function DELETE(req: Request) {
178
246
  try {
179
- const { ctx } = await buildContext(req)
247
+ const { ctx, organizationId, tenantId, translate } = await buildContext(req)
248
+ if (!organizationId || !tenantId) {
249
+ return NextResponse.json({ error: translate('customers.errors.context_required', 'Organization and tenant context required') }, { status: 400 })
250
+ }
180
251
  const body = await req.json().catch(() => ({}))
181
- const { translate } = await resolveTranslations()
182
252
  const scoped = withScopedPayload(body, ctx, translate)
253
+ const input = pipelineStageDeleteSchema.parse(scoped)
254
+
255
+ const guardResult = await validateCrudMutationGuard(ctx.container, {
256
+ tenantId,
257
+ organizationId,
258
+ userId: ctx.auth!.sub,
259
+ resourceKind: PIPELINE_STAGE_RESOURCE_KIND,
260
+ resourceId: input.id,
261
+ operation: 'delete',
262
+ requestMethod: req.method,
263
+ requestHeaders: req.headers,
264
+ mutationPayload: input,
265
+ })
266
+ if (guardResult && !guardResult.ok) {
267
+ return NextResponse.json(guardResult.body, { status: guardResult.status })
268
+ }
183
269
 
184
270
  const commandBus = (ctx.container.resolve('commandBus') as CommandBus)
185
271
  await commandBus.execute<PipelineStageDeleteInput, void>(
186
272
  'customers.pipeline-stages.delete',
187
- { input: pipelineStageDeleteSchema.parse(scoped), ctx },
273
+ { input, ctx },
188
274
  )
275
+ if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
276
+ await runCrudMutationGuardAfterSuccess(ctx.container, {
277
+ tenantId,
278
+ organizationId,
279
+ userId: ctx.auth!.sub,
280
+ resourceKind: PIPELINE_STAGE_RESOURCE_KIND,
281
+ resourceId: input.id,
282
+ operation: 'delete',
283
+ requestMethod: req.method,
284
+ requestHeaders: req.headers,
285
+ metadata: guardResult.metadata ?? null,
286
+ })
287
+ }
189
288
  return NextResponse.json({ ok: true })
190
289
  } catch (err) {
191
290
  if (isCrudHttpError(err)) {
@@ -18,8 +18,14 @@ import { withScopedPayload } from '../utils'
18
18
  import { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
19
19
  import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
20
20
  import { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'
21
+ import {
22
+ runCrudMutationGuardAfterSuccess,
23
+ validateCrudMutationGuard,
24
+ } from '@open-mercato/shared/lib/crud/mutation-guard'
21
25
  import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
22
26
 
27
+ const PIPELINE_RESOURCE_KIND = 'customers.pipeline'
28
+
23
29
  export const metadata = {
24
30
  GET: { requireAuth: true, requireFeatures: ['customers.pipelines.view'] },
25
31
  POST: { requireAuth: true, requireFeatures: ['customers.pipelines.manage'] },
@@ -29,7 +35,7 @@ export const metadata = {
29
35
 
30
36
  async function buildContext(
31
37
  req: Request
32
- ): Promise<{ ctx: CommandRuntimeContext; organizationId: string | null; tenantId: string | null }> {
38
+ ): Promise<{ ctx: CommandRuntimeContext; organizationId: string | null; tenantId: string | null; translate: (key: string, fallback?: string) => string }> {
33
39
  const container = await createRequestContainer()
34
40
  const auth = await getAuthFromRequest(req)
35
41
  const { translate } = await resolveTranslations()
@@ -45,14 +51,14 @@ async function buildContext(
45
51
  }
46
52
  const organizationId = scope?.selectedId ?? auth.orgId ?? null
47
53
  const tenantId = auth.tenantId ?? null
48
- return { ctx, organizationId, tenantId }
54
+ return { ctx, organizationId, tenantId, translate }
49
55
  }
50
56
 
51
57
  export async function GET(req: Request) {
52
58
  try {
53
- const { ctx, organizationId, tenantId } = await buildContext(req)
59
+ const { ctx, organizationId, tenantId, translate } = await buildContext(req)
54
60
  if (!organizationId || !tenantId) {
55
- return NextResponse.json({ error: 'Organization and tenant context required' }, { status: 400 })
61
+ return NextResponse.json({ error: translate('customers.errors.context_required', 'Organization and tenant context required') }, { status: 400 })
56
62
  }
57
63
  const url = new URL(req.url)
58
64
  const isDefaultParam = url.searchParams.get('isDefault')
@@ -84,16 +90,47 @@ export async function GET(req: Request) {
84
90
 
85
91
  export async function POST(req: Request) {
86
92
  try {
87
- const { ctx } = await buildContext(req)
93
+ const { ctx, organizationId, tenantId, translate } = await buildContext(req)
94
+ if (!organizationId || !tenantId) {
95
+ return NextResponse.json({ error: translate('customers.errors.context_required', 'Organization and tenant context required') }, { status: 400 })
96
+ }
88
97
  const body = await req.json().catch(() => ({}))
89
- const { translate } = await resolveTranslations()
90
98
  const scoped = withScopedPayload(body, ctx, translate)
99
+ const input = pipelineCreateSchema.parse(scoped)
100
+
101
+ const guardResult = await validateCrudMutationGuard(ctx.container, {
102
+ tenantId,
103
+ organizationId,
104
+ userId: ctx.auth!.sub,
105
+ resourceKind: PIPELINE_RESOURCE_KIND,
106
+ resourceId: organizationId,
107
+ operation: 'create',
108
+ requestMethod: req.method,
109
+ requestHeaders: req.headers,
110
+ mutationPayload: input,
111
+ })
112
+ if (guardResult && !guardResult.ok) {
113
+ return NextResponse.json(guardResult.body, { status: guardResult.status })
114
+ }
91
115
 
92
116
  const commandBus = (ctx.container.resolve('commandBus') as CommandBus)
93
117
  const { result, logEntry } = await commandBus.execute<PipelineCreateInput, { pipelineId: string }>(
94
118
  'customers.pipelines.create',
95
- { input: pipelineCreateSchema.parse(scoped), ctx },
119
+ { input, ctx },
96
120
  )
121
+ if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
122
+ await runCrudMutationGuardAfterSuccess(ctx.container, {
123
+ tenantId,
124
+ organizationId,
125
+ userId: ctx.auth!.sub,
126
+ resourceKind: PIPELINE_RESOURCE_KIND,
127
+ resourceId: result?.pipelineId ?? organizationId,
128
+ operation: 'create',
129
+ requestMethod: req.method,
130
+ requestHeaders: req.headers,
131
+ metadata: guardResult.metadata ?? null,
132
+ })
133
+ }
97
134
  const response = NextResponse.json({ id: result?.pipelineId ?? null }, { status: 201 })
98
135
  if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {
99
136
  response.headers.set(
@@ -121,16 +158,47 @@ export async function POST(req: Request) {
121
158
 
122
159
  export async function PUT(req: Request) {
123
160
  try {
124
- const { ctx } = await buildContext(req)
161
+ const { ctx, organizationId, tenantId, translate } = await buildContext(req)
162
+ if (!organizationId || !tenantId) {
163
+ return NextResponse.json({ error: translate('customers.errors.context_required', 'Organization and tenant context required') }, { status: 400 })
164
+ }
125
165
  const body = await req.json().catch(() => ({}))
126
- const { translate } = await resolveTranslations()
127
166
  const scoped = withScopedPayload(body, ctx, translate)
167
+ const input = pipelineUpdateSchema.parse(scoped)
168
+
169
+ const guardResult = await validateCrudMutationGuard(ctx.container, {
170
+ tenantId,
171
+ organizationId,
172
+ userId: ctx.auth!.sub,
173
+ resourceKind: PIPELINE_RESOURCE_KIND,
174
+ resourceId: input.id,
175
+ operation: 'update',
176
+ requestMethod: req.method,
177
+ requestHeaders: req.headers,
178
+ mutationPayload: input,
179
+ })
180
+ if (guardResult && !guardResult.ok) {
181
+ return NextResponse.json(guardResult.body, { status: guardResult.status })
182
+ }
128
183
 
129
184
  const commandBus = (ctx.container.resolve('commandBus') as CommandBus)
130
185
  const { logEntry } = await commandBus.execute<PipelineUpdateInput, void>(
131
186
  'customers.pipelines.update',
132
- { input: pipelineUpdateSchema.parse(scoped), ctx },
187
+ { input, ctx },
133
188
  )
189
+ if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
190
+ await runCrudMutationGuardAfterSuccess(ctx.container, {
191
+ tenantId,
192
+ organizationId,
193
+ userId: ctx.auth!.sub,
194
+ resourceKind: PIPELINE_RESOURCE_KIND,
195
+ resourceId: input.id,
196
+ operation: 'update',
197
+ requestMethod: req.method,
198
+ requestHeaders: req.headers,
199
+ metadata: guardResult.metadata ?? null,
200
+ })
201
+ }
134
202
  const response = NextResponse.json({ ok: true })
135
203
  if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {
136
204
  response.headers.set(
@@ -158,16 +226,47 @@ export async function PUT(req: Request) {
158
226
 
159
227
  export async function DELETE(req: Request) {
160
228
  try {
161
- const { ctx } = await buildContext(req)
229
+ const { ctx, organizationId, tenantId, translate } = await buildContext(req)
230
+ if (!organizationId || !tenantId) {
231
+ return NextResponse.json({ error: translate('customers.errors.context_required', 'Organization and tenant context required') }, { status: 400 })
232
+ }
162
233
  const body = await req.json().catch(() => ({}))
163
- const { translate } = await resolveTranslations()
164
234
  const scoped = withScopedPayload(body, ctx, translate)
235
+ const input = pipelineDeleteSchema.parse(scoped)
236
+
237
+ const guardResult = await validateCrudMutationGuard(ctx.container, {
238
+ tenantId,
239
+ organizationId,
240
+ userId: ctx.auth!.sub,
241
+ resourceKind: PIPELINE_RESOURCE_KIND,
242
+ resourceId: input.id,
243
+ operation: 'delete',
244
+ requestMethod: req.method,
245
+ requestHeaders: req.headers,
246
+ mutationPayload: input,
247
+ })
248
+ if (guardResult && !guardResult.ok) {
249
+ return NextResponse.json(guardResult.body, { status: guardResult.status })
250
+ }
165
251
 
166
252
  const commandBus = (ctx.container.resolve('commandBus') as CommandBus)
167
253
  await commandBus.execute<PipelineDeleteInput, void>(
168
254
  'customers.pipelines.delete',
169
- { input: pipelineDeleteSchema.parse(scoped), ctx },
255
+ { input, ctx },
170
256
  )
257
+ if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
258
+ await runCrudMutationGuardAfterSuccess(ctx.container, {
259
+ tenantId,
260
+ organizationId,
261
+ userId: ctx.auth!.sub,
262
+ resourceKind: PIPELINE_RESOURCE_KIND,
263
+ resourceId: input.id,
264
+ operation: 'delete',
265
+ requestMethod: req.method,
266
+ requestHeaders: req.headers,
267
+ metadata: guardResult.metadata ?? null,
268
+ })
269
+ }
171
270
  return NextResponse.json({ ok: true })
172
271
  } catch (err) {
173
272
  if (isCrudHttpError(err)) {
@@ -7,12 +7,18 @@ import type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib
7
7
  import type { EntityManager } from '@mikro-orm/postgresql'
8
8
  import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
9
9
  import { CrudHttpError, 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
  import { customerSettingsUpsertSchema, type CustomerSettingsUpsertInput } from '../../../data/validators'
11
15
  import { loadCustomerSettings } from '../../../commands/settings'
12
16
  import type { CustomerAddressFormat } from '../../../data/entities'
13
17
  import { withScopedPayload } from '../../utils'
14
18
  import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
15
19
 
20
+ const SETTINGS_RESOURCE_KIND = 'customers.settings'
21
+
16
22
  export const metadata = {
17
23
  GET: { requireAuth: true, requireFeatures: ['customers.settings.manage'] },
18
24
  PUT: { requireAuth: true, requireFeatures: ['customers.settings.manage'] },
@@ -78,17 +84,46 @@ export async function GET(req: Request) {
78
84
 
79
85
  export async function PUT(req: Request) {
80
86
  try {
81
- const { ctx, translate } = await resolveSettingsContext(req)
87
+ const { ctx, tenantId, organizationId, translate } = await resolveSettingsContext(req)
82
88
  const payload = await req.json().catch(() => ({}))
83
89
  const scoped = withScopedPayload(payload, ctx, translate)
84
90
  const input = customerSettingsUpsertSchema.parse(scoped)
85
91
 
92
+ const guardResult = await validateCrudMutationGuard(ctx.container, {
93
+ tenantId,
94
+ organizationId,
95
+ userId: ctx.auth!.sub,
96
+ resourceKind: SETTINGS_RESOURCE_KIND,
97
+ resourceId: organizationId,
98
+ operation: 'update',
99
+ requestMethod: req.method,
100
+ requestHeaders: req.headers,
101
+ mutationPayload: input,
102
+ })
103
+ if (guardResult && !guardResult.ok) {
104
+ return NextResponse.json(guardResult.body, { status: guardResult.status })
105
+ }
106
+
86
107
  const commandBus = (ctx.container.resolve('commandBus') as CommandBus)
87
108
  const { result } = await commandBus.execute<CustomerSettingsUpsertInput, { settingsId: string; addressFormat: CustomerAddressFormat }>(
88
109
  'customers.settings.save',
89
110
  { input, ctx },
90
111
  )
91
112
 
113
+ if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
114
+ await runCrudMutationGuardAfterSuccess(ctx.container, {
115
+ tenantId,
116
+ organizationId,
117
+ userId: ctx.auth!.sub,
118
+ resourceKind: SETTINGS_RESOURCE_KIND,
119
+ resourceId: organizationId,
120
+ operation: 'update',
121
+ requestMethod: req.method,
122
+ requestHeaders: req.headers,
123
+ metadata: guardResult.metadata ?? null,
124
+ })
125
+ }
126
+
92
127
  return NextResponse.json({
93
128
  addressFormat: result?.addressFormat ?? input.addressFormat,
94
129
  })
@@ -9,8 +9,14 @@ import { tagAssignmentSchema, type TagAssignmentInput } from '../../../data/vali
9
9
  import { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
10
10
  import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
11
11
  import { withScopedPayload } from '../../utils'
12
+ import {
13
+ runCrudMutationGuardAfterSuccess,
14
+ validateCrudMutationGuard,
15
+ } from '@open-mercato/shared/lib/crud/mutation-guard'
12
16
  import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
13
17
 
18
+ const TAG_ASSIGNMENT_RESOURCE_KIND = 'customers.tagAssignment'
19
+
14
20
  export const metadata = {
15
21
  POST: { requireAuth: true, requireFeatures: ['customers.activities.manage'] },
16
22
  }
@@ -37,15 +43,48 @@ async function buildContext(
37
43
  export async function POST(req: Request) {
38
44
  try {
39
45
  const { ctx, auth, translate } = await buildContext(req)
46
+ const tenantId = auth!.tenantId
47
+ const organizationId = ctx.selectedOrganizationId
48
+ if (!tenantId || !organizationId) {
49
+ return NextResponse.json({ error: translate('customers.errors.context_required', 'Organization and tenant context required') }, { status: 400 })
50
+ }
40
51
  const body = await req.json().catch(() => ({}))
41
52
  const scoped = withScopedPayload(body, ctx, translate)
42
53
  const input = tagAssignmentSchema.parse(scoped)
43
54
 
55
+ const guardResult = await validateCrudMutationGuard(ctx.container, {
56
+ tenantId,
57
+ organizationId,
58
+ userId: auth!.sub,
59
+ resourceKind: TAG_ASSIGNMENT_RESOURCE_KIND,
60
+ resourceId: input.entityId,
61
+ operation: 'custom',
62
+ requestMethod: req.method,
63
+ requestHeaders: req.headers,
64
+ mutationPayload: input,
65
+ })
66
+ if (guardResult && !guardResult.ok) {
67
+ return NextResponse.json(guardResult.body, { status: guardResult.status })
68
+ }
69
+
44
70
  const commandBus = (ctx.container.resolve('commandBus') as CommandBus)
45
71
  const { result, logEntry } = await commandBus.execute<TagAssignmentInput, { assignmentId: string }>(
46
72
  'customers.tags.assign',
47
73
  { input, ctx },
48
74
  )
75
+ if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
76
+ await runCrudMutationGuardAfterSuccess(ctx.container, {
77
+ tenantId,
78
+ organizationId,
79
+ userId: auth!.sub,
80
+ resourceKind: TAG_ASSIGNMENT_RESOURCE_KIND,
81
+ resourceId: input.entityId,
82
+ operation: 'custom',
83
+ requestMethod: req.method,
84
+ requestHeaders: req.headers,
85
+ metadata: guardResult.metadata ?? null,
86
+ })
87
+ }
49
88
  const response = NextResponse.json({ id: result?.assignmentId ?? null }, { status: 201 })
50
89
  if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {
51
90
  response.headers.set(