260318:1135 Fix UUID #3
Build and Deploy / deploy (push) Successful in 8m43s

This commit is contained in:
admin
2026-03-18 11:35:51 +07:00
parent 6172b058df
commit 5d89079c2a
43 changed files with 1073 additions and 132 deletions
@@ -14,6 +14,8 @@ import { CreateCirculationDto } from './dto/create-circulation.dto';
import { UpdateCirculationRoutingDto } from './dto/update-circulation-routing.dto';
import { SearchCirculationDto } from './dto/search-circulation.dto';
import { DocumentNumberingService } from '../document-numbering/services/document-numbering.service';
import { Project } from '../project/entities/project.entity';
import { Correspondence } from '../correspondence/entities/correspondence.entity';
@Injectable()
export class CirculationService {
@@ -26,6 +28,58 @@ export class CirculationService {
private dataSource: DataSource
) {}
/**
* 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 correspondenceId (INT or UUID string) to internal INT ID
*/
private async resolveCorrespondenceId(
corrId: number | string
): Promise<number> {
if (typeof corrId === 'number') return corrId;
const num = Number(corrId);
if (!isNaN(num)) return num;
const corr = await this.dataSource.manager.findOne(Correspondence, {
where: { uuid: corrId },
select: ['id'],
});
if (!corr)
throw new NotFoundException(
`Correspondence with UUID ${corrId} not found`
);
return corr.id;
}
/**
* ADR-019: Resolve userId (INT or UUID string) to internal user_id
*/
private async resolveUserId(userId: number | string): Promise<number> {
if (typeof userId === 'number') return userId;
const num = Number(userId);
if (!isNaN(num)) return num;
const user = await this.dataSource.manager.findOne(User, {
where: { uuid: userId },
select: ['user_id'],
});
if (!user)
throw new NotFoundException(`User with UUID ${userId} not found`);
return user.user_id;
}
async create(createDto: CreateCirculationDto, user: User) {
if (!user.primaryOrganizationId) {
throw new BadRequestException('User must belong to an organization');
@@ -36,9 +90,20 @@ export class CirculationService {
await queryRunner.startTransaction();
try {
// ADR-019: Resolve UUID references to internal INT IDs
const resolvedProjectId = createDto.projectId
? await this.resolveProjectId(createDto.projectId)
: 0;
const resolvedCorrId = await this.resolveCorrespondenceId(
createDto.correspondenceId
);
const resolvedAssigneeIds = await Promise.all(
createDto.assigneeIds.map((id) => this.resolveUserId(id))
);
// Generate No. using DocumentNumberingService (Type 900 - Circulation)
const result = await this.numberingService.generateNextNumber({
projectId: createDto.projectId || 0, // Use projectId from DTO or 0
projectId: resolvedProjectId,
originatorOrganizationId: user.primaryOrganizationId,
typeId: 900, // Fixed Type ID for Circulation
year: new Date().getFullYear(),
@@ -50,7 +115,7 @@ export class CirculationService {
const circulation = queryRunner.manager.create(Circulation, {
organizationId: user.primaryOrganizationId,
correspondenceId: createDto.correspondenceId,
correspondenceId: resolvedCorrId,
circulationNo: result.number,
subject: createDto.subject,
statusCode: 'OPEN',
@@ -58,13 +123,13 @@ export class CirculationService {
});
const savedCirculation = await queryRunner.manager.save(circulation);
if (createDto.assigneeIds && createDto.assigneeIds.length > 0) {
const routings = createDto.assigneeIds.map((userId, index) =>
if (resolvedAssigneeIds.length > 0) {
const routings = resolvedAssigneeIds.map((assigneeId, index) =>
queryRunner.manager.create(CirculationRouting, {
circulationId: savedCirculation.id,
stepNumber: index + 1,
organizationId: user.primaryOrganizationId,
assignedTo: userId,
assignedTo: assigneeId,
status: 'PENDING',
})
);
@@ -1,29 +1,25 @@
import {
IsInt,
IsString,
IsNotEmpty,
IsArray,
IsOptional,
ArrayMinSize, // ✅ เพิ่ม
ArrayMinSize,
} from 'class-validator';
export class CreateCirculationDto {
@IsInt()
@IsNotEmpty()
correspondenceId!: number; // เอกสารต้นเรื่องที่จะเวียน
correspondenceId!: number | string; // เอกสารต้นเรื่องที่จะเวียน (INT or UUID)
@IsInt()
@IsOptional()
projectId?: number; // Project ID for Numbering
projectId?: number | string; // Project ID or UUID for Numbering
@IsString()
@IsNotEmpty()
subject!: string; // หัวข้อเรื่อง (Subject)
@IsArray()
@IsInt({ each: true })
@ArrayMinSize(1) // ✅ ต้องมีผู้รับอย่างน้อย 1 คน
assigneeIds!: number[]; // รายชื่อ User ID ที่ต้องการส่งให้ (ผู้รับผิดชอบ)
assigneeIds!: (number | string)[]; // รายชื่อ User ID or UUID ที่ต้องการส่งให้ (ADR-019)
@IsString()
@IsOptional()
@@ -35,6 +35,7 @@ import { WorkflowEngineService } from '../workflow-engine/workflow-engine.servic
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)
@@ -69,7 +70,52 @@ export class CorrespondenceService {
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 },
});
@@ -94,7 +140,7 @@ export class CorrespondenceService {
}
// Impersonation Logic
if (createDto.originatorId && createDto.originatorId !== userOrgId) {
if (resolvedOriginatorId && resolvedOriginatorId !== userOrgId) {
const permissions = await this.userService.getUserPermissions(
user.user_id
);
@@ -103,7 +149,7 @@ export class CorrespondenceService {
'You do not have permission to create documents on behalf of other organizations.'
);
}
userOrgId = createDto.originatorId;
userOrgId = resolvedOriginatorId;
}
if (!userOrgId) {
@@ -134,7 +180,7 @@ export class CorrespondenceService {
const orgCode = originatorOrg?.organizationCode ?? 'UNK';
// [v1.5.1] Extract recipient organization from recipients array (Primary TO)
const toRecipient = createDto.recipients?.find((r) => r.type === 'TO');
const toRecipient = resolvedRecipients?.find((r) => r.type === 'TO');
const recipientOrganizationId = toRecipient?.organizationId;
let recipientCode = '';
@@ -146,7 +192,7 @@ export class CorrespondenceService {
}
const docNumber = await this.numberingService.generateNextNumber({
projectId: createDto.projectId,
projectId: resolvedProjectId,
originatorOrganizationId: userOrgId,
typeId: createDto.typeId,
disciplineId: createDto.disciplineId,
@@ -165,7 +211,7 @@ export class CorrespondenceService {
correspondenceNumber: docNumber.number,
correspondenceTypeId: createDto.typeId,
disciplineId: createDto.disciplineId,
projectId: createDto.projectId,
projectId: resolvedProjectId,
originatorId: userOrgId,
isInternal: createDto.isInternal || false,
createdBy: user.user_id,
@@ -195,9 +241,9 @@ export class CorrespondenceService {
});
await queryRunner.manager.save(revision);
// Save Recipients
if (createDto.recipients && createDto.recipients.length > 0) {
const recipients = createDto.recipients.map((r) =>
// 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,
@@ -459,14 +505,30 @@ export class CorrespondenceService {
}
}
// 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 (updateDto.projectId)
correspondenceUpdate.projectId = updateDto.projectId;
if (updateDto.originatorId)
correspondenceUpdate.originatorId = updateDto.originatorId;
if (updResolvedProjectId)
correspondenceUpdate.projectId = updResolvedProjectId;
if (updResolvedOriginatorId)
correspondenceUpdate.originatorId = updResolvedOriginatorId;
if (Object.keys(correspondenceUpdate).length > 0) {
await this.correspondenceRepo.update(id, correspondenceUpdate);
@@ -506,13 +568,13 @@ export class CorrespondenceService {
}
// 5. Update Recipients if provided
if (updateDto.recipients) {
if (updResolvedRecipients) {
const recipientRepo = this.dataSource.getRepository(
CorrespondenceRecipient
);
await recipientRepo.delete({ correspondenceId: id });
const newRecipients = updateDto.recipients.map((r) =>
const newRecipients = updResolvedRecipients.map((r) =>
recipientRepo.create({
correspondenceId: id,
recipientOrganizationId: r.organizationId,
@@ -539,11 +601,11 @@ export class CorrespondenceService {
// Check for ACTUAL value changes
const isProjectChanged =
updateDto.projectId !== undefined &&
updateDto.projectId !== currentCorr.projectId;
updResolvedProjectId !== undefined &&
updResolvedProjectId !== currentCorr.projectId;
const isOriginatorChanged =
updateDto.originatorId !== undefined &&
updateDto.originatorId !== currentCorr.originatorId;
updResolvedOriginatorId !== undefined &&
updResolvedOriginatorId !== currentCorr.originatorId;
const isDisciplineChanged =
updateDto.disciplineId !== undefined &&
updateDto.disciplineId !== currentCorr.disciplineId;
@@ -554,15 +616,9 @@ export class CorrespondenceService {
let isRecipientChanged = false;
let newRecipientId: number | undefined;
if (updateDto.recipients) {
// Safe check for 'type' or 'recipientType' (mismatch safeguard)
interface RecipientInput {
type?: string;
recipientType?: string;
organizationId?: number;
}
const newToRecipient = updateDto.recipients.find(
(r: RecipientInput) => r.type === 'TO' || r.recipientType === 'TO'
if (updResolvedRecipients) {
const newToRecipient = updResolvedRecipients.find(
(r) => r.type === 'TO'
);
newRecipientId = newToRecipient?.organizationId;
@@ -594,7 +650,7 @@ export class CorrespondenceService {
// [Fix #6] Fetch real ORG Code from originator organization
const originatorOrgForUpdate = await this.orgRepo.findOne({
where: {
id: updateDto.originatorId ?? currentCorr.originatorId ?? 0,
id: updResolvedOriginatorId ?? currentCorr.originatorId ?? 0,
},
});
const orgCode = originatorOrgForUpdate?.organizationCode ?? 'UNK';
@@ -610,9 +666,9 @@ export class CorrespondenceService {
};
const newCtx = {
projectId: updateDto.projectId ?? currentCorr.projectId,
projectId: updResolvedProjectId ?? currentCorr.projectId,
originatorOrganizationId:
updateDto.originatorId ?? currentCorr.originatorId ?? 0,
updResolvedOriginatorId ?? currentCorr.originatorId ?? 0,
typeId: updateDto.typeId ?? currentCorr.correspondenceTypeId,
disciplineId: updateDto.disciplineId ?? currentCorr.disciplineId,
recipientOrganizationId: targetRecipientId,
@@ -650,6 +706,20 @@ export class CorrespondenceService {
}
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 },
});
@@ -661,13 +731,13 @@ export class CorrespondenceService {
if (fullUser) userOrgId = fullUser.primaryOrganizationId;
}
if (createDto.originatorId && createDto.originatorId !== userOrgId) {
if (previewOriginatorId && previewOriginatorId !== userOrgId) {
// Allow impersonation for preview
userOrgId = createDto.originatorId;
userOrgId = previewOriginatorId;
}
// Extract recipient from recipients array
const toRecipient = createDto.recipients?.find((r) => r.type === 'TO');
const toRecipient = previewRecipients?.find((r) => r.type === 'TO');
const recipientOrganizationId = toRecipient?.organizationId;
let recipientCode = '';
@@ -679,7 +749,7 @@ export class CorrespondenceService {
}
return this.numberingService.previewNumber({
projectId: createDto.projectId,
projectId: previewProjectId,
originatorOrganizationId: userOrgId!,
typeId: createDto.typeId,
disciplineId: createDto.disciplineId,
@@ -11,10 +11,9 @@ import {
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class CreateCorrespondenceDto {
@ApiProperty({ description: 'Project ID', example: 1 })
@IsInt()
@ApiProperty({ description: 'Project ID or UUID', example: 1 })
@IsNotEmpty()
projectId!: number;
projectId!: number | string;
@ApiProperty({ description: 'Document Type ID', example: 1 })
@IsInt()
@@ -110,12 +109,11 @@ export class CreateCorrespondenceDto {
// ✅ เพิ่ม Field สำหรับ Impersonation (เลือกองค์กรผู้ส่ง)
@ApiPropertyOptional({
description: 'Originator Organization ID (for impersonation)',
description: 'Originator Organization ID or UUID (for impersonation)',
example: 1,
})
@IsInt()
@IsOptional()
originatorId?: number;
originatorId?: number | string;
@ApiPropertyOptional({
description: 'Recipients',
@@ -123,5 +121,5 @@ export class CreateCorrespondenceDto {
})
@IsArray()
@IsOptional()
recipients?: { organizationId: number; type: 'TO' | 'CC' }[];
recipients?: { organizationId: number | string; type: 'TO' | 'CC' }[];
}
@@ -1,6 +1,11 @@
import { Injectable, Logger, BadRequestException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import {
Injectable,
Logger,
BadRequestException,
NotFoundException,
} from '@nestjs/common';
import { InjectRepository, InjectEntityManager } from '@nestjs/typeorm';
import { Repository, EntityManager } from 'typeorm';
import { ConfigService } from '@nestjs/config';
import { DocumentNumberFormat } from '../entities/document-number-format.entity';
@@ -20,6 +25,7 @@ import { CounterKeyDto } from '../dto/counter-key.dto';
import { GenerateNumberContext } from '../interfaces/document-numbering.interface';
import { ReserveNumberDto } from '../dto/reserve-number.dto';
import { ConfirmReservationDto } from '../dto/confirm-reservation.dto';
import { Project } from '../../project/entities/project.entity';
@Injectable()
export class DocumentNumberingService {
@@ -39,9 +45,27 @@ export class DocumentNumberingService {
private lockService: DocumentNumberingLockService,
private configService: ConfigService,
private manualOverrideService: ManualOverrideService,
private metricsService: MetricsService
private metricsService: MetricsService,
@InjectEntityManager()
private entityManager: EntityManager
) {}
/**
* 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.entityManager.findOne(Project, {
where: { uuid: projectId },
select: ['id'],
});
if (!project)
throw new NotFoundException(`Project with UUID ${projectId} not found`);
return project.id;
}
async generateNextNumber(
ctx: GenerateNumberContext
): Promise<{ number: string; auditId: number }> {
@@ -218,11 +242,15 @@ export class DocumentNumberingService {
return this.formatRepo.find();
}
async getTemplatesByProject(projectId: number) {
return this.formatRepo.find({ where: { projectId } });
async getTemplatesByProject(projectId: number | string) {
const internalId = await this.resolveProjectId(projectId);
return this.formatRepo.find({ where: { projectId: internalId } });
}
async saveTemplate(dto: any) {
if (dto.projectId) {
dto.projectId = await this.resolveProjectId(dto.projectId);
}
return this.formatRepo.save(dto);
}
@@ -6,7 +6,10 @@ import {
UpdateDateColumn,
DeleteDateColumn,
Unique,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { Project } from '../../project/entities/project.entity';
@Entity('tags')
@Unique('ux_tag_project', ['project_id', 'tag_name'])
@@ -26,6 +29,11 @@ export class Tag {
@Column({ type: 'text', nullable: true })
description!: string | null; // เพิ่ม !
// Relations
@ManyToOne(() => Project)
@JoinColumn({ name: 'project_id' })
project?: Project;
@CreateDateColumn()
created_at!: Date; // เพิ่ม !
+4 -1
View File
@@ -132,6 +132,7 @@ export class MasterService {
}
return this.rfaTypeRepo.find({
where,
relations: ['contract'],
order: { typeCode: 'ASC' },
});
}
@@ -296,7 +297,9 @@ export class MasterService {
}
async findAllTags(query?: SearchTagDto) {
const qb = this.tagRepo.createQueryBuilder('tag');
const qb = this.tagRepo
.createQueryBuilder('tag')
.leftJoinAndSelect('tag.project', 'project');
if (query?.project_id) {
// In Tags, we use project_id (INT) directly or resolve if UUID passed via query
@@ -1,4 +1,12 @@
import { Entity, PrimaryGeneratedColumn, Column, AfterLoad } from 'typeorm';
import {
Entity,
PrimaryGeneratedColumn,
Column,
AfterLoad,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { Contract } from '../../contract/entities/contract.entity';
@Entity('rfa_types')
export class RfaType {
@@ -23,6 +31,11 @@ export class RfaType {
@Column({ name: 'is_active', default: true })
isActive!: boolean;
// Relations
@ManyToOne(() => Contract)
@JoinColumn({ name: 'contract_id' })
contract?: Contract;
// Virtual property for backward compatibility
typeName!: string;