Files
lcbp3/backend/src/modules/delegation/delegation.service.ts
T
Nattanin ef20839f99 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
2026-05-12 16:17:27 +07:00

116 lines
4.2 KiB
TypeScript

// 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);
}
}