feat(rfa-ai): Complete RFA Approval Refactor and AI Model Revision
CI / CD Pipeline / build (push) Successful in 4m54s
CI / CD Pipeline / deploy (push) Failing after 12m9s

This commit is contained in:
2026-05-16 10:59:53 +07:00
parent 6cb3ae10ee
commit 1a162bf320
105 changed files with 5088 additions and 1083 deletions
@@ -95,4 +95,8 @@ export class ReviewTask extends UuidBaseEntity {
@ManyToOne(() => User)
@JoinColumn({ name: 'delegated_from_user_id' })
delegatedFromUser?: User;
@ManyToOne('RfaRevision')
@JoinColumn({ name: 'rfa_revision_id' })
rfaRevision?: unknown; // Use unknown to avoid circular dependency and satisfy linter
}
@@ -11,7 +11,11 @@ import {
ParseUUIDPipe,
} from '@nestjs/common';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { PermissionsGuard } from '../../common/auth/guards/permissions.guard';
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
import { Audit } from '../../common/decorators/audit.decorator';
import { ReviewTaskService } from './review-task.service';
import { ConsensusService } from './services/consensus.service';
import { VetoOverrideService } from './services/veto-override.service';
import type { VetoOverrideDto } from './services/veto-override.service';
@@ -23,7 +27,7 @@ import { CurrentUser } from '../../common/decorators/current-user.decorator';
import { User } from '../user/entities/user.entity';
@Controller('review-tasks')
@UseGuards(JwtAuthGuard)
@UseGuards(JwtAuthGuard, PermissionsGuard)
export class ReviewTaskController {
constructor(
private readonly reviewTaskService: ReviewTaskService,
@@ -32,21 +36,27 @@ export class ReviewTaskController {
) {}
@Get()
@RequirePermission('document.view')
findAll(@Query() dto: SearchReviewTaskDto) {
return this.reviewTaskService.findAll(dto);
}
@Get(':publicId')
@RequirePermission('document.view')
findOne(@Param('publicId', ParseUUIDPipe) publicId: string) {
return this.reviewTaskService.findByPublicId(publicId);
}
@Patch(':publicId/start')
@RequirePermission('workflow.action_review')
@Audit('review_task.start', 'review_task')
startReview(@Param('publicId', ParseUUIDPipe) publicId: string) {
return this.reviewTaskService.startReview(publicId);
}
@Patch(':publicId/complete')
@RequirePermission('workflow.action_review')
@Audit('review_task.complete', 'review_task')
async completeReview(
@Param('publicId', ParseUUIDPipe) publicId: string,
@Body() dto: CompleteReviewTaskDto,
@@ -102,6 +112,8 @@ export class ReviewTaskController {
}
@Post('veto-override')
@RequirePermission('document.admin_edit')
@Audit('review_task.veto_override', 'review_task')
async overrideVeto(@Body() dto: VetoOverrideDto, @CurrentUser() user: User) {
return this.vetoOverrideService.executeOverride({
...dto,
@@ -18,9 +18,12 @@ import {
AddTeamMemberDto,
SearchReviewTeamDto,
} from './dto/shared/review-team.dto';
import { PermissionsGuard } from '../../common/auth/guards/permissions.guard';
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
import { Audit } from '../../common/decorators/audit.decorator';
@Controller('review-teams')
@UseGuards(JwtAuthGuard)
@UseGuards(JwtAuthGuard, PermissionsGuard)
export class ReviewTeamController {
constructor(private readonly reviewTeamService: ReviewTeamService) {}
@@ -29,6 +32,7 @@ export class ReviewTeamController {
* ดึงรายการ Review Teams ตาม project
*/
@Get()
@RequirePermission('master_data.view')
findAll(@Query() dto: SearchReviewTeamDto) {
return this.reviewTeamService.findAll(dto);
}
@@ -38,6 +42,7 @@ export class ReviewTeamController {
* ดึง Review Team เดียว (ADR-019)
*/
@Get(':publicId')
@RequirePermission('master_data.view')
findOne(@Param('publicId') publicId: string) {
return this.reviewTeamService.findByPublicId(publicId);
}
@@ -47,6 +52,8 @@ export class ReviewTeamController {
* สร้าง Review Team ใหม่
*/
@Post()
@RequirePermission('master_data.manage')
@Audit('review_team.create', 'review_team')
create(@Body() dto: CreateReviewTeamDto) {
return this.reviewTeamService.create(dto);
}
@@ -56,6 +63,8 @@ export class ReviewTeamController {
* อัปเดต Review Team
*/
@Patch(':publicId')
@RequirePermission('master_data.manage')
@Audit('review_team.update', 'review_team')
update(
@Param('publicId') publicId: string,
@Body() dto: UpdateReviewTeamDto
@@ -68,6 +77,8 @@ export class ReviewTeamController {
* เพิ่มสมาชิก
*/
@Post(':publicId/members')
@RequirePermission('master_data.manage')
@Audit('review_team.add_member', 'review_team')
addMember(
@Param('publicId') teamPublicId: string,
@Body() dto: AddTeamMemberDto
@@ -80,6 +91,8 @@ export class ReviewTeamController {
* ลบสมาชิก
*/
@Delete(':publicId/members/:memberPublicId')
@RequirePermission('master_data.manage')
@Audit('review_team.remove_member', 'review_team')
removeMember(
@Param('publicId') teamPublicId: string,
@Param('memberPublicId') memberPublicId: string
@@ -92,6 +105,8 @@ export class ReviewTeamController {
* Deactivate Review Team (soft delete)
*/
@Delete(':publicId')
@RequirePermission('master_data.manage')
@Audit('review_team.deactivate', 'review_team')
deactivate(@Param('publicId') publicId: string) {
return this.reviewTeamService.deactivate(publicId);
}
@@ -118,4 +118,23 @@ export class AggregateStatusService {
return ConsensusDecision.APPROVED_WITH_COMMENTS;
}
/**
* คืนค่า Response Code ที่เข้มงวดที่สุดจาก Tasks ที่เสร็จแล้ว (T068 Improvement)
* Code Priority: 3 > 2 > 1B > 1A
*/
async getMostRestrictiveResponseCode(rfaRevisionId: number): Promise<string> {
const tasks = await this.taskRepo.find({
where: { rfaRevisionId, status: ReviewTaskStatus.COMPLETED },
relations: ['responseCode'],
});
if (tasks.length === 0) return '1A';
const codes = tasks.map((t) => t.responseCode?.code ?? '').filter(Boolean);
if (codes.includes('3')) return '3';
if (codes.includes('2')) return '2';
if (codes.includes('1B')) return '1B';
return '1A';
}
}
@@ -6,10 +6,7 @@ import { Repository } from 'typeorm';
import { ReviewTask } from '../entities/review-task.entity';
import { AggregateStatusService } from './aggregate-status.service';
import { ApprovalListenerService } from '../../distribution/services/approval-listener.service';
import {
ConsensusDecision,
ReviewTaskStatus,
} from '../../common/enums/review.enums';
import { ConsensusDecision } from '../../common/enums/review.enums';
export interface ConsensusResult {
decision: ConsensusDecision;
@@ -72,15 +69,10 @@ export class ConsensusService {
decision === ConsensusDecision.APPROVED ||
decision === ConsensusDecision.APPROVED_WITH_COMMENTS
) {
// ดึง response code ที่ predominant
const completedTasks = await this.taskRepo.find({
where: { rfaRevisionId, status: ReviewTaskStatus.COMPLETED },
relations: ['responseCode'],
order: { completedAt: 'DESC' },
take: 1,
});
const responseCode = completedTasks[0]?.responseCode?.code ?? '1A';
const responseCode =
await this.aggregateStatusService.getMostRestrictiveResponseCode(
rfaRevisionId
);
await this.approvalListenerService.onConsensusReached({
...context,
@@ -45,6 +45,7 @@ export class TaskCreationService {
*/
async createParallelTasks(
rfaRevisionId: number,
rfaPublicId: string,
reviewTeamPublicId: string,
dueDate: Date,
manager: EntityManager,
@@ -113,7 +114,7 @@ export class TaskCreationService {
if (saved.assignedToUserId) {
await this.schedulerService.scheduleForTask({
taskPublicId: saved.publicId,
rfaPublicId: rfaRevisionId.toString(), // ใช้ rfaRevisionId เป็น placeholder
rfaPublicId: rfaPublicId, // ADR-019: Use actual UUID
assigneeUserId: saved.assignedToUserId,
dueDate: saved.dueDate ?? dueDate,
reminderType: ReminderType.DUE_SOON, // Start type, scheduler will fetch rules