251209:1453 Frontend: progress nest = UAT & Bug Fixing
Spec Validation / validate-markdown (push) Has been cancelled
Spec Validation / validate-diagrams (push) Has been cancelled
Spec Validation / check-todos (push) Has been cancelled

This commit is contained in:
admin
2025-12-09 14:53:42 +07:00
parent 8aceced902
commit aa96cd90e3
125 changed files with 11052 additions and 785 deletions
@@ -23,13 +23,14 @@ export class CorrespondenceWorkflowService {
private readonly revisionRepo: Repository<CorrespondenceRevision>,
@InjectRepository(CorrespondenceStatus)
private readonly statusRepo: Repository<CorrespondenceStatus>,
private readonly dataSource: DataSource,
private readonly dataSource: DataSource
) {}
async submitWorkflow(
correspondenceId: number,
userId: number,
note?: string,
userRoles: string[], // [FIX] Added roles for DSL requirements check
note?: string
) {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
@@ -44,7 +45,7 @@ export class CorrespondenceWorkflowService {
if (!revision) {
throw new NotFoundException(
`Correspondence Revision for ID ${correspondenceId} not found`,
`Correspondence Revision for ID ${correspondenceId} not found`
);
}
@@ -66,7 +67,7 @@ export class CorrespondenceWorkflowService {
this.WORKFLOW_CODE,
'correspondence_revision',
revision.id.toString(),
context,
context
);
const transitionResult = await this.workflowEngine.processTransition(
@@ -74,7 +75,7 @@ export class CorrespondenceWorkflowService {
'SUBMIT',
userId,
note || 'Initial Submission',
{},
{ roles: userRoles } // [FIX] Pass roles for DSL requirements check
);
await this.syncStatus(revision, transitionResult.nextState, queryRunner);
@@ -97,14 +98,14 @@ export class CorrespondenceWorkflowService {
async processAction(
instanceId: string,
userId: number,
dto: WorkflowTransitionDto,
dto: WorkflowTransitionDto
) {
const result = await this.workflowEngine.processTransition(
instanceId,
dto.action,
userId,
dto.comment,
dto.payload,
dto.payload
);
// ✅ FIX: Method exists now
@@ -125,7 +126,7 @@ export class CorrespondenceWorkflowService {
private async syncStatus(
revision: CorrespondenceRevision,
workflowState: string,
queryRunner?: any,
queryRunner?: any
) {
const statusMap: Record<string, string> = {
DRAFT: 'DRAFT',
@@ -1,28 +1,48 @@
import { Test, TestingModule } from '@nestjs/testing';
import { CorrespondenceController } from './correspondence.controller';
import { CorrespondenceService } from './correspondence.service';
import { CorrespondenceWorkflowService } from './correspondence-workflow.service';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { RbacGuard } from '../../common/guards/rbac.guard';
describe('CorrespondenceController', () => {
let controller: CorrespondenceController;
let mockCorrespondenceService: Partial<CorrespondenceService>;
let mockWorkflowService: Partial<CorrespondenceWorkflowService>;
beforeEach(async () => {
mockCorrespondenceService = {
create: jest.fn(),
findAll: jest.fn(),
findOne: jest.fn(),
getReferences: jest.fn(),
addReference: jest.fn(),
removeReference: jest.fn(),
};
mockWorkflowService = {
submitWorkflow: jest.fn(),
processAction: jest.fn(),
};
const module: TestingModule = await Test.createTestingModule({
controllers: [CorrespondenceController],
providers: [
{
provide: CorrespondenceService,
useValue: {
create: jest.fn(),
findAll: jest.fn(),
submit: jest.fn(),
processAction: jest.fn(),
getReferences: jest.fn(),
addReference: jest.fn(),
removeReference: jest.fn(),
},
useValue: mockCorrespondenceService,
},
{
provide: CorrespondenceWorkflowService,
useValue: mockWorkflowService,
},
],
}).compile();
})
.overrideGuard(JwtAuthGuard)
.useValue({ canActivate: () => true })
.overrideGuard(RbacGuard)
.useValue({ canActivate: () => true })
.compile();
controller = module.get<CorrespondenceController>(CorrespondenceController);
});
@@ -30,4 +50,67 @@ describe('CorrespondenceController', () => {
it('should be defined', () => {
expect(controller).toBeDefined();
});
describe('findAll', () => {
it('should return correspondences', async () => {
const mockResult = [{ id: 1 }];
(mockCorrespondenceService.findAll as jest.Mock).mockResolvedValue(
mockResult
);
const result = await controller.findAll({});
expect(mockCorrespondenceService.findAll).toHaveBeenCalled();
expect(result).toEqual(mockResult);
});
});
describe('create', () => {
it('should create a correspondence', async () => {
const mockCorr = { id: 1, correspondenceNumber: 'TEST-001' };
(mockCorrespondenceService.create as jest.Mock).mockResolvedValue(
mockCorr
);
const mockReq = { user: { user_id: 1 } };
const createDto = {
projectId: 1,
typeId: 1,
title: 'Test Subject',
};
const result = await controller.create(
createDto as Parameters<typeof controller.create>[0],
mockReq as Parameters<typeof controller.create>[1]
);
expect(mockCorrespondenceService.create).toHaveBeenCalledWith(
createDto,
mockReq.user
);
});
});
describe('submit', () => {
it('should submit a correspondence to workflow', async () => {
const mockResult = { instanceId: 'inst-1', currentState: 'IN_REVIEW' };
(mockWorkflowService.submitWorkflow as jest.Mock).mockResolvedValue(
mockResult
);
const mockReq = { user: { user_id: 1 } };
const result = await controller.submit(
1,
{ note: 'Test note' },
mockReq as Parameters<typeof controller.submit>[2]
);
expect(mockWorkflowService.submitWorkflow).toHaveBeenCalledWith(
1,
1,
'Test note'
);
expect(result).toEqual(mockResult);
});
});
});
@@ -17,6 +17,7 @@ import {
ApiBearerAuth,
} from '@nestjs/swagger';
import { CorrespondenceService } from './correspondence.service';
import { CorrespondenceWorkflowService } from './correspondence-workflow.service';
import { CreateCorrespondenceDto } from './dto/create-correspondence.dto';
import { SubmitCorrespondenceDto } from './dto/submit-correspondence.dto';
import { WorkflowActionDto } from './dto/workflow-action.dto';
@@ -33,18 +34,43 @@ import { Audit } from '../../common/decorators/audit.decorator';
@UseGuards(JwtAuthGuard, RbacGuard)
@ApiBearerAuth()
export class CorrespondenceController {
constructor(private readonly correspondenceService: CorrespondenceService) {}
constructor(
private readonly correspondenceService: CorrespondenceService,
private readonly workflowService: CorrespondenceWorkflowService
) {}
@Post(':id/workflow/action')
@ApiOperation({ summary: 'Process workflow action (Approve/Reject/Review)' })
@ApiResponse({ status: 201, description: 'Action processed successfully.' })
@RequirePermission('workflow.action_review')
processAction(
@Param('id', ParseIntPipe) id: number,
@Body() actionDto: WorkflowActionDto,
@Request() req: any
@Request()
req: Request & {
user: {
user_id: number;
assignments?: Array<{ role: { roleName: string } }>;
};
}
) {
return this.correspondenceService.processAction(id, actionDto, req.user);
// Extract roles from user assignments for DSL requirements check
const userRoles =
req.user.assignments?.map((a) => a.role?.roleName).filter(Boolean) || [];
// Use Unified Workflow Engine via CorrespondenceWorkflowService
if (!actionDto.instanceId) {
throw new Error('instanceId is required for workflow action');
}
return this.workflowService.processAction(
actionDto.instanceId,
req.user.user_id,
{
action: actionDto.action,
comment: actionDto.comment,
payload: { ...actionDto.payload, roles: userRoles },
}
);
}
@Post()
@@ -56,8 +82,14 @@ export class CorrespondenceController {
})
@RequirePermission('correspondence.create')
@Audit('correspondence.create', 'correspondence')
create(@Body() createDto: CreateCorrespondenceDto, @Request() req: any) {
return this.correspondenceService.create(createDto, req.user);
create(
@Body() createDto: CreateCorrespondenceDto,
@Request() req: Request & { user: unknown }
) {
return this.correspondenceService.create(
createDto,
req.user as Parameters<typeof this.correspondenceService.create>[1]
);
}
@Get()
@@ -69,25 +101,45 @@ export class CorrespondenceController {
}
@Post(':id/submit')
@ApiOperation({ summary: 'Submit correspondence to workflow' })
@ApiOperation({ summary: 'Submit correspondence to Unified Workflow Engine' })
@ApiResponse({
status: 201,
description: 'Correspondence submitted successfully.',
})
@RequirePermission('correspondence.create')
@Audit('correspondence.create', 'correspondence')
@Audit('correspondence.submit', 'correspondence')
submit(
@Param('id', ParseIntPipe) id: number,
@Body() submitDto: SubmitCorrespondenceDto,
@Request() req: any
@Request()
req: Request & {
user: {
user_id: number;
assignments?: Array<{ role: { roleName: string } }>;
};
}
) {
return this.correspondenceService.submit(
// Extract roles from user assignments
const userRoles =
req.user.assignments?.map((a) => a.role?.roleName).filter(Boolean) || [];
// Use Unified Workflow Engine - pass user roles for DSL requirements check
return this.workflowService.submitWorkflow(
id,
submitDto.templateId,
req.user
req.user.user_id,
userRoles,
submitDto.note
);
}
@Get(':id')
@ApiOperation({ summary: 'Get correspondence by ID' })
@ApiResponse({ status: 200, description: 'Return correspondence details.' })
@RequirePermission('document.view')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.correspondenceService.findOne(id);
}
@Get(':id/references')
@ApiOperation({ summary: 'Get referenced documents' })
@ApiResponse({
@@ -1,25 +1,29 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CorrespondenceController } from './correspondence.controller.js';
import { CorrespondenceService } from './correspondence.service.js';
import { CorrespondenceController } from './correspondence.controller';
import { CorrespondenceService } from './correspondence.service';
import { CorrespondenceWorkflowService } from './correspondence-workflow.service';
// Entities
import { Correspondence } from './entities/correspondence.entity';
import { CorrespondenceRevision } from './entities/correspondence-revision.entity';
import { CorrespondenceType } from './entities/correspondence-type.entity';
import { Correspondence } from './entities/correspondence.entity';
// Import Entities ใหม่
import { CorrespondenceRouting } from './entities/correspondence-routing.entity';
import { RoutingTemplateStep } from './entities/routing-template-step.entity';
import { RoutingTemplate } from './entities/routing-template.entity';
import { DocumentNumberingModule } from '../document-numbering/document-numbering.module.js'; // ต้องใช้ตอน Create
import { JsonSchemaModule } from '../json-schema/json-schema.module.js'; // ต้องใช้ Validate Details
import { SearchModule } from '../search/search.module'; // ✅ 1. เพิ่ม Import SearchModule
import { UserModule } from '../user/user.module.js'; // <--- 1. Import UserModule
import { WorkflowEngineModule } from '../workflow-engine/workflow-engine.module.js'; // <--- ✅ เพิ่มบรรทัดนี้ครับ
import { CorrespondenceReference } from './entities/correspondence-reference.entity';
import { CorrespondenceStatus } from './entities/correspondence-status.entity';
// Controllers & Services
import { CorrespondenceWorkflowService } from './correspondence-workflow.service'; // Register Service นี้
import { CorrespondenceReference } from './entities/correspondence-reference.entity';
// Dependent Modules
import { DocumentNumberingModule } from '../document-numbering/document-numbering.module';
import { JsonSchemaModule } from '../json-schema/json-schema.module';
import { UserModule } from '../user/user.module';
import { WorkflowEngineModule } from '../workflow-engine/workflow-engine.module';
import { SearchModule } from '../search/search.module';
/**
* CorrespondenceModule
*
* NOTE: RoutingTemplate and RoutingTemplateStep have been deprecated.
* All workflow operations now use the Unified Workflow Engine.
*/
@Module({
imports: [
TypeOrmModule.forFeature([
@@ -27,19 +31,16 @@ import { CorrespondenceWorkflowService } from './correspondence-workflow.service
CorrespondenceRevision,
CorrespondenceType,
CorrespondenceStatus,
RoutingTemplate, // <--- ลงทะเบียน
RoutingTemplateStep, // <--- ลงทะเบียน
CorrespondenceRouting, // <--- ลงทะเบียน
CorrespondenceReference, // <--- ลงทะเบียน
CorrespondenceReference,
]),
DocumentNumberingModule, // Import เพื่อขอเลขที่เอกสาร
JsonSchemaModule, // Import เพื่อ Validate JSON
UserModule, // <--- 2. ใส่ UserModule ใน imports เพื่อให้ RbacGuard ทำงานได้
WorkflowEngineModule, // <--- Import WorkflowEngine
SearchModule, // ✅ 2. ใส่ SearchModule ที่นี่
DocumentNumberingModule,
JsonSchemaModule,
UserModule,
WorkflowEngineModule,
SearchModule,
],
controllers: [CorrespondenceController],
providers: [CorrespondenceService, CorrespondenceWorkflowService],
exports: [CorrespondenceService],
exports: [CorrespondenceService, CorrespondenceWorkflowService],
})
export class CorrespondenceModule {}
@@ -1,12 +1,111 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
import { CorrespondenceService } from './correspondence.service';
import { Correspondence } from './entities/correspondence.entity';
import { CorrespondenceRevision } from './entities/correspondence-revision.entity';
import { CorrespondenceType } from './entities/correspondence-type.entity';
import { CorrespondenceStatus } from './entities/correspondence-status.entity';
import { RoutingTemplate } from './entities/routing-template.entity';
import { CorrespondenceRouting } from './entities/correspondence-routing.entity';
import { CorrespondenceReference } from './entities/correspondence-reference.entity';
import { DocumentNumberingService } from '../document-numbering/document-numbering.service';
import { JsonSchemaService } from '../json-schema/json-schema.service';
import { WorkflowEngineService } from '../workflow-engine/workflow-engine.service';
import { UserService } from '../user/user.service';
import { SearchService } from '../search/search.service';
describe('CorrespondenceService', () => {
let service: CorrespondenceService;
const createMockRepository = () => ({
find: jest.fn(),
findOne: jest.fn(),
create: jest.fn(),
save: jest.fn(),
softDelete: jest.fn(),
createQueryBuilder: jest.fn(() => ({
leftJoinAndSelect: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
orderBy: jest.fn().mockReturnThis(),
skip: jest.fn().mockReturnThis(),
take: jest.fn().mockReturnThis(),
getOne: jest.fn().mockResolvedValue(null),
getMany: jest.fn().mockResolvedValue([]),
getManyAndCount: jest.fn().mockResolvedValue([[], 0]),
})),
});
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [CorrespondenceService],
providers: [
CorrespondenceService,
{
provide: getRepositoryToken(Correspondence),
useValue: createMockRepository(),
},
{
provide: getRepositoryToken(CorrespondenceRevision),
useValue: createMockRepository(),
},
{
provide: getRepositoryToken(CorrespondenceType),
useValue: createMockRepository(),
},
{
provide: getRepositoryToken(CorrespondenceStatus),
useValue: createMockRepository(),
},
{
provide: getRepositoryToken(RoutingTemplate),
useValue: createMockRepository(),
},
{
provide: getRepositoryToken(CorrespondenceRouting),
useValue: createMockRepository(),
},
{
provide: getRepositoryToken(CorrespondenceReference),
useValue: createMockRepository(),
},
{
provide: DocumentNumberingService,
useValue: { generateNextNumber: jest.fn() },
},
{
provide: JsonSchemaService,
useValue: { validate: jest.fn() },
},
{
provide: WorkflowEngineService,
useValue: { startWorkflow: jest.fn(), processAction: jest.fn() },
},
{
provide: UserService,
useValue: { findOne: jest.fn() },
},
{
provide: DataSource,
useValue: {
createQueryRunner: jest.fn(() => ({
connect: jest.fn(),
startTransaction: jest.fn(),
commitTransaction: jest.fn(),
rollbackTransaction: jest.fn(),
release: jest.fn(),
manager: {
save: jest.fn(),
findOne: jest.fn(),
},
})),
},
},
{
provide: SearchService,
useValue: { indexDocument: jest.fn() },
},
],
}).compile();
service = module.get<CorrespondenceService>(CorrespondenceService);
@@ -15,4 +114,12 @@ describe('CorrespondenceService', () => {
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('findAll', () => {
it('should return paginated correspondences', async () => {
const result = await service.findAll({ projectId: 1 });
expect(result.data).toBeDefined();
expect(result.meta).toBeDefined();
});
});
});
@@ -9,27 +9,21 @@ import {
Logger,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, DataSource, Like, In } from 'typeorm';
import { Repository, DataSource } from 'typeorm';
// Entitie
// Entities
import { Correspondence } from './entities/correspondence.entity';
import { CorrespondenceRevision } from './entities/correspondence-revision.entity';
import { CorrespondenceType } from './entities/correspondence-type.entity';
import { CorrespondenceStatus } from './entities/correspondence-status.entity';
import { RoutingTemplate } from './entities/routing-template.entity';
import { CorrespondenceRouting } from './entities/correspondence-routing.entity';
import { CorrespondenceReference } from './entities/correspondence-reference.entity';
import { User } from '../user/entities/user.entity';
// DTOs
import { CreateCorrespondenceDto } from './dto/create-correspondence.dto';
import { WorkflowActionDto } from './dto/workflow-action.dto';
import { AddReferenceDto } from './dto/add-reference.dto';
import { SearchCorrespondenceDto } from './dto/search-correspondence.dto';
// Interfaces & Enums
import { WorkflowAction } from '../workflow-engine/interfaces/workflow.interface';
// Services
import { DocumentNumberingService } from '../document-numbering/document-numbering.service';
import { JsonSchemaService } from '../json-schema/json-schema.service';
@@ -37,6 +31,12 @@ import { WorkflowEngineService } from '../workflow-engine/workflow-engine.servic
import { UserService } from '../user/user.service';
import { SearchService } from '../search/search.service';
/**
* CorrespondenceService - Document management (CRUD)
*
* NOTE: Workflow operations (submit, processAction) have been moved to
* CorrespondenceWorkflowService which uses the Unified Workflow Engine.
*/
@Injectable()
export class CorrespondenceService {
private readonly logger = new Logger(CorrespondenceService.name);
@@ -50,10 +50,6 @@ export class CorrespondenceService {
private typeRepo: Repository<CorrespondenceType>,
@InjectRepository(CorrespondenceStatus)
private statusRepo: Repository<CorrespondenceStatus>,
@InjectRepository(RoutingTemplate)
private templateRepo: Repository<RoutingTemplate>,
@InjectRepository(CorrespondenceRouting)
private routingRepo: Repository<CorrespondenceRouting>,
@InjectRepository(CorrespondenceReference)
private referenceRepo: Repository<CorrespondenceReference>,
@@ -111,9 +107,9 @@ export class CorrespondenceService {
if (createDto.details) {
try {
await this.jsonSchemaService.validate(type.typeCode, createDto.details);
} catch (error: any) {
} catch (error: unknown) {
this.logger.warn(
`Schema validation warning for ${type.typeCode}: ${error.message}`
`Schema validation warning for ${type.typeCode}: ${(error as Error).message}`
);
}
}
@@ -125,13 +121,12 @@ export class CorrespondenceService {
try {
const orgCode = 'ORG'; // TODO: Fetch real ORG Code from Organization Entity
// [FIXED] เรียกใช้แบบ Object Context ตาม Requirement 6B
const docNumber = await this.numberingService.generateNextNumber({
projectId: createDto.projectId,
originatorId: userOrgId,
typeId: createDto.typeId,
disciplineId: createDto.disciplineId, // ส่ง Discipline (ถ้ามี)
subTypeId: createDto.subTypeId, // ส่ง SubType (ถ้ามี)
disciplineId: createDto.disciplineId,
subTypeId: createDto.subTypeId,
year: new Date().getFullYear(),
customTokens: {
TYPE_CODE: type.typeCode,
@@ -142,7 +137,7 @@ export class CorrespondenceService {
const correspondence = queryRunner.manager.create(Correspondence, {
correspondenceNumber: docNumber,
correspondenceTypeId: createDto.typeId,
disciplineId: createDto.disciplineId, // บันทึก Discipline ลง DB
disciplineId: createDto.disciplineId,
projectId: createDto.projectId,
originatorId: userOrgId,
isInternal: createDto.isInternal || false,
@@ -165,7 +160,7 @@ export class CorrespondenceService {
await queryRunner.commitTransaction();
// [NEW V1.5.1] Start Workflow Instance (After Commit)
// Start Workflow Instance (non-blocking)
try {
const workflowCode = `CORRESPONDENCE_${type.typeCode}`;
await this.workflowEngine.createInstance(
@@ -183,7 +178,6 @@ export class CorrespondenceService {
this.logger.warn(
`Workflow not started for ${docNumber} (Code: CORRESPONDENCE_${type.typeCode}): ${(error as Error).message}`
);
// Non-blocking: Document is created, but workflow might not be active.
}
this.searchService.indexDocument({
@@ -212,7 +206,6 @@ export class CorrespondenceService {
}
}
// ... (method อื่นๆ คงเดิม)
async findAll(searchDto: SearchCorrespondenceDto = {}) {
const { search, typeId, projectId, statusId } = searchDto;
@@ -266,182 +259,6 @@ export class CorrespondenceService {
return correspondence;
}
async submit(correspondenceId: number, templateId: number, user: User) {
const correspondence = await this.correspondenceRepo.findOne({
where: { id: correspondenceId },
relations: ['revisions'],
});
if (!correspondence) {
throw new NotFoundException('Correspondence not found');
}
const currentRevision = correspondence.revisions?.find((r) => r.isCurrent);
if (!currentRevision) {
throw new NotFoundException('Current revision not found');
}
const template = await this.templateRepo.findOne({
where: { id: templateId },
relations: ['steps'],
order: { steps: { sequence: 'ASC' } },
});
if (!template || !template.steps?.length) {
throw new BadRequestException(
'Invalid routing template or no steps defined'
);
}
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const firstStep = template.steps[0];
const routing = queryRunner.manager.create(CorrespondenceRouting, {
correspondenceId: currentRevision.id,
templateId: template.id,
sequence: 1,
fromOrganizationId: user.primaryOrganizationId,
toOrganizationId: firstStep.toOrganizationId,
stepPurpose: firstStep.stepPurpose,
status: 'SENT',
dueDate: new Date(
Date.now() + (firstStep.expectedDays || 7) * 24 * 60 * 60 * 1000
),
processedByUserId: user.user_id,
processedAt: new Date(),
});
await queryRunner.manager.save(routing);
await queryRunner.commitTransaction();
return routing;
} catch (err) {
await queryRunner.rollbackTransaction();
throw err;
} finally {
await queryRunner.release();
}
}
async processAction(
correspondenceId: number,
dto: WorkflowActionDto,
user: User
) {
const correspondence = await this.correspondenceRepo.findOne({
where: { id: correspondenceId },
relations: ['revisions'],
});
if (!correspondence)
throw new NotFoundException('Correspondence not found');
const currentRevision = correspondence.revisions?.find((r) => r.isCurrent);
if (!currentRevision)
throw new NotFoundException('Current revision not found');
const currentRouting = await this.routingRepo.findOne({
where: {
correspondenceId: currentRevision.id,
status: 'SENT',
},
order: { sequence: 'DESC' },
relations: ['toOrganization'],
});
if (!currentRouting) {
throw new BadRequestException(
'No active workflow step found for this document'
);
}
if (currentRouting.toOrganizationId !== user.primaryOrganizationId) {
throw new BadRequestException(
'You are not authorized to process this step'
);
}
if (!currentRouting.templateId) {
throw new InternalServerErrorException(
'Routing record missing templateId'
);
}
const template = await this.templateRepo.findOne({
where: { id: currentRouting.templateId },
relations: ['steps'],
});
if (!template || !template.steps) {
throw new InternalServerErrorException('Template definition not found');
}
const totalSteps = template.steps.length;
const currentSeq = currentRouting.sequence;
const result = this.workflowEngine.processAction(
currentSeq,
totalSteps,
dto.action,
dto.returnToSequence
);
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
currentRouting.status =
dto.action === WorkflowAction.REJECT ? 'REJECTED' : 'ACTIONED';
currentRouting.processedByUserId = user.user_id;
currentRouting.processedAt = new Date();
currentRouting.comments = dto.comments;
await queryRunner.manager.save(currentRouting);
if (result.nextStepSequence && dto.action !== WorkflowAction.REJECT) {
const nextStepConfig = template.steps.find(
(s) => s.sequence === result.nextStepSequence
);
if (!nextStepConfig) {
this.logger.warn(
`Next step ${result.nextStepSequence} not found in template`
);
} else {
const nextRouting = queryRunner.manager.create(
CorrespondenceRouting,
{
correspondenceId: currentRevision.id,
templateId: template.id,
sequence: result.nextStepSequence,
fromOrganizationId: user.primaryOrganizationId,
toOrganizationId: nextStepConfig.toOrganizationId,
stepPurpose: nextStepConfig.stepPurpose,
status: 'SENT',
dueDate: new Date(
Date.now() +
(nextStepConfig.expectedDays || 7) * 24 * 60 * 60 * 1000
),
}
);
await queryRunner.manager.save(nextRouting);
}
}
await queryRunner.commitTransaction();
return { message: 'Action processed successfully', result };
} catch (err) {
await queryRunner.rollbackTransaction();
throw err;
} finally {
await queryRunner.release();
}
}
async addReference(id: number, dto: AddReferenceDto) {
const source = await this.correspondenceRepo.findOne({ where: { id } });
const target = await this.correspondenceRepo.findOne({
@@ -1,12 +1,16 @@
import { IsInt, IsNotEmpty } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { IsOptional, IsString } from 'class-validator';
import { ApiPropertyOptional } from '@nestjs/swagger';
/**
* DTO for submitting correspondence to workflow
* Uses Unified Workflow Engine - no templateId required
*/
export class SubmitCorrespondenceDto {
@ApiProperty({
description: 'ID of the Workflow Template to start',
example: 1,
@ApiPropertyOptional({
description: 'Optional note for the submission',
example: 'Submitting for review',
})
@IsInt()
@IsNotEmpty()
templateId!: number;
@IsString()
@IsOptional()
note?: string;
}
@@ -1,14 +1,29 @@
import { IsEnum, IsString, IsOptional, IsInt } from 'class-validator';
import { IsEnum, IsString, IsOptional, IsUUID, IsInt } from 'class-validator';
import { WorkflowAction } from '../../workflow-engine/interfaces/workflow.interface';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
/**
* DTO for processing workflow actions
*
* Supports both:
* - New Unified Workflow Engine (uses instanceId)
* - Legacy RFA workflow (uses returnToSequence)
*/
export class WorkflowActionDto {
@ApiPropertyOptional({
description: 'Workflow Instance ID (UUID) - for Unified Workflow Engine',
example: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
})
@IsUUID()
@IsOptional()
instanceId?: string;
@ApiProperty({
description: 'Workflow Action',
enum: ['APPROVE', 'REJECT', 'RETURN', 'CANCEL', 'ACKNOWLEDGE'],
})
@IsEnum(WorkflowAction)
action!: WorkflowAction; // APPROVE, REJECT, RETURN, ACKNOWLEDGE
action!: WorkflowAction;
@ApiPropertyOptional({
description: 'Review comments',
@@ -16,13 +31,31 @@ export class WorkflowActionDto {
})
@IsString()
@IsOptional()
comment?: string;
/**
* @deprecated Use 'comment' instead
*/
@ApiPropertyOptional({
description: 'Review comments (deprecated, use comment)',
example: 'Approved with note...',
})
@IsString()
@IsOptional()
comments?: string;
@ApiPropertyOptional({
description: 'Sequence to return to (only for RETURN action)',
description: 'Sequence to return to (only for RETURN action in legacy RFA)',
example: 1,
})
@IsInt()
@IsOptional()
returnToSequence?: number; // ใช้กรณี action = RETURN
returnToSequence?: number;
@ApiPropertyOptional({
description: 'Additional payload data',
example: { priority: 'HIGH' },
})
@IsOptional()
payload?: Record<string, unknown>;
}
@@ -1,15 +1,12 @@
// File: src/modules/correspondence/entities/routing-template-step.entity.ts
import {
Entity,
PrimaryGeneratedColumn,
Column,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { RoutingTemplate } from './routing-template.entity';
import { Organization } from '../../project/entities/organization.entity';
import { Role } from '../../user/entities/role.entity';
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
/**
* @deprecated This entity is deprecated and will be removed in future versions.
* Use WorkflowDefinition from the Unified Workflow Engine instead.
*
* This entity is kept for backward compatibility and historical data.
* Relations have been removed to prevent TypeORM errors.
*/
@Entity('correspondence_routing_template_steps')
export class RoutingTemplateStep {
@PrimaryGeneratedColumn()
@@ -24,27 +21,12 @@ export class RoutingTemplateStep {
@Column({ name: 'to_organization_id' })
toOrganizationId!: number;
@Column({ name: 'role_id', nullable: true })
roleId?: number;
@Column({ name: 'step_purpose', default: 'FOR_REVIEW' })
@Column({ name: 'step_purpose', length: 50, default: 'FOR_REVIEW' })
stepPurpose!: string;
@Column({ name: 'expected_days', nullable: true })
expectedDays?: number;
@Column({ name: 'expected_days', default: 7 })
expectedDays!: number;
// Relations
@ManyToOne(() => RoutingTemplate, (template) => template.steps, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'template_id' })
template?: RoutingTemplate;
@ManyToOne(() => Organization)
@JoinColumn({ name: 'to_organization_id' })
toOrganization?: Organization;
@ManyToOne(() => Role)
@JoinColumn({ name: 'role_id' })
role?: Role;
// @deprecated - Relation removed, use WorkflowDefinition instead
// template?: RoutingTemplate;
}
@@ -1,7 +1,12 @@
import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm';
import { BaseEntity } from '../../../common/entities/base.entity'; // ถ้าไม่ได้ใช้ BaseEntity ก็ลบออกแล้วใส่ createdAt เอง
import { RoutingTemplateStep } from './routing-template-step.entity'; // เดี๋ยวสร้าง
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
/**
* @deprecated This entity is deprecated and will be removed in future versions.
* Use WorkflowDefinition from the Unified Workflow Engine instead.
*
* This entity is kept for backward compatibility and historical data.
* The relation to RoutingTemplateStep has been removed to prevent TypeORM errors.
*/
@Entity('correspondence_routing_templates')
export class RoutingTemplate {
@PrimaryGeneratedColumn()
@@ -14,14 +19,14 @@ export class RoutingTemplate {
description?: string;
@Column({ name: 'project_id', nullable: true })
projectId?: number; // NULL = แม่แบบทั่วไป
projectId?: number;
@Column({ name: 'is_active', default: true })
isActive!: boolean;
@Column({ type: 'json', nullable: true, name: 'workflow_config' })
workflowConfig?: any;
workflowConfig?: Record<string, unknown>;
@OneToMany(() => RoutingTemplateStep, (step) => step.template)
steps?: RoutingTemplateStep[];
// @deprecated - Relation removed, use WorkflowDefinition instead
// steps?: RoutingTemplateStep[];
}