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:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user