690513:0920 Refactor Workflow module: Lint error #01
This commit is contained in:
@@ -46,7 +46,9 @@ export class ReviewTeamMember extends UuidBaseEntity {
|
||||
createdAt!: Date;
|
||||
|
||||
// Relations
|
||||
@ManyToOne(() => ReviewTeam, (team: ReviewTeam) => team.members, { onDelete: 'CASCADE' })
|
||||
@ManyToOne(() => ReviewTeam, (team: ReviewTeam) => team.members, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn({ name: 'team_id' })
|
||||
team!: ReviewTeam;
|
||||
|
||||
|
||||
@@ -30,7 +30,11 @@ export class ReviewTeam extends UuidBaseEntity {
|
||||
@Column({ length: 255, nullable: true })
|
||||
description?: string;
|
||||
|
||||
@Column({ name: 'default_for_rfa_types', type: 'simple-array', nullable: true })
|
||||
@Column({
|
||||
name: 'default_for_rfa_types',
|
||||
type: 'simple-array',
|
||||
nullable: true,
|
||||
})
|
||||
defaultForRfaTypes?: string[]; // Auto-assign ให้ RFA type เช่น ['SDW','DDW']
|
||||
|
||||
@Column({ name: 'is_active', type: 'tinyint', default: 1 })
|
||||
@@ -47,6 +51,10 @@ export class ReviewTeam extends UuidBaseEntity {
|
||||
@JoinColumn({ name: 'project_id' })
|
||||
project?: Project;
|
||||
|
||||
@OneToMany(() => ReviewTeamMember, (member: ReviewTeamMember) => member.team, { cascade: true })
|
||||
@OneToMany(
|
||||
() => ReviewTeamMember,
|
||||
(member: ReviewTeamMember) => member.team,
|
||||
{ cascade: true }
|
||||
)
|
||||
members?: ReviewTeamMember[];
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
// File: src/modules/review-team/review-task.service.ts
|
||||
import { Injectable, Logger, NotFoundException, BadRequestException, ConflictException } from '@nestjs/common';
|
||||
import {
|
||||
Injectable,
|
||||
Logger,
|
||||
NotFoundException,
|
||||
BadRequestException,
|
||||
ConflictException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { ReviewTask } from './entities/review-task.entity';
|
||||
@@ -19,7 +25,7 @@ export class ReviewTaskService {
|
||||
@InjectRepository(ReviewTask)
|
||||
private readonly reviewTaskRepo: Repository<ReviewTask>,
|
||||
@InjectRepository(ResponseCode)
|
||||
private readonly responseCodeRepo: Repository<ResponseCode>,
|
||||
private readonly responseCodeRepo: Repository<ResponseCode>
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -41,8 +47,11 @@ export class ReviewTaskService {
|
||||
.leftJoinAndSelect('task.team', 'team');
|
||||
|
||||
if (dto.rfaRevisionPublicId) {
|
||||
qb.innerJoin('rfa_revisions', 'rev', 'rev.id = task.rfa_revision_id')
|
||||
.where('rev.uuid = :uuid', { uuid: dto.rfaRevisionPublicId });
|
||||
qb.innerJoin(
|
||||
'rfa_revisions',
|
||||
'rev',
|
||||
'rev.id = task.rfa_revision_id'
|
||||
).where('rev.uuid = :uuid', { uuid: dto.rfaRevisionPublicId });
|
||||
}
|
||||
|
||||
if (dto.status) {
|
||||
@@ -50,7 +59,9 @@ export class ReviewTaskService {
|
||||
}
|
||||
|
||||
if (dto.assignedToUserPublicId) {
|
||||
qb.andWhere('user.uuid = :userUuid', { userUuid: dto.assignedToUserPublicId });
|
||||
qb.andWhere('user.uuid = :userUuid', {
|
||||
userUuid: dto.assignedToUserPublicId,
|
||||
});
|
||||
}
|
||||
|
||||
if (dto.dueDateFrom) {
|
||||
@@ -94,7 +105,8 @@ export class ReviewTaskService {
|
||||
const total = tasks.length;
|
||||
const completed = tasks.filter(
|
||||
(t: ReviewTask) =>
|
||||
t.status === ReviewTaskStatus.COMPLETED || t.status === ReviewTaskStatus.CANCELLED,
|
||||
t.status === ReviewTaskStatus.COMPLETED ||
|
||||
t.status === ReviewTaskStatus.CANCELLED
|
||||
).length;
|
||||
const pending = total - completed;
|
||||
|
||||
@@ -114,7 +126,7 @@ export class ReviewTaskService {
|
||||
|
||||
if (task.status !== ReviewTaskStatus.PENDING) {
|
||||
throw new BadRequestException(
|
||||
`Cannot start review: task is already ${task.status}`,
|
||||
`Cannot start review: task is already ${task.status}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -126,7 +138,10 @@ export class ReviewTaskService {
|
||||
* บันทึกผลการตรวจสอบ (FR-009, T069)
|
||||
* ใช้ Optimistic Locking (@VersionColumn) ป้องกัน race condition (ADR-002)
|
||||
*/
|
||||
async completeReview(publicId: string, dto: CompleteReviewTaskDto): Promise<ReviewTask> {
|
||||
async completeReview(
|
||||
publicId: string,
|
||||
dto: CompleteReviewTaskDto
|
||||
): Promise<ReviewTask> {
|
||||
const task = await this.findByPublicId(publicId);
|
||||
|
||||
if (
|
||||
@@ -134,7 +149,7 @@ export class ReviewTaskService {
|
||||
task.status === ReviewTaskStatus.CANCELLED
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
`Cannot complete review: task is already ${task.status}`,
|
||||
`Cannot complete review: task is already ${task.status}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -145,7 +160,7 @@ export class ReviewTaskService {
|
||||
|
||||
if (!responseCode) {
|
||||
throw new NotFoundException(
|
||||
`Response Code not found: ${dto.responseCodePublicId}`,
|
||||
`Response Code not found: ${dto.responseCodePublicId}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -154,7 +169,7 @@ export class ReviewTaskService {
|
||||
ReviewTaskStatus.COMPLETED,
|
||||
responseCode.id,
|
||||
false, // requiresComments checked at controller level via ResponseCodeRule
|
||||
dto.comments,
|
||||
dto.comments
|
||||
);
|
||||
|
||||
task.status = ReviewTaskStatus.COMPLETED;
|
||||
@@ -168,9 +183,12 @@ export class ReviewTaskService {
|
||||
return await this.reviewTaskRepo.save(task);
|
||||
} catch (err: unknown) {
|
||||
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||
if (errorMessage.includes('OptimisticLock') || errorMessage.includes('version')) {
|
||||
if (
|
||||
errorMessage.includes('OptimisticLock') ||
|
||||
errorMessage.includes('version')
|
||||
) {
|
||||
throw new ConflictException(
|
||||
'Review task was modified concurrently. Please refresh and try again.',
|
||||
'Review task was modified concurrently. Please refresh and try again.'
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
|
||||
@@ -56,7 +56,10 @@ export class ReviewTeamController {
|
||||
* อัปเดต Review Team
|
||||
*/
|
||||
@Patch(':publicId')
|
||||
update(@Param('publicId') publicId: string, @Body() dto: UpdateReviewTeamDto) {
|
||||
update(
|
||||
@Param('publicId') publicId: string,
|
||||
@Body() dto: UpdateReviewTeamDto
|
||||
) {
|
||||
return this.reviewTeamService.update(publicId, dto);
|
||||
}
|
||||
|
||||
@@ -65,7 +68,10 @@ export class ReviewTeamController {
|
||||
* เพิ่มสมาชิก
|
||||
*/
|
||||
@Post(':publicId/members')
|
||||
addMember(@Param('publicId') teamPublicId: string, @Body() dto: AddTeamMemberDto) {
|
||||
addMember(
|
||||
@Param('publicId') teamPublicId: string,
|
||||
@Body() dto: AddTeamMemberDto
|
||||
) {
|
||||
return this.reviewTeamService.addMember(teamPublicId, dto);
|
||||
}
|
||||
|
||||
@@ -76,7 +82,7 @@ export class ReviewTeamController {
|
||||
@Delete(':publicId/members/:memberPublicId')
|
||||
removeMember(
|
||||
@Param('publicId') teamPublicId: string,
|
||||
@Param('memberPublicId') memberPublicId: string,
|
||||
@Param('memberPublicId') memberPublicId: string
|
||||
) {
|
||||
return this.reviewTeamService.removeMember(teamPublicId, memberPublicId);
|
||||
}
|
||||
|
||||
@@ -30,14 +30,23 @@ import { UserModule } from '../user/user.module';
|
||||
import { DistributionModule } from '../distribution/distribution.module';
|
||||
|
||||
// Queue constants
|
||||
import { QUEUE_REMINDERS, QUEUE_VETO_NOTIFICATIONS } from '../common/constants/queue.constants';
|
||||
import {
|
||||
QUEUE_REMINDERS,
|
||||
QUEUE_VETO_NOTIFICATIONS,
|
||||
} from '../common/constants/queue.constants';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([ReviewTeam, ReviewTeamMember, ReviewTask, User, Discipline]),
|
||||
TypeOrmModule.forFeature([
|
||||
ReviewTeam,
|
||||
ReviewTeamMember,
|
||||
ReviewTask,
|
||||
User,
|
||||
Discipline,
|
||||
]),
|
||||
BullModule.registerQueue(
|
||||
{ name: QUEUE_REMINDERS },
|
||||
{ name: QUEUE_VETO_NOTIFICATIONS },
|
||||
{ name: QUEUE_VETO_NOTIFICATIONS }
|
||||
),
|
||||
ResponseCodeModule,
|
||||
NotificationModule,
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
// File: src/modules/review-team/review-team.service.ts
|
||||
import { Injectable, Logger, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
import {
|
||||
Injectable,
|
||||
Logger,
|
||||
NotFoundException,
|
||||
BadRequestException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { ReviewTeam } from './entities/review-team.entity';
|
||||
@@ -25,7 +30,7 @@ export class ReviewTeamService {
|
||||
@InjectRepository(User)
|
||||
private readonly userRepo: Repository<User>,
|
||||
@InjectRepository(Discipline)
|
||||
private readonly disciplineRepo: Repository<Discipline>,
|
||||
private readonly disciplineRepo: Repository<Discipline>
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -74,14 +79,17 @@ export class ReviewTeamService {
|
||||
/**
|
||||
* ดึง Teams ที่เป็น Default สำหรับ RFA type นั้นๆ (FR-002)
|
||||
*/
|
||||
async findDefaultForRfaType(rfaTypeCode: string, projectId: number): Promise<ReviewTeam[]> {
|
||||
async findDefaultForRfaType(
|
||||
rfaTypeCode: string,
|
||||
projectId: number
|
||||
): Promise<ReviewTeam[]> {
|
||||
const teams = await this.teamRepo.find({
|
||||
where: { projectId, isActive: true },
|
||||
relations: ['members'],
|
||||
});
|
||||
|
||||
return teams.filter(
|
||||
(t: ReviewTeam) => t.defaultForRfaTypes?.includes(rfaTypeCode) ?? false,
|
||||
(t: ReviewTeam) => t.defaultForRfaTypes?.includes(rfaTypeCode) ?? false
|
||||
);
|
||||
}
|
||||
|
||||
@@ -90,9 +98,11 @@ export class ReviewTeamService {
|
||||
*/
|
||||
async create(dto: CreateReviewTeamDto): Promise<ReviewTeam> {
|
||||
// ตรวจสอบว่า project มีอยู่จริง (via publicId)
|
||||
const project = await this.teamRepo.manager.getRepository('projects').findOne({
|
||||
where: { uuid: dto.projectPublicId } as Record<string, unknown>,
|
||||
});
|
||||
const project = await this.teamRepo.manager
|
||||
.getRepository('projects')
|
||||
.findOne({
|
||||
where: { uuid: dto.projectPublicId } as Record<string, unknown>,
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
throw new NotFoundException(`Project not found: ${dto.projectPublicId}`);
|
||||
@@ -112,12 +122,16 @@ export class ReviewTeamService {
|
||||
/**
|
||||
* อัปเดต Review Team
|
||||
*/
|
||||
async update(publicId: string, dto: UpdateReviewTeamDto): Promise<ReviewTeam> {
|
||||
async update(
|
||||
publicId: string,
|
||||
dto: UpdateReviewTeamDto
|
||||
): Promise<ReviewTeam> {
|
||||
const team = await this.findByPublicId(publicId);
|
||||
|
||||
if (dto.name !== undefined) team.name = dto.name;
|
||||
if (dto.description !== undefined) team.description = dto.description;
|
||||
if (dto.defaultForRfaTypes !== undefined) team.defaultForRfaTypes = dto.defaultForRfaTypes;
|
||||
if (dto.defaultForRfaTypes !== undefined)
|
||||
team.defaultForRfaTypes = dto.defaultForRfaTypes;
|
||||
if (dto.isActive !== undefined) team.isActive = dto.isActive;
|
||||
|
||||
return this.teamRepo.save(team);
|
||||
@@ -126,27 +140,40 @@ export class ReviewTeamService {
|
||||
/**
|
||||
* เพิ่มสมาชิกใน Review Team (FR-001)
|
||||
*/
|
||||
async addMember(teamPublicId: string, dto: AddTeamMemberDto): Promise<ReviewTeamMember> {
|
||||
async addMember(
|
||||
teamPublicId: string,
|
||||
dto: AddTeamMemberDto
|
||||
): Promise<ReviewTeamMember> {
|
||||
const team = await this.findByPublicId(teamPublicId);
|
||||
|
||||
// ตรวจสอบ User
|
||||
const user = await this.userRepo.findOne({ where: { publicId: dto.userPublicId } });
|
||||
if (!user) throw new NotFoundException(`User not found: ${dto.userPublicId}`);
|
||||
const user = await this.userRepo.findOne({
|
||||
where: { publicId: dto.userPublicId },
|
||||
});
|
||||
if (!user)
|
||||
throw new NotFoundException(`User not found: ${dto.userPublicId}`);
|
||||
|
||||
// ตรวจสอบ Discipline
|
||||
const discipline = await this.disciplineRepo.findOne({
|
||||
where: { id: Number(dto.disciplinePublicId) },
|
||||
});
|
||||
if (!discipline) throw new NotFoundException(`Discipline not found: ${dto.disciplinePublicId}`);
|
||||
if (!discipline)
|
||||
throw new NotFoundException(
|
||||
`Discipline not found: ${dto.disciplinePublicId}`
|
||||
);
|
||||
|
||||
// ตรวจสอบซ้ำ
|
||||
const existing = await this.memberRepo.findOne({
|
||||
where: { teamId: team.id, userId: user.user_id, disciplineId: discipline.id },
|
||||
where: {
|
||||
teamId: team.id,
|
||||
userId: user.user_id,
|
||||
disciplineId: discipline.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
throw new BadRequestException(
|
||||
`User ${dto.userPublicId} is already a member of this team for discipline ${dto.disciplinePublicId}`,
|
||||
`User ${dto.userPublicId} is already a member of this team for discipline ${dto.disciplinePublicId}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -164,7 +191,10 @@ export class ReviewTeamService {
|
||||
/**
|
||||
* ลบสมาชิกออกจาก Review Team
|
||||
*/
|
||||
async removeMember(teamPublicId: string, memberPublicId: string): Promise<void> {
|
||||
async removeMember(
|
||||
teamPublicId: string,
|
||||
memberPublicId: string
|
||||
): Promise<void> {
|
||||
const team = await this.findByPublicId(teamPublicId);
|
||||
const member = await this.memberRepo.findOne({
|
||||
where: { publicId: memberPublicId, teamId: team.id },
|
||||
|
||||
@@ -4,7 +4,10 @@ import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { ReviewTask } from '../entities/review-task.entity';
|
||||
import { ReviewTaskStatus, ConsensusDecision } from '../../common/enums/review.enums';
|
||||
import {
|
||||
ReviewTaskStatus,
|
||||
ConsensusDecision,
|
||||
} from '../../common/enums/review.enums';
|
||||
|
||||
export interface AggregateStatus {
|
||||
total: number;
|
||||
@@ -24,7 +27,7 @@ export class AggregateStatusService {
|
||||
|
||||
constructor(
|
||||
@InjectRepository(ReviewTask)
|
||||
private readonly taskRepo: Repository<ReviewTask>,
|
||||
private readonly taskRepo: Repository<ReviewTask>
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -47,12 +50,23 @@ export class AggregateStatusService {
|
||||
|
||||
for (const task of tasks) {
|
||||
switch (task.status) {
|
||||
case ReviewTaskStatus.COMPLETED: counts.completed++; break;
|
||||
case ReviewTaskStatus.PENDING: counts.pending++; break;
|
||||
case ReviewTaskStatus.IN_PROGRESS: counts.inProgress++; break;
|
||||
case ReviewTaskStatus.DELEGATED: counts.delegated++; break;
|
||||
case ReviewTaskStatus.EXPIRED: counts.expired++; break;
|
||||
default: break;
|
||||
case ReviewTaskStatus.COMPLETED:
|
||||
counts.completed++;
|
||||
break;
|
||||
case ReviewTaskStatus.PENDING:
|
||||
counts.pending++;
|
||||
break;
|
||||
case ReviewTaskStatus.IN_PROGRESS:
|
||||
counts.inProgress++;
|
||||
break;
|
||||
case ReviewTaskStatus.DELEGATED:
|
||||
counts.delegated++;
|
||||
break;
|
||||
case ReviewTaskStatus.EXPIRED:
|
||||
counts.expired++;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +108,7 @@ export class AggregateStatusService {
|
||||
|
||||
// All approved: Code 1A or 1B = APPROVED
|
||||
const allApproved = tasks.every((t) =>
|
||||
['1A', '1B'].includes(t.responseCode?.code ?? ''),
|
||||
['1A', '1B'].includes(t.responseCode?.code ?? '')
|
||||
);
|
||||
if (allApproved) return ConsensusDecision.APPROVED;
|
||||
|
||||
|
||||
@@ -6,7 +6,10 @@ 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,
|
||||
ReviewTaskStatus,
|
||||
} from '../../common/enums/review.enums';
|
||||
|
||||
export interface ConsensusResult {
|
||||
decision: ConsensusDecision;
|
||||
@@ -23,7 +26,7 @@ export class ConsensusService {
|
||||
@InjectRepository(ReviewTask)
|
||||
private readonly taskRepo: Repository<ReviewTask>,
|
||||
private readonly aggregateStatusService: AggregateStatusService,
|
||||
private readonly approvalListenerService: ApprovalListenerService,
|
||||
private readonly approvalListenerService: ApprovalListenerService
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -36,15 +39,17 @@ export class ConsensusService {
|
||||
rfaRevisionPublicId: string;
|
||||
projectId: number;
|
||||
documentTypeCode: string;
|
||||
},
|
||||
}
|
||||
): Promise<ConsensusResult> {
|
||||
const isReady = await this.aggregateStatusService.isReadyForConsensus(rfaRevisionId);
|
||||
const isReady =
|
||||
await this.aggregateStatusService.isReadyForConsensus(rfaRevisionId);
|
||||
|
||||
const status = await this.aggregateStatusService.getForRevision(rfaRevisionId);
|
||||
const status =
|
||||
await this.aggregateStatusService.getForRevision(rfaRevisionId);
|
||||
|
||||
if (!isReady) {
|
||||
this.logger.debug(
|
||||
`Revision ${rfaRevisionId}: ${status.completed}/${status.total} tasks done — not ready for consensus`,
|
||||
`Revision ${rfaRevisionId}: ${status.completed}/${status.total} tasks done — not ready for consensus`
|
||||
);
|
||||
return {
|
||||
decision: ConsensusDecision.PENDING,
|
||||
@@ -54,10 +59,11 @@ export class ConsensusService {
|
||||
};
|
||||
}
|
||||
|
||||
const decision = await this.aggregateStatusService.evaluateConsensus(rfaRevisionId);
|
||||
const decision =
|
||||
await this.aggregateStatusService.evaluateConsensus(rfaRevisionId);
|
||||
|
||||
this.logger.log(
|
||||
`Revision ${rfaRevisionId}: consensus = ${decision} (${status.total} tasks)`,
|
||||
`Revision ${rfaRevisionId}: consensus = ${decision} (${status.total} tasks)`
|
||||
);
|
||||
|
||||
let triggeredDistribution = false;
|
||||
|
||||
@@ -8,7 +8,10 @@ import { Repository, EntityManager } from 'typeorm';
|
||||
import { ReviewTeam } from '../entities/review-team.entity';
|
||||
import { ReviewTeamMember } from '../entities/review-team-member.entity';
|
||||
import { ReviewTask } from '../entities/review-task.entity';
|
||||
import { ReviewTaskStatus } from '../../common/enums/review.enums';
|
||||
import {
|
||||
ReviewTaskStatus,
|
||||
ReviewTeamMemberRole,
|
||||
} from '../../common/enums/review.enums';
|
||||
|
||||
@Injectable()
|
||||
export class TaskCreationService {
|
||||
@@ -20,7 +23,7 @@ export class TaskCreationService {
|
||||
@InjectRepository(ReviewTeamMember)
|
||||
private readonly memberRepo: Repository<ReviewTeamMember>,
|
||||
@InjectRepository(ReviewTask)
|
||||
private readonly reviewTaskRepo: Repository<ReviewTask>,
|
||||
private readonly reviewTaskRepo: Repository<ReviewTask>
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -36,7 +39,7 @@ export class TaskCreationService {
|
||||
rfaRevisionId: number,
|
||||
reviewTeamPublicId: string,
|
||||
dueDate: Date,
|
||||
manager: EntityManager,
|
||||
manager: EntityManager
|
||||
): Promise<ReviewTask[]> {
|
||||
// ดึง ReviewTeam พร้อม members
|
||||
const team = await this.reviewTeamRepo.findOne({
|
||||
@@ -46,7 +49,7 @@ export class TaskCreationService {
|
||||
|
||||
if (!team || !team.isActive) {
|
||||
this.logger.warn(
|
||||
`ReviewTeam ${reviewTeamPublicId} not found or inactive — skipping task creation`,
|
||||
`ReviewTeam ${reviewTeamPublicId} not found or inactive — skipping task creation`
|
||||
);
|
||||
return [];
|
||||
}
|
||||
@@ -55,7 +58,7 @@ export class TaskCreationService {
|
||||
|
||||
if (members.length === 0) {
|
||||
this.logger.warn(
|
||||
`ReviewTeam ${reviewTeamPublicId} has no members — skipping task creation`,
|
||||
`ReviewTeam ${reviewTeamPublicId} has no members — skipping task creation`
|
||||
);
|
||||
return [];
|
||||
}
|
||||
@@ -65,7 +68,7 @@ export class TaskCreationService {
|
||||
for (const member of members) {
|
||||
// LEAD มี priority สูงสุด ถ้ามีหลายคนใน Discipline เดียวกัน
|
||||
const existing = disciplineMap.get(member.disciplineId);
|
||||
if (!existing || member.role === 'LEAD') {
|
||||
if (!existing || member.role === ReviewTeamMemberRole.LEAD) {
|
||||
disciplineMap.set(member.disciplineId, member);
|
||||
}
|
||||
}
|
||||
@@ -87,7 +90,7 @@ export class TaskCreationService {
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Created ${tasks.length} parallel review tasks for RFA revision ${rfaRevisionId}, team ${reviewTeamPublicId}`,
|
||||
`Created ${tasks.length} parallel review tasks for RFA revision ${rfaRevisionId}, team ${reviewTeamPublicId}`
|
||||
);
|
||||
|
||||
return tasks;
|
||||
@@ -106,7 +109,7 @@ export class TaskCreationService {
|
||||
return tasks.every(
|
||||
(t: ReviewTask) =>
|
||||
t.status === ReviewTaskStatus.COMPLETED ||
|
||||
t.status === ReviewTaskStatus.CANCELLED,
|
||||
t.status === ReviewTaskStatus.CANCELLED
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
// File: src/modules/review-team/services/veto-override.service.ts
|
||||
// PM Veto Override — บังคับผ่าน RFA Revision แม้มี Code 3 (T068.5)
|
||||
import { Injectable, Logger, ForbiddenException, NotFoundException } from '@nestjs/common';
|
||||
import {
|
||||
Injectable,
|
||||
Logger,
|
||||
ForbiddenException,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, DataSource } from 'typeorm';
|
||||
import { ReviewTask } from '../entities/review-task.entity';
|
||||
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 VetoOverrideDto {
|
||||
rfaRevisionId: number;
|
||||
@@ -25,34 +30,42 @@ export class VetoOverrideService {
|
||||
@InjectRepository(ReviewTask)
|
||||
private readonly taskRepo: Repository<ReviewTask>,
|
||||
private readonly approvalListenerService: ApprovalListenerService,
|
||||
private readonly dataSource: DataSource,
|
||||
private readonly dataSource: DataSource
|
||||
) {}
|
||||
|
||||
/**
|
||||
* PM Override: บังคับ APPROVED แม้ว่าจะมี Code 3 rejection (FR-012)
|
||||
* ต้องมี justification reason และ audit trail
|
||||
*/
|
||||
async executeOverride(dto: VetoOverrideDto): Promise<{ decision: ConsensusDecision }> {
|
||||
async executeOverride(
|
||||
dto: VetoOverrideDto
|
||||
): Promise<{ decision: ConsensusDecision }> {
|
||||
const tasks = await this.taskRepo.find({
|
||||
where: { rfaRevisionId: dto.rfaRevisionId },
|
||||
relations: ['responseCode'],
|
||||
});
|
||||
|
||||
if (tasks.length === 0) {
|
||||
throw new NotFoundException(`No review tasks found for revision ${dto.rfaRevisionId}`);
|
||||
throw new NotFoundException(
|
||||
`No review tasks found for revision ${dto.rfaRevisionId}`
|
||||
);
|
||||
}
|
||||
|
||||
const hasVeto = tasks.some((t) => t.responseCode?.code === '3');
|
||||
if (!hasVeto) {
|
||||
throw new ForbiddenException('No Code 3 veto found — override not needed');
|
||||
throw new ForbiddenException(
|
||||
'No Code 3 veto found — override not needed'
|
||||
);
|
||||
}
|
||||
|
||||
if (!dto.overrideReason || dto.overrideReason.trim().length < 10) {
|
||||
throw new ForbiddenException('Override reason must be at least 10 characters');
|
||||
throw new ForbiddenException(
|
||||
'Override reason must be at least 10 characters'
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.warn(
|
||||
`PM Override executed by user ${dto.overriddenByUserId} for revision ${dto.rfaRevisionId}. Reason: ${dto.overrideReason}`,
|
||||
`PM Override executed by user ${dto.overriddenByUserId} for revision ${dto.rfaRevisionId}. Reason: ${dto.overrideReason}`
|
||||
);
|
||||
|
||||
await this.approvalListenerService.onConsensusReached({
|
||||
|
||||
Reference in New Issue
Block a user