260318:1237 Fix UUID #4
Build and Deploy / deploy (push) Successful in 11m17s

This commit is contained in:
admin
2026-03-18 12:37:29 +07:00
parent 5d89079c2a
commit ba642e7e42
71 changed files with 533 additions and 319 deletions
@@ -88,14 +88,31 @@ export class DocumentNumberingController {
})
@RequirePermission('correspondence.read')
async previewNumber(@Body() dto: PreviewNumberDto) {
// ADR-019: Resolve UUID→INT for project and organization IDs
const resolvedProjectId = await this.numberingService.resolveIdForPreview(
'project',
dto.projectId
);
const resolvedOriginatorId =
await this.numberingService.resolveIdForPreview(
'organization',
dto.originatorOrganizationId
);
const resolvedRecipientId = dto.recipientOrganizationId
? await this.numberingService.resolveIdForPreview(
'organization',
dto.recipientOrganizationId
)
: undefined;
return this.numberingService.previewNumber({
projectId: dto.projectId,
originatorOrganizationId: dto.originatorOrganizationId,
projectId: resolvedProjectId,
originatorOrganizationId: resolvedOriginatorId,
typeId: dto.correspondenceTypeId,
subTypeId: dto.subTypeId,
rfaTypeId: dto.rfaTypeId,
disciplineId: dto.disciplineId,
recipientOrganizationId: dto.recipientOrganizationId,
recipientOrganizationId: resolvedRecipientId,
year: dto.year,
customTokens: dto.customTokens,
});
@@ -1,18 +1,16 @@
// File: src/modules/document-numbering/dto/preview-number.dto.ts
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsInt, IsOptional, IsObject } from 'class-validator';
import { IsInt, IsNotEmpty, IsOptional, IsObject } from 'class-validator';
import { Type } from 'class-transformer';
export class PreviewNumberDto {
@ApiProperty({ description: 'Project ID' })
@IsInt()
@Type(() => Number)
projectId!: number;
@ApiProperty({ description: 'Project ID or UUID' })
@IsNotEmpty()
projectId!: number | string;
@ApiProperty({ description: 'Originator organization ID' })
@IsInt()
@Type(() => Number)
originatorOrganizationId!: number;
@ApiProperty({ description: 'Originator organization ID or UUID' })
@IsNotEmpty()
originatorOrganizationId!: number | string;
@ApiProperty({ description: 'Correspondence type ID' })
@IsInt()
@@ -43,11 +41,9 @@ export class PreviewNumberDto {
@Type(() => Number)
year?: number;
@ApiPropertyOptional({ description: 'Recipient organization ID' })
@ApiPropertyOptional({ description: 'Recipient organization ID or UUID' })
@IsOptional()
@IsInt()
@Type(() => Number)
recipientOrganizationId?: number;
recipientOrganizationId?: number | string;
@ApiPropertyOptional({ description: 'Custom tokens' })
@IsOptional()
@@ -26,6 +26,7 @@ import { GenerateNumberContext } from '../interfaces/document-numbering.interfac
import { ReserveNumberDto } from '../dto/reserve-number.dto';
import { ConfirmReservationDto } from '../dto/confirm-reservation.dto';
import { Project } from '../../project/entities/project.entity';
import { Organization } from '../../organization/entities/organization.entity';
@Injectable()
export class DocumentNumberingService {
@@ -66,6 +67,33 @@ export class DocumentNumberingService {
return project.id;
}
/**
* ADR-019: Public facade for controllers to resolve project/organization IDs
*/
async resolveIdForPreview(
type: 'project' | 'organization',
id: number | string
): Promise<number> {
if (type === 'project') return this.resolveProjectId(id);
return this.resolveOrganizationId(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.entityManager.findOne(Organization, {
where: { uuid: orgId },
select: ['id'],
});
if (!org)
throw new NotFoundException(`Organization with UUID ${orgId} not found`);
return org.id;
}
async generateNextNumber(
ctx: GenerateNumberContext
): Promise<{ number: string; auditId: number }> {
@@ -13,6 +13,7 @@ import { AsBuiltDrawingRevision } from './entities/asbuilt-drawing-revision.enti
import { ShopDrawingRevision } from './entities/shop-drawing-revision.entity';
import { Attachment } from '../../common/file-storage/entities/attachment.entity';
import { User } from '../user/entities/user.entity';
import { Project } from '../project/entities/project.entity';
// DTOs
import { CreateAsBuiltDrawingDto } from './dto/create-asbuilt-drawing.dto';
@@ -39,6 +40,22 @@ export class AsBuiltDrawingService {
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;
}
/**
* สร้าง AS Built Drawing ใหม่ พร้อม Revision แรก (Rev 0)
*/
@@ -73,9 +90,14 @@ export class AsBuiltDrawingService {
});
}
// ADR-019: Resolve UUID→INT
const internalProjectId = await this.resolveProjectId(
createDto.projectId
);
// 3. Create Master AS Built Drawing
const asBuiltDrawing = queryRunner.manager.create(AsBuiltDrawing, {
projectId: createDto.projectId,
projectId: internalProjectId,
drawingNumber: createDto.drawingNumber,
mainCategoryId: createDto.mainCategoryId,
subCategoryId: createDto.subCategoryId,
@@ -12,6 +12,7 @@ import { ContractDrawing } from './entities/contract-drawing.entity';
import { Attachment } from '../../common/file-storage/entities/attachment.entity';
import { User } from '../user/entities/user.entity';
import { Contract } from '../contract/entities/contract.entity';
import { Project } from '../project/entities/project.entity';
// DTOs
import { CreateContractDrawingDto } from './dto/create-contract-drawing.dto';
@@ -36,6 +37,22 @@ export class ContractDrawingService {
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;
}
/**
* Resolve issueDate from contract.startDate for file storage path
* Fallback: contract.startDate → current date
@@ -54,10 +71,13 @@ export class ContractDrawingService {
* - ผูกไฟล์แนบและ Commit ไฟล์จาก Temp -> Permanent
*/
async create(createDto: CreateContractDrawingDto, user: User) {
// ADR-019: Resolve UUID→INT for projectId
const internalProjectId = await this.resolveProjectId(createDto.projectId);
// 1. ตรวจสอบเลขที่แบบซ้ำ (Unique per Project)
const exists = await this.drawingRepo.findOne({
where: {
projectId: createDto.projectId,
projectId: internalProjectId,
contractDrawingNo: createDto.contractDrawingNo,
},
});
@@ -83,7 +103,7 @@ export class ContractDrawingService {
// 3. สร้าง Entity
const drawing = queryRunner.manager.create(ContractDrawing, {
projectId: createDto.projectId,
projectId: internalProjectId,
contractDrawingNo: createDto.contractDrawingNo,
title: createDto.title,
mapCatId: createDto.mapCatId, // Updated
@@ -98,9 +118,8 @@ export class ContractDrawingService {
// 4. Commit Files (ย้ายไฟล์จริง)
if (createDto.attachmentIds?.length) {
// ✅ FIX TS2345: แปลง number[] เป็น string[] ก่อนส่ง
const issueDate = await this.resolveIssueDateByProject(
createDto.projectId
);
const issueDate =
await this.resolveIssueDateByProject(internalProjectId);
await this.fileStorageService.commit(
createDto.attachmentIds.map(String),
{ issueDate, documentType: 'ContractDrawing' }
@@ -13,6 +13,7 @@ import { ShopDrawingRevision } from './entities/shop-drawing-revision.entity';
import { ContractDrawing } from './entities/contract-drawing.entity';
import { Attachment } from '../../common/file-storage/entities/attachment.entity';
import { User } from '../user/entities/user.entity';
import { Project } from '../project/entities/project.entity';
// DTOs
import { CreateShopDrawingDto } from './dto/create-shop-drawing.dto';
@@ -39,6 +40,22 @@ export class ShopDrawingService {
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;
}
/**
* สร้าง Shop Drawing ใหม่ พร้อม Revision แรก (Rev 0)
*/
@@ -73,9 +90,14 @@ export class ShopDrawingService {
});
}
// ADR-019: Resolve UUID→INT
const internalProjectId = await this.resolveProjectId(
createDto.projectId
);
// 3. Create Master Shop Drawing
const shopDrawing = queryRunner.manager.create(ShopDrawing, {
projectId: createDto.projectId,
projectId: internalProjectId,
drawingNumber: createDto.drawingNumber,
mainCategoryId: createDto.mainCategoryId,
subCategoryId: createDto.subCategoryId,
+11 -3
View File
@@ -11,10 +11,18 @@ import {
} from 'class-validator';
export class CreateRfaDto {
@ApiProperty({ description: 'ID ของโครงการ', example: 1 })
@IsInt()
@ApiProperty({ description: 'ID or UUID ของโครงการ', example: 1 })
@IsNotEmpty()
projectId!: number;
projectId!: number | string;
@ApiProperty({ description: 'Contract ID or UUID', required: false })
@IsString()
@IsOptional()
contractId?: string;
@ApiProperty({ description: 'To Organization ID or UUID', required: false })
@IsOptional()
toOrganizationId?: number | string;
@ApiProperty({ description: 'ID ของประเภท RFA', example: 1 })
@IsInt()
+41 -4
View File
@@ -12,6 +12,8 @@ import { InjectRepository } from '@nestjs/typeorm';
import { DataSource, In, Repository } from 'typeorm';
// Entities
import { Project } from '../project/entities/project.entity';
import { Organization } from '../organization/entities/organization.entity';
import { CorrespondenceRouting } from '../correspondence/entities/correspondence-routing.entity';
import { Correspondence } from '../correspondence/entities/correspondence.entity';
import { CorrespondenceRevision } from '../correspondence/entities/correspondence-revision.entity';
@@ -81,7 +83,42 @@ export class RfaService {
private searchService: SearchService
) {}
/**
* 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.dataSource.manager.findOne(Organization, {
where: { uuid: orgId },
select: ['id'],
});
if (!org)
throw new NotFoundException(`Organization with UUID ${orgId} not found`);
return org.id;
}
async create(createDto: CreateRfaDto, user: User) {
// ADR-019: Resolve UUID→INT for projectId
const internalProjectId = await this.resolveProjectId(createDto.projectId);
const rfaType = await this.rfaTypeRepo.findOne({
where: { id: createDto.rfaTypeId },
});
@@ -115,7 +152,7 @@ export class RfaService {
// [UPDATED] Generate Document Number with Discipline
const docNumber = await this.numberingService.generateNextNumber({
projectId: createDto.projectId,
projectId: internalProjectId,
originatorOrganizationId: userOrgId,
typeId: createDto.rfaTypeId,
disciplineId: createDto.disciplineId ?? 0, // ✅ ส่ง disciplineId ไปด้วย (0 ถ้าไม่มี)
@@ -142,7 +179,7 @@ export class RfaService {
const correspondence = queryRunner.manager.create(Correspondence, {
correspondenceNumber: docNumber.number,
correspondenceTypeId: createDto.rfaTypeId,
projectId: createDto.projectId,
projectId: internalProjectId,
originatorId: userOrgId,
isInternal: false,
createdBy: user.user_id,
@@ -219,7 +256,7 @@ export class RfaService {
'rfa',
savedRfa.id.toString(),
{
projectId: createDto.projectId,
projectId: internalProjectId,
originatorId: userOrgId,
disciplineId: createDto.disciplineId,
initiatorId: user.user_id,
@@ -240,7 +277,7 @@ export class RfaService {
title: createDto.subject,
description: createDto.description,
status: 'DRAFT',
projectId: createDto.projectId,
projectId: internalProjectId,
createdAt: new Date(),
})
.catch((err) => this.logger.error(`Indexing failed: ${err}`));
@@ -16,6 +16,7 @@ import { Correspondence } from '../correspondence/entities/correspondence.entity
import { CorrespondenceRevision } from '../correspondence/entities/correspondence-revision.entity';
import { CorrespondenceType } from '../correspondence/entities/correspondence-type.entity';
import { CorrespondenceStatus } from '../correspondence/entities/correspondence-status.entity';
import { Project } from '../project/entities/project.entity';
@Injectable()
export class TransmittalService {
@@ -34,6 +35,22 @@ export class TransmittalService {
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;
}
async create(createDto: CreateTransmittalDto, user: User) {
// 1. Get Transmittal Type (Assuming Code '901' or 'TRN')
const type = await this.typeRepo.findOne({
@@ -58,9 +75,14 @@ export class TransmittalService {
}
try {
// ADR-019: Resolve UUID→INT for projectId
const internalProjectId = await this.resolveProjectId(
createDto.projectId
);
// 2. Generate Number
const docNumber = await this.numberingService.generateNextNumber({
projectId: createDto.projectId,
projectId: internalProjectId,
originatorOrganizationId: user.primaryOrganizationId,
typeId: type.id,
year: new Date().getFullYear(),
@@ -74,7 +96,7 @@ export class TransmittalService {
const correspondence = queryRunner.manager.create(Correspondence, {
correspondenceNumber: docNumber.number,
correspondenceTypeId: type.id,
projectId: createDto.projectId,
projectId: internalProjectId,
originatorId: user.primaryOrganizationId,
isInternal: false,
createdBy: user.user_id,
@@ -5,7 +5,6 @@ import {
MinLength,
IsOptional,
IsBoolean,
IsInt,
} from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
@@ -44,10 +43,12 @@ export class CreateUserDto {
@IsOptional()
lineId?: string;
@ApiPropertyOptional({ description: 'Primary Organization ID', example: 1 })
@IsInt()
@ApiPropertyOptional({
description: 'Primary Organization ID or UUID',
example: 1,
})
@IsOptional()
primaryOrganizationId?: number; // รับเป็น ID ของ Organization
primaryOrganizationId?: number | string; // ADR-019: Accept INT or UUID
@ApiPropertyOptional({ description: 'Is user active?', default: true })
@IsBoolean()
@@ -16,11 +16,9 @@ export class SearchUserDto {
@Type(() => Number)
roleId?: number;
@ApiPropertyOptional({ description: 'Filter by Organization ID' })
@ApiPropertyOptional({ description: 'Filter by Organization ID or UUID' })
@IsOptional()
@IsInt()
@Type(() => Number)
primaryOrganizationId?: number;
primaryOrganizationId?: number | string; // ADR-019: Accept INT or UUID
@ApiPropertyOptional({ description: 'Page number', default: 1 })
@IsOptional()
+40 -2
View File
@@ -17,6 +17,7 @@ import { Role } from './entities/role.entity';
import { Permission } from './entities/permission.entity';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { Organization } from '../organization/entities/organization.entity';
@Injectable()
export class UserService {
@@ -30,13 +31,35 @@ export class UserService {
@Inject(CACHE_MANAGER) private cacheManager: Cache
) {}
/**
* 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.usersRepository.manager.findOne(Organization, {
where: { uuid: orgId },
select: ['id'],
});
if (!org)
throw new NotFoundException(`Organization with UUID ${orgId} not found`);
return org.id;
}
// 1. สร้างผู้ใช้ (Hash Password ก่อนบันทึก)
async create(createUserDto: CreateUserDto): Promise<User> {
const salt = await bcrypt.genSalt();
const hashedPassword = await bcrypt.hash(createUserDto.password, salt);
// ADR-019: Resolve UUID→INT for primaryOrganizationId
const resolvedOrgId = createUserDto.primaryOrganizationId
? await this.resolveOrganizationId(createUserDto.primaryOrganizationId)
: undefined;
const newUser = this.usersRepository.create({
...createUserDto,
primaryOrganizationId: resolvedOrgId,
password: hashedPassword,
});
@@ -91,8 +114,12 @@ export class UserService {
}
if (primaryOrganizationId) {
// ADR-019: Resolve UUID→INT for filtering
const resolvedOrgId = await this.resolveOrganizationId(
primaryOrganizationId
);
query.andWhere('user.primaryOrganizationId = :orgId', {
orgId: primaryOrganizationId,
orgId: resolvedOrgId,
});
}
@@ -164,7 +191,18 @@ export class UserService {
updateUserDto.password = await bcrypt.hash(updateUserDto.password, salt);
}
const updatedUser = this.usersRepository.merge(user, updateUserDto);
// ADR-019: Resolve UUID→INT for primaryOrganizationId before merge
const resolvedDto: Record<string, unknown> = { ...updateUserDto };
if (updateUserDto.primaryOrganizationId !== undefined) {
resolvedDto.primaryOrganizationId = await this.resolveOrganizationId(
updateUserDto.primaryOrganizationId
);
}
const updatedUser = this.usersRepository.merge(
user,
resolvedDto as Partial<User>
);
const savedUser = await this.usersRepository.save(updatedUser);
// ⚠️ สำคัญ: เมื่อมีการแก้ไขข้อมูล User ต้องเคลียร์ Cache สิทธิ์เสมอ