690514:2019 204-rfa-approval-refactor #01
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
// File: src/modules/response-code/dto/create-response-code.dto.ts
|
||||
// Change Log:
|
||||
// - 2026-05-13: Add DTO for creating custom response codes.
|
||||
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
IsObject,
|
||||
IsOptional,
|
||||
IsString,
|
||||
MaxLength,
|
||||
MinLength,
|
||||
} from 'class-validator';
|
||||
import { ResponseCodeCategory } from '../../common/enums/review.enums';
|
||||
|
||||
export class CreateResponseCodeDto {
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
@MaxLength(10)
|
||||
code!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(10)
|
||||
subStatus?: string;
|
||||
|
||||
@IsEnum(ResponseCodeCategory)
|
||||
category!: ResponseCodeCategory;
|
||||
|
||||
@IsString()
|
||||
descriptionTh!: string;
|
||||
|
||||
@IsString()
|
||||
descriptionEn!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
implications?: {
|
||||
affectsSchedule?: boolean;
|
||||
affectsCost?: boolean;
|
||||
requiresContractReview?: boolean;
|
||||
requiresEiaAmendment?: boolean;
|
||||
};
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
notifyRoles?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isActive?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// File: src/modules/response-code/dto/update-response-code.dto.ts
|
||||
// Change Log:
|
||||
// - 2026-05-13: Add DTO for updating response codes by publicId.
|
||||
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
IsObject,
|
||||
IsOptional,
|
||||
IsString,
|
||||
MaxLength,
|
||||
MinLength,
|
||||
} from 'class-validator';
|
||||
import { ResponseCodeCategory } from '../../common/enums/review.enums';
|
||||
|
||||
export class UpdateResponseCodeDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
@MaxLength(10)
|
||||
code?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(10)
|
||||
subStatus?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(ResponseCodeCategory)
|
||||
category?: ResponseCodeCategory;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
descriptionTh?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
descriptionEn?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
implications?: {
|
||||
affectsSchedule?: boolean;
|
||||
affectsCost?: boolean;
|
||||
requiresContractReview?: boolean;
|
||||
requiresEiaAmendment?: boolean;
|
||||
};
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
notifyRoles?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isActive?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// File: src/modules/response-code/dto/upsert-response-code-rule.dto.ts
|
||||
// Change Log:
|
||||
// - 2026-05-13: Add DTO for response code matrix rule upsert endpoints.
|
||||
|
||||
import { IsBoolean, IsInt, IsOptional, IsUUID, Min } from 'class-validator';
|
||||
|
||||
export class UpsertResponseCodeRuleDto {
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
documentTypeId!: number;
|
||||
|
||||
@IsUUID()
|
||||
responseCodePublicId!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
projectPublicId?: string;
|
||||
|
||||
@IsBoolean()
|
||||
isEnabled!: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
requiresComments?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
triggersNotification?: boolean;
|
||||
}
|
||||
@@ -1,19 +1,51 @@
|
||||
// File: src/modules/response-code/response-code.controller.ts
|
||||
import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common';
|
||||
// Change Log:
|
||||
// - 2026-05-13: Resolve project query identifiers through UuidResolverService and stop numeric coercion on public IDs.
|
||||
// - 2026-05-13: Add basic CRUD endpoints with RBAC enforcement for response code management.
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Patch,
|
||||
ParseIntPipe,
|
||||
Post,
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
import { RbacGuard } from '../../common/guards/rbac.guard';
|
||||
import { ParseUuidPipe } from '../../common/pipes/parse-uuid.pipe';
|
||||
import { UuidResolverService } from '../../common/services/uuid-resolver.service';
|
||||
import { ResponseCodeService } from './response-code.service';
|
||||
import { ResponseCodeCategory } from '../common/enums/review.enums';
|
||||
import { CreateResponseCodeDto } from './dto/create-response-code.dto';
|
||||
import { UpdateResponseCodeDto } from './dto/update-response-code.dto';
|
||||
import { UpsertResponseCodeRuleDto } from './dto/upsert-response-code-rule.dto';
|
||||
import { MatrixManagementService } from './services/matrix-management.service';
|
||||
import { InheritanceService } from './services/inheritance.service';
|
||||
|
||||
@ApiTags('Response Codes')
|
||||
@ApiBearerAuth()
|
||||
@Controller('response-codes')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@UseGuards(JwtAuthGuard, RbacGuard)
|
||||
export class ResponseCodeController {
|
||||
constructor(private readonly responseCodeService: ResponseCodeService) {}
|
||||
constructor(
|
||||
private readonly responseCodeService: ResponseCodeService,
|
||||
private readonly uuidResolver: UuidResolverService,
|
||||
private readonly matrixManagementService: MatrixManagementService,
|
||||
private readonly inheritanceService: InheritanceService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* GET /response-codes
|
||||
* ดึง Response Codes ทั้งหมด
|
||||
*/
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Get all active response codes' })
|
||||
findAll() {
|
||||
return this.responseCodeService.findAll();
|
||||
}
|
||||
@@ -23,6 +55,7 @@ export class ResponseCodeController {
|
||||
* ดึง Response Codes ตาม Category (FR-006)
|
||||
*/
|
||||
@Get('category/:category')
|
||||
@ApiOperation({ summary: 'Get response codes by category' })
|
||||
findByCategory(@Param('category') category: ResponseCodeCategory) {
|
||||
return this.responseCodeService.findByCategory(category);
|
||||
}
|
||||
@@ -32,13 +65,18 @@ export class ResponseCodeController {
|
||||
* ดึง Response Codes ที่ใช้ได้กับ document type + project
|
||||
*/
|
||||
@Get('document-type/:documentTypeId')
|
||||
findByDocumentType(
|
||||
@Param('documentTypeId') documentTypeId: string,
|
||||
@ApiOperation({ summary: 'Get response codes by document type and project' })
|
||||
async findByDocumentType(
|
||||
@Param('documentTypeId', ParseIntPipe) documentTypeId: number,
|
||||
@Query('projectId') projectId?: string
|
||||
) {
|
||||
const resolvedProjectId = projectId
|
||||
? await this.uuidResolver.resolveProjectId(projectId)
|
||||
: undefined;
|
||||
|
||||
return this.responseCodeService.findByDocumentType(
|
||||
Number(documentTypeId),
|
||||
projectId ? Number(projectId) : undefined
|
||||
documentTypeId,
|
||||
resolvedProjectId
|
||||
);
|
||||
}
|
||||
|
||||
@@ -47,7 +85,79 @@ export class ResponseCodeController {
|
||||
* ดึง Response Code ตาม publicId (ADR-019)
|
||||
*/
|
||||
@Get(':publicId')
|
||||
findOne(@Param('publicId') publicId: string) {
|
||||
@ApiOperation({ summary: 'Get response code by publicId' })
|
||||
findOne(@Param('publicId', ParseUuidPipe) publicId: string) {
|
||||
return this.responseCodeService.findByPublicId(publicId);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@RequirePermission('master_data.manage')
|
||||
@ApiOperation({ summary: 'Create a custom response code' })
|
||||
create(@Body() dto: CreateResponseCodeDto) {
|
||||
return this.responseCodeService.create(dto);
|
||||
}
|
||||
|
||||
@Patch(':publicId')
|
||||
@RequirePermission('master_data.manage')
|
||||
@ApiOperation({ summary: 'Update response code by publicId' })
|
||||
update(
|
||||
@Param('publicId', ParseUuidPipe) publicId: string,
|
||||
@Body() dto: UpdateResponseCodeDto
|
||||
) {
|
||||
return this.responseCodeService.update(publicId, dto);
|
||||
}
|
||||
|
||||
@Delete(':publicId')
|
||||
@RequirePermission('master_data.manage')
|
||||
@ApiOperation({ summary: 'Deactivate response code by publicId' })
|
||||
async remove(@Param('publicId', ParseUuidPipe) publicId: string) {
|
||||
await this.responseCodeService.deactivate(publicId);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
@Get('matrix/:documentTypeId')
|
||||
@ApiOperation({ summary: 'Resolve response code matrix by document type' })
|
||||
async getMatrix(
|
||||
@Param('documentTypeId', ParseIntPipe) documentTypeId: number,
|
||||
@Query('projectId') projectId?: string
|
||||
) {
|
||||
const resolvedProjectId = projectId
|
||||
? await this.uuidResolver.resolveProjectId(projectId)
|
||||
: undefined;
|
||||
|
||||
return this.inheritanceService.resolveMatrix(
|
||||
documentTypeId,
|
||||
resolvedProjectId
|
||||
);
|
||||
}
|
||||
|
||||
@Post('matrix/rules')
|
||||
@RequirePermission('master_data.manage')
|
||||
@ApiOperation({ summary: 'Create or update a response code matrix rule' })
|
||||
async upsertRule(@Body() dto: UpsertResponseCodeRuleDto) {
|
||||
const resolvedProjectId = dto.projectPublicId
|
||||
? await this.uuidResolver.resolveProjectId(dto.projectPublicId)
|
||||
: undefined;
|
||||
|
||||
return this.matrixManagementService.upsertRule({
|
||||
documentTypeId: dto.documentTypeId,
|
||||
responseCodePublicId: dto.responseCodePublicId,
|
||||
projectId: resolvedProjectId,
|
||||
isEnabled: dto.isEnabled,
|
||||
requiresComments: dto.requiresComments,
|
||||
triggersNotification: dto.triggersNotification,
|
||||
});
|
||||
}
|
||||
|
||||
@Delete('matrix/rules/:rulePublicId')
|
||||
@RequirePermission('master_data.manage')
|
||||
@ApiOperation({
|
||||
summary: 'Delete a project-specific response code matrix override',
|
||||
})
|
||||
async deleteRuleOverride(
|
||||
@Param('rulePublicId', ParseUuidPipe) rulePublicId: string
|
||||
) {
|
||||
await this.matrixManagementService.deleteProjectOverride(rulePublicId);
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// File: src/modules/response-code/response-code.module.ts
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { AuditLog } from '../../common/entities/audit-log.entity';
|
||||
import { ResponseCode } from './entities/response-code.entity';
|
||||
import { ResponseCodeRule } from './entities/response-code-rule.entity';
|
||||
import { ResponseCodeService } from './response-code.service';
|
||||
import { ResponseCodeController } from './response-code.controller';
|
||||
import { ResponseCodeAuditService } from './services/audit.service';
|
||||
import { ImplicationsService } from './services/implications.service';
|
||||
import { NotificationTriggerService } from './services/notification-trigger.service';
|
||||
import { MatrixManagementService } from './services/matrix-management.service';
|
||||
@@ -14,11 +16,12 @@ import { NotificationModule } from '../notification/notification.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([ResponseCode, ResponseCodeRule, User]),
|
||||
TypeOrmModule.forFeature([ResponseCode, ResponseCodeRule, User, AuditLog]),
|
||||
NotificationModule,
|
||||
],
|
||||
providers: [
|
||||
ResponseCodeService,
|
||||
ResponseCodeAuditService,
|
||||
ImplicationsService,
|
||||
NotificationTriggerService,
|
||||
MatrixManagementService,
|
||||
@@ -27,6 +30,7 @@ import { NotificationModule } from '../notification/notification.module';
|
||||
controllers: [ResponseCodeController],
|
||||
exports: [
|
||||
ResponseCodeService,
|
||||
ResponseCodeAuditService,
|
||||
ImplicationsService,
|
||||
NotificationTriggerService,
|
||||
MatrixManagementService,
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
// File: src/modules/response-code/response-code.service.ts
|
||||
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
|
||||
// Change Log:
|
||||
// - 2026-05-13: Add basic CRUD methods for response codes to support controller mutations.
|
||||
import {
|
||||
ConflictException,
|
||||
Injectable,
|
||||
Logger,
|
||||
NotFoundException,
|
||||
BadRequestException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, IsNull } from 'typeorm';
|
||||
import { ResponseCode } from './entities/response-code.entity';
|
||||
import { ResponseCodeRule } from './entities/response-code-rule.entity';
|
||||
import { ResponseCodeCategory } from '../common/enums/review.enums';
|
||||
import { CreateResponseCodeDto } from './dto/create-response-code.dto';
|
||||
import { UpdateResponseCodeDto } from './dto/update-response-code.dto';
|
||||
|
||||
@Injectable()
|
||||
export class ResponseCodeService {
|
||||
@@ -85,6 +95,83 @@ export class ResponseCodeService {
|
||||
return code;
|
||||
}
|
||||
|
||||
/**
|
||||
* สร้าง Response Code ใหม่สำหรับ Master Approval Matrix
|
||||
*/
|
||||
async create(dto: CreateResponseCodeDto): Promise<ResponseCode> {
|
||||
const existing = await this.responseCodeRepo.findOne({
|
||||
where: {
|
||||
code: dto.code,
|
||||
category: dto.category,
|
||||
},
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
throw new ConflictException(
|
||||
`Response Code already exists for code=${dto.code}, category=${dto.category}`
|
||||
);
|
||||
}
|
||||
|
||||
const entity = this.responseCodeRepo.create({
|
||||
code: dto.code,
|
||||
subStatus: dto.subStatus,
|
||||
category: dto.category,
|
||||
descriptionTh: dto.descriptionTh,
|
||||
descriptionEn: dto.descriptionEn,
|
||||
implications: dto.implications,
|
||||
notifyRoles: dto.notifyRoles,
|
||||
isActive: dto.isActive ?? true,
|
||||
isSystem: false,
|
||||
});
|
||||
|
||||
return this.responseCodeRepo.save(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* อัปเดต Response Code ตาม publicId
|
||||
*/
|
||||
async update(
|
||||
publicId: string,
|
||||
dto: UpdateResponseCodeDto
|
||||
): Promise<ResponseCode> {
|
||||
const entity = await this.findByPublicId(publicId);
|
||||
|
||||
if (
|
||||
(dto.code && dto.code !== entity.code) ||
|
||||
(dto.category && dto.category !== entity.category)
|
||||
) {
|
||||
const existing = await this.responseCodeRepo.findOne({
|
||||
where: {
|
||||
code: dto.code ?? entity.code,
|
||||
category: dto.category ?? entity.category,
|
||||
},
|
||||
});
|
||||
|
||||
if (existing && existing.publicId !== entity.publicId) {
|
||||
throw new ConflictException(
|
||||
`Response Code already exists for code=${dto.code ?? entity.code}, category=${dto.category ?? entity.category}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(entity, dto);
|
||||
return this.responseCodeRepo.save(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* ปิดการใช้งาน Response Code โดยไม่ลบข้อมูล
|
||||
*/
|
||||
async deactivate(publicId: string): Promise<void> {
|
||||
const entity = await this.findByPublicId(publicId);
|
||||
|
||||
if (entity.isSystem) {
|
||||
throw new BadRequestException('Cannot deactivate a system response code');
|
||||
}
|
||||
|
||||
entity.isActive = false;
|
||||
await this.responseCodeRepo.save(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* ตรวจสอบว่า Response Code triggers notification หรือไม่ (FR-007)
|
||||
* Code 1C, 1D, 3 → trigger notification
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
// File: src/modules/response-code/services/audit.service.ts
|
||||
// Change Log:
|
||||
// - 2026-05-13: Add response code audit service for review task response code changes.
|
||||
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { AuditLog } from '../../../common/entities/audit-log.entity';
|
||||
|
||||
@Injectable()
|
||||
export class ResponseCodeAuditService {
|
||||
private readonly logger = new Logger(ResponseCodeAuditService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(AuditLog)
|
||||
private readonly auditLogRepo: Repository<AuditLog>
|
||||
) {}
|
||||
|
||||
/**
|
||||
* บันทึก audit trail เมื่อมีการเลือกหรือเปลี่ยน Response Code บน Review Task
|
||||
*/
|
||||
async logReviewTaskResponseCodeChange(input: {
|
||||
reviewTaskPublicId: string;
|
||||
responseCodePublicId: string;
|
||||
previousResponseCodeId?: number;
|
||||
currentResponseCodeId: number;
|
||||
comments?: string;
|
||||
userId?: number;
|
||||
}): Promise<void> {
|
||||
const auditLog = this.auditLogRepo.create({
|
||||
userId: input.userId ?? null,
|
||||
action: 'response_code.change',
|
||||
severity: 'INFO',
|
||||
entityType: 'review_task',
|
||||
entityId: input.reviewTaskPublicId,
|
||||
detailsJson: {
|
||||
previousResponseCodeId: input.previousResponseCodeId ?? null,
|
||||
currentResponseCodeId: input.currentResponseCodeId,
|
||||
responseCodePublicId: input.responseCodePublicId,
|
||||
comments: input.comments ?? null,
|
||||
},
|
||||
});
|
||||
|
||||
await this.auditLogRepo.save(auditLog);
|
||||
this.logger.debug(
|
||||
`Recorded response code audit for review task ${input.reviewTaskPublicId}`
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user