@open-mercato/core 0.6.6-develop.6460.1.f94c74174c → 0.6.6-develop.6465.1.019f0fb26f

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 (50) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/helpers/integration/auth.js +148 -82
  3. package/dist/helpers/integration/auth.js.map +2 -2
  4. package/dist/modules/sales/api/document-addresses/route.js +96 -6
  5. package/dist/modules/sales/api/document-addresses/route.js.map +2 -2
  6. package/dist/modules/sales/commands/documentAddresses.js +7 -0
  7. package/dist/modules/sales/commands/documentAddresses.js.map +2 -2
  8. package/dist/modules/staff/commands/activities.js +23 -12
  9. package/dist/modules/staff/commands/activities.js.map +2 -2
  10. package/dist/modules/staff/commands/addresses.js +20 -12
  11. package/dist/modules/staff/commands/addresses.js.map +2 -2
  12. package/dist/modules/staff/commands/comments.js +18 -11
  13. package/dist/modules/staff/commands/comments.js.map +2 -2
  14. package/dist/modules/staff/commands/job-histories.js +23 -12
  15. package/dist/modules/staff/commands/job-histories.js.map +2 -2
  16. package/dist/modules/staff/commands/leave-requests.js +37 -19
  17. package/dist/modules/staff/commands/leave-requests.js.map +2 -2
  18. package/dist/modules/staff/commands/shared.js +28 -1
  19. package/dist/modules/staff/commands/shared.js.map +2 -2
  20. package/dist/modules/staff/commands/tag-assignments.js +20 -3
  21. package/dist/modules/staff/commands/tag-assignments.js.map +2 -2
  22. package/dist/modules/staff/commands/team-members.js +21 -11
  23. package/dist/modules/staff/commands/team-members.js.map +2 -2
  24. package/dist/modules/staff/commands/team-roles.js +21 -11
  25. package/dist/modules/staff/commands/team-roles.js.map +2 -2
  26. package/dist/modules/staff/commands/teams.js +21 -11
  27. package/dist/modules/staff/commands/teams.js.map +2 -2
  28. package/dist/modules/staff/commands/timesheets-entries.js +24 -14
  29. package/dist/modules/staff/commands/timesheets-entries.js.map +2 -2
  30. package/dist/modules/staff/commands/timesheets-projects.js +34 -18
  31. package/dist/modules/staff/commands/timesheets-projects.js.map +2 -2
  32. package/dist/modules/workflows/api/instances/route.js +1 -1
  33. package/dist/modules/workflows/api/instances/route.js.map +2 -2
  34. package/package.json +7 -7
  35. package/src/helpers/integration/auth.ts +178 -86
  36. package/src/modules/sales/api/document-addresses/route.ts +140 -7
  37. package/src/modules/sales/commands/documentAddresses.ts +8 -0
  38. package/src/modules/staff/commands/activities.ts +23 -11
  39. package/src/modules/staff/commands/addresses.ts +20 -11
  40. package/src/modules/staff/commands/comments.ts +18 -10
  41. package/src/modules/staff/commands/job-histories.ts +27 -11
  42. package/src/modules/staff/commands/leave-requests.ts +38 -19
  43. package/src/modules/staff/commands/shared.ts +38 -0
  44. package/src/modules/staff/commands/tag-assignments.ts +19 -2
  45. package/src/modules/staff/commands/team-members.ts +21 -10
  46. package/src/modules/staff/commands/team-roles.ts +21 -10
  47. package/src/modules/staff/commands/teams.ts +21 -10
  48. package/src/modules/staff/commands/timesheets-entries.ts +24 -13
  49. package/src/modules/staff/commands/timesheets-projects.ts +34 -17
  50. package/src/modules/workflows/api/instances/route.ts +2 -2
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/workflows/api/instances/route.ts"],
4
- "sourcesContent": ["/**\n * Workflow Instances API\n *\n * Endpoints:\n * - GET /api/workflows/instances - List workflow instances\n * - POST /api/workflows/instances - Start a new workflow instance\n */\n\nimport { NextRequest, 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 { resolveOrganizationScopeFilter } from '@open-mercato/core/modules/directory/utils/organizationScopeFilter'\nimport { WorkflowInstance } from '../../data/entities'\nimport {\n startWorkflowInputSchema,\n type StartWorkflowApiInput,\n workflowInstanceStatusSchema,\n} from '../../data/validators'\nimport {\n workflowInstanceResponseSchema,\n workflowBackgroundStartSchema,\n paginationSchema,\n} from '../openapi'\nimport * as workflowExecutor from '../../lib/workflow-executor'\n\nexport const metadata = {\n requireAuth: true,\n requireFeatures: ['workflows.instances.view'],\n}\n\n/**\n * GET /api/workflows/instances\n *\n * List workflow instances with optional filters\n */\nexport async function GET(request: NextRequest) {\n try {\n const container = await createRequestContainer()\n const em = container.resolve('em')\n const auth = await getAuthFromRequest(request)\n\n if (!auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request })\n const tenantId = auth.tenantId\n const orgFilter = resolveOrganizationScopeFilter(scope, auth)\n\n if (!tenantId) {\n return NextResponse.json(\n { error: 'Missing tenant context' },\n { status: 400 }\n )\n }\n\n const { searchParams } = new URL(request.url)\n const workflowId = searchParams.get('workflowId')\n const status = searchParams.get('status')\n const correlationKey = searchParams.get('correlationKey')\n const entityType = searchParams.get('entityType')\n const entityId = searchParams.get('entityId')\n const limit = parseInt(searchParams.get('limit') || '50')\n const offset = parseInt(searchParams.get('offset') || '0')\n\n // Build where clause with tenant scoping\n const where: any = {\n tenantId,\n ...orgFilter.where,\n }\n\n if (workflowId) {\n where.workflowId = workflowId\n }\n\n if (status) {\n // Support comma-separated status values (e.g., \"RUNNING,PAUSED,WAITING_FOR_ACTIVITIES\")\n const statuses = status.split(',').map(s => s.trim()).filter(Boolean)\n if (statuses.length === 1) {\n where.status = statuses[0]\n } else if (statuses.length > 1) {\n where.status = { $in: statuses }\n }\n }\n\n if (correlationKey) {\n where.correlationKey = correlationKey\n }\n\n // For JSONB metadata filtering, use $contains with explicit key-value pairs\n // MikroORM's dot notation creates table joins, not JSON access\n if (entityType || entityId) {\n where.$and = where.$and || []\n if (entityType) {\n where.$and.push({\n metadata: { $contains: { entityType: entityType } }\n })\n }\n if (entityId) {\n where.$and.push({\n metadata: { $contains: { entityId: entityId } }\n })\n }\n }\n\n const [instances, total] = await em.findAndCount(\n WorkflowInstance,\n where,\n {\n orderBy: { createdAt: 'DESC' },\n limit,\n offset,\n }\n )\n\n return NextResponse.json({\n data: instances,\n pagination: {\n total,\n limit,\n offset,\n hasMore: offset + limit < total,\n },\n })\n } catch (error) {\n console.error('Error listing workflow instances:', error)\n return NextResponse.json(\n { error: 'Failed to list workflow instances' },\n { status: 500 }\n )\n }\n}\n\n/**\n * POST /api/workflows/instances\n *\n * Start a new workflow instance\n */\nexport async function POST(request: NextRequest) {\n try {\n const container = await createRequestContainer()\n const em = container.resolve('em')\n const auth = await getAuthFromRequest(request)\n\n if (!auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request })\n const tenantId = auth.tenantId\n const organizationId = scope?.selectedId ?? auth.orgId\n\n if (!tenantId || !organizationId) {\n return NextResponse.json(\n { error: 'Missing tenant or organization context' },\n { status: 400 }\n )\n }\n\n // Check create permission\n const rbacService = container.resolve('rbacService')\n const hasPermission = await rbacService.userHasAllFeatures(\n auth.sub,\n ['workflows.instances.create'],\n {\n tenantId,\n organizationId,\n }\n )\n\n if (!hasPermission) {\n return NextResponse.json(\n { error: 'Insufficient permissions' },\n { status: 403 }\n )\n }\n\n const body = await request.json()\n\n // Validate input\n const validation = startWorkflowInputSchema.safeParse(body)\n if (!validation.success) {\n return NextResponse.json(\n {\n error: 'Validation failed',\n details: validation.error.issues,\n },\n { status: 400 }\n )\n }\n\n const input: StartWorkflowApiInput = validation.data\n\n // Inject metadata.initiatedBy if not provided\n const metadata = {\n ...input.metadata,\n initiatedBy: input.metadata?.initiatedBy || auth.sub,\n }\n\n // Start workflow\n const instance = await workflowExecutor.startWorkflow(em, {\n workflowId: input.workflowId,\n version: input.version,\n initialContext: input.initialContext || {},\n correlationKey: input.correlationKey,\n metadata,\n tenantId,\n organizationId,\n })\n\n // Execute workflow in background (non-blocking for demo visibility)\n // This allows the frontend to see step-by-step progress via polling\n setImmediate(async () => {\n try {\n // Create new container and EM for background execution\n const bgContainer = await createRequestContainer()\n const bgEm = bgContainer.resolve('em')\n await workflowExecutor.executeWorkflow(bgEm, bgContainer, instance.id)\n } catch (error) {\n console.error('Background workflow execution error:', error)\n }\n })\n\n return NextResponse.json(\n {\n data: {\n instance,\n execution: {\n status: instance.status,\n currentStep: instance.currentStepId,\n message: 'Workflow execution started in background',\n },\n },\n message: 'Workflow started successfully',\n },\n { status: 201 }\n )\n } catch (error) {\n console.error('Error starting workflow:', error)\n\n // Handle specific errors\n if (error instanceof workflowExecutor.WorkflowExecutionError) {\n if (error.code === 'DEFINITION_NOT_FOUND') {\n return NextResponse.json(\n { error: error.message },\n { status: 404 }\n )\n }\n if (error.code === 'DEFINITION_DISABLED') {\n return NextResponse.json(\n { error: error.message },\n { status: 400 }\n )\n }\n if (error.code === 'INVALID_DEFINITION') {\n return NextResponse.json(\n { error: error.message },\n { status: 400 }\n )\n }\n if (error.code === 'START_PRE_CONDITIONS_FAILED') {\n return NextResponse.json(\n {\n error: error.message,\n code: error.code,\n details: error.details,\n },\n { status: 422 }\n )\n }\n }\n\n return NextResponse.json(\n { error: 'Failed to start workflow' },\n { status: 500 }\n )\n }\n}\n\nexport const openApi = {\n methods: {\n GET: {\n summary: 'List workflow instances',\n description: 'Get a list of workflow instances with optional filters. Supports pagination and filtering by status, workflowId, correlationKey, etc.',\n tags: ['Workflows'],\n query: z.object({\n workflowId: z.string().optional(),\n status: workflowInstanceStatusSchema.optional(),\n correlationKey: z.string().optional(),\n entityType: z.string().optional(),\n entityId: z.string().optional(),\n limit: z.number().int().positive().default(50).optional(),\n offset: z.number().int().min(0).default(0).optional(),\n }),\n responses: [\n {\n status: 200,\n description: 'List of workflow instances',\n schema: z.object({\n data: z.array(workflowInstanceResponseSchema),\n pagination: paginationSchema,\n }),\n },\n {\n status: 401,\n description: 'Unauthorized',\n schema: z.object({ error: z.string() }),\n },\n {\n status: 500,\n description: 'Internal server error',\n schema: z.object({ error: z.string() }),\n },\n ],\n },\n POST: {\n summary: 'Start workflow instance',\n description: 'Start a new workflow instance from a workflow definition. The workflow will execute immediately.',\n tags: ['Workflows'],\n requestBody: {\n contentType: 'application/json',\n schema: startWorkflowInputSchema,\n description: 'Workflow instance configuration including workflowId, initial context, and metadata.',\n },\n responses: [\n {\n status: 201,\n description: 'Workflow started successfully',\n schema: z.object({\n data: z.object({\n instance: workflowInstanceResponseSchema,\n execution: workflowBackgroundStartSchema,\n }),\n message: z.string(),\n }),\n },\n {\n status: 400,\n description: 'Bad request - Validation failed or definition disabled/invalid',\n schema: z.object({\n error: z.string(),\n details: z.any().optional(),\n }),\n },\n {\n status: 401,\n description: 'Unauthorized',\n schema: z.object({ error: z.string() }),\n },\n {\n status: 403,\n description: 'Forbidden - Insufficient permissions',\n schema: z.object({ error: z.string() }),\n },\n {\n status: 404,\n description: 'Workflow definition not found',\n schema: z.object({ error: z.string() }),\n },\n {\n status: 500,\n description: 'Internal server error',\n schema: z.object({ error: z.string() }),\n },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Workflow Instances API\n *\n * Endpoints:\n * - GET /api/workflows/instances - List workflow instances\n * - POST /api/workflows/instances - Start a new workflow instance\n */\n\nimport { NextRequest, 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 { resolveOrganizationScopeFilter } from '@open-mercato/core/modules/directory/utils/organizationScopeFilter'\nimport { WorkflowInstance } from '../../data/entities'\nimport {\n startWorkflowInputSchema,\n type StartWorkflowApiInput,\n workflowInstanceStatusSchema,\n} from '../../data/validators'\nimport {\n workflowInstanceResponseSchema,\n workflowBackgroundStartSchema,\n paginationSchema,\n} from '../openapi'\nimport * as workflowExecutor from '../../lib/workflow-executor'\n\nexport const metadata = {\n requireAuth: true,\n requireFeatures: ['workflows.instances.view'],\n}\n\n/**\n * GET /api/workflows/instances\n *\n * List workflow instances with optional filters\n */\nexport async function GET(request: NextRequest) {\n try {\n const container = await createRequestContainer()\n const em = container.resolve('em')\n const auth = await getAuthFromRequest(request)\n\n if (!auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request })\n const tenantId = auth.tenantId\n const orgFilter = resolveOrganizationScopeFilter(scope, auth)\n\n if (!tenantId) {\n return NextResponse.json(\n { error: 'Missing tenant context' },\n { status: 400 }\n )\n }\n\n const { searchParams } = new URL(request.url)\n const workflowId = searchParams.get('workflowId')\n const status = searchParams.get('status')\n const correlationKey = searchParams.get('correlationKey')\n const entityType = searchParams.get('entityType')\n const entityId = searchParams.get('entityId')\n const limit = parseInt(searchParams.get('limit') || '50')\n const offset = parseInt(searchParams.get('offset') || '0')\n\n // Build where clause with tenant scoping\n const where: any = {\n tenantId,\n ...orgFilter.where,\n }\n\n if (workflowId) {\n where.workflowId = workflowId\n }\n\n if (status) {\n // Support comma-separated status values (e.g., \"RUNNING,PAUSED,WAITING_FOR_ACTIVITIES\")\n const statuses = status.split(',').map(s => s.trim()).filter(Boolean)\n if (statuses.length === 1) {\n where.status = statuses[0]\n } else if (statuses.length > 1) {\n where.status = { $in: statuses }\n }\n }\n\n if (correlationKey) {\n where.correlationKey = correlationKey\n }\n\n // For JSONB metadata filtering, use $contains with explicit key-value pairs\n // MikroORM's dot notation creates table joins, not JSON access\n if (entityType || entityId) {\n where.$and = where.$and || []\n if (entityType) {\n where.$and.push({\n metadata: { $contains: { entityType: entityType } }\n })\n }\n if (entityId) {\n where.$and.push({\n metadata: { $contains: { entityId: entityId } }\n })\n }\n }\n\n const [instances, total] = await em.findAndCount(\n WorkflowInstance,\n where,\n {\n orderBy: { createdAt: 'DESC' },\n limit,\n offset,\n }\n )\n\n return NextResponse.json({\n data: instances,\n pagination: {\n total,\n limit,\n offset,\n hasMore: offset + limit < total,\n },\n })\n } catch (error) {\n console.error('Error listing workflow instances:', error)\n return NextResponse.json(\n { error: 'Failed to list workflow instances' },\n { status: 500 }\n )\n }\n}\n\n/**\n * POST /api/workflows/instances\n *\n * Start a new workflow instance\n */\nexport async function POST(request: NextRequest) {\n try {\n const container = await createRequestContainer()\n const em = container.resolve('em')\n const auth = await getAuthFromRequest(request)\n\n if (!auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request })\n const tenantId = auth.tenantId\n const organizationId = scope?.selectedId ?? auth.orgId\n\n if (!tenantId || !organizationId) {\n return NextResponse.json(\n { error: 'Missing tenant or organization context' },\n { status: 400 }\n )\n }\n\n // Check create permission\n const rbacService = container.resolve('rbacService')\n const hasPermission = await rbacService.userHasAllFeatures(\n auth.sub,\n ['workflows.instances.create'],\n {\n tenantId,\n organizationId,\n }\n )\n\n if (!hasPermission) {\n return NextResponse.json(\n { error: 'Insufficient permissions' },\n { status: 403 }\n )\n }\n\n const body = await request.json()\n\n // Validate input\n const validation = startWorkflowInputSchema.safeParse(body)\n if (!validation.success) {\n return NextResponse.json(\n {\n error: 'Validation failed',\n details: validation.error.issues,\n },\n { status: 400 }\n )\n }\n\n const input: StartWorkflowApiInput = validation.data\n\n // User-started instances must always record the authenticated caller.\n const metadata = {\n ...input.metadata,\n initiatedBy: auth.sub,\n }\n\n // Start workflow\n const instance = await workflowExecutor.startWorkflow(em, {\n workflowId: input.workflowId,\n version: input.version,\n initialContext: input.initialContext || {},\n correlationKey: input.correlationKey,\n metadata,\n tenantId,\n organizationId,\n })\n\n // Execute workflow in background (non-blocking for demo visibility)\n // This allows the frontend to see step-by-step progress via polling\n setImmediate(async () => {\n try {\n // Create new container and EM for background execution\n const bgContainer = await createRequestContainer()\n const bgEm = bgContainer.resolve('em')\n await workflowExecutor.executeWorkflow(bgEm, bgContainer, instance.id)\n } catch (error) {\n console.error('Background workflow execution error:', error)\n }\n })\n\n return NextResponse.json(\n {\n data: {\n instance,\n execution: {\n status: instance.status,\n currentStep: instance.currentStepId,\n message: 'Workflow execution started in background',\n },\n },\n message: 'Workflow started successfully',\n },\n { status: 201 }\n )\n } catch (error) {\n console.error('Error starting workflow:', error)\n\n // Handle specific errors\n if (error instanceof workflowExecutor.WorkflowExecutionError) {\n if (error.code === 'DEFINITION_NOT_FOUND') {\n return NextResponse.json(\n { error: error.message },\n { status: 404 }\n )\n }\n if (error.code === 'DEFINITION_DISABLED') {\n return NextResponse.json(\n { error: error.message },\n { status: 400 }\n )\n }\n if (error.code === 'INVALID_DEFINITION') {\n return NextResponse.json(\n { error: error.message },\n { status: 400 }\n )\n }\n if (error.code === 'START_PRE_CONDITIONS_FAILED') {\n return NextResponse.json(\n {\n error: error.message,\n code: error.code,\n details: error.details,\n },\n { status: 422 }\n )\n }\n }\n\n return NextResponse.json(\n { error: 'Failed to start workflow' },\n { status: 500 }\n )\n }\n}\n\nexport const openApi = {\n methods: {\n GET: {\n summary: 'List workflow instances',\n description: 'Get a list of workflow instances with optional filters. Supports pagination and filtering by status, workflowId, correlationKey, etc.',\n tags: ['Workflows'],\n query: z.object({\n workflowId: z.string().optional(),\n status: workflowInstanceStatusSchema.optional(),\n correlationKey: z.string().optional(),\n entityType: z.string().optional(),\n entityId: z.string().optional(),\n limit: z.number().int().positive().default(50).optional(),\n offset: z.number().int().min(0).default(0).optional(),\n }),\n responses: [\n {\n status: 200,\n description: 'List of workflow instances',\n schema: z.object({\n data: z.array(workflowInstanceResponseSchema),\n pagination: paginationSchema,\n }),\n },\n {\n status: 401,\n description: 'Unauthorized',\n schema: z.object({ error: z.string() }),\n },\n {\n status: 500,\n description: 'Internal server error',\n schema: z.object({ error: z.string() }),\n },\n ],\n },\n POST: {\n summary: 'Start workflow instance',\n description: 'Start a new workflow instance from a workflow definition. The workflow will execute immediately.',\n tags: ['Workflows'],\n requestBody: {\n contentType: 'application/json',\n schema: startWorkflowInputSchema,\n description: 'Workflow instance configuration including workflowId, initial context, and metadata.',\n },\n responses: [\n {\n status: 201,\n description: 'Workflow started successfully',\n schema: z.object({\n data: z.object({\n instance: workflowInstanceResponseSchema,\n execution: workflowBackgroundStartSchema,\n }),\n message: z.string(),\n }),\n },\n {\n status: 400,\n description: 'Bad request - Validation failed or definition disabled/invalid',\n schema: z.object({\n error: z.string(),\n details: z.any().optional(),\n }),\n },\n {\n status: 401,\n description: 'Unauthorized',\n schema: z.object({ error: z.string() }),\n },\n {\n status: 403,\n description: 'Forbidden - Insufficient permissions',\n schema: z.object({ error: z.string() }),\n },\n {\n status: 404,\n description: 'Workflow definition not found',\n schema: z.object({ error: z.string() }),\n },\n {\n status: 500,\n description: 'Internal server error',\n schema: z.object({ error: z.string() }),\n },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["metadata"]
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.6.6-develop.6460.1.f94c74174c",
3
+ "version": "0.6.6-develop.6465.1.019f0fb26f",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -253,16 +253,16 @@
253
253
  "zod": "^4.4.3"
254
254
  },
