feat(rfa): complete RFA Approval Refactor - all 9 phases (T001-T080)

Phase 1-2: Setup, SQL schema, enums, queue constants, base entities
Phase 3 (US1): ReviewTeam, ReviewTeamMember, ReviewTask, TaskCreationService
Phase 4 (US2): ResponseCode, ResponseCodeRule, ImplicationsService, NotificationTriggerService
Phase 5 (US3): Delegation entity, CircularDetectionService, DelegationService/Controller/Module
Phase 6 (US4): ReminderRule, SchedulerService, EscalationService, ReminderProcessor, ReminderModule
Phase 7 (US5): DistributionMatrix, DistributionRecipient, ApprovalListenerService (Strangler),
               TransmittalCreatorService, DistributionProcessor, DistributionModule
Phase 8 (US6): MatrixManagementService, InheritanceService (global→project override)
Phase 9 (Polish): AggregateStatusService, ConsensusService, VetoOverrideService,
                  ParallelGatewayHandler, review-validators, optimistic locking in completeReview,
                  test stubs (unit/integration/e2e), jest.config.js updated for tests/ directory

Frontend: ReviewTaskInbox, ParallelProgress, VetoOverrideDialog, DelegationForm,
          DelegatedBadge, MatrixEditor, ProjectOverrideManager, DistributionStatus,
          ReminderHistory, ResponseCodeSelector, CodeImplications, CompleteReviewForm,
          ReviewTeamForm, ReviewTeamSelector, TeamMemberManager

Closes #1
This commit is contained in:
Nattanin
2026-05-12 16:17:27 +07:00
parent 3df8707b7f
commit ef20839f99
82 changed files with 7052 additions and 104 deletions
@@ -0,0 +1,76 @@
// File: src/modules/delegation/services/circular-detection.service.ts
// ตรวจจับ Circular Delegation (A→B→C→A) ป้องกัน infinite loop (FR-012)
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Delegation } from '../entities/delegation.entity';
@Injectable()
export class CircularDetectionService {
constructor(
@InjectRepository(Delegation)
private readonly delegationRepo: Repository<Delegation>,
) {}
/**
* ตรวจสอบ Circular Delegation ด้วย Depth-First Search
* ตัวอย่าง: A→B→C→A จะถูกจับได้เมื่อ proposedFrom=A, proposedTo=B
*
* @param proposedFrom - delegatorUserId ที่กำลังจะสร้าง delegation
* @param proposedTo - delegateUserId ที่กำลังจะสร้าง delegation
* @param today - วันที่ตรวจสอบ (default: now)
* @returns true ถ้าจะเกิด circular delegation
*/
async wouldCreateCircle(
proposedFrom: number,
proposedTo: number,
today: Date = new Date(),
): Promise<boolean> {
// ถ้า A→B และ proposedFrom=B, proposedTo=A → circular ชัดเจน
if (proposedFrom === proposedTo) return true;
// ดึง delegations ที่ active ทั้งหมดในช่วงเวลานั้น
const activeDelegations = await this.delegationRepo
.createQueryBuilder('d')
.where('d.is_active = 1')
.andWhere('d.start_date <= :today', { today })
.andWhere('d.end_date >= :today', { today })
.select(['d.delegatorUserId', 'd.delegateUserId'])
.getMany();
// สร้าง adjacency list: from → [to, ...]
const graph = new Map<number, number[]>();
for (const d of activeDelegations) {
if (!graph.has(d.delegatorUserId)) graph.set(d.delegatorUserId, []);
graph.get(d.delegatorUserId)!.push(d.delegateUserId);
}
// เพิ่ม edge ที่กำลังจะสร้าง
if (!graph.has(proposedFrom)) graph.set(proposedFrom, []);
graph.get(proposedFrom)!.push(proposedTo);
// DFS จาก proposedTo เพื่อหา path กลับมาที่ proposedFrom
return this.dfsHasCycle(proposedTo, proposedFrom, graph, new Set());
}
private dfsHasCycle(
current: number,
target: number,
graph: Map<number, number[]>,
visited: Set<number>,
): boolean {
if (current === target) return true;
if (visited.has(current)) return false;
visited.add(current);
const neighbors = graph.get(current) ?? [];
for (const neighbor of neighbors) {
if (this.dfsHasCycle(neighbor, target, graph, visited)) {
return true;
}
}
return false;
}
}