@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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/helpers/integration/auth.js +148 -82
- package/dist/helpers/integration/auth.js.map +2 -2
- package/dist/modules/sales/api/document-addresses/route.js +96 -6
- package/dist/modules/sales/api/document-addresses/route.js.map +2 -2
- package/dist/modules/sales/commands/documentAddresses.js +7 -0
- package/dist/modules/sales/commands/documentAddresses.js.map +2 -2
- package/dist/modules/staff/commands/activities.js +23 -12
- package/dist/modules/staff/commands/activities.js.map +2 -2
- package/dist/modules/staff/commands/addresses.js +20 -12
- package/dist/modules/staff/commands/addresses.js.map +2 -2
- package/dist/modules/staff/commands/comments.js +18 -11
- package/dist/modules/staff/commands/comments.js.map +2 -2
- package/dist/modules/staff/commands/job-histories.js +23 -12
- package/dist/modules/staff/commands/job-histories.js.map +2 -2
- package/dist/modules/staff/commands/leave-requests.js +37 -19
- package/dist/modules/staff/commands/leave-requests.js.map +2 -2
- package/dist/modules/staff/commands/shared.js +28 -1
- package/dist/modules/staff/commands/shared.js.map +2 -2
- package/dist/modules/staff/commands/tag-assignments.js +20 -3
- package/dist/modules/staff/commands/tag-assignments.js.map +2 -2
- package/dist/modules/staff/commands/team-members.js +21 -11
- package/dist/modules/staff/commands/team-members.js.map +2 -2
- package/dist/modules/staff/commands/team-roles.js +21 -11
- package/dist/modules/staff/commands/team-roles.js.map +2 -2
- package/dist/modules/staff/commands/teams.js +21 -11
- package/dist/modules/staff/commands/teams.js.map +2 -2
- package/dist/modules/staff/commands/timesheets-entries.js +24 -14
- package/dist/modules/staff/commands/timesheets-entries.js.map +2 -2
- package/dist/modules/staff/commands/timesheets-projects.js +34 -18
- package/dist/modules/staff/commands/timesheets-projects.js.map +2 -2
- package/dist/modules/workflows/api/instances/route.js +1 -1
- package/dist/modules/workflows/api/instances/route.js.map +2 -2
- package/package.json +7 -7
- package/src/helpers/integration/auth.ts +178 -86
- package/src/modules/sales/api/document-addresses/route.ts +140 -7
- package/src/modules/sales/commands/documentAddresses.ts +8 -0
- package/src/modules/staff/commands/activities.ts +23 -11
- package/src/modules/staff/commands/addresses.ts +20 -11
- package/src/modules/staff/commands/comments.ts +18 -10
- package/src/modules/staff/commands/job-histories.ts +27 -11
- package/src/modules/staff/commands/leave-requests.ts +38 -19
- package/src/modules/staff/commands/shared.ts +38 -0
- package/src/modules/staff/commands/tag-assignments.ts +19 -2
- package/src/modules/staff/commands/team-members.ts +21 -10
- package/src/modules/staff/commands/team-roles.ts +21 -10
- package/src/modules/staff/commands/teams.ts +21 -10
- package/src/modules/staff/commands/timesheets-entries.ts +24 -13
- package/src/modules/staff/commands/timesheets-projects.ts +34 -17
- 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.
|
|
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.
|
|
257
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
258
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
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.
|
|
264
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
265
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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 (
|
|
191
|
-
await
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
306
|
+
if (await waitForBackend(7_000)) return;
|
|
220
307
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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, {
|