255
255
  "peerDependencies": {
256
- "@open-mercato/ai-assistant": "0.6.6-develop.6460.1.f94c74174c",
257
- "@open-mercato/shared": "0.6.6-develop.6460.1.f94c74174c",
258
- "@open-mercato/ui": "0.6.6-develop.6460.1.f94c74174c",
256
+ "@open-mercato/ai-assistant": "0.6.6-develop.6465.1.019f0fb26f",
257
+ "@open-mercato/shared": "0.6.6-develop.6465.1.019f0fb26f",
258
+ "@open-mercato/ui": "0.6.6-develop.6465.1.019f0fb26f",
259
259
  "react": "^19.0.0",
260
260
  "react-dom": "^19.0.0"
261
261
  },
262
262
  "devDependencies": {
263
- "@open-mercato/ai-assistant": "0.6.6-develop.6460.1.f94c74174c",
264
- "@open-mercato/shared": "0.6.6-develop.6460.1.f94c74174c",
265
- "@open-mercato/ui": "0.6.6-develop.6460.1.f94c74174c",
263
+ "@open-mercato/ai-assistant": "0.6.6-develop.6465.1.019f0fb26f",
264
+ "@open-mercato/shared": "0.6.6-develop.6465.1.019f0fb26f",
265
+ "@open-mercato/ui": "0.6.6-develop.6465.1.019f0fb26f",
266
266
  "@testing-library/dom": "^10.4.1",
267
267
  "@testing-library/jest-dom": "^6.9.1",
268
268
  "@testing-library/react": "^16.3.1",
@@ -1,4 +1,4 @@
1
- import { type Page } from '@playwright/test';
1
+ import { type Page, type Response } from '@playwright/test';
2
2
  import { readFileSync } from 'fs';
3
3
  import { resolve } from 'path';
4
4
 
@@ -136,102 +136,194 @@ async function recoverGenericErrorPageIfPresent(page: Page): Promise<void> {
136
136
  await dismissGlobalNoticesIfPresent(page);
137
137
  }
138
138
 
139
- export async function login(page: Page, role: Role = 'admin'): Promise<void> {
140
- const creds = DEFAULT_CREDENTIALS[role];
141
- const loginReadySelector = 'form[data-auth-ready="1"]';
142
- const hasBackendUrl = (): boolean => /\/backend(?:\/.*)?$/.test(page.url());
143
- const waitForBackend = async (timeout: number): Promise<boolean> => {
144
- try {
145
- await page.waitForURL(/\/backend(?:\/.*)?$/, { timeout });
146
- return true;
147
- } catch {
148
- return hasBackendUrl();
149
- }
150
- };
139
+ type AuthNavigationTraceEntry = {
140
+ path: string;
141
+ status: number;
142
+ location: string | null;
143
+ };
151
144
 
152
- await acknowledgeGlobalNotices(page);
153
- const apiLoginForm = new URLSearchParams();
154
- apiLoginForm.set('email', creds.email);
155
- apiLoginForm.set('password', creds.password);
156
- // Retry-on-429 against the auth rate limit (5/60s per email). Capped
157
- // exponential backoff: 1s, 2s, 4s — worst-case ~7s.
158
- let apiLoginResponse: Awaited<ReturnType<typeof page.request.post>> | null = null;
159
- for (let retry = 0; retry < 4; retry += 1) {
160
- apiLoginResponse = await page.request.post('/api/auth/login', {
161
- headers: {
162
- 'content-type': 'application/x-www-form-urlencoded',
163
- },
164
- data: apiLoginForm.toString(),
165
- }).catch(() => null);
166
- if (!apiLoginResponse || apiLoginResponse.status() !== 429) break;
167
- await new Promise((resolve) => setTimeout(resolve, 1000 * 2 ** retry));
145
+ function toPathOnly(rawUrl: string): string {
146
+ try {
147
+ const url = new URL(rawUrl);
148
+ return `${url.pathname}${url.search}`;
149
+ } catch {
150
+ return '[unparseable-url]';
151
+ }
152
+ }
153
+
154
+ function toRedactedLocation(rawLocation: string | undefined): string | null {
155
+ if (!rawLocation) return null;
156
+ try {
157
+ const url = new URL(rawLocation, 'http://integration.local');
158
+ return `${url.pathname}${url.search}`;
159
+ } catch {
160
+ return '[unparseable-location]';
168
161
  }
169
- if (apiLoginResponse?.ok()) {
170
- const apiLoginBody = (await apiLoginResponse.json().catch(() => null)) as { token?: string } | null;
171
- const claims = typeof apiLoginBody?.token === 'string' ? decodeJwtClaims(apiLoginBody.token) : null;
172
- const baseUrl = process.env.BASE_URL || 'http://localhost:3000';
173
- const cookies = [];
174
- if (claims?.tenantId) {
175
- cookies.push({
176
- name: 'om_selected_tenant',
177
- value: claims.tenantId,
178
- url: baseUrl,
179
- sameSite: 'Lax' as const,
162
+ }
163
+
164
+ function createAuthNavigationTrace(page: Page): {
165
+ entries: AuthNavigationTraceEntry[];
166
+ stop: () => void;
167
+ } {
168
+ const entries: AuthNavigationTraceEntry[] = [];
169
+ const onResponse = (response: Response) => {
170
+ const path = toPathOnly(response.url());
171
+ if (path === '/backend' || path.startsWith('/api/auth/session/refresh')) {
172
+ entries.push({
173
+ path,
174
+ status: response.status(),
175
+ location: toRedactedLocation(response.headers().location),
180
176
  });
181
177
  }
182
- if (claims?.orgId) {
183
- cookies.push({
184
- name: 'om_selected_org',
185
- value: claims.orgId,
186
- url: baseUrl,
187
- sameSite: 'Lax' as const,
188
- });
178
+ };
179
+ page.on('response', onResponse);
180
+ return {
181
+ entries,
182
+ stop: () => page.off('response', onResponse),
183
+ };
184
+ }
185
+
186
+ async function formatLoginDiagnostics(
187
+ page: Page,
188
+ role: Role,
189
+ entries: AuthNavigationTraceEntry[],
190
+ ): Promise<string> {
191
+ const cookieNames = Array.from(
192
+ new Set((await page.context().cookies()).map((cookie) => cookie.name)),
193
+ ).sort((left, right) => left.localeCompare(right));
194
+ const traceLines = entries.length
195
+ ? entries.map((entry) => {
196
+ const location = entry.location ? ` -> ${entry.location}` : '';
197
+ return `${entry.status} ${entry.path}${location}`;
198
+ })
199
+ : ['none'];
200
+
201
+ return [
202
+ `Login diagnostics for role "${role}":`,
203
+ `current URL: ${toPathOnly(page.url())}`,
204
+ `browser cookie names: ${cookieNames.length ? cookieNames.join(', ') : 'none'}`,
205
+ 'backend/refresh trace:',
206
+ ...traceLines,
207
+ ].join('\n');
208
+ }
209
+
210
+ async function throwLoginErrorWithDiagnostics(
211
+ page: Page,
212
+ role: Role,
213
+ entries: AuthNavigationTraceEntry[],
214
+ error: unknown,
215
+ ): Promise<never> {
216
+ const message = error instanceof Error ? error.message : String(error);
217
+ throw new Error([
218
+ `Login did not reach backend for role: ${role}.`,
219
+ await formatLoginDiagnostics(page, role, entries),
220
+ `Original error: ${message}`,
221
+ ].join('\n'));
222
+ }
223
+
224
+ export async function login(page: Page, role: Role = 'admin'): Promise<void> {
225
+ const authTrace = createAuthNavigationTrace(page);
226
+ try {
227
+ const creds = DEFAULT_CREDENTIALS[role];
228
+ const loginReadySelector = 'form[data-auth-ready="1"]';
229
+ const hasBackendUrl = (): boolean => /\/backend(?:\/.*)?$/.test(page.url());
230
+ const waitForBackend = async (timeout: number): Promise<boolean> => {
231
+ try {
232
+ await page.waitForURL(/\/backend(?:\/.*)?$/, { timeout });
233
+ return true;
234
+ } catch {
235
+ return hasBackendUrl();
236
+ }
237
+ };
238
+
239
+ await acknowledgeGlobalNotices(page);
240
+ const apiLoginForm = new URLSearchParams();
241
+ apiLoginForm.set('email', creds.email);
242
+ apiLoginForm.set('password', creds.password);
243
+ // Retry-on-429 against the auth rate limit (5/60s per email). Capped
244
+ // exponential backoff: 1s, 2s, 4s — worst-case ~7s.
245
+ let apiLoginResponse: Awaited<ReturnType<typeof page.request.post>> | null = null;
246
+ for (let retry = 0; retry < 4; retry += 1) {
247
+ apiLoginResponse = await page.request.post('/api/auth/login', {
248
+ headers: {
249
+ 'content-type': 'application/x-www-form-urlencoded',
250
+ },
251
+ data: apiLoginForm.toString(),
252
+ }).catch(() => null);
253
+ if (!apiLoginResponse || apiLoginResponse.status() !== 429) break;
254
+ await new Promise((resolve) => setTimeout(resolve, 1000 * 2 ** retry));
189
255
  }
190
- if (cookies.length > 0) {
191
- await page.context().addCookies(cookies);
256
+ if (apiLoginResponse?.ok()) {
257
+ const apiLoginBody = (await apiLoginResponse.json().catch(() => null)) as { token?: string } | null;
258
+ const claims = typeof apiLoginBody?.token === 'string' ? decodeJwtClaims(apiLoginBody.token) : null;
259
+ const baseUrl = process.env.BASE_URL || 'http://localhost:3000';
260
+ const cookies = [];
261
+ if (claims?.tenantId) {
262
+ cookies.push({
263
+ name: 'om_selected_tenant',
264
+ value: claims.tenantId,
265
+ url: baseUrl,
266
+ sameSite: 'Lax' as const,
267
+ });
268
+ }
269
+ if (claims?.orgId) {
270
+ cookies.push({
271
+ name: 'om_selected_org',
272
+ value: claims.orgId,
273
+ url: baseUrl,
274
+ sameSite: 'Lax' as const,
275
+ });
276
+ }
277
+ if (cookies.length > 0) {
278
+ await page.context().addCookies(cookies);
279
+ }
280
+ await page.goto('/backend', { waitUntil: 'domcontentloaded' });
281
+ if (await waitForBackend(8_000)) return;
192
282
  }
193
- await page.goto('/backend', { waitUntil: 'domcontentloaded' });
194
- if (await waitForBackend(8_000)) return;
195
- }
196
283
 
197
- for (let attempt = 0; attempt < 4; attempt += 1) {
198
- await page.goto('/login', { waitUntil: 'domcontentloaded' });
199
- await dismissGlobalNoticesIfPresent(page);
200
- await recoverClientSideErrorPageIfPresent(page);
201
- await recoverGenericErrorPageIfPresent(page);
202
- await page.waitForSelector(loginReadySelector, { state: 'visible', timeout: 3_000 }).catch(() => null);
203
- if (await page.getByLabel('Email').isVisible().catch(() => false)) break;
204
- if (attempt === 3) {
205
- throw new Error(`Login form is unavailable for role: ${role}; current URL: ${page.url()}`);
284
+ for (let attempt = 0; attempt < 4; attempt += 1) {
285
+ await page.goto('/login', { waitUntil: 'domcontentloaded' });
286
+ await dismissGlobalNoticesIfPresent(page);
287
+ await recoverClientSideErrorPageIfPresent(page);
288
+ await recoverGenericErrorPageIfPresent(page);
289
+ await page.waitForSelector(loginReadySelector, { state: 'visible', timeout: 3_000 }).catch(() => null);
290
+ if (await page.getByLabel('Email').isVisible().catch(() => false)) break;
291
+ if (attempt === 3) {
292
+ throw new Error(`Login form is unavailable for role: ${role}; current URL: ${page.url()}`);
293
+ }
206
294
  }
207
- }
208
- await page.getByLabel('Email').fill(creds.email);
295
+ await page.getByLabel('Email').fill(creds.email);
209
296
 
210
- const passwordInput = page.getByLabel('Password', { exact: true }).first();
211
- if (await passwordInput.isVisible().catch(() => false)) {
212
- await passwordInput.fill(creds.password);
213
- await passwordInput.press('Enter');
214
- } else {
215
- const submitButton = page.getByRole('button', { name: /login|sign in|continue with sso/i }).first();
216
- await submitButton.click();
217
- }
297
+ const passwordInput = page.getByLabel('Password', { exact: true }).first();
298
+ if (await passwordInput.isVisible().catch(() => false)) {
299
+ await passwordInput.fill(creds.password);
300
+ await passwordInput.press('Enter');
301
+ } else {
302
+ const submitButton = page.getByRole('button', { name: /login|sign in|continue with sso/i }).first();
303
+ await submitButton.click();
304
+ }
218
305
 
219
- if (await waitForBackend(7_000)) return;
306
+ if (await waitForBackend(7_000)) return;
220
307
 
221
- const loginForm = page.locator('form').first();
222
- if (await loginForm.isVisible().catch(() => false)) {
223
- await loginForm.evaluate((element) => {
224
- const form = element as HTMLFormElement
225
- form.requestSubmit()
226
- }).catch(() => {})
227
- }
228
- if (await waitForBackend(5_000)) return;
308
+ const loginForm = page.locator('form').first();
309
+ if (await loginForm.isVisible().catch(() => false)) {
310
+ await loginForm.evaluate((element) => {
311
+ const form = element as HTMLFormElement
312
+ form.requestSubmit()
313
+ }).catch(() => {})
314
+ }
315
+ if (await waitForBackend(5_000)) return;
229
316
 
230
- const loginButton = page.getByRole('button', { name: /login|sign in|continue with sso/i }).first();
231
- if (await loginButton.isVisible().catch(() => false)) {
232
- await loginButton.click({ force: true });
233
- }
234
- if (await waitForBackend(8_000)) return;
317
+ const loginButton = page.getByRole('button', { name: /login|sign in|continue with sso/i }).first();
318
+ if (await loginButton.isVisible().catch(() => false)) {
319
+ await loginButton.click({ force: true });
320
+ }
321
+ if (await waitForBackend(8_000)) return;
235
322
 
236
- throw new Error(`Login did not reach backend for role: ${role}; current URL: ${page.url()}`);
323
+ throw new Error(`Login did not reach backend for role: ${role}; current URL: ${page.url()}`);
324
+ } catch (error) {
325
+ await throwLoginErrorWithDiagnostics(page, role, authTrace.entries, error);
326
+ } finally {
327
+ authTrace.stop();
328
+ }
237
329
  }
@@ -1,10 +1,13 @@
1
1
  import { z } from 'zod'
2
- import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
2
+ import { makeCrudRoute, type CrudCtx } from '@open-mercato/shared/lib/crud/factory'
3
3
  import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
4
4
  import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
5
+ import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
6
+ import type { EntityManager } from '@mikro-orm/postgresql'
7
+ import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
5
8
  import { createSalesCrudOpenApi, createPagedListResponseSchema, defaultOkResponseSchema } from '../openapi'
6
9
  import { withScopedPayload } from '../utils'
7
- import { SalesDocumentAddress } from '../../data/entities'
10
+ import { SalesDocumentAddress, SalesOrder, SalesQuote } from '../../data/entities'
8
11
  import {
9
12
  documentAddressCreateSchema,
10
13
  documentAddressDeleteSchema,
@@ -34,8 +37,131 @@ const routeMetadata = {
34
37
 
35
38
  export const metadata = routeMetadata
36
39
 
40
+ type DocumentKind = 'order' | 'quote'
41
+ type DocumentAddressAccess = 'view' | 'manage'
42
+ type Translate = (key: string, fallback?: string) => string
43
+
44
+ const DOCUMENT_ADDRESS_FEATURES: Record<DocumentKind, Record<DocumentAddressAccess, string>> = {
45
+ order: { view: 'sales.orders.view', manage: 'sales.orders.manage' },
46
+ quote: { view: 'sales.quotes.view', manage: 'sales.quotes.manage' },
47
+ }
48
+
49
+ function toDocumentKind(value: unknown): DocumentKind | null {
50
+ return value === 'order' || value === 'quote' ? value : null
51
+ }
52
+
53
+ async function resolveDocumentKindForRead(query: { documentId?: string; documentKind?: DocumentKind }, ctx: CrudCtx) {
54
+ if (query.documentKind) return query.documentKind
55
+
56
+ const tenantId = ctx.auth?.tenantId ?? null
57
+ const organizationId =
58
+ ctx.selectedOrganizationId ??
59
+ ctx.auth?.orgId ??
60
+ (Array.isArray(ctx.organizationIds) && ctx.organizationIds.length === 1 ? ctx.organizationIds[0] : null)
61
+ if (!query.documentId || !tenantId || !organizationId) return null
62
+
63
+ const em = ctx.container.resolve('em') as EntityManager
64
+ const scope = { tenantId, organizationId }
65
+ const order = await findOneWithDecryption(em, SalesOrder, { id: query.documentId, tenantId, organizationId }, {}, scope)
66
+ if (order) return 'order'
67
+ const quote = await findOneWithDecryption(em, SalesQuote, { id: query.documentId, tenantId, organizationId }, {}, scope)
68
+ if (quote) return 'quote'
69
+ return null
70
+ }
71
+
72
+ async function ensureDocumentAddressReadAccess(query: { documentId?: string; documentKind?: DocumentKind }, ctx: CrudCtx) {
73
+ const documentKind = await resolveDocumentKindForRead(query, ctx)
74
+ const requiredFeatures = documentKind
75
+ ? [DOCUMENT_ADDRESS_FEATURES[documentKind].view]
76
+ : [DOCUMENT_ADDRESS_FEATURES.order.view, DOCUMENT_ADDRESS_FEATURES.quote.view]
77
+ await ensureDocumentAddressAccess(ctx, requiredFeatures)
78
+ return documentKind
79
+ }
80
+
81
+ async function resolveExistingAddressKind(
82
+ ctx: CrudCtx,
83
+ id: string,
84
+ scope: { tenantId: string; organizationId: string },
85
+ ): Promise<DocumentKind | null> {
86
+ const em = ctx.container.resolve('em') as EntityManager
87
+ const address = await findOneWithDecryption(
88
+ em,
89
+ SalesDocumentAddress,
90
+ { id, tenantId: scope.tenantId, organizationId: scope.organizationId },
91
+ {},
92
+ scope,
93
+ )
94
+ return toDocumentKind(address?.documentKind)
95
+ }
96
+
97
+ async function ensureDocumentAddressAccess(
98
+ ctx: CrudCtx,
99
+ requiredFeatures: string[],
100
+ translate?: Translate,
101
+ ): Promise<void> {
102
+ const resolvedTranslate = translate ?? (await resolveTranslations()).translate
103
+ const auth = ctx.auth
104
+ if (!auth?.sub) {
105
+ throw new CrudHttpError(401, { error: resolvedTranslate('api.errors.unauthorized', 'Unauthorized') })
106
+ }
107
+
108
+ let rbac: RbacService | null = null
109
+ try {
110
+ rbac = ctx.container.resolve('rbacService') as RbacService
111
+ } catch {
112
+ rbac = null
113
+ }
114
+
115
+ const ok = rbac
116
+ ? await rbac.userHasAllFeatures(auth.sub, requiredFeatures, {
117
+ tenantId: auth.tenantId ?? null,
118
+ organizationId: ctx.selectedOrganizationId ?? auth.orgId ?? null,
119
+ })
120
+ : false
121
+ if (!ok) {
122
+ throw new CrudHttpError(403, {
123
+ error: resolvedTranslate('api.errors.forbidden', 'Forbidden'),
124
+ requiredFeatures,
125
+ })
126
+ }
127
+ }
128
+
129
+ async function ensureDocumentAddressKindAccess(
130
+ ctx: CrudCtx,
131
+ documentKind: DocumentKind,
132
+ access: DocumentAddressAccess,
133
+ translate?: Translate,
134
+ ): Promise<void> {
135
+ await ensureDocumentAddressAccess(ctx, [DOCUMENT_ADDRESS_FEATURES[documentKind][access]], translate)
136
+ }
137
+
138
+ async function ensureDocumentAddressMutationAccess(
139
+ ctx: CrudCtx,
140
+ input: { id: string; documentKind: DocumentKind; tenantId: string; organizationId: string },
141
+ options: { includeTargetKind: boolean; translate?: Translate },
142
+ ): Promise<void> {
143
+ const existingKind = await resolveExistingAddressKind(ctx, input.id, {
144
+ tenantId: input.tenantId,
145
+ organizationId: input.organizationId,
146
+ })
147
+ const featureKinds = new Set<DocumentKind>()
148
+ if (existingKind) featureKinds.add(existingKind)
149
+ if (!existingKind || options.includeTargetKind) featureKinds.add(input.documentKind)
150
+
151
+ await ensureDocumentAddressAccess(
152
+ ctx,
153
+ [...featureKinds].map((kind) => DOCUMENT_ADDRESS_FEATURES[kind].manage),
154
+ options.translate,
155
+ )
156
+ }
157
+
37
158
  const crud = makeCrudRoute({
38
159
  metadata: routeMetadata,
160
+ hooks: {
161
+ beforeList: async (query, ctx) => {
162
+ await ensureDocumentAddressReadAccess(query, ctx)
163
+ },
164
+ },
39
165
  orm: {
40
166
  entity: SalesDocumentAddress,
41
167
  idField: 'id',
@@ -76,12 +202,13 @@ const crud = makeCrudRoute({
76
202
  createdAt: 'created_at',
77
203
  updatedAt: 'updated_at',
78
204
  },
79
- buildFilters: async (query: any) => {
205
+ buildFilters: async (query: any, ctx) => {
206
+ const documentKind = await resolveDocumentKindForRead(query, ctx)
80
207
  const filters: Record<string, any> = {
81
208
  document_id: { $eq: query.documentId },
82
209
  }
83
210
 
84
- if (query.documentKind) filters.document_kind = { $eq: query.documentKind }
211
+ if (query.documentKind ?? documentKind) filters.document_kind = { $eq: query.documentKind ?? documentKind }
85
212
  return filters
86
213
  },
87
214
  },
@@ -91,7 +218,9 @@ const crud = makeCrudRoute({
91
218
  schema: rawBodySchema,
92
219
  mapInput: async ({ raw, ctx }) => {
93
220
  const { translate } = await resolveTranslations()
94
- return documentAddressCreateSchema.parse(withScopedPayload(raw ?? {}, ctx, translate))
221
+ const input = documentAddressCreateSchema.parse(withScopedPayload(raw ?? {}, ctx, translate))
222
+ await ensureDocumentAddressKindAccess(ctx, input.documentKind, 'manage', translate)
223
+ return input
95
224
  },
96
225
  response: ({ result }) => ({ id: result?.id ?? null }),
97
226
  status: 201,
@@ -101,7 +230,9 @@ const crud = makeCrudRoute({
101
230
  schema: rawBodySchema,
102
231
  mapInput: async ({ raw, ctx }) => {
103
232
  const { translate } = await resolveTranslations()
104
- return documentAddressUpdateSchema.parse(withScopedPayload(raw ?? {}, ctx, translate))
233
+ const input = documentAddressUpdateSchema.parse(withScopedPayload(raw ?? {}, ctx, translate))
234
+ await ensureDocumentAddressMutationAccess(ctx, input, { includeTargetKind: true, translate })
235
+ return input
105
236
  },
106
237
  response: () => ({ ok: true }),
107
238
  },
@@ -128,7 +259,9 @@ const crud = makeCrudRoute({
128
259
  error: translate('sales.documents.detail.error', 'Document not found or inaccessible.'),
129
260
  })
130
261
  }
131
- return documentAddressDeleteSchema.parse(withScopedPayload({ id, documentId, documentKind }, ctx, translate))
262
+ const input = documentAddressDeleteSchema.parse(withScopedPayload({ id, documentId, documentKind }, ctx, translate))
263
+ await ensureDocumentAddressMutationAccess(ctx, input, { includeTargetKind: false, translate })
264
+ return input
132
265
  },
133
266
  response: () => ({ ok: true }),
134
267
  },
@@ -104,6 +104,12 @@ function applyDocumentAddressSnapshot(em: EntityManager, entity: SalesDocumentAd
104
104
  entity.quote = snapshot.documentKind === 'quote' ? em.getReference(SalesQuote, snapshot.documentId) : null
105
105
  }
106
106
 
107
+ function assertDocumentAddressParent(entity: SalesDocumentAddress, input: { documentId: string; documentKind: 'order' | 'quote' }): void {
108
+ if (entity.documentId !== input.documentId || entity.documentKind !== input.documentKind) {
109
+ throw new CrudHttpError(404, { error: 'sales.document.address.not_found' })
110
+ }
111
+ }
112
+
107
113
  async function emitDocumentAddressIndexSideEffects(
108
114
  ctx: { container: { resolve: (name: string) => unknown } },
109
115
  action: 'created' | 'updated' | 'deleted',
@@ -256,6 +262,7 @@ const updateDocumentAddress: CommandHandler<DocumentAddressUpdateInput, { id: st
256
262
  'sales.document.address.not_found'
257
263
  )
258
264
  ensureSameScope(entity, input.organizationId, input.tenantId)
265
+ assertDocumentAddressParent(entity, input)
259
266
  const document = await requireDocument(em, input.documentKind, input.documentId, input.organizationId, input.tenantId)
260
267
  if (input.documentKind === 'order') {
261
268
  await assertAddressEditable(em, {
@@ -344,6 +351,7 @@ const deleteDocumentAddress: CommandHandler<
344
351
  'sales.document.address.not_found'
345
352
  )
346
353
  ensureSameScope(entity, input.organizationId, input.tenantId)
354
+ assertDocumentAddressParent(entity, input)
347
355
  const document = await requireDocument(em, input.documentKind, input.documentId, input.organizationId, input.tenantId)
348
356
  if (input.documentKind === 'order') {
349
357
  await assertAddressEditable(em, {