From 1aff83214f9905418f1aecad9d0bb1fb4e1296a6 Mon Sep 17 00:00:00 2001 From: admin Date: Thu, 26 Mar 2026 13:47:07 +0700 Subject: [PATCH] 260326:1347 Fixing Refactor ADR-019 Naming convention uuid #01 --- .../common/entities/uuid-base.entity.spec.ts | 42 +++++++++--------- .../src/common/entities/uuid-base.entity.ts | 20 +++++---- .../circulation/circulation.service.ts | 14 +++--- .../circulation/dto/search-circulation.dto.ts | 2 +- .../src/modules/contract/contract.service.ts | 16 ++++--- .../contract/entities/contract.entity.ts | 24 ++-------- .../correspondence-workflow.service.ts | 2 +- .../correspondence/correspondence.service.ts | 34 +++++++------- .../due-date-reminder.service.ts | 2 +- .../organization/organization.service.ts | 8 ++-- .../project/entities/project.entity.ts | 31 +++---------- .../src/modules/project/project.service.ts | 20 +++++---- backend/src/modules/rfa/rfa.controller.ts | 10 ++--- backend/src/modules/rfa/rfa.service.ts | 34 +++++++------- backend/src/modules/search/search.service.ts | 10 ++++- .../transmittal/transmittal.controller.ts | 4 +- .../transmittal/transmittal.service.ts | 16 ++++--- backend/src/modules/user/user.service.ts | 8 ++-- .../admin/doc-control/reference/tags/page.tsx | 8 ++-- .../doc-control/workflows/[id]/edit/page.tsx | 4 +- .../(dashboard)/circulation/[uuid]/page.tsx | 10 ++--- .../circulation/circulation-list.tsx | 2 +- .../circulation-status-card.tsx | 2 +- .../correspondences/tag-manager.tsx | 14 +++--- frontend/types/circulation.ts | 8 ++-- frontend/types/correspondence.ts | 14 +++--- frontend/types/drawing.ts | 10 ++--- frontend/types/notification.ts | 2 +- frontend/types/organization.ts | 2 +- frontend/types/rfa.ts | 14 +++--- frontend/types/search.ts | 2 +- frontend/types/transmittal.ts | 4 +- frontend/types/user.ts | 2 +- .../ADR-019-hybrid-identifier-strategy.md | 44 ++++++++++++------- 34 files changed, 217 insertions(+), 222 deletions(-) diff --git a/backend/src/common/entities/uuid-base.entity.spec.ts b/backend/src/common/entities/uuid-base.entity.spec.ts index 51356db..3be2715 100644 --- a/backend/src/common/entities/uuid-base.entity.spec.ts +++ b/backend/src/common/entities/uuid-base.entity.spec.ts @@ -14,45 +14,45 @@ class TestEntity extends UuidBaseEntity { describe('UuidBaseEntity', () => { // ========================================================== - // generateUuid() — @BeforeInsert hook + // generatePublicId() — @BeforeInsert hook // ========================================================== - describe('generateUuid()', () => { - it('should generate a UUIDv7 when uuid is not set', () => { + describe('generatePublicId()', () => { + it('should generate a UUIDv7 when publicId is not set', () => { const entity = new TestEntity(); - expect(entity.uuid).toBeUndefined(); + expect(entity.publicId).toBeUndefined(); - entity.generateUuid(); + entity.generatePublicId(); - expect(entity.uuid).toBe('01912345-6789-7abc-8def-0123456789ab'); + expect(entity.publicId).toBe('01912345-6789-7abc-8def-0123456789ab'); }); - it('should not overwrite an existing uuid', () => { + it('should not overwrite an existing publicId', () => { const entity = new TestEntity(); - entity.uuid = 'existing-uuid-value-should-be-kept'; + entity.publicId = 'existing-publicId-value-should-be-kept'; - entity.generateUuid(); + entity.generatePublicId(); - expect(entity.uuid).toBe('existing-uuid-value-should-be-kept'); + expect(entity.publicId).toBe('existing-publicId-value-should-be-kept'); }); it('should not overwrite a pre-set UUIDv1 from DB default', () => { const entity = new TestEntity(); - entity.uuid = '550e8400-e29b-11d4-a716-446655440000'; + entity.publicId = '550e8400-e29b-11d4-a716-446655440000'; - entity.generateUuid(); + entity.generatePublicId(); - expect(entity.uuid).toBe('550e8400-e29b-11d4-a716-446655440000'); + expect(entity.publicId).toBe('550e8400-e29b-11d4-a716-446655440000'); }); - it('should generate uuid when uuid is empty string', () => { + it('should generate publicId when publicId is empty string', () => { const entity = new TestEntity(); - entity.uuid = ''; + entity.publicId = ''; - entity.generateUuid(); + entity.generatePublicId(); - // Empty string is falsy, so generateUuid should assign a new value - expect(entity.uuid).toBe('01912345-6789-7abc-8def-0123456789ab'); + // Empty string is falsy, so generatePublicId should assign a new value + expect(entity.publicId).toBe('01912345-6789-7abc-8def-0123456789ab'); }); }); @@ -66,12 +66,12 @@ describe('UuidBaseEntity', () => { expect(entity).toBeInstanceOf(UuidBaseEntity); }); - it('should have uuid property accessible from subclass', () => { + it('should have publicId property accessible from subclass', () => { const entity = new TestEntity(); - entity.uuid = 'test-uuid'; + entity.publicId = 'test-publicId'; entity.id = 42; - expect(entity.uuid).toBe('test-uuid'); + expect(entity.publicId).toBe('test-publicId'); expect(entity.id).toBe(42); }); }); diff --git a/backend/src/common/entities/uuid-base.entity.ts b/backend/src/common/entities/uuid-base.entity.ts index c629b2b..d45b45b 100644 --- a/backend/src/common/entities/uuid-base.entity.ts +++ b/backend/src/common/entities/uuid-base.entity.ts @@ -2,27 +2,29 @@ import { Column, BeforeInsert } from 'typeorm'; import { v7 as uuidv7 } from 'uuid'; /** - * Abstract base entity providing a UUID public identifier column. - * Uses MariaDB native UUID type (stored as BINARY(16) internally, - * auto-converts to string format — no transformer needed). + * Abstract base entity providing a UUID public identifier. * - * App generates UUIDv7 via @BeforeInsert(); DB DEFAULT UUID() is fallback. + * Naming Convention (ADR-019 v1.8.1): + * - TypeScript Property: `publicId` — semantic name indicating this is the public-facing identifier + * - Database Column: `uuid` — MariaDB native UUID type (stored as BINARY(16)) * - * @see ADR-019 Hybrid Identifier Strategy + * This avoids confusion between the property name and the DB data type, + * while clearly indicating this is the ID exposed via API (not internal INT PK). */ export abstract class UuidBaseEntity { @Column({ type: 'uuid', + name: 'uuid', // DB column name (MariaDB native UUID type) unique: true, nullable: false, comment: 'UUID Public Identifier (ADR-019)', }) - uuid!: string; + publicId!: string; // TypeScript property name — semantic, avoids type confusion @BeforeInsert() - generateUuid(): void { - if (!this.uuid) { - this.uuid = uuidv7(); + generatePublicId(): void { + if (!this.publicId) { + this.publicId = uuidv7(); } } } diff --git a/backend/src/modules/circulation/circulation.service.ts b/backend/src/modules/circulation/circulation.service.ts index 061b15b..a39d72a 100644 --- a/backend/src/modules/circulation/circulation.service.ts +++ b/backend/src/modules/circulation/circulation.service.ts @@ -95,7 +95,7 @@ export class CirculationService { } async findAll(searchDto: SearchCirculationDto, user: User) { - const { status, correspondenceUuid, page = 1, limit = 20 } = searchDto; + const { status, correspondencePublicId, page = 1, limit = 20 } = searchDto; const query = this.circulationRepo .createQueryBuilder('c') .leftJoinAndSelect('c.creator', 'creator') @@ -103,9 +103,9 @@ export class CirculationService { .leftJoinAndSelect('routings.assignee', 'assignee') .leftJoinAndSelect('c.correspondence', 'correspondence'); - if (correspondenceUuid) { - query.where('correspondence.uuid = :corrUuid', { - corrUuid: correspondenceUuid, + if (correspondencePublicId) { + query.where('correspondence.publicId = :corrPublicId', { + corrPublicId: correspondencePublicId, }); } else { query.where('c.organizationId = :orgId', { @@ -136,14 +136,14 @@ export class CirculationService { return circulation; } - async findOneByUuid(uuid: string) { + async findOneByUuid(publicId: string) { const circulation = await this.circulationRepo.findOne({ - where: { uuid }, + where: { publicId }, relations: ['routings', 'routings.assignee', 'correspondence', 'creator'], order: { routings: { stepNumber: 'ASC' } }, }); if (!circulation) - throw new NotFoundException(`Circulation UUID ${uuid} not found`); + throw new NotFoundException(`Circulation publicId ${publicId} not found`); return circulation; } diff --git a/backend/src/modules/circulation/dto/search-circulation.dto.ts b/backend/src/modules/circulation/dto/search-circulation.dto.ts index 088c06e..22d0e4a 100644 --- a/backend/src/modules/circulation/dto/search-circulation.dto.ts +++ b/backend/src/modules/circulation/dto/search-circulation.dto.ts @@ -8,7 +8,7 @@ export class SearchCirculationDto { @IsOptional() @IsUUID('all') - correspondenceUuid?: string; // กรองตาม correspondence UUID (ADR-019) + correspondencePublicId?: string; // กรองตาม correspondence publicId (ADR-019) @IsOptional() @IsString() diff --git a/backend/src/modules/contract/contract.service.ts b/backend/src/modules/contract/contract.service.ts index b35117a..97ead7b 100644 --- a/backend/src/modules/contract/contract.service.ts +++ b/backend/src/modules/contract/contract.service.ts @@ -100,18 +100,20 @@ export class ContractService { return contract; } - async findOneByUuid(uuid: string) { + async findOneByPublicId(publicId: string) { const contract = await this.contractRepo.findOne({ - where: { uuid }, + where: { publicId }, relations: ['project'], }); if (!contract) - throw new NotFoundException(`Contract UUID ${uuid} not found`); + throw new NotFoundException( + `Contract with publicId ${publicId} not found` + ); return contract; } - async update(uuid: string, dto: UpdateContractDto) { - const contract = await this.findOneByUuid(uuid); + async update(publicId: string, dto: UpdateContractDto) { + const contract = await this.findOneByPublicId(publicId); if (dto.projectId) { dto.projectId = await this.uuidResolver.resolveProjectId(dto.projectId); } @@ -119,8 +121,8 @@ export class ContractService { return this.contractRepo.save(contract); } - async remove(uuid: string) { - const contract = await this.findOneByUuid(uuid); + async remove(publicId: string) { + const contract = await this.findOneByPublicId(publicId); return this.contractRepo.remove(contract); } } diff --git a/backend/src/modules/contract/entities/contract.entity.ts b/backend/src/modules/contract/entities/contract.entity.ts index dcc9d9d..1aac551 100644 --- a/backend/src/modules/contract/entities/contract.entity.ts +++ b/backend/src/modules/contract/entities/contract.entity.ts @@ -4,34 +4,18 @@ import { PrimaryGeneratedColumn, ManyToOne, JoinColumn, - BeforeInsert, } from 'typeorm'; -import { v7 as uuidv7 } from 'uuid'; -import { Exclude, Expose } from 'class-transformer'; -import { BaseEntity } from '../../../common/entities/base.entity'; +import { Exclude } from 'class-transformer'; +import { UuidBaseEntity } from '../../../common/entities/uuid-base.entity'; import { Project } from '../../project/entities/project.entity'; @Entity('contracts') -export class Contract extends BaseEntity { +export class Contract extends UuidBaseEntity { @PrimaryGeneratedColumn() @Exclude() id!: number; - @Expose({ name: 'id' }) - @Column({ - type: 'uuid', - unique: true, - nullable: false, - comment: 'UUID Public Identifier (ADR-019)', - }) - uuid!: string; - - @BeforeInsert() - generateUuid(): void { - if (!this.uuid) { - this.uuid = uuidv7(); - } - } + // publicId inherited from UuidBaseEntity (DB column: uuid) @Column({ name: 'project_id' }) projectId!: number; diff --git a/backend/src/modules/correspondence/correspondence-workflow.service.ts b/backend/src/modules/correspondence/correspondence-workflow.service.ts index 4e86261..384e54e 100644 --- a/backend/src/modules/correspondence/correspondence-workflow.service.ts +++ b/backend/src/modules/correspondence/correspondence-workflow.service.ts @@ -112,7 +112,7 @@ export class CorrespondenceWorkflowService { type: 'EMAIL', entityType: 'correspondence', entityId: revision.correspondenceId, - link: `/correspondences/${corrForNotify.uuid}`, + link: `/correspondences/${corrForNotify.publicId}`, }); } } diff --git a/backend/src/modules/correspondence/correspondence.service.ts b/backend/src/modules/correspondence/correspondence.service.ts index 0577f82..56f2bdd 100644 --- a/backend/src/modules/correspondence/correspondence.service.ts +++ b/backend/src/modules/correspondence/correspondence.service.ts @@ -344,7 +344,7 @@ export class CorrespondenceService { // Fire-and-forget search indexing (non-blocking, void intentional) void this.searchService.indexDocument({ id: savedCorr.id, - uuid: savedCorr.uuid, + publicId: savedCorr.publicId, type: 'correspondence', docNumber: docNumber.number, title: createDto.subject, @@ -459,9 +459,9 @@ export class CorrespondenceService { return correspondence; } - async findOneByUuid(uuid: string) { + async findOneByUuid(publicId: string) { const correspondence = await this.correspondenceRepo.findOne({ - where: { uuid }, + where: { publicId }, relations: [ 'revisions', 'revisions.status', @@ -474,16 +474,18 @@ export class CorrespondenceService { }); if (!correspondence) { - throw new NotFoundException(`Correspondence with UUID ${uuid} not found`); + throw new NotFoundException( + `Correspondence with UUID ${publicId} 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 + // ADR-019: Resolve target publicId → internal INT id const target = await this.correspondenceRepo.findOne({ - where: { uuid: dto.targetUuid }, + where: { publicId: dto.targetUuid }, }); if (!source || !target) { @@ -814,7 +816,7 @@ export class CorrespondenceService { // Re-index updated document in Elasticsearch (fire-and-forget) void this.searchService.indexDocument({ id: updated.id, - uuid: updated.uuid, + publicId: updated.publicId, type: 'correspondence', docNumber: updated.correspondenceNumber, title: updateDto.subject ?? updated.revisions?.[0]?.subject, @@ -896,8 +898,8 @@ export class CorrespondenceService { * Business Rule Implementation: EC-CORR-001 - Cancel Correspondence with Downstream Circulation * Cancel correspondence and handle related circulations */ - async cancel(uuid: string, reason: string, user: User) { - const correspondence = await this.findOneByUuid(uuid); + async cancel(publicId: string, reason: string, user: User) { + const correspondence = await this.findOneByUuid(publicId); // Check if user has permission to cancel (Org Admin or Superadmin only) const permissions = await this.userService.getUserPermissions(user.user_id); @@ -983,7 +985,7 @@ export class CorrespondenceService { // Re-index cancelled status in Elasticsearch (fire-and-forget) void this.searchService.indexDocument({ id: correspondence.id, - uuid: correspondence.uuid, + publicId: correspondence.publicId, type: 'correspondence', docNumber: correspondence.correspondenceNumber, title: currentRevision.subject, @@ -1005,7 +1007,7 @@ export class CorrespondenceService { type: 'EMAIL', entityType: 'correspondence', entityId: correspondence.id, - link: `/correspondences/${correspondence.uuid}`, + link: `/correspondences/${correspondence.publicId}`, }); } }) @@ -1031,19 +1033,19 @@ export class CorrespondenceService { } async bulkCancel( - uuids: string[], + publicIds: string[], reason: string, user: User ): Promise<{ succeeded: string[]; failed: string[] }> { const succeeded: string[] = []; const failed: string[] = []; - for (const uuid of uuids) { + for (const publicId of publicIds) { try { - await this.cancel(uuid, reason, user); - succeeded.push(uuid); + await this.cancel(publicId, reason, user); + succeeded.push(publicId); } catch { - failed.push(uuid); + failed.push(publicId); } } diff --git a/backend/src/modules/correspondence/due-date-reminder.service.ts b/backend/src/modules/correspondence/due-date-reminder.service.ts index 0f4deea..861de2e 100644 --- a/backend/src/modules/correspondence/due-date-reminder.service.ts +++ b/backend/src/modules/correspondence/due-date-reminder.service.ts @@ -63,7 +63,7 @@ export class DueDateReminderService { type: 'EMAIL', entityType: 'correspondence', entityId: corr.id, - link: `/correspondences/${corr.uuid}`, + link: `/correspondences/${corr.publicId}`, }); } catch (err) { this.logger.warn( diff --git a/backend/src/modules/organization/organization.service.ts b/backend/src/modules/organization/organization.service.ts index 547eacc..a916bc0 100644 --- a/backend/src/modules/organization/organization.service.ts +++ b/backend/src/modules/organization/organization.service.ts @@ -89,10 +89,12 @@ export class OrganizationService { return org; } - async findOneByUuid(uuid: string) { - const org = await this.orgRepo.findOne({ where: { uuid } }); + async findOneByUuid(publicId: string) { + const org = await this.orgRepo.findOne({ where: { publicId } }); if (!org) - throw new NotFoundException(`Organization UUID ${uuid} not found`); + throw new NotFoundException( + `Organization publicId ${publicId} not found` + ); return org; } diff --git a/backend/src/modules/project/entities/project.entity.ts b/backend/src/modules/project/entities/project.entity.ts index 216cde4..02da77c 100644 --- a/backend/src/modules/project/entities/project.entity.ts +++ b/backend/src/modules/project/entities/project.entity.ts @@ -1,36 +1,15 @@ -import { - Entity, - Column, - PrimaryGeneratedColumn, - OneToMany, - BeforeInsert, -} from 'typeorm'; -import { v7 as uuidv7 } from 'uuid'; -import { Exclude, Expose } from 'class-transformer'; -import { BaseEntity } from '../../../common/entities/base.entity'; +import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm'; +import { Exclude } from 'class-transformer'; +import { UuidBaseEntity } from '../../../common/entities/uuid-base.entity'; import { Contract } from '../../contract/entities/contract.entity'; @Entity('projects') -export class Project extends BaseEntity { +export class Project extends UuidBaseEntity { @PrimaryGeneratedColumn() @Exclude() id!: number; - @Expose({ name: 'id' }) - @Column({ - type: 'uuid', - unique: true, - nullable: false, - comment: 'UUID Public Identifier (ADR-019)', - }) - uuid!: string; - - @BeforeInsert() - generateUuid(): void { - if (!this.uuid) { - this.uuid = uuidv7(); - } - } + // publicId inherited from UuidBaseEntity (DB column: uuid) @Column({ name: 'project_code', unique: true, length: 50 }) projectCode!: string; diff --git a/backend/src/modules/project/project.service.ts b/backend/src/modules/project/project.service.ts index 1b4368f..4c8c286 100644 --- a/backend/src/modules/project/project.service.ts +++ b/backend/src/modules/project/project.service.ts @@ -91,21 +91,23 @@ export class ProjectService { return project; } - async findOneByUuid(uuid: string) { + async findOneByUuid(publicId: string) { const project = await this.projectRepository.findOne({ - where: { uuid }, + where: { publicId }, relations: ['contracts'], }); if (!project) { - throw new NotFoundException(`Project UUID ${uuid} not found`); + throw new NotFoundException( + `Project with publicId ${publicId} not found` + ); } return project; } - async update(uuid: string, updateDto: UpdateProjectDto) { - const project = await this.findOneByUuid(uuid); + async update(publicId: string, updateDto: UpdateProjectDto) { + const project = await this.findOneByUuid(publicId); // Merge ข้อมูลใหม่ใส่ข้อมูลเดิม this.projectRepository.merge(project, updateDto); @@ -113,14 +115,14 @@ export class ProjectService { return this.projectRepository.save(project); } - async remove(uuid: string) { - const project = await this.findOneByUuid(uuid); + async remove(publicId: string) { + const project = await this.findOneByUuid(publicId); // ใช้ Soft Delete return this.projectRepository.softRemove(project); } - async findContracts(uuid: string) { - const project = await this.findOneByUuid(uuid); + async findContracts(publicId: string) { + const project = await this.findOneByUuid(publicId); return project.contracts; } diff --git a/backend/src/modules/rfa/rfa.controller.ts b/backend/src/modules/rfa/rfa.controller.ts index d9da71e..05d8c4e 100644 --- a/backend/src/modules/rfa/rfa.controller.ts +++ b/backend/src/modules/rfa/rfa.controller.ts @@ -63,7 +63,7 @@ export class RfaController { @ApiOperation({ summary: 'Submit RFA to Workflow' }) @ApiParam({ name: 'uuid', - description: 'RFA UUID (from correspondences.uuid)', + description: 'RFA publicId (from correspondences.publicId)', }) @ApiBody({ type: SubmitRfaDto }) @ApiResponse({ status: 200, description: 'RFA submitted successfully' }) @@ -83,7 +83,7 @@ export class RfaController { @ApiOperation({ summary: 'Process Workflow Action (Approve/Reject)' }) @ApiParam({ name: 'uuid', - description: 'RFA UUID (from correspondences.uuid)', + description: 'RFA publicId (from correspondences.publicId)', }) @ApiBody({ type: WorkflowActionDto }) @ApiResponse({ @@ -120,7 +120,7 @@ export class RfaController { @ApiOperation({ summary: 'Get RFA details with revisions and items' }) @ApiParam({ name: 'uuid', - description: 'RFA UUID (from correspondences.uuid)', + description: 'RFA publicId (from correspondences.publicId)', }) @ApiResponse({ status: 200, description: 'RFA details' }) @RequirePermission('document.view') @@ -130,7 +130,7 @@ export class RfaController { @Put(':uuid') @ApiOperation({ summary: 'Update Draft RFA fields (EC-RFA-002: DFT only)' }) - @ApiParam({ name: 'uuid', description: 'RFA UUID' }) + @ApiParam({ name: 'uuid', description: 'RFA publicId' }) @ApiBody({ type: UpdateRfaDto }) @ApiResponse({ status: 200, description: 'RFA updated successfully' }) @RequirePermission('rfa.create') @@ -146,7 +146,7 @@ export class RfaController { @Delete(':uuid') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Cancel Draft RFA (sets status to CC)' }) - @ApiParam({ name: 'uuid', description: 'RFA UUID' }) + @ApiParam({ name: 'uuid', description: 'RFA publicId' }) @ApiResponse({ status: 200, description: 'RFA cancelled successfully' }) @RequirePermission('rfa.create') @Audit('rfa.cancel', 'rfa') diff --git a/backend/src/modules/rfa/rfa.service.ts b/backend/src/modules/rfa/rfa.service.ts index f9b935a..329a4b6 100644 --- a/backend/src/modules/rfa/rfa.service.ts +++ b/backend/src/modules/rfa/rfa.service.ts @@ -44,7 +44,7 @@ type CorrRevWithRfa = CorrespondenceRevision & { rfaRevision?: RfaRevision }; /** RFA entity + a flat `revisions` convenience array for the frontend */ export interface RfaMapped extends Rfa { - uuid?: string; // ADR-019: top-level UUID from correspondence + publicId?: string; // ADR-019: top-level publicId from correspondence revisions: CorrRevWithRfa[]; } @@ -429,7 +429,7 @@ export class RfaService { this.searchService .indexDocument({ id: savedCorr.id, - uuid: savedCorr.uuid, // ADR-019: index UUID for search + publicId: savedCorr.publicId, // ADR-019: index publicId for search type: 'rfa', docNumber: docNumber.number, title: createDto.subject, @@ -536,7 +536,7 @@ export class RfaService { (rfa.correspondence?.revisions as CorrRevWithRfa[] | undefined) ?? []; return { ...rfa, - uuid: rfa.correspondence?.uuid, // ADR-019: expose UUID at top level + publicId: rfa.correspondence?.publicId, // ADR-019: expose publicId at top level revisions: revisions.map((cr) => ({ ...cr, ...(cr.rfaRevision ?? {}), @@ -557,27 +557,27 @@ export class RfaService { } /** - * ADR-019: Find RFA by the parent Correspondence UUID (public identifier). - * Resolves correspondence.uuid → internal rfa.id + * ADR-019: Find RFA by the parent Correspondence publicId (public identifier). + * Resolves correspondence.publicId → internal rfa.id */ - async findOneByUuid(uuid: string) { + async findOneByUuid(publicId: string) { const correspondence = await this.correspondenceRepo.findOne({ - where: { uuid }, + where: { publicId }, select: ['id'], }); if (!correspondence) { - throw new NotFoundException(`RFA with UUID ${uuid} not found`); + throw new NotFoundException(`RFA with publicId ${publicId} not found`); } return this.findOne(correspondence.id); } - async findOneByUuidRaw(uuid: string) { + async findOneByUuidRaw(publicId: string) { const correspondence = await this.correspondenceRepo.findOne({ - where: { uuid }, + where: { publicId }, select: ['id'], }); if (!correspondence) { - throw new NotFoundException(`RFA with UUID ${uuid} not found`); + throw new NotFoundException(`RFA with publicId ${publicId} not found`); } return this.findOne(correspondence.id, true); } @@ -618,7 +618,7 @@ export class RfaService { (rfa.correspondence?.revisions as CorrRevWithRfa[] | undefined) ?? []; const mappedRfa: RfaMapped = { ...rfa, - uuid: rfa.correspondence?.uuid, // ADR-019: expose UUID at top level + publicId: rfa.correspondence?.publicId, // ADR-019: expose publicId at top level revisions: revisions.map((cr) => ({ ...cr, ...(cr.rfaRevision ?? {}), @@ -846,8 +846,8 @@ export class RfaService { * 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); + async update(publicId: string, dto: UpdateRfaDto, _user: User) { + const rfa = await this.findOneByUuidRaw(publicId); const corrRevisions = (rfa.correspondence?.revisions as CorrRevWithRfa[] | undefined) ?? []; const currentCorrRev = corrRevisions.find((r) => r.isCurrent); @@ -880,15 +880,15 @@ export class RfaService { await this.rfaRevisionRepo.save(currentRfaRev); } - return this.findOneByUuid(uuid); + return this.findOneByUuid(publicId); } /** * 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); + async cancel(publicId: string, user: User) { + const rfa = await this.findOneByUuidRaw(publicId); const corrRevisions = (rfa.correspondence?.revisions as CorrRevWithRfa[] | undefined) ?? []; const currentCorrRev = corrRevisions.find((r) => r.isCurrent); diff --git a/backend/src/modules/search/search.service.ts b/backend/src/modules/search/search.service.ts index dcf68ff..fece79e 100644 --- a/backend/src/modules/search/search.service.ts +++ b/backend/src/modules/search/search.service.ts @@ -73,12 +73,18 @@ export class SearchService implements OnModuleInit { * Index เอกสาร (Create/Update) */ async indexDocument( - doc: Record & { type: string; id?: number; uuid?: string } + doc: Record & { + type: string; + id?: number; + publicId?: string; + } ) { try { return await this.esService.index({ index: this.indexName, - id: doc.uuid ? `${doc.type}_${doc.uuid}` : `${doc.type}_${doc.id}`, // ADR-019: prefer UUID key + id: doc.publicId + ? `${doc.type}_${doc.publicId}` + : `${doc.type}_${doc.id}`, // ADR-019: prefer publicId key document: doc, // ✅ Library รุ่นใหม่ใช้ 'document' แทน 'body' ในบางเวอร์ชัน }); } catch (error) { diff --git a/backend/src/modules/transmittal/transmittal.controller.ts b/backend/src/modules/transmittal/transmittal.controller.ts index 5147ce9..1c2f936 100644 --- a/backend/src/modules/transmittal/transmittal.controller.ts +++ b/backend/src/modules/transmittal/transmittal.controller.ts @@ -61,8 +61,8 @@ export class TransmittalController { @Get(':uuid') @ApiOperation({ summary: 'Get Transmittal details' }) @ApiParam({ - name: 'uuid', - description: 'Transmittal UUID (from correspondences.uuid)', + name: 'publicId', + description: 'Transmittal publicId (from correspondences.publicId)', }) @RequirePermission('document.view') findOne(@Param('uuid', ParseUuidPipe) uuid: string) { diff --git a/backend/src/modules/transmittal/transmittal.service.ts b/backend/src/modules/transmittal/transmittal.service.ts index fdb849f..f7ada83 100644 --- a/backend/src/modules/transmittal/transmittal.service.ts +++ b/backend/src/modules/transmittal/transmittal.service.ts @@ -159,16 +159,18 @@ export class TransmittalService { } /** - * ADR-019: Find Transmittal by parent Correspondence UUID (public identifier). - * Resolves correspondence.uuid → internal correspondenceId (INT) + * ADR-019: Find Transmittal by parent Correspondence publicId (public identifier). + * Resolves correspondence.publicId → internal correspondenceId (INT) */ - async findOneByUuid(uuid: string): Promise { + async findOneByUuid(publicId: string): Promise { const correspondence = await this.dataSource.manager.findOne( Correspondence, - { where: { uuid }, select: ['id'] } + { where: { publicId }, select: ['id'] } ); if (!correspondence) { - throw new NotFoundException(`Transmittal with UUID ${uuid} not found`); + throw new NotFoundException( + `Transmittal with publicId ${publicId} not found` + ); } return this.findOne(correspondence.id); } @@ -218,10 +220,10 @@ export class TransmittalService { .take(limit) .getManyAndCount(); - // ADR-019: Map correspondence.uuid to top level for frontend convenience + // ADR-019: Map correspondence.publicId to top level for frontend convenience const mappedItems = items.map((t) => ({ ...t, - uuid: t.correspondence?.uuid, + publicId: t.correspondence?.publicId, })); return { diff --git a/backend/src/modules/user/user.service.ts b/backend/src/modules/user/user.service.ts index dd989bc..1a015a8 100644 --- a/backend/src/modules/user/user.service.ts +++ b/backend/src/modules/user/user.service.ts @@ -86,7 +86,7 @@ export class UserService { .leftJoinAndSelect('assignments.role', 'role') .select([ 'user.user_id', - 'user.uuid', + 'user.publicId', 'user.username', 'user.email', 'user.firstName', @@ -156,9 +156,9 @@ export class UserService { return user; } - async findOneByUuid(uuid: string): Promise { + async findOneByUuid(publicId: string): Promise { const user = await this.usersRepository.findOne({ - where: { uuid }, + where: { publicId }, relations: [ 'preference', 'assignments', @@ -168,7 +168,7 @@ export class UserService { }); if (!user) { - throw new NotFoundException(`User with UUID ${uuid} not found`); + throw new NotFoundException(`User with publicId ${publicId} not found`); } return user; diff --git a/frontend/app/(admin)/admin/doc-control/reference/tags/page.tsx b/frontend/app/(admin)/admin/doc-control/reference/tags/page.tsx index b71f7f0..0ee4365 100644 --- a/frontend/app/(admin)/admin/doc-control/reference/tags/page.tsx +++ b/frontend/app/(admin)/admin/doc-control/reference/tags/page.tsx @@ -37,7 +37,7 @@ export default function TagsPage() { accessorKey: 'tag_name', header: 'Tag Name', cell: ({ row }) => { - const color = String(row.original.color_code || 'default'); + const color = String(row.original.colorCode || 'default'); const isHex = color.startsWith('#'); return (
@@ -45,7 +45,7 @@ export default function TagsPage() { className="w-3 h-3 rounded-full border border-border" style={{ backgroundColor: isHex ? color : color === 'default' ? '#e2e8f0' : color }} /> - {String(row.original.tag_name)} + {String(row.original.tagName)}
); }, @@ -97,13 +97,13 @@ export default function TagsPage() { required: false, }, { - name: 'tag_name', + name: 'tagName', label: 'Tag Name', type: 'text', required: true, }, { - name: 'color_code', + name: 'colorCode', label: 'Color Code (Hex or Name)', type: 'text', required: false, diff --git a/frontend/app/(admin)/admin/doc-control/workflows/[id]/edit/page.tsx b/frontend/app/(admin)/admin/doc-control/workflows/[id]/edit/page.tsx index 31ace37..153926b 100644 --- a/frontend/app/(admin)/admin/doc-control/workflows/[id]/edit/page.tsx +++ b/frontend/app/(admin)/admin/doc-control/workflows/[id]/edit/page.tsx @@ -52,13 +52,13 @@ export default function WorkflowEditPage() { try { const dto: CreateWorkflowDefinitionDto = { - workflow_code: workflowData.workflowType || 'CORRESPONDENCE', + workflowCode: workflowData.workflowType || 'CORRESPONDENCE', dsl: { workflowName: workflowData.workflowName, description: workflowData.description, dslDefinition: workflowData.dslDefinition, }, - is_active: workflowData.isActive, + isActive: workflowData.isActive, }; if (id) { diff --git a/frontend/app/(dashboard)/circulation/[uuid]/page.tsx b/frontend/app/(dashboard)/circulation/[uuid]/page.tsx index 2840223..6b80d5b 100644 --- a/frontend/app/(dashboard)/circulation/[uuid]/page.tsx +++ b/frontend/app/(dashboard)/circulation/[uuid]/page.tsx @@ -124,13 +124,13 @@ export default function CirculationDetailPage() {

Organization

-

{circulation.organization?.organization_name || '-'}

+

{circulation.organization?.organizationName || '-'}

Created By

{circulation.creator - ? `${circulation.creator.first_name || ''} ${circulation.creator.last_name || ''}`.trim() || + ? `${circulation.creator.firstName || ''} ${circulation.creator.lastName || ''}`.trim() || circulation.creator.username : '-'}

@@ -146,7 +146,7 @@ export default function CirculationDetailPage() { href={`/correspondences/${circulation.correspondence.uuid}`} className="font-medium text-primary hover:underline" > - {circulation.correspondence.correspondence_number} + {circulation.correspondence.correspondenceNumber}
)} @@ -166,13 +166,13 @@ export default function CirculationDetailPage() {
- {getInitials(routing.assignee?.first_name, routing.assignee?.last_name)} + {getInitials(routing.assignee?.firstName, routing.assignee?.lastName)}

{routing.assignee - ? `${routing.assignee.first_name || ''} ${routing.assignee.last_name || ''}`.trim() || + ? `${routing.assignee.firstName || ''} ${routing.assignee.lastName || ''}`.trim() || routing.assignee.username : 'Unassigned'}

diff --git a/frontend/components/circulation/circulation-list.tsx b/frontend/components/circulation/circulation-list.tsx index 79e418e..12194f5 100644 --- a/frontend/components/circulation/circulation-list.tsx +++ b/frontend/components/circulation/circulation-list.tsx @@ -63,7 +63,7 @@ export function CirculationList({ data }: CirculationListProps) { header: 'Organization', cell: ({ row }) => { const org = row.original.organization; - return org?.organization_name || '-'; + return org?.organizationName || '-'; }, }, { diff --git a/frontend/components/correspondences/circulation-status-card.tsx b/frontend/components/correspondences/circulation-status-card.tsx index bd31d99..1532751 100644 --- a/frontend/components/correspondences/circulation-status-card.tsx +++ b/frontend/components/correspondences/circulation-status-card.tsx @@ -30,7 +30,7 @@ function RoutingStep({ routing }: { routing: CirculationRouting }) { const meta = ROUTING_STATUS_META[routing.status] ?? ROUTING_STATUS_META.PENDING; const Icon = meta.icon; const assigneeName = routing.assignee - ? `${routing.assignee.first_name ?? ''} ${routing.assignee.last_name ?? ''}`.trim() || + ? `${routing.assignee.firstName ?? ''} ${routing.assignee.lastName ?? ''}`.trim() || routing.assignee.username : '—'; diff --git a/frontend/components/correspondences/tag-manager.tsx b/frontend/components/correspondences/tag-manager.tsx index e1bcf36..8bc6d6a 100644 --- a/frontend/components/correspondences/tag-manager.tsx +++ b/frontend/components/correspondences/tag-manager.tsx @@ -74,16 +74,16 @@ export function TagManager({ uuid, canEdit }: TagManagerProps) { key={tag.id} className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium border" style={{ - backgroundColor: `${getTagColor(tag.color_code)}22`, - borderColor: `${getTagColor(tag.color_code)}66`, - color: getTagColor(tag.color_code) === '#e2e8f0' ? 'inherit' : getTagColor(tag.color_code), + backgroundColor: `${getTagColor(tag.colorCode)}22`, + borderColor: `${getTagColor(tag.colorCode)}66`, + color: getTagColor(tag.colorCode) === '#e2e8f0' ? 'inherit' : getTagColor(tag.colorCode), }} > - {tag.tag_name} + {tag.tagName} {canEdit && ( )) )} diff --git a/frontend/types/circulation.ts b/frontend/types/circulation.ts index e6de126..6dfda25 100644 --- a/frontend/types/circulation.ts +++ b/frontend/types/circulation.ts @@ -38,7 +38,7 @@ export interface CirculationRouting { * Main Circulation entity */ export interface Circulation { - uuid: string; + publicId: string; // ADR-019: exposed as 'id' in API responses id?: number; // Excluded from API responses (ADR-019) correspondenceId?: number; organizationId: number; @@ -53,18 +53,18 @@ export interface Circulation { // Joined relations from API routings?: CirculationRouting[]; correspondence?: { - uuid: string; + publicId: string; id?: number; correspondenceNumber: string; }; organization?: { - uuid: string; + publicId: string; id?: number; organizationCode: string; organizationName: string; }; creator?: { - uuid: string; + publicId: string; userId?: number; username: string; firstName?: string; diff --git a/frontend/types/correspondence.ts b/frontend/types/correspondence.ts index c9037bf..2a492ea 100644 --- a/frontend/types/correspondence.ts +++ b/frontend/types/correspondence.ts @@ -1,12 +1,12 @@ export interface Organization { - uuid: string; + publicId: string; // ADR-019: exposed as 'id' in API responses id?: number; // Excluded from API responses (ADR-019) organizationName: string; organizationCode: string; } export interface Attachment { - uuid: string; + publicId: string; // ADR-019: exposed as 'id' in API responses id?: number; // Excluded from API responses (ADR-019) name: string; url: string; @@ -17,7 +17,7 @@ export interface Attachment { // Used in List View mainly export interface CorrespondenceRevision { - uuid: string; + publicId: string; // ADR-019: exposed as 'id' in API responses id?: number; // Excluded from API responses (ADR-019) revisionNumber: number; revisionLabel?: string; // e.g. "A", "00" @@ -42,21 +42,21 @@ export interface CorrespondenceRevision { // Nested Relation from Backend Refactor correspondence: { - uuid: string; + publicId: string; id?: number; // Excluded from API responses (ADR-019) correspondenceNumber: string; projectId: number; originatorId?: number; isInternal: boolean; originator?: Organization; - project?: { uuid: string; id?: number; projectName: string; projectCode: string }; + project?: { publicId: string; id?: number; projectName: string; projectCode: string }; type?: { id: number; typeName: string; typeCode: string }; }; } // Keep explicit Correspondence for Detail View if needed, or merge concepts export interface Correspondence { - uuid: string; + publicId: string; // ADR-019: exposed as 'id' in API responses id?: number; // Excluded from API responses (ADR-019) correspondenceNumber: string; projectId: number; @@ -67,7 +67,7 @@ export interface Correspondence { // Relations originator?: Organization; - project?: { uuid: string; id?: number; projectName: string; projectCode: string }; + project?: { publicId: string; id?: number; projectName: string; projectCode: string }; type?: { id: number; typeName: string; typeCode: string }; revisions?: CorrespondenceRevision[]; // Nested revisions recipients?: { diff --git a/frontend/types/drawing.ts b/frontend/types/drawing.ts index 8617a63..c07f715 100644 --- a/frontend/types/drawing.ts +++ b/frontend/types/drawing.ts @@ -1,6 +1,6 @@ // Entity Interfaces export interface DrawingRevision { - uuid: string; + publicId: string; // ADR-019: exposed as 'id' in API responses revisionId?: number; // Excluded from API responses (ADR-019) revisionNumber: string; title?: string; // Added @@ -15,7 +15,7 @@ export interface DrawingRevision { } export interface ContractDrawing { - uuid: string; + publicId: string; // ADR-019: exposed as 'id' in API responses id?: number; // Excluded from API responses (ADR-019) contractDrawingNo: string; title: string; @@ -28,7 +28,7 @@ export interface ContractDrawing { } export interface ShopDrawing { - uuid: string; + publicId: string; // ADR-019: exposed as 'id' in API responses id?: number; // Excluded from API responses (ADR-019) drawingNumber: string; projectId: number; @@ -41,7 +41,7 @@ export interface ShopDrawing { } export interface AsBuiltDrawing { - uuid: string; + publicId: string; // ADR-019: exposed as 'id' in API responses id?: number; // Excluded from API responses (ADR-019) drawingNumber: string; projectId: number; @@ -54,7 +54,7 @@ export interface AsBuiltDrawing { // Unified Type for List export interface Drawing { - uuid?: string; + publicId?: string; // ADR-019: exposed as 'id' in API responses drawingId?: number; // Excluded from API responses (ADR-019) drawingNumber: string; title: string; // Display title (from current revision for Shop/AsBuilt) diff --git a/frontend/types/notification.ts b/frontend/types/notification.ts index 983e715..22fe4d7 100644 --- a/frontend/types/notification.ts +++ b/frontend/types/notification.ts @@ -1,5 +1,5 @@ export interface Notification { - uuid: string; + publicId: string; // ADR-019: exposed as 'id' in API responses notificationId?: number; // Excluded from API responses (ADR-019) title: string; message: string; diff --git a/frontend/types/organization.ts b/frontend/types/organization.ts index c6a7dbc..8d41437 100644 --- a/frontend/types/organization.ts +++ b/frontend/types/organization.ts @@ -1,5 +1,5 @@ export interface Organization { - uuid: string; + publicId: string; // ADR-019: exposed as 'id' in API responses id?: number; // Excluded from API responses (ADR-019) organizationCode: string; organizationName: string; diff --git a/frontend/types/rfa.ts b/frontend/types/rfa.ts index 59e5911..d5acb0f 100644 --- a/frontend/types/rfa.ts +++ b/frontend/types/rfa.ts @@ -2,33 +2,33 @@ export interface RFAItem { id?: number; itemType: 'SHOP' | 'AS_BUILT'; shopDrawingRevision?: { - uuid?: string; + publicId?: string; revisionLabel?: string; revisionNumber?: number; title?: string; legacyDrawingNumber?: string; attachments?: { id?: number; url?: string; name?: string }[]; shopDrawing?: { - uuid?: string; + publicId?: string; drawingNumber?: string; }; }; asBuiltDrawingRevision?: { - uuid?: string; + publicId?: string; revisionLabel?: string; revisionNumber?: number; title?: string; legacyDrawingNumber?: string; attachments?: { id?: number; url?: string; name?: string }[]; asBuiltDrawing?: { - uuid?: string; + publicId?: string; drawingNumber?: string; }; }; } export interface RFA { - uuid: string; // ADR-019: from correspondence.uuid + publicId: string; // ADR-019: from correspondence.publicId id?: number; // Excluded from API responses (ADR-019) rfaTypeId: number; createdBy: number; @@ -56,14 +56,14 @@ export interface RFA { }; // Shared Correspondence Relation correspondence?: { - uuid: string; + publicId: string; id?: number; // Excluded from API responses (ADR-019) correspondenceNumber: string; projectId: number; originatorId?: number; createdAt?: string; project?: { - uuid: string; + publicId: string; projectName: string; projectCode: string; }; diff --git a/frontend/types/search.ts b/frontend/types/search.ts index ae2357f..726d61d 100644 --- a/frontend/types/search.ts +++ b/frontend/types/search.ts @@ -1,5 +1,5 @@ export interface SearchResult { - uuid: string; + publicId: string; // ADR-019: exposed as 'id' in API responses id?: number; // Excluded from API responses (ADR-019) type: 'correspondence' | 'rfa' | 'drawing'; title: string; diff --git a/frontend/types/transmittal.ts b/frontend/types/transmittal.ts index c5176f7..f0bcd82 100644 --- a/frontend/types/transmittal.ts +++ b/frontend/types/transmittal.ts @@ -28,7 +28,7 @@ export interface TransmittalItem { * Main Transmittal entity */ export interface Transmittal { - uuid: string; // ADR-019: from correspondence.uuid + publicId: string; // ADR-019: from correspondence.publicId id?: number; // Excluded from API responses (ADR-019) correspondenceId?: number | string; transmittalNo: string; @@ -39,7 +39,7 @@ export interface Transmittal { // Joined relations from API items?: TransmittalItem[]; correspondence?: { - uuid: string; + publicId: string; id?: number; correspondenceNumber: string; projectId: number; diff --git a/frontend/types/user.ts b/frontend/types/user.ts index 92ea8bb..8273f38 100644 --- a/frontend/types/user.ts +++ b/frontend/types/user.ts @@ -12,7 +12,7 @@ export interface UserOrganization { } export interface User { - uuid: string; + publicId: string; // ADR-019: exposed as 'id' in API responses userId?: number; // Excluded from API responses (ADR-019) username: string; email: string; diff --git a/specs/06-Decision-Records/ADR-019-hybrid-identifier-strategy.md b/specs/06-Decision-Records/ADR-019-hybrid-identifier-strategy.md index dca1a54..808b901 100644 --- a/specs/06-Decision-Records/ADR-019-hybrid-identifier-strategy.md +++ b/specs/06-Decision-Records/ADR-019-hybrid-identifier-strategy.md @@ -179,19 +179,30 @@ UNIQUE INDEX idx_{table}_uuid (uuid) import { Column, BeforeInsert } from 'typeorm'; import { v7 as uuidv7 } from 'uuid'; -export abstract class BaseUuidEntity { +/** + * Abstract base entity providing a UUID public identifier. + * + * Naming Convention: + * - TypeScript Property: `publicId` — semantic name indicating this is the public-facing identifier + * - Database Column: `uuid` — MariaDB native UUID type (stored as BINARY(16)) + * + * This avoids confusion between the property name and the DB data type, + * while clearly indicating this is the ID exposed via API (not internal INT PK). + */ +export abstract class UuidBaseEntity { @Column({ type: 'uuid', + name: 'uuid', // DB column name (MariaDB native UUID type) unique: true, nullable: false, comment: 'UUID Public Identifier (ADR-019)', }) - uuid!: string; + publicId!: string; // TypeScript property name — semantic, avoids type confusion @BeforeInsert() generateUuid(): void { - if (!this.uuid) { - this.uuid = uuidv7(); + if (!this.publicId) { + this.publicId = uuidv7(); } } } @@ -209,7 +220,10 @@ import { BaseUuidEntity } from '../../common/entities/base-uuid.entity'; @Entity('correspondences') export class Correspondence extends BaseUuidEntity { @PrimaryGeneratedColumn() - id!: number; + id!: number; // Internal INT PK — never exposed via API + + // publicId (from BaseUuidEntity) is the UUID exposed via API + // DB Column: uuid (MariaDB native UUID type) // ... existing columns } @@ -243,9 +257,9 @@ async findOne(@Param('uuid', ParseUUIDPipe) uuid: string) { ### Service Pattern ```typescript -async findByUuid(uuid: string): Promise { +async findByUuid(publicId: string): Promise { const entity = await this.repository.findOne({ - where: { uuid }, + where: { publicId }, // Use publicId property (DB column is 'uuid') }); if (!entity) throw new NotFoundException(); return this.toDto(entity); @@ -256,17 +270,17 @@ async findByUuid(uuid: string): Promise { ```typescript export class CorrespondenceResponseDto { - // ✅ Expose UUID as 'id' in API response + // ✅ Expose publicId as 'id' in API response @Expose({ name: 'id' }) - uuid!: string; + publicId!: string; // ❌ Never expose internal INT id // id: number; — REMOVED from response // ... other fields - // For FK references, also use UUID + // For FK references, also use publicId @Expose({ name: 'project_id' }) - projectUuid!: string; + projectPublicId!: string; } ``` @@ -454,10 +468,10 @@ WHERE c.uuid = '019505a1-7c3e-7000-8000-abc123def456'; ### Phase 2: Backend (Dual-Mode) -- เพิ่ม `uuid` field ใน TypeORM Entities -- สร้าง `BaseUuidEntity` class +- เพิ่ม `publicId` field ใน TypeORM Entities (DB column ยังชื่อ `uuid`) +- สร้าง `BaseUuidEntity` class ด้วย `publicId` property - API รับได้ทั้ง INT และ UUID ผ่าน `FindByIdOrUuid` pattern -- API Response รวม UUID เป็น `id` field +- API Response รวม UUID เป็น `id` field (via @Expose) ### Phase 3: Frontend (Gradual Migration) @@ -486,4 +500,4 @@ WHERE c.uuid = '019505a1-7c3e-7000-8000-abc123def456'; --- -_สำหรับรายละเอียดการ Implement ดูที่ Implementation Plan ใน ADR-019-implementation-plan.md_ +_สำหรับรายละเอียดการ Implement ดูที่ Implementation Plan ใน `05-07-hybrid-uuid-implementation-plan.md`_