@open-mercato/core 0.6.6-develop.6228.1.cf483c68b5 → 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
@@ -5,9 +5,14 @@ import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
5
5
  import { resolveOrganizationScopeForRequest } from "@open-mercato/core/modules/directory/utils/organizationScope";
6
6
  import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
7
7
  import { CrudHttpError, isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
8
+ import {
9
+ runCrudMutationGuardAfterSuccess,
10
+ validateCrudMutationGuard
11
+ } from "@open-mercato/shared/lib/crud/mutation-guard";
8
12
  import { customerSettingsUpsertSchema } from "../../../data/validators.js";
9
13
  import { loadCustomerSettings } from "../../../commands/settings.js";
10
14
  import { withScopedPayload } from "../../utils.js";
15
+ const SETTINGS_RESOURCE_KIND = "customers.settings";
11
16
  const metadata = {
12
17
  GET: { requireAuth: true, requireFeatures: ["customers.settings.manage"] },
13
18
  PUT: { requireAuth: true, requireFeatures: ["customers.settings.manage"] }
@@ -59,15 +64,42 @@ async function GET(req) {
59
64
  }
60
65
  async function PUT(req) {
61
66
  try {
62
- const { ctx, translate } = await resolveSettingsContext(req);
67
+ const { ctx, tenantId, organizationId, translate } = await resolveSettingsContext(req);
63
68
  const payload = await req.json().catch(() => ({}));
64
69
  const scoped = withScopedPayload(payload, ctx, translate);
65
70
  const input = customerSettingsUpsertSchema.parse(scoped);
71
+ const guardResult = await validateCrudMutationGuard(ctx.container, {
72
+ tenantId,
73
+ organizationId,
74
+ userId: ctx.auth.sub,
75
+ resourceKind: SETTINGS_RESOURCE_KIND,
76
+ resourceId: organizationId,
77
+ operation: "update",
78
+ requestMethod: req.method,
79
+ requestHeaders: req.headers,
80
+ mutationPayload: input
81
+ });
82
+ if (guardResult && !guardResult.ok) {
83
+ return NextResponse.json(guardResult.body, { status: guardResult.status });
84
+ }
66
85
  const commandBus = ctx.container.resolve("commandBus");
67
86
  const { result } = await commandBus.execute(
68
87
  "customers.settings.save",
69
88
  { input, ctx }
70
89
  );
90
+ if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
91
+ await runCrudMutationGuardAfterSuccess(ctx.container, {
92
+ tenantId,
93
+ organizationId,
94
+ userId: ctx.auth.sub,
95
+ resourceKind: SETTINGS_RESOURCE_KIND,
96
+ resourceId: organizationId,
97
+ operation: "update",
98
+ requestMethod: req.method,
99
+ requestHeaders: req.headers,
100
+ metadata: guardResult.metadata ?? null
101
+ });
102
+ }
71
103
  return NextResponse.json({
72
104
  addressFormat: result?.addressFormat ?? input.addressFormat
73
105
  });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/customers/api/settings/address-format/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { customerSettingsUpsertSchema, type CustomerSettingsUpsertInput } from '../../../data/validators'\nimport { loadCustomerSettings } from '../../../commands/settings'\nimport type { CustomerAddressFormat } from '../../../data/entities'\nimport { withScopedPayload } from '../../utils'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['customers.settings.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['customers.settings.manage'] },\n}\n\ntype SettingsRouteContext = {\n ctx: CommandRuntimeContext\n tenantId: string\n organizationId: string\n translate: (key: string, fallback?: string) => string\n em: EntityManager\n}\n\nasync function resolveSettingsContext(req: Request): Promise<SettingsRouteContext> {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const { translate } = await resolveTranslations()\n if (!auth || !auth.tenantId) {\n throw new CrudHttpError(401, { error: translate('customers.errors.unauthorized', 'Unauthorized') })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const organizationId = scope?.selectedId ?? auth.orgId ?? null\n if (!organizationId) {\n throw new CrudHttpError(400, { error: translate('customers.errors.organization_required', 'Organization context is required') })\n }\n\n const ctx: CommandRuntimeContext = {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: organizationId,\n organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),\n request: req,\n }\n\n const em = (container.resolve('em') as EntityManager)\n return {\n ctx,\n tenantId: auth.tenantId,\n organizationId,\n translate,\n em,\n }\n}\n\nexport async function GET(req: Request) {\n try {\n const { em, tenantId, organizationId } = await resolveSettingsContext(req)\n const record = await loadCustomerSettings(em, { tenantId, organizationId })\n return NextResponse.json({\n addressFormat: record?.addressFormat ?? 'line_first',\n })\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const { translate } = await resolveTranslations()\n console.error('customers.settings.address-format.get failed', err)\n return NextResponse.json({ error: translate('customers.errors.lookup_failed', 'Failed to load settings') }, { status: 400 })\n }\n}\n\nexport async function PUT(req: Request) {\n try {\n const { ctx, translate } = await resolveSettingsContext(req)\n const payload = await req.json().catch(() => ({}))\n const scoped = withScopedPayload(payload, ctx, translate)\n const input = customerSettingsUpsertSchema.parse(scoped)\n\n const commandBus = (ctx.container.resolve('commandBus') as CommandBus)\n const { result } = await commandBus.execute<CustomerSettingsUpsertInput, { settingsId: string; addressFormat: CustomerAddressFormat }>(\n 'customers.settings.save',\n { input, ctx },\n )\n\n return NextResponse.json({\n addressFormat: result?.addressFormat ?? input.addressFormat,\n })\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const { translate } = await resolveTranslations()\n console.error('customers.settings.address-format.put failed', err)\n return NextResponse.json({ error: translate('customers.errors.save_failed', 'Failed to save settings') }, { status: 400 })\n }\n}\n\nconst addressFormatResponseSchema = z.object({\n addressFormat: z.string(),\n})\n\nconst addressFormatErrorSchema = z.object({\n error: z.string(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Customers',\n summary: 'Customer address format settings',\n methods: {\n GET: {\n summary: 'Retrieve address format',\n description: 'Returns the current address formatting preference for the selected organization.',\n responses: [\n { status: 200, description: 'Current address format', schema: addressFormatResponseSchema },\n { status: 401, description: 'Unauthorized', schema: addressFormatErrorSchema },\n { status: 400, description: 'Organization context missing', schema: addressFormatErrorSchema },\n ],\n },\n PUT: {\n summary: 'Update address format',\n description: 'Updates the address format preference for the selected organization.',\n requestBody: {\n contentType: 'application/json',\n schema: customerSettingsUpsertSchema,\n },\n responses: [\n { status: 200, description: 'Updated address format', schema: addressFormatResponseSchema },\n { status: 401, description: 'Unauthorized', schema: addressFormatErrorSchema },\n { status: 400, description: 'Invalid payload or organization context', schema: addressFormatErrorSchema },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { customerSettingsUpsertSchema, type CustomerSettingsUpsertInput } from '../../../data/validators'\nimport { loadCustomerSettings } from '../../../commands/settings'\nimport type { CustomerAddressFormat } from '../../../data/entities'\nimport { withScopedPayload } from '../../utils'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\n\nconst SETTINGS_RESOURCE_KIND = 'customers.settings'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['customers.settings.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['customers.settings.manage'] },\n}\n\ntype SettingsRouteContext = {\n ctx: CommandRuntimeContext\n tenantId: string\n organizationId: string\n translate: (key: string, fallback?: string) => string\n em: EntityManager\n}\n\nasync function resolveSettingsContext(req: Request): Promise<SettingsRouteContext> {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const { translate } = await resolveTranslations()\n if (!auth || !auth.tenantId) {\n throw new CrudHttpError(401, { error: translate('customers.errors.unauthorized', 'Unauthorized') })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const organizationId = scope?.selectedId ?? auth.orgId ?? null\n if (!organizationId) {\n throw new CrudHttpError(400, { error: translate('customers.errors.organization_required', 'Organization context is required') })\n }\n\n const ctx: CommandRuntimeContext = {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: organizationId,\n organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),\n request: req,\n }\n\n const em = (container.resolve('em') as EntityManager)\n return {\n ctx,\n tenantId: auth.tenantId,\n organizationId,\n translate,\n em,\n }\n}\n\nexport async function GET(req: Request) {\n try {\n const { em, tenantId, organizationId } = await resolveSettingsContext(req)\n const record = await loadCustomerSettings(em, { tenantId, organizationId })\n return NextResponse.json({\n addressFormat: record?.addressFormat ?? 'line_first',\n })\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const { translate } = await resolveTranslations()\n console.error('customers.settings.address-format.get failed', err)\n return NextResponse.json({ error: translate('customers.errors.lookup_failed', 'Failed to load settings') }, { status: 400 })\n }\n}\n\nexport async function PUT(req: Request) {\n try {\n const { ctx, tenantId, organizationId, translate } = await resolveSettingsContext(req)\n const payload = await req.json().catch(() => ({}))\n const scoped = withScopedPayload(payload, ctx, translate)\n const input = customerSettingsUpsertSchema.parse(scoped)\n\n const guardResult = await validateCrudMutationGuard(ctx.container, {\n tenantId,\n organizationId,\n userId: ctx.auth!.sub,\n resourceKind: SETTINGS_RESOURCE_KIND,\n resourceId: organizationId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: input,\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 } = await commandBus.execute<CustomerSettingsUpsertInput, { settingsId: string; addressFormat: CustomerAddressFormat }>(\n 'customers.settings.save',\n { input, ctx },\n )\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(ctx.container, {\n tenantId,\n organizationId,\n userId: ctx.auth!.sub,\n resourceKind: SETTINGS_RESOURCE_KIND,\n resourceId: organizationId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return NextResponse.json({\n addressFormat: result?.addressFormat ?? input.addressFormat,\n })\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const { translate } = await resolveTranslations()\n console.error('customers.settings.address-format.put failed', err)\n return NextResponse.json({ error: translate('customers.errors.save_failed', 'Failed to save settings') }, { status: 400 })\n }\n}\n\nconst addressFormatResponseSchema = z.object({\n addressFormat: z.string(),\n})\n\nconst addressFormatErrorSchema = z.object({\n error: z.string(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Customers',\n summary: 'Customer address format settings',\n methods: {\n GET: {\n summary: 'Retrieve address format',\n description: 'Returns the current address formatting preference for the selected organization.',\n responses: [\n { status: 200, description: 'Current address format', schema: addressFormatResponseSchema },\n { status: 401, description: 'Unauthorized', schema: addressFormatErrorSchema },\n { status: 400, description: 'Organization context missing', schema: addressFormatErrorSchema },\n ],\n },\n PUT: {\n summary: 'Update address format',\n description: 'Updates the address format preference for the selected organization.',\n requestBody: {\n contentType: 'application/json',\n schema: customerSettingsUpsertSchema,\n },\n responses: [\n { status: 200, description: 'Updated address format', schema: addressFormatResponseSchema },\n { status: 401, description: 'Unauthorized', schema: addressFormatErrorSchema },\n { status: 400, description: 'Invalid payload or organization context', schema: addressFormatErrorSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -8,6 +8,11 @@ import { tagAssignmentSchema } from "../../../data/validators.js";
8
8
  import { CrudHttpError, isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
9
9
  import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
10
10
  import { withScopedPayload } from "../../utils.js";
11
+ import {
12
+ runCrudMutationGuardAfterSuccess,
13
+ validateCrudMutationGuard
14
+ } from "@open-mercato/shared/lib/crud/mutation-guard";
15
+ const TAG_ASSIGNMENT_RESOURCE_KIND = "customers.tagAssignment";
11
16
  const metadata = {
12
17
  POST: { requireAuth: true, requireFeatures: ["customers.activities.manage"] }
13
18
  };
@@ -30,14 +35,46 @@ async function buildContext(req) {
30
35
  async function POST(req) {
31
36
  try {
32
37
  const { ctx, auth, translate } = await buildContext(req);
38
+ const tenantId = auth.tenantId;
39
+ const organizationId = ctx.selectedOrganizationId;
40
+ if (!tenantId || !organizationId) {
41
+ return NextResponse.json({ error: translate("customers.errors.context_required", "Organization and tenant context required") }, { status: 400 });
42
+ }
33
43
  const body = await req.json().catch(() => ({}));
34
44
  const scoped = withScopedPayload(body, ctx, translate);
35
45
  const input = tagAssignmentSchema.parse(scoped);
46
+ const guardResult = await validateCrudMutationGuard(ctx.container, {
47
+ tenantId,
48
+ organizationId,
49
+ userId: auth.sub,
50
+ resourceKind: TAG_ASSIGNMENT_RESOURCE_KIND,
51
+ resourceId: input.entityId,
52
+ operation: "custom",
53
+ requestMethod: req.method,
54
+ requestHeaders: req.headers,
55
+ mutationPayload: input
56
+ });
57
+ if (guardResult && !guardResult.ok) {
58
+ return NextResponse.json(guardResult.body, { status: guardResult.status });
59
+ }
36
60
  const commandBus = ctx.container.resolve("commandBus");
37
61
  const { result, logEntry } = await commandBus.execute(
38
62
  "customers.tags.assign",
39
63
  { input, ctx }
40
64
  );
65
+ if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
66
+ await runCrudMutationGuardAfterSuccess(ctx.container, {
67
+ tenantId,
68
+ organizationId,
69
+ userId: auth.sub,
70
+ resourceKind: TAG_ASSIGNMENT_RESOURCE_KIND,
71
+ resourceId: input.entityId,
72
+ operation: "custom",
73
+ requestMethod: req.method,
74
+ requestHeaders: req.headers,
75
+ metadata: guardResult.metadata ?? null
76
+ });
77
+ }
41
78
  const response = NextResponse.json({ id: result?.assignmentId ?? null }, { status: 201 });
42
79
  if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {
43
80
  response.headers.set(
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/customers/api/tags/assign/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport type { CommandRuntimeContext, CommandBus } from '@open-mercato/shared/lib/commands'\nimport { tagAssignmentSchema, type TagAssignmentInput } from '../../../data/validators'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { withScopedPayload } from '../../utils'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['customers.activities.manage'] },\n}\n\nasync function buildContext(\n req: Request\n): Promise<{ ctx: CommandRuntimeContext; auth: Awaited<ReturnType<typeof getAuthFromRequest>>; translate: (key: string, fallback?: string) => string }> {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const { translate } = await resolveTranslations()\n if (!auth) throw new CrudHttpError(401, { error: translate('customers.errors.unauthorized', 'Unauthorized') })\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const ctx: CommandRuntimeContext = {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: scope?.selectedId ?? auth.orgId ?? null,\n organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),\n request: req,\n }\n return { ctx, auth, translate }\n}\n\nexport async function POST(req: Request) {\n try {\n const { ctx, auth, translate } = await buildContext(req)\n const body = await req.json().catch(() => ({}))\n const scoped = withScopedPayload(body, ctx, translate)\n const input = tagAssignmentSchema.parse(scoped)\n\n const commandBus = (ctx.container.resolve('commandBus') as CommandBus)\n const { result, logEntry } = await commandBus.execute<TagAssignmentInput, { assignmentId: string }>(\n 'customers.tags.assign',\n { input, ctx },\n )\n const response = NextResponse.json({ id: result?.assignmentId ?? null }, { status: 201 })\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 ?? 'customers.tagAssignment',\n resourceId: logEntry.resourceId ?? result?.assignmentId ?? null,\n executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : undefined,\n })\n )\n }\n return response\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const { translate } = await resolveTranslations()\n console.error('customers.tags.assign failed', err)\n return NextResponse.json({ error: translate('customers.errors.assign_failed', 'Failed to assign tag') }, { status: 400 })\n }\n}\n\nconst tagAssignmentResponseSchema = z.object({\n id: z.string().uuid().nullable(),\n})\n\nconst tagAssignmentErrorSchema = z.object({\n error: z.string(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Customers',\n summary: 'Assign customer tag',\n methods: {\n POST: {\n summary: 'Assign tag to customer entity',\n description: 'Links a tag to a customer entity within the validated tenant / organization scope.',\n requestBody: {\n contentType: 'application/json',\n schema: tagAssignmentSchema,\n },\n responses: [\n { status: 201, description: 'Tag assigned to customer', schema: tagAssignmentResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Validation or assignment failed', schema: tagAssignmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: tagAssignmentErrorSchema },\n { status: 403, description: 'Insufficient tenant/organization access', schema: tagAssignmentErrorSchema },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport type { CommandRuntimeContext, CommandBus } from '@open-mercato/shared/lib/commands'\nimport { tagAssignmentSchema, type TagAssignmentInput } from '../../../data/validators'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { withScopedPayload } from '../../utils'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\n\nconst TAG_ASSIGNMENT_RESOURCE_KIND = 'customers.tagAssignment'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['customers.activities.manage'] },\n}\n\nasync function buildContext(\n req: Request\n): Promise<{ ctx: CommandRuntimeContext; auth: Awaited<ReturnType<typeof getAuthFromRequest>>; translate: (key: string, fallback?: string) => string }> {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const { translate } = await resolveTranslations()\n if (!auth) throw new CrudHttpError(401, { error: translate('customers.errors.unauthorized', 'Unauthorized') })\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const ctx: CommandRuntimeContext = {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: scope?.selectedId ?? auth.orgId ?? null,\n organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),\n request: req,\n }\n return { ctx, auth, translate }\n}\n\nexport async function POST(req: Request) {\n try {\n const { ctx, auth, translate } = await buildContext(req)\n const tenantId = auth!.tenantId\n const organizationId = ctx.selectedOrganizationId\n if (!tenantId || !organizationId) {\n return NextResponse.json({ error: translate('customers.errors.context_required', 'Organization and tenant context required') }, { status: 400 })\n }\n const body = await req.json().catch(() => ({}))\n const scoped = withScopedPayload(body, ctx, translate)\n const input = tagAssignmentSchema.parse(scoped)\n\n const guardResult = await validateCrudMutationGuard(ctx.container, {\n tenantId,\n organizationId,\n userId: auth!.sub,\n resourceKind: TAG_ASSIGNMENT_RESOURCE_KIND,\n resourceId: input.entityId,\n operation: 'custom',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: input,\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<TagAssignmentInput, { assignmentId: string }>(\n 'customers.tags.assign',\n { input, ctx },\n )\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(ctx.container, {\n tenantId,\n organizationId,\n userId: auth!.sub,\n resourceKind: TAG_ASSIGNMENT_RESOURCE_KIND,\n resourceId: input.entityId,\n operation: 'custom',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n const response = NextResponse.json({ id: result?.assignmentId ?? null }, { status: 201 })\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 ?? 'customers.tagAssignment',\n resourceId: logEntry.resourceId ?? result?.assignmentId ?? null,\n executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : undefined,\n })\n )\n }\n return response\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const { translate } = await resolveTranslations()\n console.error('customers.tags.assign failed', err)\n return NextResponse.json({ error: translate('customers.errors.assign_failed', 'Failed to assign tag') }, { status: 400 })\n }\n}\n\nconst tagAssignmentResponseSchema = z.object({\n id: z.string().uuid().nullable(),\n})\n\nconst tagAssignmentErrorSchema = z.object({\n error: z.string(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Customers',\n summary: 'Assign customer tag',\n methods: {\n POST: {\n summary: 'Assign tag to customer entity',\n description: 'Links a tag to a customer entity within the validated tenant / organization scope.',\n requestBody: {\n contentType: 'application/json',\n schema: tagAssignmentSchema,\n },\n responses: [\n { status: 201, description: 'Tag assigned to customer', schema: tagAssignmentResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Validation or assignment failed', schema: tagAssignmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: tagAssignmentErrorSchema },\n { status: 403, description: 'Insufficient tenant/organization access', schema: tagAssignmentErrorSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -8,6 +8,11 @@ import { tagAssignmentSchema } from "../../../data/validators.js";
8
8
  import { CrudHttpError, isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
9
9
  import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
10
10
  import { withScopedPayload } from "../../utils.js";
11
+ import {
12
+ runCrudMutationGuardAfterSuccess,
13
+ validateCrudMutationGuard
14
+ } from "@open-mercato/shared/lib/crud/mutation-guard";
15
+ const TAG_ASSIGNMENT_RESOURCE_KIND = "customers.tagAssignment";
11
16
  const metadata = {
12
17
  POST: { requireAuth: true, requireFeatures: ["customers.activities.manage"] }
13
18
  };
@@ -30,14 +35,46 @@ async function buildContext(req) {
30
35
  async function POST(req) {
31
36
  try {
32
37
  const { ctx, auth, translate } = await buildContext(req);
38
+ const tenantId = auth.tenantId;
39
+ const organizationId = ctx.selectedOrganizationId;
40
+ if (!tenantId || !organizationId) {
41
+ return NextResponse.json({ error: translate("customers.errors.context_required", "Organization and tenant context required") }, { status: 400 });
42
+ }
33
43
  const body = await req.json().catch(() => ({}));
34
44
  const scoped = withScopedPayload(body, ctx, translate);
35
45
  const input = tagAssignmentSchema.parse(scoped);
46
+ const guardResult = await validateCrudMutationGuard(ctx.container, {
47
+ tenantId,
48
+ organizationId,
49
+ userId: auth.sub,
50
+ resourceKind: TAG_ASSIGNMENT_RESOURCE_KIND,
51
+ resourceId: input.entityId,
52
+ operation: "custom",
53
+ requestMethod: req.method,
54
+ requestHeaders: req.headers,
55
+ mutationPayload: input
56
+ });
57
+ if (guardResult && !guardResult.ok) {
58
+ return NextResponse.json(guardResult.body, { status: guardResult.status });
59
+ }
36
60
  const commandBus = ctx.container.resolve("commandBus");
37
61
  const { result, logEntry } = await commandBus.execute(
38
62
  "customers.tags.unassign",
39
63
  { input, ctx }
40
64
  );
65
+ if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
66
+ await runCrudMutationGuardAfterSuccess(ctx.container, {
67
+ tenantId,
68
+ organizationId,
69
+ userId: auth.sub,
70
+ resourceKind: TAG_ASSIGNMENT_RESOURCE_KIND,
71
+ resourceId: input.entityId,
72
+ operation: "custom",
73
+ requestMethod: req.method,
74
+ requestHeaders: req.headers,
75
+ metadata: guardResult.metadata ?? null
76
+ });
77
+ }
41
78
  const response = NextResponse.json({ id: result?.assignmentId ?? null });
42
79
  if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {
43
80
  response.headers.set(
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/customers/api/tags/unassign/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport type { CommandRuntimeContext, CommandBus } from '@open-mercato/shared/lib/commands'\nimport { tagAssignmentSchema, type TagAssignmentInput } from '../../../data/validators'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { withScopedPayload } from '../../utils'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['customers.activities.manage'] },\n}\n\nasync function buildContext(\n req: Request\n): Promise<{ ctx: CommandRuntimeContext; auth: Awaited<ReturnType<typeof getAuthFromRequest>>; translate: (key: string, fallback?: string) => string }> {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const { translate } = await resolveTranslations()\n if (!auth) throw new CrudHttpError(401, { error: translate('customers.errors.unauthorized', 'Unauthorized') })\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const ctx: CommandRuntimeContext = {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: scope?.selectedId ?? auth.orgId ?? null,\n organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),\n request: req,\n }\n return { ctx, auth, translate }\n}\n\nexport async function POST(req: Request) {\n try {\n const { ctx, auth, translate } = await buildContext(req)\n const body = await req.json().catch(() => ({}))\n const scoped = withScopedPayload(body, ctx, translate)\n const input = tagAssignmentSchema.parse(scoped)\n\n const commandBus = (ctx.container.resolve('commandBus') as CommandBus)\n const { result, logEntry } = await commandBus.execute<TagAssignmentInput, { assignmentId: string | null }>(\n 'customers.tags.unassign',\n { input, ctx },\n )\n const response = NextResponse.json({ id: result?.assignmentId ?? null })\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 ?? 'customers.tagAssignment',\n resourceId: logEntry.resourceId ?? result?.assignmentId ?? null,\n executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : undefined,\n })\n )\n }\n return response\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const { translate } = await resolveTranslations()\n console.error('customers.tags.unassign failed', err)\n return NextResponse.json({ error: translate('customers.errors.unassign_failed', 'Failed to unassign tag') }, { status: 400 })\n }\n}\n\nconst tagUnassignResponseSchema = z.object({\n id: z.string().uuid().nullable(),\n})\n\nconst tagUnassignErrorSchema = z.object({\n error: z.string(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Customers',\n summary: 'Unassign customer tag',\n methods: {\n POST: {\n summary: 'Remove tag from customer entity',\n description: 'Detaches a tag from a customer entity within the validated tenant / organization scope.',\n requestBody: {\n contentType: 'application/json',\n schema: tagAssignmentSchema,\n },\n responses: [\n { status: 200, description: 'Tag unassigned from customer', schema: tagUnassignResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Validation or unassignment failed', schema: tagUnassignErrorSchema },\n { status: 401, description: 'Unauthorized', schema: tagUnassignErrorSchema },\n { status: 403, description: 'Insufficient tenant/organization access', schema: tagUnassignErrorSchema },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport type { CommandRuntimeContext, CommandBus } from '@open-mercato/shared/lib/commands'\nimport { tagAssignmentSchema, type TagAssignmentInput } from '../../../data/validators'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { withScopedPayload } from '../../utils'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\n\nconst TAG_ASSIGNMENT_RESOURCE_KIND = 'customers.tagAssignment'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['customers.activities.manage'] },\n}\n\nasync function buildContext(\n req: Request\n): Promise<{ ctx: CommandRuntimeContext; auth: Awaited<ReturnType<typeof getAuthFromRequest>>; translate: (key: string, fallback?: string) => string }> {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const { translate } = await resolveTranslations()\n if (!auth) throw new CrudHttpError(401, { error: translate('customers.errors.unauthorized', 'Unauthorized') })\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const ctx: CommandRuntimeContext = {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: scope?.selectedId ?? auth.orgId ?? null,\n organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),\n request: req,\n }\n return { ctx, auth, translate }\n}\n\nexport async function POST(req: Request) {\n try {\n const { ctx, auth, translate } = await buildContext(req)\n const tenantId = auth!.tenantId\n const organizationId = ctx.selectedOrganizationId\n if (!tenantId || !organizationId) {\n return NextResponse.json({ error: translate('customers.errors.context_required', 'Organization and tenant context required') }, { status: 400 })\n }\n const body = await req.json().catch(() => ({}))\n const scoped = withScopedPayload(body, ctx, translate)\n const input = tagAssignmentSchema.parse(scoped)\n\n const guardResult = await validateCrudMutationGuard(ctx.container, {\n tenantId,\n organizationId,\n userId: auth!.sub,\n resourceKind: TAG_ASSIGNMENT_RESOURCE_KIND,\n resourceId: input.entityId,\n operation: 'custom',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: input,\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<TagAssignmentInput, { assignmentId: string | null }>(\n 'customers.tags.unassign',\n { input, ctx },\n )\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(ctx.container, {\n tenantId,\n organizationId,\n userId: auth!.sub,\n resourceKind: TAG_ASSIGNMENT_RESOURCE_KIND,\n resourceId: input.entityId,\n operation: 'custom',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n const response = NextResponse.json({ id: result?.assignmentId ?? null })\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 ?? 'customers.tagAssignment',\n resourceId: logEntry.resourceId ?? result?.assignmentId ?? null,\n executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : undefined,\n })\n )\n }\n return response\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const { translate } = await resolveTranslations()\n console.error('customers.tags.unassign failed', err)\n return NextResponse.json({ error: translate('customers.errors.unassign_failed', 'Failed to unassign tag') }, { status: 400 })\n }\n}\n\nconst tagUnassignResponseSchema = z.object({\n id: z.string().uuid().nullable(),\n})\n\nconst tagUnassignErrorSchema = z.object({\n error: z.string(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Customers',\n summary: 'Unassign customer tag',\n methods: {\n POST: {\n summary: 'Remove tag from customer entity',\n description: 'Detaches a tag from a customer entity within the validated tenant / organization scope.',\n requestBody: {\n contentType: 'application/json',\n schema: tagAssignmentSchema,\n },\n responses: [\n { status: 200, description: 'Tag unassigned from customer', schema: tagUnassignResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Validation or unassignment failed', schema: tagUnassignErrorSchema },\n { status: 401, description: 'Unauthorized', schema: tagUnassignErrorSchema },\n { status: 403, description: 'Insufficient tenant/organization access', schema: tagUnassignErrorSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,0CAA0C;AACnD,SAAS,kCAAkC;AAE3C,SAAS,2BAAoD;AAC7D,SAAS,eAAe,uBAAuB;AAC/C,SAAS,2BAA2B;AACpC,SAAS,yBAAyB;AAClC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AAGP,MAAM,+BAA+B;AAE9B,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,6BAA6B,EAAE;AAC9E;AAEA,eAAe,aACb,KACsJ;AACtJ,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,QAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,MAAI,CAAC,KAAM,OAAM,IAAI,cAAc,KAAK,EAAE,OAAO,UAAU,iCAAiC,cAAc,EAAE,CAAC;AAC7G,QAAM,QAAQ,MAAM,mCAAmC,EAAE,WAAW,MAAM,SAAS,IAAI,CAAC;AACxF,QAAM,MAA6B;AAAA,IACjC;AAAA,IACA;AAAA,IACA,mBAAmB;AAAA,IACnB,wBAAwB,OAAO,cAAc,KAAK,SAAS;AAAA,IAC3D,iBAAiB,OAAO,cAAc,KAAK,QAAQ,CAAC,KAAK,KAAK,IAAI;AAAA,IAClE,SAAS;AAAA,EACX;AACA,SAAO,EAAE,KAAK,MAAM,UAAU;AAChC;AAEA,eAAsB,KAAK,KAAc;AACvC,MAAI;AACF,UAAM,EAAE,KAAK,MAAM,UAAU,IAAI,MAAM,aAAa,GAAG;AACvD,UAAM,WAAW,KAAM;AACvB,UAAM,iBAAiB,IAAI;AAC3B,QAAI,CAAC,YAAY,CAAC,gBAAgB;AAChC,aAAO,aAAa,KAAK,EAAE,OAAO,UAAU,qCAAqC,0CAA0C,EAAE,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACjJ;AACA,UAAM,OAAO,MAAM,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE;AAC9C,UAAM,SAAS,kBAAkB,MAAM,KAAK,SAAS;AACrD,UAAM,QAAQ,oBAAoB,MAAM,MAAM;AAE9C,UAAM,cAAc,MAAM,0BAA0B,IAAI,WAAW;AAAA,MACjE;AAAA,MACA;AAAA,MACA,QAAQ,KAAM;AAAA,MACd,cAAc;AAAA,MACd,YAAY,MAAM;AAAA,MAClB,WAAW;AAAA,MACX,eAAe,IAAI;AAAA,MACnB,gBAAgB,IAAI;AAAA,MACpB,iBAAiB;AAAA,IACnB,CAAC;AACD,QAAI,eAAe,CAAC,YAAY,IAAI;AAClC,aAAO,aAAa,KAAK,YAAY,MAAM,EAAE,QAAQ,YAAY,OAAO,CAAC;AAAA,IAC3E;AAEA,UAAM,aAAc,IAAI,UAAU,QAAQ,YAAY;AACtD,UAAM,EAAE,QAAQ,SAAS,IAAI,MAAM,WAAW;AAAA,MAC9C;AAAA,MACA,EAAE,OAAO,IAAI;AAAA,IACf;AACE,QAAI,aAAa,MAAM,YAAY,uBAAuB;AACxD,YAAM,iCAAiC,IAAI,WAAW;AAAA,QACpD;AAAA,QACA;AAAA,QACA,QAAQ,KAAM;AAAA,QACd,cAAc;AAAA,QACd,YAAY,MAAM;AAAA,QAClB,WAAW;AAAA,QACX,eAAe,IAAI;AAAA,QACnB,gBAAgB,IAAI;AAAA,QACpB,UAAU,YAAY,YAAY;AAAA,MACpC,CAAC;AAAA,IACH;AACA,UAAM,WAAW,aAAa,KAAK,EAAE,IAAI,QAAQ,gBAAgB,KAAK,CAAC;AACvE,QAAI,UAAU,aAAa,UAAU,MAAM,UAAU,WAAW;AAC9D,eAAS,QAAQ;AAAA,QACf;AAAA,QACA,2BAA2B;AAAA,UACzB,IAAI,SAAS;AAAA,UACb,WAAW,SAAS;AAAA,UACpB,WAAW,SAAS;AAAA,UACpB,aAAa,SAAS,eAAe;AAAA,UACrC,cAAc,SAAS,gBAAgB;AAAA,UACvC,YAAY,SAAS,cAAc,QAAQ,gBAAgB;AAAA,UAC3D,YAAY,SAAS,qBAAqB,OAAO,SAAS,UAAU,YAAY,IAAI;AAAA,QACtF,CAAC;AAAA,MACH;AAAA,IACF;AACA,WAAO;AAAA,EACT,SAAS,KAAK;AACZ,QAAI,gBAAgB,GAAG,GAAG;AACxB,aAAO,aAAa,KAAK,IAAI,MAAM,EAAE,QAAQ,IAAI,OAAO,CAAC;AAAA,IAC3D;AACA,UAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,YAAQ,MAAM,kCAAkC,GAAG;AACnD,WAAO,aAAa,KAAK,EAAE,OAAO,UAAU,oCAAoC,wBAAwB,EAAE,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC9H;AACF;AAEA,MAAM,4BAA4B,EAAE,OAAO;AAAA,EACzC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AACjC,CAAC;AAED,MAAM,yBAAyB,EAAE,OAAO;AAAA,EACtC,OAAO,EAAE,OAAO;AAClB,CAAC;AAEM,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,aAAa;AAAA,MACb,aAAa;AAAA,QACX,aAAa;AAAA,QACb,QAAQ;AAAA,MACV;AAAA,MACA,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,gCAAgC,QAAQ,0BAA0B;AAAA,MAChG;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,qCAAqC,QAAQ,uBAAuB;AAAA,QAChG,EAAE,QAAQ,KAAK,aAAa,gBAAgB,QAAQ,uBAAuB;AAAA,QAC3E,EAAE,QAAQ,KAAK,aAAa,2CAA2C,QAAQ,uBAAuB;AAAA,MACxG;AAAA,IACF;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -4,6 +4,13 @@ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
4
4
  import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
5
5
  import { EncryptionMap } from "@open-mercato/core/modules/entities/data/entities";
6
6
  import { upsertEncryptionMapSchema } from "@open-mercato/core/modules/entities/data/validators";
7
+ import { isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
8
+ import { enforceCommandOptimisticLock } from "@open-mercato/shared/lib/crud/optimistic-lock-command";
9
+ import {
10
+ runCrudMutationGuardAfterSuccess,
11
+ validateCrudMutationGuard
12
+ } from "@open-mercato/shared/lib/crud/mutation-guard";
13
+ const ENCRYPTION_MAP_RESOURCE_KIND = "entities.encryption_map";
7
14
  const metadata = {
8
15
  GET: { requireAuth: true, requireFeatures: ["entities.definitions.manage"] },
9
16
  POST: { requireAuth: true, requireFeatures: ["entities.definitions.manage"] }
@@ -14,6 +21,15 @@ function resolveScope(auth) {
14
21
  organizationId: auth.orgId ?? null
15
22
  };
16
23
  }
24
+ function toIsoOrNull(value) {
25
+ if (value == null) return null;
26
+ if (value instanceof Date) {
27
+ const ms = value.getTime();
28
+ return Number.isFinite(ms) ? new Date(ms).toISOString() : null;
29
+ }
30
+ const trimmed = String(value).trim();
31
+ return trimmed.length ? trimmed : null;
32
+ }
17
33
  async function GET(req) {
18
34
  const url = new URL(req.url);
19
35
  const entityId = url.searchParams.get("entityId") || "";
@@ -21,8 +37,8 @@ async function GET(req) {
21
37
  const auth = await getAuthFromRequest(req);
22
38
  if (!auth?.tenantId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
23
39
  const { tenantId, organizationId } = resolveScope(auth);
24
- const { resolve } = await createRequestContainer();
25
- const em = resolve("em");
40
+ const container = await createRequestContainer();
41
+ const em = container.resolve("em");
26
42
  const repo = em.getRepository(EncryptionMap);
27
43
  const candidates = [
28
44
  { entityId, tenantId, organizationId },
@@ -42,47 +58,99 @@ async function GET(req) {
42
58
  tenantId,
43
59
  organizationId,
44
60
  fields: record?.fieldsJson ?? [],
45
- isActive: record?.isActive ?? true
61
+ isActive: record?.isActive ?? true,
62
+ updatedAt: toIsoOrNull(record?.updatedAt)
46
63
  });
47
64
  }
48
65
  async function POST(req) {
49
- const body = await req.json().catch(() => ({}));
50
- const parsed = upsertEncryptionMapSchema.safeParse(body);
51
- if (!parsed.success) {
52
- return NextResponse.json({ error: "Invalid payload", details: parsed.error.flatten() }, { status: 400 });
53
- }
54
- const auth = await getAuthFromRequest(req);
55
- if (!auth?.tenantId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
56
- const scope = resolveScope(auth);
57
- const payload = parsed.data;
58
- const tenantId = scope.tenantId;
59
- const organizationId = scope.organizationId;
60
- const { resolve } = await createRequestContainer();
61
- const em = resolve("em");
62
- const repo = em.getRepository(EncryptionMap);
63
- const existing = await repo.findOne({ entityId: payload.entityId, tenantId, organizationId, deletedAt: null });
64
- if (existing) {
65
- existing.fieldsJson = payload.fields;
66
- existing.isActive = payload.isActive ?? true;
67
- existing.updatedAt = /* @__PURE__ */ new Date();
68
- await em.persist(existing).flush();
69
- } else {
70
- const map = repo.create({
71
- entityId: payload.entityId,
66
+ try {
67
+ const body = await req.json().catch(() => ({}));
68
+ const parsed = upsertEncryptionMapSchema.safeParse(body);
69
+ if (!parsed.success) {
70
+ return NextResponse.json({ error: "Invalid payload", details: parsed.error.flatten() }, { status: 400 });
71
+ }
72
+ const auth = await getAuthFromRequest(req);
73
+ if (!auth?.tenantId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
74
+ const scope = resolveScope(auth);
75
+ const payload = parsed.data;
76
+ const tenantId = auth.tenantId;
77
+ const organizationId = scope.organizationId;
78
+ const container = await createRequestContainer();
79
+ const em = container.resolve("em");
80
+ const repo = em.getRepository(EncryptionMap);
81
+ const existing = await repo.findOne({ entityId: payload.entityId, tenantId, organizationId, deletedAt: null });
82
+ if (existing) {
83
+ enforceCommandOptimisticLock({
84
+ resourceKind: ENCRYPTION_MAP_RESOURCE_KIND,
85
+ resourceId: existing.id,
86
+ current: existing.updatedAt,
87
+ request: req
88
+ });
89
+ }
90
+ const guardResult = await validateCrudMutationGuard(container, {
72
91
  tenantId,
73
92
  organizationId,
74
- fieldsJson: payload.fields,
75
- isActive: payload.isActive ?? true
93
+ userId: auth.sub,
94
+ resourceKind: ENCRYPTION_MAP_RESOURCE_KIND,
95
+ resourceId: existing?.id ?? payload.entityId,
96
+ operation: existing ? "update" : "create",
97
+ requestMethod: req.method,
98
+ requestHeaders: req.headers,
99
+ mutationPayload: payload
76
100
  });
77
- await em.persist(map).flush();
78
- }
79
- try {
80
- const svc = resolve("tenantEncryptionService");
81
- await svc?.invalidateMap?.(payload.entityId, tenantId, organizationId);
82
- } catch {
101
+ if (guardResult && !guardResult.ok) {
102
+ return NextResponse.json(guardResult.body, { status: guardResult.status });
103
+ }
104
+ let saved;
105
+ if (existing) {
106
+ existing.fieldsJson = payload.fields;
107
+ existing.isActive = payload.isActive ?? true;
108
+ existing.updatedAt = /* @__PURE__ */ new Date();
109
+ await em.persist(existing).flush();
110
+ saved = existing;
111
+ } else {
112
+ const map = repo.create({
113
+ entityId: payload.entityId,
114
+ tenantId,
115
+ organizationId,
116
+ fieldsJson: payload.fields,
117
+ isActive: payload.isActive ?? true
118
+ });
119
+ await em.persist(map).flush();
120
+ saved = map;
121
+ }
122
+ if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
123
+ await runCrudMutationGuardAfterSuccess(container, {
124
+ tenantId,
125
+ organizationId,
126
+ userId: auth.sub,
127
+ resourceKind: ENCRYPTION_MAP_RESOURCE_KIND,
128
+ resourceId: saved?.id ?? payload.entityId,
129
+ operation: existing ? "update" : "create",
130
+ requestMethod: req.method,
131
+ requestHeaders: req.headers,
132
+ metadata: guardResult.metadata ?? null
133
+ });
134
+ }
135
+ try {
136
+ const svc = container.resolve("tenantEncryptionService");
137
+ await svc?.invalidateMap?.(payload.entityId, tenantId, organizationId);
138
+ } catch {
139
+ }
140
+ return NextResponse.json({ ok: true, updatedAt: toIsoOrNull(saved?.updatedAt) });
141
+ } catch (err) {
142
+ if (isCrudHttpError(err)) {
143
+ return NextResponse.json(err.body, { status: err.status });
144
+ }
145
+ throw err;
83
146
  }
84
- return NextResponse.json({ ok: true });
85
147
  }
148
+ const conflictResponseSchema = z.object({
149
+ error: z.string(),
150
+ code: z.string(),
151
+ currentUpdatedAt: z.string(),
152
+ expectedUpdatedAt: z.string()
153
+ });
86
154
  const openApi = {
87
155
  tag: "Entities",
88
156
  summary: "Manage encryption maps",
@@ -91,13 +159,16 @@ const openApi = {
91
159
  summary: "Fetch encryption map",
92
160
  description: "Returns the encrypted field map for the current tenant/organization scope.",
93
161
  query: z.object({ entityId: z.string() }),
94
- responses: [{ status: 200, description: "Map", schema: z.object({ entityId: z.string(), fields: z.array(z.object({ field: z.string(), hashField: z.string().nullable().optional() })), isActive: z.boolean().optional() }) }]
162
+ responses: [{ status: 200, description: "Map", schema: z.object({ entityId: z.string(), fields: z.array(z.object({ field: z.string(), hashField: z.string().nullable().optional() })), isActive: z.boolean().optional(), updatedAt: z.string().nullable().optional() }) }]
95
163
  },
96
164
  POST: {
97
165
  summary: "Upsert encryption map",
98
- description: "Creates or updates the encryption map for the current tenant/organization scope.",
166
+ description: "Creates or updates the encryption map for the current tenant/organization scope. Enforces optimistic locking when the caller sends the expected version header.",
99
167
  requestBody: { contentType: "application/json", schema: upsertEncryptionMapSchema },
100
- responses: [{ status: 200, description: "Saved", schema: z.object({ ok: z.boolean() }) }]
168
+ responses: [
169
+ { status: 200, description: "Saved", schema: z.object({ ok: z.boolean(), updatedAt: z.string().nullable().optional() }) },
170
+ { status: 409, description: "Optimistic-lock conflict (stale write)", schema: conflictResponseSchema }
171
+ ]
101
172
  }
102
173
  }
103
174
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/entities/api/encryption.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { EncryptionMap } from '@open-mercato/core/modules/entities/data/entities'\nimport { upsertEncryptionMapSchema } from '@open-mercato/core/modules/entities/data/validators'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['entities.definitions.manage'] },\n POST: { requireAuth: true, requireFeatures: ['entities.definitions.manage'] },\n}\n\nfunction resolveScope(auth: { tenantId?: string | null; orgId?: string | null }) {\n return {\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n }\n}\n\nexport async function GET(req: Request) {\n const url = new URL(req.url)\n const entityId = url.searchParams.get('entityId') || ''\n if (!entityId) return NextResponse.json({ error: 'entityId is required' }, { status: 400 })\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const { tenantId, organizationId } = resolveScope(auth)\n\n const { resolve } = await createRequestContainer()\n const em = resolve('em') as any\n const repo = em.getRepository(EncryptionMap)\n // Prefer tenant+org, then tenant-global, then global\n const candidates = [\n { entityId, tenantId, organizationId },\n { entityId, tenantId, organizationId: null },\n { entityId, tenantId: null, organizationId: null },\n ]\n let record: any = null\n for (const where of candidates) {\n // eslint-disable-next-line no-await-in-loop\n const found = await repo.findOne({ ...where, deletedAt: null })\n if (found) {\n record = found\n break\n }\n }\n\n return NextResponse.json({\n entityId,\n tenantId,\n organizationId,\n fields: record?.fieldsJson ?? [],\n isActive: record?.isActive ?? true,\n })\n}\n\nexport async function POST(req: Request) {\n const body = await req.json().catch(() => ({}))\n const parsed = upsertEncryptionMapSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload', details: parsed.error.flatten() }, { status: 400 })\n }\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const scope = resolveScope(auth)\n const payload = parsed.data\n const tenantId = scope.tenantId\n const organizationId = scope.organizationId\n\n const { resolve } = await createRequestContainer()\n const em = resolve('em') as any\n const repo = em.getRepository(EncryptionMap)\n const existing = await repo.findOne({ entityId: payload.entityId, tenantId, organizationId, deletedAt: null })\n if (existing) {\n existing.fieldsJson = payload.fields\n existing.isActive = payload.isActive ?? true\n existing.updatedAt = new Date()\n await em.persist(existing).flush()\n } else {\n const map = repo.create({\n entityId: payload.entityId,\n tenantId,\n organizationId,\n fieldsJson: payload.fields,\n isActive: payload.isActive ?? true,\n })\n await em.persist(map).flush()\n }\n\n try {\n const svc = resolve('tenantEncryptionService') as { invalidateMap?: (e: string, t: string | null, o: string | null) => Promise<void> }\n await svc?.invalidateMap?.(payload.entityId, tenantId, organizationId)\n } catch {\n // best-effort cache bust\n }\n\n return NextResponse.json({ ok: true })\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Entities',\n summary: 'Manage encryption maps',\n methods: {\n GET: {\n summary: 'Fetch encryption map',\n description: 'Returns the encrypted field map for the current tenant/organization scope.',\n query: z.object({ entityId: z.string() }),\n responses: [{ status: 200, description: 'Map', schema: z.object({ entityId: z.string(), fields: z.array(z.object({ field: z.string(), hashField: z.string().nullable().optional() })), isActive: z.boolean().optional() }) }],\n },\n POST: {\n summary: 'Upsert encryption map',\n description: 'Creates or updates the encryption map for the current tenant/organization scope.',\n requestBody: { contentType: 'application/json', schema: upsertEncryptionMapSchema },\n responses: [{ status: 200, description: 'Saved', schema: z.object({ ok: z.boolean() }) }],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { EncryptionMap } from '@open-mercato/core/modules/entities/data/entities'\nimport { upsertEncryptionMapSchema } from '@open-mercato/core/modules/entities/data/validators'\nimport { CrudHttpError, 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 type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\n\nconst ENCRYPTION_MAP_RESOURCE_KIND = 'entities.encryption_map'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['entities.definitions.manage'] },\n POST: { requireAuth: true, requireFeatures: ['entities.definitions.manage'] },\n}\n\nfunction resolveScope(auth: { tenantId?: string | null; orgId?: string | null }) {\n return {\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n }\n}\n\nfunction toIsoOrNull(value: Date | string | null | undefined): string | null {\n if (value == null) return null\n if (value instanceof Date) {\n const ms = value.getTime()\n return Number.isFinite(ms) ? new Date(ms).toISOString() : null\n }\n const trimmed = String(value).trim()\n return trimmed.length ? trimmed : null\n}\n\nexport async function GET(req: Request) {\n const url = new URL(req.url)\n const entityId = url.searchParams.get('entityId') || ''\n if (!entityId) return NextResponse.json({ error: 'entityId is required' }, { status: 400 })\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const { tenantId, organizationId } = resolveScope(auth)\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as any\n const repo = em.getRepository(EncryptionMap)\n // Prefer tenant+org, then tenant-global, then global\n const candidates = [\n { entityId, tenantId, organizationId },\n { entityId, tenantId, organizationId: null },\n { entityId, tenantId: null, organizationId: null },\n ]\n let record: any = null\n for (const where of candidates) {\n // eslint-disable-next-line no-await-in-loop\n const found = await repo.findOne({ ...where, deletedAt: null })\n if (found) {\n record = found\n break\n }\n }\n\n return NextResponse.json({\n entityId,\n tenantId,\n organizationId,\n fields: record?.fieldsJson ?? [],\n isActive: record?.isActive ?? true,\n updatedAt: toIsoOrNull(record?.updatedAt),\n })\n}\n\nexport async function POST(req: Request) {\n try {\n const body = await req.json().catch(() => ({}))\n const parsed = upsertEncryptionMapSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload', details: parsed.error.flatten() }, { status: 400 })\n }\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const scope = resolveScope(auth)\n const payload = parsed.data\n const tenantId: string = auth.tenantId\n const organizationId = scope.organizationId\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as any\n const repo = em.getRepository(EncryptionMap)\n const existing = await repo.findOne({ entityId: payload.entityId, tenantId, organizationId, deletedAt: null })\n\n // Reject stale writes: a save started from an older tab must not silently\n // overwrite a newer encryption configuration. No-op when the client did not\n // send the expected-version header (strictly additive).\n if (existing) {\n enforceCommandOptimisticLock({\n resourceKind: ENCRYPTION_MAP_RESOURCE_KIND,\n resourceId: existing.id,\n current: existing.updatedAt,\n request: req,\n })\n }\n\n // Mutation-guard contract for custom write routes. The resource is the\n // encryption map for this entity scoped to the tenant/organization.\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId,\n organizationId,\n userId: auth.sub,\n resourceKind: ENCRYPTION_MAP_RESOURCE_KIND,\n resourceId: existing?.id ?? payload.entityId,\n operation: existing ? 'update' : 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: payload,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n let saved: any\n if (existing) {\n existing.fieldsJson = payload.fields\n existing.isActive = payload.isActive ?? true\n existing.updatedAt = new Date()\n await em.persist(existing).flush()\n saved = existing\n } else {\n const map = repo.create({\n entityId: payload.entityId,\n tenantId,\n organizationId,\n fieldsJson: payload.fields,\n isActive: payload.isActive ?? true,\n })\n await em.persist(map).flush()\n saved = map\n }\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId,\n organizationId,\n userId: auth.sub,\n resourceKind: ENCRYPTION_MAP_RESOURCE_KIND,\n resourceId: saved?.id ?? payload.entityId,\n operation: existing ? 'update' : 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n try {\n const svc = container.resolve('tenantEncryptionService') as { invalidateMap?: (e: string, t: string | null, o: string | null) => Promise<void> }\n await svc?.invalidateMap?.(payload.entityId, tenantId, organizationId)\n } catch {\n // best-effort cache bust\n }\n\n return NextResponse.json({ ok: true, updatedAt: toIsoOrNull(saved?.updatedAt) })\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n throw err\n }\n}\n\nconst conflictResponseSchema = z.object({\n error: z.string(),\n code: z.string(),\n currentUpdatedAt: z.string(),\n expectedUpdatedAt: z.string(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Entities',\n summary: 'Manage encryption maps',\n methods: {\n GET: {\n summary: 'Fetch encryption map',\n description: 'Returns the encrypted field map for the current tenant/organization scope.',\n query: z.object({ entityId: z.string() }),\n responses: [{ status: 200, description: 'Map', schema: z.object({ entityId: z.string(), fields: z.array(z.object({ field: z.string(), hashField: z.string().nullable().optional() })), isActive: z.boolean().optional(), updatedAt: z.string().nullable().optional() }) }],\n },\n POST: {\n summary: 'Upsert encryption map',\n description: 'Creates or updates the encryption map for the current tenant/organization scope. Enforces optimistic locking when the caller sends the expected version header.',\n requestBody: { contentType: 'application/json', schema: upsertEncryptionMapSchema },\n responses: [\n { status: 200, description: 'Saved', schema: z.object({ ok: z.boolean(), updatedAt: z.string().nullable().optional() }) },\n { status: 409, description: 'Optimistic-lock conflict (stale write)', schema: conflictResponseSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }