// File: src/modules/rfa/rfa.service.ts import { BadRequestException, ForbiddenException, Injectable, InternalServerErrorException, Logger, NotFoundException, } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { DataSource, In, Repository } from 'typeorm'; // Entities import { CorrespondenceRouting } from '../correspondence/entities/correspondence-routing.entity'; import { Correspondence } from '../correspondence/entities/correspondence.entity'; import { RoutingTemplate } from '../correspondence/entities/routing-template.entity'; import { ShopDrawingRevision } from '../drawing/entities/shop-drawing-revision.entity'; import { User } from '../user/entities/user.entity'; import { RfaApproveCode } from './entities/rfa-approve-code.entity'; import { RfaItem } from './entities/rfa-item.entity'; import { RfaRevision } from './entities/rfa-revision.entity'; import { RfaStatusCode } from './entities/rfa-status-code.entity'; import { RfaType } from './entities/rfa-type.entity'; import { Rfa } from './entities/rfa.entity'; // DTOs import { WorkflowActionDto } from '../correspondence/dto/workflow-action.dto'; import { CreateRfaDto } from './dto/create-rfa.dto'; // Interfaces & Enums // Services import { DocumentNumberingService } from '../document-numbering/document-numbering.service'; import { NotificationService } from '../notification/notification.service'; import { SearchService } from '../search/search.service'; import { UserService } from '../user/user.service'; import { WorkflowEngineService } from '../workflow-engine/workflow-engine.service'; @Injectable() export class RfaService { private readonly logger = new Logger(RfaService.name); constructor( @InjectRepository(Rfa) private rfaRepo: Repository, @InjectRepository(RfaRevision) private rfaRevisionRepo: Repository, @InjectRepository(RfaItem) private rfaItemRepo: Repository, @InjectRepository(Correspondence) private correspondenceRepo: Repository, @InjectRepository(RfaType) private rfaTypeRepo: Repository, @InjectRepository(RfaStatusCode) private rfaStatusRepo: Repository, @InjectRepository(RfaApproveCode) private rfaApproveRepo: Repository, @InjectRepository(ShopDrawingRevision) private shopDrawingRevRepo: Repository, @InjectRepository(CorrespondenceRouting) private routingRepo: Repository, @InjectRepository(RoutingTemplate) private templateRepo: Repository, private numberingService: DocumentNumberingService, private userService: UserService, private workflowEngine: WorkflowEngineService, private notificationService: NotificationService, private dataSource: DataSource, private searchService: SearchService, ) {} async create(createDto: CreateRfaDto, user: User) { const rfaType = await this.rfaTypeRepo.findOne({ where: { id: createDto.rfaTypeId }, }); if (!rfaType) throw new NotFoundException('RFA Type not found'); const statusDraft = await this.rfaStatusRepo.findOne({ where: { statusCode: 'DFT' }, }); if (!statusDraft) { throw new InternalServerErrorException( 'Status DFT (Draft) not found in Master Data', ); } // Determine User Organization let userOrgId = user.primaryOrganizationId; if (!userOrgId) { const fullUser = await this.userService.findOne(user.user_id); if (fullUser) userOrgId = fullUser.primaryOrganizationId; } if (!userOrgId) { throw new BadRequestException('User must belong to an organization'); } 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: createDto.projectId, originatorId: userOrgId, typeId: createDto.rfaTypeId, disciplineId: createDto.disciplineId ?? 0, // ✅ ส่ง disciplineId ไปด้วย (0 ถ้าไม่มี) year: new Date().getFullYear(), customTokens: { TYPE_CODE: rfaType.typeCode, ORG_CODE: orgCode, }, }); // 1. Create Correspondence Record const correspondence = queryRunner.manager.create(Correspondence, { correspondenceNumber: docNumber, correspondenceTypeId: createDto.rfaTypeId, // Assuming RFA Type maps directly or via logic // Note: ถ้า CorrespondenceType แยก ID กับ RFA Type ต้อง Map ให้ถูก // ในที่นี้สมมติว่าใช้ ID เดียวกัน หรือ RFA Type เป็น SubType ของ Correspondence projectId: createDto.projectId, originatorId: userOrgId, isInternalCommunication: false, createdBy: user.user_id, // ✅ Add disciplineId column if correspondence table supports it (as per Data Dictionary Update) // disciplineId: createDto.disciplineId }); const savedCorr = await queryRunner.manager.save(correspondence); // 2. Create RFA Master Record const rfa = queryRunner.manager.create(Rfa, { rfaTypeId: createDto.rfaTypeId, createdBy: user.user_id, // ✅ ถ้า Entity Rfa มี disciplineId ให้ใส่ตรงนี้ด้วย // disciplineId: createDto.disciplineId }); const savedRfa = await queryRunner.manager.save(rfa); // 3. Create First Revision (Draft) const rfaRevision = queryRunner.manager.create(RfaRevision, { correspondenceId: savedCorr.id, rfaId: savedRfa.id, revisionNumber: 0, revisionLabel: '0', isCurrent: true, rfaStatusCodeId: statusDraft.id, title: createDto.title, description: createDto.description, documentDate: createDto.documentDate ? new Date(createDto.documentDate) : new Date(), createdBy: user.user_id, details: createDto.details, // ✅ Save JSON Details schemaVersion: 1, // ✅ Default Schema Version }); const savedRevision = await queryRunner.manager.save(rfaRevision); // 4. Link Shop Drawings if ( createDto.shopDrawingRevisionIds && createDto.shopDrawingRevisionIds.length > 0 ) { const shopDrawings = await this.shopDrawingRevRepo.findBy({ id: In(createDto.shopDrawingRevisionIds), }); if (shopDrawings.length !== createDto.shopDrawingRevisionIds.length) { throw new NotFoundException('Some Shop Drawing Revisions not found'); } const rfaItems = shopDrawings.map((sd) => queryRunner.manager.create(RfaItem, { rfaRevisionId: savedCorr.id, // ใช้ ID ของ Correspondence (ตาม Schema ที่ออกแบบไว้) หรือ RFA Revision ID แล้วแต่การ Map Entity // ตาม Entity RfaItem ที่ให้มา: rfaRevisionId map ไปที่ correspondence_id shopDrawingRevisionId: sd.id, }), ); await queryRunner.manager.save(rfaItems); } await queryRunner.commitTransaction(); // Indexing for Search this.searchService.indexDocument({ id: savedCorr.id, type: 'rfa', docNumber: docNumber, title: createDto.title, description: createDto.description, status: 'DRAFT', projectId: createDto.projectId, createdAt: new Date(), }); return { ...savedRfa, correspondenceNumber: docNumber, currentRevision: savedRevision, }; } catch (err) { await queryRunner.rollbackTransaction(); this.logger.error(`Failed to create RFA: ${(err as Error).message}`); throw err; } finally { await queryRunner.release(); } } // ... (ส่วน findOne, submit, processAction คงเดิมจากไฟล์ที่แนบมา แค่ปรับปรุงเล็กน้อยตาม Context) ... async findOne(id: number) { const rfa = await this.rfaRepo.findOne({ where: { id }, relations: [ 'rfaType', 'revisions', 'revisions.statusCode', 'revisions.approveCode', 'revisions.correspondence', 'revisions.items', 'revisions.items.shopDrawingRevision', 'revisions.items.shopDrawingRevision.shopDrawing', ], order: { revisions: { revisionNumber: 'DESC' }, }, }); if (!rfa) { throw new NotFoundException(`RFA ID ${id} not found`); } return rfa; } async submit(rfaId: number, templateId: number, user: User) { const rfa = await this.findOne(rfaId); const currentRevision = rfa.revisions.find((r) => r.isCurrent); if (!currentRevision) throw new NotFoundException('Current revision not found'); if (currentRevision.statusCode.statusCode !== 'DFT') { throw new BadRequestException('Only DRAFT documents can be submitted'); } const template = await this.templateRepo.findOne({ where: { id: templateId }, relations: ['steps'], order: { steps: { sequence: 'ASC' } }, }); if (!template || !template.steps || template.steps.length === 0) { throw new BadRequestException('Invalid routing template'); } const statusForApprove = await this.rfaStatusRepo.findOne({ where: { statusCode: 'FAP' }, }); if (!statusForApprove) throw new InternalServerErrorException('Status FAP not found'); const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { // Update Revision Status currentRevision.rfaStatusCodeId = statusForApprove.id; currentRevision.issuedDate = new Date(); await queryRunner.manager.save(currentRevision); // Create First Routing Step const firstStep = template.steps[0]; const routing = queryRunner.manager.create(CorrespondenceRouting, { correspondenceId: currentRevision.correspondenceId, templateId: template.id, sequence: 1, fromOrganizationId: user.primaryOrganizationId, toOrganizationId: firstStep.toOrganizationId, stepPurpose: firstStep.stepPurpose, status: 'SENT', dueDate: new Date( Date.now() + (firstStep.expectedDays || 7) * 24 * 60 * 60 * 1000, ), processedByUserId: user.user_id, processedAt: new Date(), }); await queryRunner.manager.save(routing); // Notify const recipientUserId = await this.userService.findDocControlIdByOrg( firstStep.toOrganizationId, ); if (recipientUserId) { await this.notificationService.send({ userId: recipientUserId, title: `RFA Submitted: ${currentRevision.title}`, message: `RFA ${currentRevision.correspondence.correspondenceNumber} submitted for approval.`, type: 'SYSTEM', entityType: 'rfa', entityId: rfa.id, }); } await queryRunner.commitTransaction(); return { message: 'RFA Submitted successfully', routing }; } catch (err) { await queryRunner.rollbackTransaction(); throw err; } finally { await queryRunner.release(); } } async processAction(rfaId: number, dto: WorkflowActionDto, user: User) { // Logic คงเดิม: หา Current Routing -> Check Permission -> Call Workflow Engine -> Update DB // ใช้ this.workflowEngine.processAction (Legacy Support) // ... (สามารถใช้ Code เดิมจากที่คุณแนบมาได้เลย เพราะ Logic ถูกต้องแล้วสำหรับการใช้ CorrespondenceRouting) ... const rfa = await this.findOne(rfaId); const currentRevision = rfa.revisions.find((r) => r.isCurrent); if (!currentRevision) throw new NotFoundException('Current revision not found'); const currentRouting = await this.routingRepo.findOne({ where: { correspondenceId: currentRevision.correspondenceId, status: 'SENT', }, order: { sequence: 'DESC' }, relations: ['toOrganization'], }); if (!currentRouting) throw new BadRequestException('No active workflow step found'); if (currentRouting.toOrganizationId !== user.primaryOrganizationId) { throw new ForbiddenException( 'You are not authorized to process this step', ); } const template = await this.templateRepo.findOne({ where: { id: currentRouting.templateId }, relations: ['steps'], }); if (!template || !template.steps) throw new InternalServerErrorException('Template not found'); // Call Engine to calculate next step const result = this.workflowEngine.processAction( currentRouting.sequence, template.steps.length, dto.action, dto.returnToSequence, ); const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { // Update current routing currentRouting.status = dto.action === 'REJECT' ? 'REJECTED' : 'ACTIONED'; currentRouting.processedByUserId = user.user_id; currentRouting.processedAt = new Date(); currentRouting.comments = dto.comments; await queryRunner.manager.save(currentRouting); // Create next routing if available if (result.nextStepSequence && dto.action !== 'REJECT') { const nextStep = template.steps.find( (s) => s.sequence === result.nextStepSequence, ); if (nextStep) { const nextRouting = queryRunner.manager.create( CorrespondenceRouting, { correspondenceId: currentRevision.correspondenceId, templateId: template.id, sequence: result.nextStepSequence, fromOrganizationId: user.primaryOrganizationId, toOrganizationId: nextStep.toOrganizationId, stepPurpose: nextStep.stepPurpose, status: 'SENT', dueDate: new Date( Date.now() + (nextStep.expectedDays || 7) * 24 * 60 * 60 * 1000, ), }, ); await queryRunner.manager.save(nextRouting); } } else if (result.nextStepSequence === null) { // Workflow Ended (Completed or Rejected) // Update RFA Status (Approved/Rejected Code) if (dto.action !== 'REJECT') { const approveCode = await this.rfaApproveRepo.findOne({ where: { approveCode: dto.action === 'APPROVE' ? '1A' : '4X' }, }); // Logic Map Code อย่างง่าย if (approveCode) { currentRevision.rfaApproveCodeId = approveCode.id; currentRevision.approvedDate = new Date(); } } else { const rejectCode = await this.rfaApproveRepo.findOne({ where: { approveCode: '4X' }, }); if (rejectCode) currentRevision.rfaApproveCodeId = rejectCode.id; } await queryRunner.manager.save(currentRevision); } await queryRunner.commitTransaction(); return { message: 'Action processed', result }; } catch (err) { await queryRunner.rollbackTransaction(); throw err; } finally { await queryRunner.release(); } } }