690514:2019 204-rfa-approval-refactor #01
CI / CD Pipeline / build (push) Successful in 6m1s
CI / CD Pipeline / deploy (push) Failing after 6m42s

This commit is contained in:
2026-05-14 20:19:21 +07:00
parent 07cc6d47b1
commit 0240d80da5
183 changed files with 20050 additions and 1017 deletions
@@ -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}`
);
}
}