251209:1453 Frontend: progress nest = UAT & Bug Fixing
This commit is contained in:
@@ -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[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user