768 lines
25 KiB
TypeScript
768 lines
25 KiB
TypeScript
// File: src/modules/correspondence/correspondence.service.ts
|
|
|
|
import {
|
|
Injectable,
|
|
NotFoundException,
|
|
BadRequestException,
|
|
InternalServerErrorException,
|
|
ForbiddenException,
|
|
Logger,
|
|
} from '@nestjs/common';
|
|
import { InjectRepository } from '@nestjs/typeorm';
|
|
import { Repository, DataSource } from 'typeorm';
|
|
|
|
// Entities
|
|
import { Correspondence } from './entities/correspondence.entity';
|
|
import { CorrespondenceRevision } from './entities/correspondence-revision.entity';
|
|
import { CorrespondenceType } from './entities/correspondence-type.entity';
|
|
import { CorrespondenceStatus } from './entities/correspondence-status.entity';
|
|
import { CorrespondenceReference } from './entities/correspondence-reference.entity';
|
|
import { CorrespondenceRecipient } from './entities/correspondence-recipient.entity';
|
|
import { User } from '../user/entities/user.entity';
|
|
import { Organization } from '../organization/entities/organization.entity';
|
|
|
|
// DTOs
|
|
import { CreateCorrespondenceDto } from './dto/create-correspondence.dto';
|
|
import { UpdateCorrespondenceDto } from './dto/update-correspondence.dto';
|
|
import { AddReferenceDto } from './dto/add-reference.dto';
|
|
import { SearchCorrespondenceDto } from './dto/search-correspondence.dto';
|
|
import { DeepPartial } from 'typeorm';
|
|
|
|
// Services
|
|
import { DocumentNumberingService } from '../document-numbering/services/document-numbering.service';
|
|
import { JsonSchemaService } from '../json-schema/json-schema.service';
|
|
import { WorkflowEngineService } from '../workflow-engine/workflow-engine.service';
|
|
import { UserService } from '../user/user.service';
|
|
import { SearchService } from '../search/search.service';
|
|
import { FileStorageService } from '../../common/file-storage/file-storage.service';
|
|
import { Project } from '../project/entities/project.entity';
|
|
|
|
/**
|
|
* CorrespondenceService - Document management (CRUD)
|
|
*
|
|
* NOTE: Workflow operations (submit, processAction) have been moved to
|
|
* CorrespondenceWorkflowService which uses the Unified Workflow Engine.
|
|
*/
|
|
@Injectable()
|
|
export class CorrespondenceService {
|
|
private readonly logger = new Logger(CorrespondenceService.name);
|
|
|
|
constructor(
|
|
@InjectRepository(Correspondence)
|
|
private correspondenceRepo: Repository<Correspondence>,
|
|
@InjectRepository(CorrespondenceRevision)
|
|
private revisionRepo: Repository<CorrespondenceRevision>,
|
|
@InjectRepository(CorrespondenceType)
|
|
private typeRepo: Repository<CorrespondenceType>,
|
|
@InjectRepository(CorrespondenceStatus)
|
|
private statusRepo: Repository<CorrespondenceStatus>,
|
|
@InjectRepository(CorrespondenceReference)
|
|
private referenceRepo: Repository<CorrespondenceReference>,
|
|
@InjectRepository(Organization)
|
|
private orgRepo: Repository<Organization>,
|
|
|
|
private numberingService: DocumentNumberingService,
|
|
private jsonSchemaService: JsonSchemaService,
|
|
private workflowEngine: WorkflowEngineService,
|
|
private userService: UserService,
|
|
private dataSource: DataSource,
|
|
private searchService: SearchService,
|
|
private fileStorageService: FileStorageService
|
|
) {}
|
|
|
|
/**
|
|
* ADR-019: Resolve projectId (INT or UUID string) to internal INT ID
|
|
*/
|
|
private async resolveProjectId(projectId: number | string): Promise<number> {
|
|
if (typeof projectId === 'number') return projectId;
|
|
const num = Number(projectId);
|
|
if (!isNaN(num)) return num;
|
|
const project = await this.dataSource.manager.findOne(Project, {
|
|
where: { uuid: projectId },
|
|
select: ['id'],
|
|
});
|
|
if (!project)
|
|
throw new NotFoundException(`Project with UUID ${projectId} not found`);
|
|
return project.id;
|
|
}
|
|
|
|
/**
|
|
* ADR-019: Resolve organizationId (INT or UUID string) to internal INT ID
|
|
*/
|
|
private async resolveOrganizationId(orgId: number | string): Promise<number> {
|
|
if (typeof orgId === 'number') return orgId;
|
|
const num = Number(orgId);
|
|
if (!isNaN(num)) return num;
|
|
const org = await this.orgRepo.findOne({
|
|
where: { uuid: orgId },
|
|
select: ['id'],
|
|
});
|
|
if (!org)
|
|
throw new NotFoundException(`Organization with UUID ${orgId} not found`);
|
|
return org.id;
|
|
}
|
|
|
|
async create(createDto: CreateCorrespondenceDto, user: User) {
|
|
// ADR-019: Resolve UUID references to internal INT IDs
|
|
const resolvedProjectId = await this.resolveProjectId(createDto.projectId);
|
|
const resolvedOriginatorId = createDto.originatorId
|
|
? await this.resolveOrganizationId(createDto.originatorId)
|
|
: undefined;
|
|
const resolvedRecipients = createDto.recipients
|
|
? await Promise.all(
|
|
createDto.recipients.map(async (r) => ({
|
|
organizationId: await this.resolveOrganizationId(r.organizationId),
|
|
type: r.type,
|
|
}))
|
|
)
|
|
: undefined;
|
|
const type = await this.typeRepo.findOne({
|
|
where: { id: createDto.typeId },
|
|
});
|
|
if (!type) throw new NotFoundException('Document Type not found');
|
|
|
|
const statusDraft = await this.statusRepo.findOne({
|
|
where: { statusCode: 'DRAFT' },
|
|
});
|
|
if (!statusDraft) {
|
|
throw new InternalServerErrorException(
|
|
'Status DRAFT not found in Master Data'
|
|
);
|
|
}
|
|
|
|
let userOrgId = user.primaryOrganizationId;
|
|
|
|
if (!userOrgId) {
|
|
const fullUser = await this.userService.findOne(user.user_id);
|
|
if (fullUser) {
|
|
userOrgId = fullUser.primaryOrganizationId;
|
|
}
|
|
}
|
|
|
|
// Impersonation Logic
|
|
if (resolvedOriginatorId && resolvedOriginatorId !== userOrgId) {
|
|
const permissions = await this.userService.getUserPermissions(
|
|
user.user_id
|
|
);
|
|
if (!permissions.includes('system.manage_all')) {
|
|
throw new ForbiddenException(
|
|
'You do not have permission to create documents on behalf of other organizations.'
|
|
);
|
|
}
|
|
userOrgId = resolvedOriginatorId;
|
|
}
|
|
|
|
if (!userOrgId) {
|
|
throw new BadRequestException(
|
|
'User must belong to an organization to create documents'
|
|
);
|
|
}
|
|
|
|
if (createDto.details) {
|
|
try {
|
|
await this.jsonSchemaService.validate(type.typeCode, createDto.details);
|
|
} catch (error: unknown) {
|
|
this.logger.warn(
|
|
`Schema validation warning for ${type.typeCode}: ${(error as Error).message}`
|
|
);
|
|
}
|
|
}
|
|
|
|
const queryRunner = this.dataSource.createQueryRunner();
|
|
await queryRunner.connect();
|
|
await queryRunner.startTransaction();
|
|
|
|
try {
|
|
// [Fix #6] Fetch real ORG Code from Organization entity
|
|
const originatorOrg = await this.orgRepo.findOne({
|
|
where: { id: userOrgId },
|
|
});
|
|
const orgCode = originatorOrg?.organizationCode ?? 'UNK';
|
|
|
|
// [v1.5.1] Extract recipient organization from recipients array (Primary TO)
|
|
const toRecipient = resolvedRecipients?.find((r) => r.type === 'TO');
|
|
const recipientOrganizationId = toRecipient?.organizationId;
|
|
|
|
let recipientCode = '';
|
|
if (recipientOrganizationId) {
|
|
const recOrg = await this.orgRepo.findOne({
|
|
where: { id: recipientOrganizationId },
|
|
});
|
|
if (recOrg) recipientCode = recOrg.organizationCode;
|
|
}
|
|
|
|
const docNumber = await this.numberingService.generateNextNumber({
|
|
projectId: resolvedProjectId,
|
|
originatorOrganizationId: userOrgId,
|
|
typeId: createDto.typeId,
|
|
disciplineId: createDto.disciplineId,
|
|
subTypeId: createDto.subTypeId,
|
|
recipientOrganizationId, // [v1.5.1] Pass recipient for document number format
|
|
year: new Date().getFullYear(),
|
|
customTokens: {
|
|
TYPE_CODE: type.typeCode,
|
|
ORG_CODE: orgCode,
|
|
RECIPIENT_CODE: recipientCode,
|
|
REC_CODE: recipientCode,
|
|
},
|
|
});
|
|
|
|
const correspondence = queryRunner.manager.create(Correspondence, {
|
|
correspondenceNumber: docNumber.number,
|
|
correspondenceTypeId: createDto.typeId,
|
|
disciplineId: createDto.disciplineId,
|
|
projectId: resolvedProjectId,
|
|
originatorId: userOrgId,
|
|
isInternal: createDto.isInternal || false,
|
|
createdBy: user.user_id,
|
|
});
|
|
const savedCorr = await queryRunner.manager.save(correspondence);
|
|
|
|
const revision = queryRunner.manager.create(CorrespondenceRevision, {
|
|
correspondenceId: savedCorr.id,
|
|
revisionNumber: 0,
|
|
revisionLabel: 'A',
|
|
isCurrent: true,
|
|
statusId: statusDraft.id,
|
|
subject: createDto.subject,
|
|
body: createDto.body,
|
|
remarks: createDto.remarks,
|
|
dueDate: createDto.dueDate ? new Date(createDto.dueDate) : undefined,
|
|
documentDate: createDto.documentDate
|
|
? new Date(createDto.documentDate)
|
|
: undefined,
|
|
issuedDate: createDto.issuedDate
|
|
? new Date(createDto.issuedDate)
|
|
: undefined,
|
|
description: createDto.description,
|
|
details: createDto.details,
|
|
createdBy: user.user_id,
|
|
schemaVersion: 1,
|
|
});
|
|
await queryRunner.manager.save(revision);
|
|
|
|
// Save Recipients (using resolved INT IDs)
|
|
if (resolvedRecipients && resolvedRecipients.length > 0) {
|
|
const recipients = resolvedRecipients.map((r) =>
|
|
queryRunner.manager.create(CorrespondenceRecipient, {
|
|
correspondenceId: savedCorr.id,
|
|
recipientOrganizationId: r.organizationId,
|
|
recipientType: r.type,
|
|
})
|
|
);
|
|
await queryRunner.manager.save(recipients);
|
|
}
|
|
|
|
// Commit attachments from Temp → Permanent (Two-Phase Storage)
|
|
if (createDto.attachmentTempIds?.length) {
|
|
const issueDate = createDto.issuedDate
|
|
? new Date(createDto.issuedDate)
|
|
: createDto.documentDate
|
|
? new Date(createDto.documentDate)
|
|
: undefined;
|
|
|
|
await this.fileStorageService.commit(createDto.attachmentTempIds, {
|
|
issueDate,
|
|
documentType: 'Correspondence',
|
|
});
|
|
}
|
|
|
|
await queryRunner.commitTransaction();
|
|
|
|
// Start Workflow Instance (non-blocking)
|
|
try {
|
|
const workflowCode = `CORRESPONDENCE_${type.typeCode}`;
|
|
await this.workflowEngine.createInstance(
|
|
workflowCode,
|
|
'correspondence',
|
|
savedCorr.id.toString(),
|
|
{
|
|
projectId: resolvedProjectId,
|
|
originatorId: userOrgId,
|
|
disciplineId: createDto.disciplineId,
|
|
initiatorId: user.user_id,
|
|
}
|
|
);
|
|
} catch (error) {
|
|
this.logger.warn(
|
|
`Workflow not started for ${docNumber.number} (Code: CORRESPONDENCE_${type.typeCode}): ${(error as Error).message}`
|
|
);
|
|
}
|
|
|
|
// Fire-and-forget search indexing (non-blocking, void intentional)
|
|
void this.searchService.indexDocument({
|
|
id: savedCorr.id,
|
|
type: 'correspondence',
|
|
docNumber: docNumber.number,
|
|
title: createDto.subject,
|
|
description: createDto.description,
|
|
status: 'DRAFT',
|
|
projectId: resolvedProjectId,
|
|
createdAt: new Date(),
|
|
});
|
|
|
|
return {
|
|
...savedCorr,
|
|
currentRevision: revision,
|
|
};
|
|
} catch (err) {
|
|
await queryRunner.rollbackTransaction();
|
|
this.logger.error(
|
|
`Failed to create correspondence: ${(err as Error).message}`
|
|
);
|
|
throw err;
|
|
} finally {
|
|
await queryRunner.release();
|
|
}
|
|
}
|
|
|
|
async findAll(searchDto: SearchCorrespondenceDto = {}) {
|
|
const {
|
|
search,
|
|
typeId,
|
|
projectId,
|
|
statusId,
|
|
page = 1,
|
|
limit = 10,
|
|
} = searchDto;
|
|
const skip = (page - 1) * limit;
|
|
|
|
// Change: Query from Revision Repo
|
|
const query = this.revisionRepo
|
|
.createQueryBuilder('rev')
|
|
.leftJoinAndSelect('rev.correspondence', 'corr')
|
|
.leftJoinAndSelect('corr.type', 'type')
|
|
.leftJoinAndSelect('corr.project', 'project')
|
|
.leftJoinAndSelect('corr.originator', 'org')
|
|
.leftJoinAndSelect('rev.status', 'status');
|
|
|
|
// Filter by Revision Status
|
|
const revStatus = searchDto.revisionStatus || 'CURRENT';
|
|
|
|
if (revStatus === 'CURRENT') {
|
|
query.where('rev.isCurrent = :isCurrent', { isCurrent: true });
|
|
} else if (revStatus === 'OLD') {
|
|
query.where('rev.isCurrent = :isCurrent', { isCurrent: false });
|
|
}
|
|
// If 'ALL', no filter needed on isCurrent
|
|
|
|
if (projectId) {
|
|
query.andWhere('corr.projectId = :projectId', { projectId });
|
|
}
|
|
|
|
if (typeId) {
|
|
query.andWhere('corr.correspondenceTypeId = :typeId', { typeId });
|
|
}
|
|
|
|
if (statusId) {
|
|
query.andWhere('rev.statusId = :statusId', { statusId });
|
|
}
|
|
|
|
if (search) {
|
|
query.andWhere(
|
|
'(corr.correspondenceNumber LIKE :search OR rev.subject LIKE :search)',
|
|
{ search: `%${search}%` }
|
|
);
|
|
}
|
|
|
|
// Default Sort: Latest Created
|
|
query.orderBy('rev.createdAt', 'DESC').skip(skip).take(limit);
|
|
|
|
const [items, total] = await query.getManyAndCount();
|
|
|
|
return {
|
|
data: items,
|
|
meta: {
|
|
total,
|
|
page,
|
|
limit,
|
|
totalPages: Math.ceil(total / limit),
|
|
},
|
|
};
|
|
}
|
|
|
|
async findOne(id: number) {
|
|
const correspondence = await this.correspondenceRepo.findOne({
|
|
where: { id },
|
|
relations: [
|
|
'revisions',
|
|
'revisions.status',
|
|
'type',
|
|
'project',
|
|
'originator',
|
|
'recipients',
|
|
'recipients.recipientOrganization', // [v1.5.1] Fixed relation name
|
|
],
|
|
});
|
|
|
|
if (!correspondence) {
|
|
throw new NotFoundException(`Correspondence with ID ${id} not found`);
|
|
}
|
|
return correspondence;
|
|
}
|
|
|
|
async findOneByUuid(uuid: string) {
|
|
const correspondence = await this.correspondenceRepo.findOne({
|
|
where: { uuid },
|
|
relations: [
|
|
'revisions',
|
|
'revisions.status',
|
|
'type',
|
|
'project',
|
|
'originator',
|
|
'recipients',
|
|
'recipients.recipientOrganization',
|
|
],
|
|
});
|
|
|
|
if (!correspondence) {
|
|
throw new NotFoundException(`Correspondence with UUID ${uuid} not found`);
|
|
}
|
|
return correspondence;
|
|
}
|
|
|
|
async addReference(id: number, dto: AddReferenceDto) {
|
|
const source = await this.correspondenceRepo.findOne({ where: { id } });
|
|
// ADR-019: Resolve target UUID → internal INT id
|
|
const target = await this.correspondenceRepo.findOne({
|
|
where: { uuid: dto.targetUuid },
|
|
});
|
|
|
|
if (!source || !target) {
|
|
throw new NotFoundException('Source or Target correspondence not found');
|
|
}
|
|
|
|
if (source.id === target.id) {
|
|
throw new BadRequestException('Cannot reference self');
|
|
}
|
|
|
|
const exists = await this.referenceRepo.findOne({
|
|
where: {
|
|
sourceId: id,
|
|
targetId: target.id,
|
|
},
|
|
});
|
|
|
|
if (exists) {
|
|
return exists;
|
|
}
|
|
|
|
const ref = this.referenceRepo.create({
|
|
sourceId: id,
|
|
targetId: target.id,
|
|
});
|
|
|
|
return this.referenceRepo.save(ref);
|
|
}
|
|
|
|
async removeReference(id: number, targetId: number) {
|
|
const result = await this.referenceRepo.delete({
|
|
sourceId: id,
|
|
targetId: targetId,
|
|
});
|
|
|
|
if (result.affected === 0) {
|
|
throw new NotFoundException('Reference not found');
|
|
}
|
|
}
|
|
|
|
async getReferences(id: number) {
|
|
const outgoing = await this.referenceRepo.find({
|
|
where: { sourceId: id },
|
|
relations: ['target', 'target.type'],
|
|
});
|
|
|
|
const incoming = await this.referenceRepo.find({
|
|
where: { targetId: id },
|
|
relations: ['source', 'source.type'],
|
|
});
|
|
|
|
return { outgoing, incoming };
|
|
}
|
|
|
|
async update(id: number, updateDto: UpdateCorrespondenceDto, user: User) {
|
|
// 1. Find Current Revision
|
|
const revision = await this.revisionRepo.findOne({
|
|
where: {
|
|
correspondenceId: id,
|
|
isCurrent: true,
|
|
},
|
|
relations: ['correspondence'],
|
|
});
|
|
|
|
if (!revision) {
|
|
throw new NotFoundException(
|
|
`Current revision for correspondence ${id} not found`
|
|
);
|
|
}
|
|
|
|
// 2. Check Permission
|
|
if (revision.statusId) {
|
|
const status = await this.statusRepo.findOne({
|
|
where: { id: revision.statusId },
|
|
});
|
|
if (status && status.statusCode !== 'DRAFT') {
|
|
throw new BadRequestException('Only DRAFT documents can be updated');
|
|
}
|
|
}
|
|
|
|
// ADR-019: Resolve UUID references in update DTO
|
|
const updResolvedProjectId = updateDto.projectId
|
|
? await this.resolveProjectId(updateDto.projectId)
|
|
: undefined;
|
|
const updResolvedOriginatorId = updateDto.originatorId
|
|
? await this.resolveOrganizationId(updateDto.originatorId)
|
|
: undefined;
|
|
const updResolvedRecipients = updateDto.recipients
|
|
? await Promise.all(
|
|
updateDto.recipients.map(async (r) => ({
|
|
organizationId: await this.resolveOrganizationId(r.organizationId),
|
|
type: r.type,
|
|
}))
|
|
)
|
|
: undefined;
|
|
|
|
// 3. Update Correspondence Entity if needed
|
|
const correspondenceUpdate: DeepPartial<Correspondence> = {};
|
|
if (updateDto.disciplineId)
|
|
correspondenceUpdate.disciplineId = updateDto.disciplineId;
|
|
if (updResolvedProjectId)
|
|
correspondenceUpdate.projectId = updResolvedProjectId;
|
|
if (updResolvedOriginatorId)
|
|
correspondenceUpdate.originatorId = updResolvedOriginatorId;
|
|
|
|
if (Object.keys(correspondenceUpdate).length > 0) {
|
|
await this.correspondenceRepo.update(id, correspondenceUpdate);
|
|
}
|
|
|
|
// 4. Update Revision Entity
|
|
const revisionUpdate: DeepPartial<CorrespondenceRevision> = {};
|
|
if (updateDto.subject) revisionUpdate.subject = updateDto.subject;
|
|
if (updateDto.body) revisionUpdate.body = updateDto.body;
|
|
if (updateDto.remarks) revisionUpdate.remarks = updateDto.remarks;
|
|
// Format Date correctly if string
|
|
if (updateDto.dueDate) revisionUpdate.dueDate = new Date(updateDto.dueDate);
|
|
if (updateDto.documentDate)
|
|
revisionUpdate.documentDate = new Date(updateDto.documentDate);
|
|
if (updateDto.issuedDate)
|
|
revisionUpdate.issuedDate = new Date(updateDto.issuedDate);
|
|
if (updateDto.description)
|
|
revisionUpdate.description = updateDto.description;
|
|
if (updateDto.details) revisionUpdate.details = updateDto.details;
|
|
|
|
if (Object.keys(revisionUpdate).length > 0) {
|
|
await this.revisionRepo.update(revision.id, revisionUpdate);
|
|
}
|
|
|
|
// 4.5 Commit new attachments from Temp → Permanent (Two-Phase Storage)
|
|
if (updateDto.attachmentTempIds?.length) {
|
|
const issueDate = updateDto.issuedDate
|
|
? new Date(updateDto.issuedDate)
|
|
: updateDto.documentDate
|
|
? new Date(updateDto.documentDate)
|
|
: revision.issuedDate || revision.documentDate || undefined;
|
|
|
|
await this.fileStorageService.commit(updateDto.attachmentTempIds, {
|
|
issueDate: issueDate ? new Date(issueDate) : undefined,
|
|
documentType: 'Correspondence',
|
|
});
|
|
}
|
|
|
|
// 5. Update Recipients if provided
|
|
if (updResolvedRecipients) {
|
|
const recipientRepo = this.dataSource.getRepository(
|
|
CorrespondenceRecipient
|
|
);
|
|
await recipientRepo.delete({ correspondenceId: id });
|
|
|
|
const newRecipients = updResolvedRecipients.map((r) =>
|
|
recipientRepo.create({
|
|
correspondenceId: id,
|
|
recipientOrganizationId: r.organizationId,
|
|
recipientType: r.type,
|
|
})
|
|
);
|
|
await recipientRepo.save(newRecipients);
|
|
}
|
|
|
|
// 6. Regenerate Document Number if structural fields changed (Recipient, Discipline, Type, Project)
|
|
// AND it is a DRAFT.
|
|
|
|
// Fetch fresh data for context and comparison
|
|
const currentCorr = await this.correspondenceRepo.findOne({
|
|
where: { id },
|
|
relations: ['type', 'recipients', 'recipients.recipientOrganization'],
|
|
});
|
|
|
|
if (currentCorr) {
|
|
const currentToRecipient = currentCorr.recipients?.find(
|
|
(r) => r.recipientType === 'TO'
|
|
);
|
|
const currentRecipientId = currentToRecipient?.recipientOrganizationId;
|
|
|
|
// Check for ACTUAL value changes
|
|
const isProjectChanged =
|
|
updResolvedProjectId !== undefined &&
|
|
updResolvedProjectId !== currentCorr.projectId;
|
|
const isOriginatorChanged =
|
|
updResolvedOriginatorId !== undefined &&
|
|
updResolvedOriginatorId !== currentCorr.originatorId;
|
|
const isDisciplineChanged =
|
|
updateDto.disciplineId !== undefined &&
|
|
updateDto.disciplineId !== currentCorr.disciplineId;
|
|
const isTypeChanged =
|
|
updateDto.typeId !== undefined &&
|
|
updateDto.typeId !== currentCorr.correspondenceTypeId;
|
|
|
|
let isRecipientChanged = false;
|
|
let newRecipientId: number | undefined;
|
|
|
|
if (updResolvedRecipients) {
|
|
const newToRecipient = updResolvedRecipients.find(
|
|
(r) => r.type === 'TO'
|
|
);
|
|
newRecipientId = newToRecipient?.organizationId;
|
|
|
|
if (newRecipientId !== currentRecipientId) {
|
|
isRecipientChanged = true;
|
|
}
|
|
}
|
|
|
|
if (
|
|
isProjectChanged ||
|
|
isDisciplineChanged ||
|
|
isTypeChanged ||
|
|
isRecipientChanged ||
|
|
isOriginatorChanged
|
|
) {
|
|
const targetRecipientId = isRecipientChanged
|
|
? newRecipientId
|
|
: currentRecipientId;
|
|
|
|
// Resolve Recipient Code for the NEW context
|
|
let recipientCode = '';
|
|
if (targetRecipientId) {
|
|
const recOrg = await this.orgRepo.findOne({
|
|
where: { id: targetRecipientId },
|
|
});
|
|
if (recOrg) recipientCode = recOrg.organizationCode;
|
|
}
|
|
|
|
// [Fix #6] Fetch real ORG Code from originator organization
|
|
const originatorOrgForUpdate = await this.orgRepo.findOne({
|
|
where: {
|
|
id: updResolvedOriginatorId ?? currentCorr.originatorId ?? 0,
|
|
},
|
|
});
|
|
const orgCode = originatorOrgForUpdate?.organizationCode ?? 'UNK';
|
|
|
|
// Prepare Contexts
|
|
const oldCtx = {
|
|
projectId: currentCorr.projectId,
|
|
originatorOrganizationId: currentCorr.originatorId ?? 0,
|
|
typeId: currentCorr.correspondenceTypeId,
|
|
disciplineId: currentCorr.disciplineId,
|
|
recipientOrganizationId: currentRecipientId,
|
|
year: new Date().getFullYear(),
|
|
};
|
|
|
|
const newCtx = {
|
|
projectId: updResolvedProjectId ?? currentCorr.projectId,
|
|
originatorOrganizationId:
|
|
updResolvedOriginatorId ?? currentCorr.originatorId ?? 0,
|
|
typeId: updateDto.typeId ?? currentCorr.correspondenceTypeId,
|
|
disciplineId: updateDto.disciplineId ?? currentCorr.disciplineId,
|
|
recipientOrganizationId: targetRecipientId,
|
|
year: new Date().getFullYear(),
|
|
userId: user.user_id, // Pass User ID for Audit
|
|
customTokens: {
|
|
TYPE_CODE: currentCorr.type?.typeCode || '',
|
|
ORG_CODE: orgCode,
|
|
RECIPIENT_CODE: recipientCode,
|
|
REC_CODE: recipientCode,
|
|
},
|
|
};
|
|
|
|
// If Type Changed, need NEW Type Code
|
|
if (isTypeChanged) {
|
|
const newType = await this.typeRepo.findOne({
|
|
where: { id: newCtx.typeId },
|
|
});
|
|
if (newType) newCtx.customTokens.TYPE_CODE = newType.typeCode;
|
|
}
|
|
|
|
const newDocNumber = await this.numberingService.updateNumberForDraft(
|
|
currentCorr.correspondenceNumber,
|
|
oldCtx,
|
|
newCtx
|
|
);
|
|
|
|
await this.correspondenceRepo.update(id, {
|
|
correspondenceNumber: newDocNumber,
|
|
});
|
|
}
|
|
}
|
|
|
|
return this.findOne(id);
|
|
}
|
|
|
|
async previewDocumentNumber(createDto: CreateCorrespondenceDto, user: User) {
|
|
// ADR-019: Resolve UUID references
|
|
const previewProjectId = await this.resolveProjectId(createDto.projectId);
|
|
const previewOriginatorId = createDto.originatorId
|
|
? await this.resolveOrganizationId(createDto.originatorId)
|
|
: undefined;
|
|
const previewRecipients = createDto.recipients
|
|
? await Promise.all(
|
|
createDto.recipients.map(async (r) => ({
|
|
organizationId: await this.resolveOrganizationId(r.organizationId),
|
|
type: r.type,
|
|
}))
|
|
)
|
|
: undefined;
|
|
|
|
const type = await this.typeRepo.findOne({
|
|
where: { id: createDto.typeId },
|
|
});
|
|
if (!type) throw new NotFoundException('Document Type not found');
|
|
|
|
let userOrgId = user.primaryOrganizationId;
|
|
if (!userOrgId) {
|
|
const fullUser = await this.userService.findOne(user.user_id);
|
|
if (fullUser) userOrgId = fullUser.primaryOrganizationId;
|
|
}
|
|
|
|
if (previewOriginatorId && previewOriginatorId !== userOrgId) {
|
|
// Allow impersonation for preview
|
|
userOrgId = previewOriginatorId;
|
|
}
|
|
|
|
// Extract recipient from recipients array
|
|
const toRecipient = previewRecipients?.find((r) => r.type === 'TO');
|
|
const recipientOrganizationId = toRecipient?.organizationId;
|
|
|
|
let recipientCode = '';
|
|
if (recipientOrganizationId) {
|
|
const recOrg = await this.orgRepo.findOne({
|
|
where: { id: recipientOrganizationId },
|
|
});
|
|
if (recOrg) recipientCode = recOrg.organizationCode;
|
|
}
|
|
|
|
return this.numberingService.previewNumber({
|
|
projectId: previewProjectId,
|
|
originatorOrganizationId: userOrgId!,
|
|
typeId: createDto.typeId,
|
|
disciplineId: createDto.disciplineId,
|
|
subTypeId: createDto.subTypeId,
|
|
recipientOrganizationId,
|
|
year: new Date().getFullYear(),
|
|
customTokens: {
|
|
TYPE_CODE: type.typeCode,
|
|
RECIPIENT_CODE: recipientCode,
|
|
REC_CODE: recipientCode,
|
|
},
|
|
});
|
|
}
|
|
}
|