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,115 @@
|
||||
// File: src/modules/delegation/delegation.service.ts
|
||||
import { Injectable, Logger, BadRequestException, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Delegation } from './entities/delegation.entity';
|
||||
import { User } from '../user/entities/user.entity';
|
||||
import { CircularDetectionService } from './services/circular-detection.service';
|
||||
import { CreateDelegationDto } from './dto/create-delegation.dto';
|
||||
|
||||
@Injectable()
|
||||
export class DelegationService {
|
||||
private readonly logger = new Logger(DelegationService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(Delegation)
|
||||
private readonly delegationRepo: Repository<Delegation>,
|
||||
@InjectRepository(User)
|
||||
private readonly userRepo: Repository<User>,
|
||||
private readonly circularDetectionService: CircularDetectionService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* สร้าง Delegation ใหม่ พร้อมตรวจสอบ Circular (FR-011, FR-012)
|
||||
*/
|
||||
async create(delegatorPublicId: string, dto: CreateDelegationDto): Promise<Delegation> {
|
||||
const delegator = await this.userRepo.findOne({ where: { publicId: delegatorPublicId } });
|
||||
if (!delegator) throw new NotFoundException(`User not found: ${delegatorPublicId}`);
|
||||
|
||||
const delegate = await this.userRepo.findOne({ where: { publicId: dto.delegateUserPublicId } });
|
||||
if (!delegate) throw new NotFoundException(`Delegate user not found: ${dto.delegateUserPublicId}`);
|
||||
|
||||
// ตรวจสอบ date range
|
||||
if (dto.startDate >= dto.endDate) {
|
||||
throw new BadRequestException('startDate must be before endDate');
|
||||
}
|
||||
|
||||
// ตรวจสอบ Circular Delegation (ADR requirement)
|
||||
const isCircular = await this.circularDetectionService.wouldCreateCircle(
|
||||
delegator.user_id,
|
||||
delegate.user_id,
|
||||
dto.startDate,
|
||||
);
|
||||
|
||||
if (isCircular) {
|
||||
throw new BadRequestException(
|
||||
'Circular delegation detected — this would create a delegation loop',
|
||||
);
|
||||
}
|
||||
|
||||
const delegation = this.delegationRepo.create({
|
||||
delegatorUserId: delegator.user_id,
|
||||
delegateUserId: delegate.user_id,
|
||||
scope: dto.scope,
|
||||
startDate: dto.startDate,
|
||||
endDate: dto.endDate,
|
||||
reason: dto.reason,
|
||||
isActive: true,
|
||||
});
|
||||
|
||||
return this.delegationRepo.save(delegation);
|
||||
}
|
||||
|
||||
/**
|
||||
* ดึง Delegations ของ User ทั้งหมด (ในฐานะผู้มอบหมาย)
|
||||
*/
|
||||
async findByDelegator(delegatorPublicId: string): Promise<Delegation[]> {
|
||||
const user = await this.userRepo.findOne({ where: { publicId: delegatorPublicId } });
|
||||
if (!user) throw new NotFoundException(delegatorPublicId);
|
||||
|
||||
return this.delegationRepo.find({
|
||||
where: { delegatorUserId: user.user_id },
|
||||
relations: ['delegate'],
|
||||
order: { startDate: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ดึง Active Delegations สำหรับ User ณ วันที่กำหนด (FR-013)
|
||||
* ใช้ใน ReviewTaskService ก่อน assign task
|
||||
*/
|
||||
async findActiveDelegate(userId: number, date: Date = new Date()): Promise<User | null> {
|
||||
const delegation = await this.delegationRepo
|
||||
.createQueryBuilder('d')
|
||||
.innerJoinAndSelect('d.delegate', 'delegate')
|
||||
.where('d.delegator_user_id = :userId', { userId })
|
||||
.andWhere('d.is_active = 1')
|
||||
.andWhere('d.start_date <= :date', { date })
|
||||
.andWhere('d.end_date >= :date', { date })
|
||||
.orderBy('d.created_at', 'DESC')
|
||||
.getOne();
|
||||
|
||||
return delegation?.delegate ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke delegation ก่อนกำหนด
|
||||
*/
|
||||
async revoke(publicId: string, delegatorPublicId: string): Promise<void> {
|
||||
const delegation = await this.delegationRepo.findOne({
|
||||
where: { publicId },
|
||||
});
|
||||
|
||||
if (!delegation) throw new NotFoundException(`Delegation not found: ${publicId}`);
|
||||
|
||||
// ตรวจสอบ ownership
|
||||
const delegator = await this.userRepo.findOne({ where: { publicId: delegatorPublicId } });
|
||||
if (!delegator || delegation.delegatorUserId !== delegator.user_id) {
|
||||
throw new BadRequestException('You can only revoke your own delegations');
|
||||
}
|
||||
|
||||
delegation.isActive = false;
|
||||
delegation.endDate = new Date(); // หยุดทันที
|
||||
await this.delegationRepo.save(delegation);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user