260324:1349 Refactor RFA #01
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
IsObject,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Min,
|
||||
} from 'class-validator';
|
||||
|
||||
export class CreateRfaDto {
|
||||
@@ -30,12 +31,14 @@ export class CreateRfaDto {
|
||||
rfaTypeId!: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'ID ของสาขางาน (Discipline) ตาม Req 6B',
|
||||
description:
|
||||
'ID ของสาขางาน (Discipline) ตาม Req 6B — Required per spec §3.3.4',
|
||||
example: 1,
|
||||
})
|
||||
@IsInt()
|
||||
@IsOptional() // Optional ไว้ก่อนเผื่อบางโครงการไม่บังคับ
|
||||
disciplineId?: number;
|
||||
@Min(1)
|
||||
@IsNotEmpty()
|
||||
disciplineId!: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'หัวข้อเอกสาร',
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
// File: src/modules/rfa/rfa-workflow.service.ts
|
||||
//
|
||||
// NOTE (Phase 4.1 Refactor):
|
||||
// This service was written as an alternative workflow integration layer using the
|
||||
// Unified WorkflowEngineService (ADR-001). It is currently NOT called by RfaController.
|
||||
// Active workflow logic lives in RfaService.submit() / RfaService.processAction().
|
||||
//
|
||||
// Reserved for Phase 3: When the Unified Workflow Engine is fully wired to RFA,
|
||||
// RfaService workflow methods should be migrated here and RfaController updated
|
||||
// to delegate to RfaWorkflowService.
|
||||
//
|
||||
|
||||
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
@@ -2,9 +2,13 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
@@ -20,6 +24,7 @@ import {
|
||||
import { WorkflowActionDto } from '../correspondence/dto/workflow-action.dto';
|
||||
import { User } from '../user/entities/user.entity';
|
||||
import { CreateRfaDto } from './dto/create-rfa.dto';
|
||||
import { UpdateRfaDto } from './dto/update-rfa.dto';
|
||||
import { SubmitRfaDto } from './dto/submit-rfa.dto';
|
||||
import { SearchRfaDto } from './dto/search-rfa.dto';
|
||||
import { RfaService } from './rfa.service';
|
||||
@@ -31,6 +36,7 @@ import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
import { RbacGuard } from '../../common/guards/rbac.guard';
|
||||
import { ParseUuidPipe } from '../../common/pipes/parse-uuid.pipe';
|
||||
import { ProjectService } from '../project/project.service';
|
||||
import { UuidResolverService } from '../../common/services/uuid-resolver.service';
|
||||
|
||||
@ApiTags('RFA (Request for Approval)')
|
||||
@ApiBearerAuth()
|
||||
@@ -39,7 +45,8 @@ import { ProjectService } from '../project/project.service';
|
||||
export class RfaController {
|
||||
constructor(
|
||||
private readonly rfaService: RfaService,
|
||||
private readonly projectService: ProjectService
|
||||
private readonly projectService: ProjectService,
|
||||
private readonly uuidResolver: UuidResolverService
|
||||
) {}
|
||||
|
||||
@Post()
|
||||
@@ -99,17 +106,14 @@ export class RfaController {
|
||||
@ApiOperation({ summary: 'List all RFAs with pagination' })
|
||||
@ApiResponse({ status: 200, description: 'List of RFAs' })
|
||||
@RequirePermission('document.view')
|
||||
async findAll(@Query() query: SearchRfaDto) {
|
||||
async findAll(@Query() query: SearchRfaDto, @CurrentUser() user: User) {
|
||||
// ADR-019: resolve projectId UUID→INT if provided
|
||||
if (query.projectId) {
|
||||
const pid = query.projectId;
|
||||
const num = Number(pid);
|
||||
if (typeof pid === 'string' && isNaN(num)) {
|
||||
const project = await this.projectService.findOneByUuid(pid);
|
||||
query.projectId = project.id;
|
||||
}
|
||||
query.projectId = await this.uuidResolver.resolveProjectId(
|
||||
query.projectId
|
||||
);
|
||||
}
|
||||
return this.rfaService.findAll(query);
|
||||
return this.rfaService.findAll(query, user);
|
||||
}
|
||||
|
||||
@Get(':uuid')
|
||||
@@ -123,4 +127,33 @@ export class RfaController {
|
||||
findOne(@Param('uuid', ParseUuidPipe) uuid: string) {
|
||||
return this.rfaService.findOneByUuid(uuid);
|
||||
}
|
||||
|
||||
@Put(':uuid')
|
||||
@ApiOperation({ summary: 'Update Draft RFA fields (EC-RFA-002: DFT only)' })
|
||||
@ApiParam({ name: 'uuid', description: 'RFA UUID' })
|
||||
@ApiBody({ type: UpdateRfaDto })
|
||||
@ApiResponse({ status: 200, description: 'RFA updated successfully' })
|
||||
@RequirePermission('rfa.create')
|
||||
@Audit('rfa.update', 'rfa')
|
||||
async update(
|
||||
@Param('uuid', ParseUuidPipe) uuid: string,
|
||||
@Body() updateDto: UpdateRfaDto,
|
||||
@CurrentUser() user: User
|
||||
) {
|
||||
return this.rfaService.update(uuid, updateDto, user);
|
||||
}
|
||||
|
||||
@Delete(':uuid')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: 'Cancel Draft RFA (sets status to CC)' })
|
||||
@ApiParam({ name: 'uuid', description: 'RFA UUID' })
|
||||
@ApiResponse({ status: 200, description: 'RFA cancelled successfully' })
|
||||
@RequirePermission('rfa.create')
|
||||
@Audit('rfa.cancel', 'rfa')
|
||||
async cancel(
|
||||
@Param('uuid', ParseUuidPipe) uuid: string,
|
||||
@CurrentUser() user: User
|
||||
) {
|
||||
return this.rfaService.cancel(uuid, user);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { CorrespondenceType } from '../correspondence/entities/correspondence-ty
|
||||
import { CorrespondenceRecipient } from '../correspondence/entities/correspondence-recipient.entity';
|
||||
import { RoutingTemplate } from '../correspondence/entities/routing-template.entity';
|
||||
import { RoutingTemplateStep } from '../correspondence/entities/routing-template-step.entity';
|
||||
import { Organization } from '../organization/entities/organization.entity';
|
||||
import { AsBuiltDrawingRevision } from '../drawing/entities/asbuilt-drawing-revision.entity';
|
||||
import { ShopDrawingRevision } from '../drawing/entities/shop-drawing-revision.entity';
|
||||
import { Discipline } from '../master/entities/discipline.entity';
|
||||
@@ -60,6 +61,7 @@ import { WorkflowEngineModule } from '../workflow-engine/workflow-engine.module'
|
||||
RoutingTemplate,
|
||||
RoutingTemplateStep,
|
||||
CorrespondenceRecipient,
|
||||
Organization,
|
||||
]),
|
||||
DocumentNumberingModule,
|
||||
UserModule,
|
||||
|
||||
@@ -23,6 +23,7 @@ import { RoutingTemplateStep } from '../correspondence/entities/routing-template
|
||||
import { AsBuiltDrawingRevision } from '../drawing/entities/asbuilt-drawing-revision.entity';
|
||||
import { ShopDrawingRevision } from '../drawing/entities/shop-drawing-revision.entity';
|
||||
import { Discipline } from '../master/entities/discipline.entity';
|
||||
import { Organization } from '../organization/entities/organization.entity';
|
||||
import { User } from '../user/entities/user.entity';
|
||||
import { RfaApproveCode } from './entities/rfa-approve-code.entity';
|
||||
import { RfaItem } from './entities/rfa-item.entity';
|
||||
@@ -35,6 +36,7 @@ import { Rfa } from './entities/rfa.entity';
|
||||
import { WorkflowActionDto } from '../correspondence/dto/workflow-action.dto';
|
||||
import { CreateRfaDto } from './dto/create-rfa.dto';
|
||||
import { SearchRfaDto } from './dto/search-rfa.dto';
|
||||
import { UpdateRfaDto } from './dto/update-rfa.dto';
|
||||
|
||||
// ------- Local type helpers (no-any ADR-019) -------
|
||||
/** CorrespondenceRevision with the rfaRevision relation loaded at runtime */
|
||||
@@ -94,6 +96,8 @@ export class RfaService {
|
||||
private templateRepo: Repository<RoutingTemplate>,
|
||||
@InjectRepository(RoutingTemplateStep)
|
||||
private templateStepRepo: Repository<RoutingTemplateStep>,
|
||||
@InjectRepository(Organization)
|
||||
private orgRepo: Repository<Organization>,
|
||||
|
||||
private numberingService: DocumentNumberingService,
|
||||
private userService: UserService,
|
||||
@@ -232,13 +236,40 @@ export class RfaService {
|
||||
throw new BadRequestException('User must belong to an organization');
|
||||
}
|
||||
|
||||
// EC-RFA-001: Check for existing active RFA per Shop Drawing Revision
|
||||
if (shopDrawingRevisionIds.length > 0) {
|
||||
const conflictingItems = await this.rfaItemRepo
|
||||
.createQueryBuilder('item')
|
||||
.innerJoin('item.rfaRevision', 'rfaRev')
|
||||
.innerJoin('rfaRev.statusCode', 'status')
|
||||
.where('item.shopDrawingRevisionId IN (:...ids)', {
|
||||
ids: shopDrawingRevisionIds,
|
||||
})
|
||||
.andWhere('status.statusCode NOT IN (:...codes)', {
|
||||
codes: ['CC', 'OBS'],
|
||||
})
|
||||
.getMany();
|
||||
|
||||
if (conflictingItems.length > 0) {
|
||||
throw new BadRequestException(
|
||||
'[EC-RFA-001] One or more selected Shop Drawing Revisions already have an active RFA. ' +
|
||||
'A Shop Drawing Revision can only be referenced by one active RFA at a time.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch real Organization Code for document numbering
|
||||
const userOrg = await this.orgRepo.findOne({
|
||||
where: { id: userOrgId },
|
||||
select: ['organizationCode'],
|
||||
});
|
||||
const orgCode = userOrg?.organizationCode ?? 'ORG';
|
||||
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
const orgCode = 'ORG'; // TODO: Fetch real ORG Code from Org Service if needed
|
||||
|
||||
// [UPDATED] Generate Document Number with Discipline
|
||||
const docNumber = await this.numberingService.generateNextNumber({
|
||||
projectId: internalProjectId,
|
||||
@@ -427,7 +458,7 @@ export class RfaService {
|
||||
|
||||
// ... (ส่วน findOne, submit, processAction คงเดิมจากไฟล์ที่แนบมา แค่ปรับปรุงเล็กน้อยตาม Context) ...
|
||||
|
||||
async findAll(query: SearchRfaDto) {
|
||||
async findAll(query: SearchRfaDto, _user?: User) {
|
||||
const {
|
||||
page = 1,
|
||||
limit = 20,
|
||||
@@ -481,14 +512,22 @@ export class RfaService {
|
||||
);
|
||||
}
|
||||
|
||||
// RBAC: DFT documents are visible only to the originator org (spec §3.3.10)
|
||||
if (_user?.primaryOrganizationId) {
|
||||
queryBuilder.andWhere(
|
||||
'(rfaRev.id IS NULL OR status.statusCode != :dftCode OR corr.originatorId = :userOrgId)',
|
||||
{ dftCode: 'DFT', userOrgId: _user.primaryOrganizationId }
|
||||
);
|
||||
}
|
||||
|
||||
const [items, total] = await queryBuilder
|
||||
.orderBy('corr.createdAt', 'DESC')
|
||||
.skip(skip)
|
||||
.take(limit)
|
||||
.getManyAndCount();
|
||||
|
||||
this.logger.log(
|
||||
`[DEBUG] RFA findAll: Found ${total} items. Query: ${JSON.stringify(query)}`
|
||||
this.logger.debug(
|
||||
`RFA findAll: Found ${total} items. Query: ${JSON.stringify(query)}`
|
||||
);
|
||||
|
||||
// Map `revisions` property back to the expected payload for the frontend
|
||||
@@ -802,4 +841,83 @@ export class RfaService {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a Draft RFA's revision fields (subject, body, remarks, description, dueDate).
|
||||
* EC-RFA-002: Only allowed when current revision is in DFT status.
|
||||
*/
|
||||
async update(uuid: string, dto: UpdateRfaDto, _user: User) {
|
||||
const rfa = await this.findOneByUuidRaw(uuid);
|
||||
const corrRevisions =
|
||||
(rfa.correspondence?.revisions as CorrRevWithRfa[] | undefined) ?? [];
|
||||
const currentCorrRev = corrRevisions.find((r) => r.isCurrent);
|
||||
if (!currentCorrRev || !currentCorrRev.rfaRevision)
|
||||
throw new NotFoundException('Current revision not found');
|
||||
|
||||
const currentRfaRev = currentCorrRev.rfaRevision;
|
||||
|
||||
if (currentRfaRev.statusCode.statusCode !== 'DFT') {
|
||||
throw new BadRequestException(
|
||||
'Only DRAFT documents can be edited. Submit a new revision for non-draft documents.'
|
||||
);
|
||||
}
|
||||
|
||||
const updatedFields: Partial<CorrespondenceRevision> = {};
|
||||
if (dto.subject !== undefined) updatedFields.subject = dto.subject;
|
||||
if (dto.body !== undefined) updatedFields.body = dto.body;
|
||||
if (dto.remarks !== undefined) updatedFields.remarks = dto.remarks;
|
||||
if (dto.description !== undefined)
|
||||
updatedFields.description = dto.description;
|
||||
if (dto.dueDate !== undefined)
|
||||
updatedFields.dueDate = new Date(dto.dueDate);
|
||||
|
||||
Object.assign(currentCorrRev, updatedFields);
|
||||
|
||||
await this.corrRevRepo.save(currentCorrRev);
|
||||
|
||||
if (dto.details !== undefined) {
|
||||
currentRfaRev.details = dto.details;
|
||||
await this.rfaRevisionRepo.save(currentRfaRev);
|
||||
}
|
||||
|
||||
return this.findOneByUuid(uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel (soft-delete) a Draft RFA by setting its status to CC.
|
||||
* EC-RFA-002: Only allowed when current revision is in DFT status.
|
||||
*/
|
||||
async cancel(uuid: string, user: User) {
|
||||
const rfa = await this.findOneByUuidRaw(uuid);
|
||||
const corrRevisions =
|
||||
(rfa.correspondence?.revisions as CorrRevWithRfa[] | undefined) ?? [];
|
||||
const currentCorrRev = corrRevisions.find((r) => r.isCurrent);
|
||||
if (!currentCorrRev || !currentCorrRev.rfaRevision)
|
||||
throw new NotFoundException('Current revision not found');
|
||||
|
||||
const currentRfaRev = currentCorrRev.rfaRevision;
|
||||
|
||||
if (currentRfaRev.statusCode.statusCode !== 'DFT') {
|
||||
throw new BadRequestException(
|
||||
'Only DRAFT documents can be cancelled. Contact an Org Admin to cancel submitted documents.'
|
||||
);
|
||||
}
|
||||
|
||||
const statusCC = await this.rfaStatusRepo.findOne({
|
||||
where: { statusCode: 'CC' },
|
||||
});
|
||||
if (!statusCC)
|
||||
throw new InternalServerErrorException(
|
||||
'Status CC (Cancelled) not found in Master Data'
|
||||
);
|
||||
|
||||
currentRfaRev.rfaStatusCodeId = statusCC.id;
|
||||
await this.rfaRevisionRepo.save(currentRfaRev);
|
||||
|
||||
this.logger.log(
|
||||
`RFA ${rfa.correspondence?.correspondenceNumber} cancelled by user ${user.user_id}`
|
||||
);
|
||||
|
||||
return { message: 'RFA cancelled successfully' };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user