diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 460e575..2c4b7a4 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -48,6 +48,7 @@ import { MonitoringModule } from './modules/monitoring/monitoring.module'; import { ResilienceModule } from './common/resilience/resilience.module'; import { SearchModule } from './modules/search/search.module'; import { AuditLogModule } from './modules/audit-log/audit-log.module'; +import { MigrationModule } from './modules/migration/migration.module'; @Module({ imports: [ @@ -158,6 +159,7 @@ import { AuditLogModule } from './modules/audit-log/audit-log.module'; NotificationModule, DashboardModule, AuditLogModule, + MigrationModule, ], controllers: [AppController], providers: [ diff --git a/backend/src/common/file-storage/file-storage.service.spec.ts b/backend/src/common/file-storage/file-storage.service.spec.ts index 75c9a21..7191412 100644 --- a/backend/src/common/file-storage/file-storage.service.spec.ts +++ b/backend/src/common/file-storage/file-storage.service.spec.ts @@ -64,11 +64,16 @@ describe('FileStorageService', () => { attachmentRepo = module.get(getRepositoryToken(Attachment)); jest.clearAllMocks(); - (fs.ensureDirSync as jest.Mock).mockReturnValue(true); - (fs.writeFile as jest.Mock).mockResolvedValue(undefined); - (fs.pathExists as jest.Mock).mockResolvedValue(true); - (fs.move as jest.Mock).mockResolvedValue(undefined); - (fs.remove as jest.Mock).mockResolvedValue(undefined); + (fs.ensureDirSync as unknown as jest.Mock).mockReturnValue(true); + (fs.writeFile as unknown as jest.Mock).mockResolvedValue(undefined); + (fs.pathExists as unknown as jest.Mock).mockResolvedValue(true); + (fs.move as unknown as jest.Mock).mockResolvedValue(undefined); + (fs.remove as unknown as jest.Mock).mockResolvedValue(undefined); + (fs.readFile as unknown as jest.Mock).mockResolvedValue( + Buffer.from('test') + ); + (fs.stat as unknown as jest.Mock).mockResolvedValue({ size: 1024 }); + (fs.ensureDir as unknown as jest.Mock).mockResolvedValue(undefined); }); it('should be defined', () => { @@ -86,7 +91,7 @@ describe('FileStorageService', () => { }); it('should throw BadRequestException if write fails', async () => { - (fs.writeFile as jest.Mock).mockRejectedValueOnce( + (fs.writeFile as unknown as jest.Mock).mockRejectedValueOnce( new Error('Write error') ); await expect(service.upload(mockFile, 1)).rejects.toThrow( diff --git a/backend/src/common/file-storage/file-storage.service.ts b/backend/src/common/file-storage/file-storage.service.ts index 6ffe56b..147247d 100644 --- a/backend/src/common/file-storage/file-storage.service.ts +++ b/backend/src/common/file-storage/file-storage.service.ts @@ -201,6 +201,77 @@ export class FileStorageService { return crypto.createHash('sha256').update(buffer).digest('hex'); } + /** + * ✅ NEW: Import Staging File (For Legacy Migration) + * ย้ายไฟล์จาก staging_ai ไปยัง permanent storage โดยตรง + */ + async importStagingFile( + sourceFilePath: string, + userId: number, + options?: { issueDate?: Date; documentType?: string } + ): Promise { + if (!(await fs.pathExists(sourceFilePath))) { + this.logger.error(`Staging file not found: ${sourceFilePath}`); + throw new NotFoundException(`Source file not found: ${sourceFilePath}`); + } + + // 1. Get file stats & checksum + const stats = await fs.stat(sourceFilePath); + const fileExt = path.extname(sourceFilePath); + const originalFilename = path.basename(sourceFilePath); + const storedFilename = `${uuidv4()}${fileExt}`; + + // Determine mime type basic + let mimeType = 'application/octet-stream'; + if (fileExt.toLowerCase() === '.pdf') mimeType = 'application/pdf'; + else if (fileExt.toLowerCase() === '.xlsx') + mimeType = + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; + + const fileBuffer = await fs.readFile(sourceFilePath); + const checksum = this.calculateChecksum(fileBuffer); + + // 2. Generate Permanent Path + const refDate = options?.issueDate || new Date(); + const effectiveDate = isNaN(refDate.getTime()) ? new Date() : refDate; + const year = effectiveDate.getFullYear().toString(); + const month = (effectiveDate.getMonth() + 1).toString().padStart(2, '0'); + const docTypeFolder = options?.documentType || 'General'; + + const permanentDir = path.join( + this.permanentDir, + docTypeFolder, + year, + month + ); + await fs.ensureDir(permanentDir); + + const newPath = path.join(permanentDir, storedFilename); + + // 3. Move File + try { + await fs.move(sourceFilePath, newPath, { overwrite: true }); + } catch (error) { + this.logger.error(`Failed to move staging file to ${newPath}`, error); + throw new BadRequestException('Failed to process staging file'); + } + + // 4. Create Database Record + const attachment = this.attachmentRepository.create({ + originalFilename, + storedFilename, + filePath: newPath, + mimeType, + fileSize: stats.size, + isTemporary: false, + referenceDate: effectiveDate, + checksum, + uploadedByUserId: userId, + }); + + return this.attachmentRepository.save(attachment); + } + /** * ✅ NEW: Delete File * ลบไฟล์ออกจาก Disk และ Database diff --git a/backend/src/modules/correspondence/correspondence.controller.spec.ts b/backend/src/modules/correspondence/correspondence.controller.spec.ts index aa6a2e2..ace17f1 100644 --- a/backend/src/modules/correspondence/correspondence.controller.spec.ts +++ b/backend/src/modules/correspondence/correspondence.controller.spec.ts @@ -76,7 +76,7 @@ describe('CorrespondenceController', () => { const createDto = { projectId: 1, typeId: 1, - title: 'Test Subject', + subject: 'Test Subject', }; const result = await controller.create( diff --git a/backend/src/modules/document-numbering/services/manual-override.service.spec.ts b/backend/src/modules/document-numbering/services/manual-override.service.spec.ts index 14163b4..279f9a9 100644 --- a/backend/src/modules/document-numbering/services/manual-override.service.spec.ts +++ b/backend/src/modules/document-numbering/services/manual-override.service.spec.ts @@ -41,9 +41,9 @@ describe('ManualOverrideService', () => { originatorOrganizationId: 2, recipientOrganizationId: 3, correspondenceTypeId: 4, - subTypeId: null, - rfaTypeId: null, - disciplineId: null, + subTypeId: 5, + rfaTypeId: 6, + disciplineId: 7, resetScope: 'YEAR_2024', newLastNumber: 999, reason: 'System sync', diff --git a/backend/src/modules/migration/dto/import-correspondence.dto.ts b/backend/src/modules/migration/dto/import-correspondence.dto.ts new file mode 100644 index 0000000..5112184 --- /dev/null +++ b/backend/src/modules/migration/dto/import-correspondence.dto.ts @@ -0,0 +1,44 @@ +import { + IsString, + IsNotEmpty, + IsOptional, + IsNumber, + IsObject, +} from 'class-validator'; + +export class ImportCorrespondenceDto { + @IsString() + @IsNotEmpty() + document_number!: string; + + @IsString() + @IsNotEmpty() + title!: string; + + @IsString() + @IsNotEmpty() + category!: string; + + @IsString() + @IsNotEmpty() + source_file_path!: string; + + @IsNumber() + @IsOptional() + ai_confidence?: number; + + @IsOptional() + ai_issues?: any; + + @IsString() + @IsNotEmpty() + migrated_by!: string; // "SYSTEM_IMPORT" + + @IsString() + @IsNotEmpty() + batch_id!: string; + + @IsObject() + @IsOptional() + details?: Record; +} diff --git a/backend/src/modules/migration/entities/import-transaction.entity.ts b/backend/src/modules/migration/entities/import-transaction.entity.ts new file mode 100644 index 0000000..6c26614 --- /dev/null +++ b/backend/src/modules/migration/entities/import-transaction.entity.ts @@ -0,0 +1,29 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + Index, +} from 'typeorm'; + +@Entity('import_transactions') +export class ImportTransaction { + @PrimaryGeneratedColumn() + id!: number; + + @Index('idx_idem_key', { unique: true }) + @Column({ name: 'idempotency_key', length: 255, unique: true }) + idempotencyKey!: string; + + @Column({ name: 'document_number', length: 100, nullable: true }) + documentNumber!: string; + + @Column({ name: 'batch_id', length: 100, nullable: true }) + batchId!: string; + + @Column({ name: 'status_code', default: 201 }) + statusCode!: number; + + @CreateDateColumn({ name: 'created_at' }) + createdAt!: Date; +} diff --git a/backend/src/modules/migration/migration.controller.spec.ts b/backend/src/modules/migration/migration.controller.spec.ts new file mode 100644 index 0000000..44ba627 --- /dev/null +++ b/backend/src/modules/migration/migration.controller.spec.ts @@ -0,0 +1,58 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { MigrationController } from './migration.controller'; +import { MigrationService } from './migration.service'; +import { ImportCorrespondenceDto } from './dto/import-correspondence.dto'; + +describe('MigrationController', () => { + let controller: MigrationController; + let service: MigrationService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [MigrationController], + providers: [ + { + provide: MigrationService, + useValue: { + importCorrespondence: jest + .fn() + .mockResolvedValue({ message: 'Success' }), + }, + }, + ], + }).compile(); + + controller = module.get(MigrationController); + service = module.get(MigrationService); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + it('should call importCorrespondence on service', async () => { + const dto: ImportCorrespondenceDto = { + document_number: 'DOC-001', + title: 'Legacy Record', + category: 'Correspondence', + source_file_path: '/staging_ai/test.pdf', + migrated_by: 'SYSTEM_IMPORT', + batch_id: 'batch1', + }; + + const idempotencyKey = 'key123'; + const user = { userId: 5 }; + + const result = await controller.importCorrespondence( + dto, + idempotencyKey, + user + ); + expect(result).toEqual({ message: 'Success' }); + expect(service.importCorrespondence).toHaveBeenCalledWith( + dto, + idempotencyKey, + 5 + ); + }); +}); diff --git a/backend/src/modules/migration/migration.controller.ts b/backend/src/modules/migration/migration.controller.ts new file mode 100644 index 0000000..36b7536 --- /dev/null +++ b/backend/src/modules/migration/migration.controller.ts @@ -0,0 +1,30 @@ +import { Controller, Post, Body, Headers, UseGuards } from '@nestjs/common'; +import { MigrationService } from './migration.service'; +import { ImportCorrespondenceDto } from './dto/import-correspondence.dto'; +import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; +import { CurrentUser } from '../../common/decorators/current-user.decorator'; +import { ApiTags, ApiOperation, ApiBearerAuth, ApiHeader } from '@nestjs/swagger'; + +@ApiTags('Migration') +@ApiBearerAuth() +@Controller('migration') +export class MigrationController { + constructor(private readonly migrationService: MigrationService) {} + + @Post('import') + @UseGuards(JwtAuthGuard) + @ApiOperation({ summary: 'Import generic legacy correspondence record via n8n integration' }) + @ApiHeader({ + name: 'Idempotency-Key', + description: 'Unique key per document and batch to prevent duplicate inserts', + required: true, + }) + async importCorrespondence( + @Body() dto: ImportCorrespondenceDto, + @Headers('idempotency-key') idempotencyKey: string, + @CurrentUser() user: any + ) { + const userId = user?.id || user?.userId || 5; + return this.migrationService.importCorrespondence(dto, idempotencyKey, userId); + } +} diff --git a/backend/src/modules/migration/migration.module.ts b/backend/src/modules/migration/migration.module.ts new file mode 100644 index 0000000..04d16b0 --- /dev/null +++ b/backend/src/modules/migration/migration.module.ts @@ -0,0 +1,28 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { MigrationController } from './migration.controller'; +import { MigrationService } from './migration.service'; +import { ImportTransaction } from './entities/import-transaction.entity'; +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'; +// Import any other required modules for JwtAuthGuard (usually AuthModule or similar, but global guards handle this mostly) + +@Module({ + imports: [ + TypeOrmModule.forFeature([ + ImportTransaction, + Correspondence, + CorrespondenceRevision, + CorrespondenceType, + CorrespondenceStatus, + Project, + ]), + ], + controllers: [MigrationController], + providers: [MigrationService], + exports: [MigrationService], +}) +export class MigrationModule {} diff --git a/backend/src/modules/migration/migration.service.spec.ts b/backend/src/modules/migration/migration.service.spec.ts new file mode 100644 index 0000000..2084c69 --- /dev/null +++ b/backend/src/modules/migration/migration.service.spec.ts @@ -0,0 +1,83 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { MigrationService } from './migration.service'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { ImportTransaction } from './entities/import-transaction.entity'; +import { CorrespondenceType } from '../correspondence/entities/correspondence-type.entity'; +import { CorrespondenceStatus } from '../correspondence/entities/correspondence-status.entity'; +import { Project } from '../project/entities/project.entity'; +import { DataSource } from 'typeorm'; + +describe('MigrationService', () => { + let service: MigrationService; + + const mockTransactionRepo = { + findOne: jest.fn(), + create: jest.fn(), + save: jest.fn(), + }; + + const mockTypeRepo = { + findOne: jest.fn(), + }; + + const mockStatusRepo = { + findOne: jest.fn(), + }; + + const mockProjectRepo = { + findOne: jest.fn(), + }; + + const mockQueryRunner = { + connect: jest.fn(), + startTransaction: jest.fn(), + commitTransaction: jest.fn(), + rollbackTransaction: jest.fn(), + release: jest.fn(), + manager: { + findOne: jest.fn(), + create: jest.fn(), + save: jest.fn(), + count: jest.fn(), + update: jest.fn(), + }, + }; + + const mockDataSource = { + createQueryRunner: jest.fn().mockReturnValue(mockQueryRunner), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + MigrationService, + { + provide: getRepositoryToken(ImportTransaction), + useValue: mockTransactionRepo, + }, + { + provide: getRepositoryToken(CorrespondenceType), + useValue: mockTypeRepo, + }, + { + provide: getRepositoryToken(CorrespondenceStatus), + useValue: mockStatusRepo, + }, + { + provide: getRepositoryToken(Project), + useValue: mockProjectRepo, + }, + { + provide: DataSource, + useValue: mockDataSource, + }, + ], + }).compile(); + + service = module.get(MigrationService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/backend/src/modules/migration/migration.service.ts b/backend/src/modules/migration/migration.service.ts new file mode 100644 index 0000000..687769c --- /dev/null +++ b/backend/src/modules/migration/migration.service.ts @@ -0,0 +1,244 @@ +import { + Injectable, + Logger, + ConflictException, + BadRequestException, + InternalServerErrorException, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository, DataSource } from 'typeorm'; +import { ImportCorrespondenceDto } from './dto/import-correspondence.dto'; +import { ImportTransaction } from './entities/import-transaction.entity'; +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'; +import { FileStorageService } from '../../common/file-storage/file-storage.service'; + +@Injectable() +export class MigrationService { + private readonly logger = new Logger(MigrationService.name); + + constructor( + private readonly dataSource: DataSource, + @InjectRepository(ImportTransaction) + private readonly importTransactionRepo: Repository, + @InjectRepository(CorrespondenceType) + private readonly correspondenceTypeRepo: Repository, + @InjectRepository(CorrespondenceStatus) + private readonly correspondenceStatusRepo: Repository, + @InjectRepository(Project) + private readonly projectRepo: Repository, + private readonly fileStorageService: FileStorageService + ) {} + + async importCorrespondence( + dto: ImportCorrespondenceDto, + idempotencyKey: string, + userId: number + ) { + if (!idempotencyKey) { + throw new BadRequestException('Idempotency-Key header is required'); + } + + // 1. Idempotency Check + const existingTransaction = await this.importTransactionRepo.findOne({ + where: { idempotencyKey }, + }); + + if (existingTransaction) { + if (existingTransaction.statusCode === 201) { + this.logger.log( + `Idempotency key ${idempotencyKey} already processed. Returning cached success.` + ); + return { + message: 'Already processed', + transaction: existingTransaction, + }; + } else { + throw new ConflictException( + `Transaction failed previously with status ${existingTransaction.statusCode}` + ); + } + } + + // 2. Fetch Dependencies + const type = await this.correspondenceTypeRepo.findOne({ + where: { typeName: dto.category }, + }); + + // If exact name isn't found, try typeCode just in case + const typeId = type + ? type.id + : ( + await this.correspondenceTypeRepo.findOne({ + where: { typeCode: dto.category }, + }) + )?.id; + + if (!typeId) { + throw new BadRequestException( + `Category "${dto.category}" not found in system.` + ); + } + + // Migrate documents typically end up as 'Closed by Owner' or a similar terminal state, unless specifically pending. + // For legacy, let's use a default terminal status 'CLBOWN' if available. If not, fallback to 'DRAFT'. + let status = await this.correspondenceStatusRepo.findOne({ + where: { statusCode: 'CLBOWN' }, + }); + if (!status) { + status = await this.correspondenceStatusRepo.findOne({ + where: { statusCode: 'DRAFT' }, + }); + } + if (!status) { + throw new InternalServerErrorException( + 'CRITICAL: No default correspondence status found (missing CLBOWN/DRAFT)' + ); + } + + // We assume migration runs for LCBP3 project + const project = await this.projectRepo.findOne({ + where: { projectCode: 'LCBP3' }, + }); + if (!project) { + throw new InternalServerErrorException( + 'Project LCBP3 not found in database' + ); + } + + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); + + try { + // 3. Find or Create Correspondence + let correspondence = await queryRunner.manager.findOne(Correspondence, { + where: { + correspondenceNumber: dto.document_number, + projectId: project.id, + }, + }); + + if (!correspondence) { + correspondence = queryRunner.manager.create(Correspondence, { + correspondenceNumber: dto.document_number, + correspondenceTypeId: typeId, + projectId: project.id, + isInternal: false, + createdBy: userId, + }); + await queryRunner.manager.save(correspondence); + } + + // 4. File Handling + // We will map the source file and create an Attachment record using FileStorageService + // For legacy migrations, we pass document_number mapping logic or basic processing + let attachmentId: number | null = null; + if (dto.source_file_path) { + try { + const attachment = await this.fileStorageService.importStagingFile( + dto.source_file_path, + userId, + { documentType: dto.category } // use category from DTO directly + ); + attachmentId = attachment.id; + } catch (fileError: unknown) { + const errMsg = + fileError instanceof Error ? fileError.message : String(fileError); + + this.logger.warn( + `Failed to import file for [${dto.document_number}], continuing without attachment: ${errMsg}` + ); + } + } + + // 5. Create Revision + const revisionCount = await queryRunner.manager.count( + CorrespondenceRevision, + { + where: { correspondenceId: correspondence.id }, + } + ); + + // Determine revision number. Support mapping multiple batches to the same document number by incrementing revision. + const revNum = revisionCount; + const revision = queryRunner.manager.create(CorrespondenceRevision, { + correspondenceId: correspondence.id, + revisionNumber: revNum, + revisionLabel: revNum === 0 ? '0' : revNum.toString(), + isCurrent: true, + statusId: status.id, + subject: dto.title, + description: 'Migrated from legacy system via Auto Ingest', + details: { + ...dto.details, + ai_confidence: dto.ai_confidence, + ai_issues: dto.ai_issues as unknown, + source_file_path: dto.source_file_path, + attachment_id: attachmentId, // Link attachment ID if successful + }, + schemaVersion: 1, + createdBy: userId, // Bot ID + }); + + if (revisionCount > 0) { + await queryRunner.manager.update( + CorrespondenceRevision, + { correspondenceId: correspondence.id, isCurrent: true }, + { isCurrent: false } + ); + } + + await queryRunner.manager.save(revision); + + // 5. Track Transaction + const transaction = queryRunner.manager.create(ImportTransaction, { + idempotencyKey, + documentNumber: dto.document_number, + batchId: dto.batch_id, + statusCode: 201, + }); + await queryRunner.manager.save(transaction); + + await queryRunner.commitTransaction(); + + this.logger.log( + `Ingested document [${dto.document_number}] successfully (Batch: ${dto.batch_id})` + ); + + return { + message: 'Import successful', + correspondenceId: correspondence.id, + revisionId: revision.id, + transactionId: transaction.id, + }; + } catch (error: unknown) { + await queryRunner.rollbackTransaction(); + const errorMessage = + error instanceof Error ? error.message : String(error); + const errorStack = error instanceof Error ? error.stack : undefined; + + this.logger.error( + `Import failed for document [${dto.document_number}]: ${errorMessage}`, + errorStack + ); + + const failedTransaction = this.importTransactionRepo.create({ + idempotencyKey, + documentNumber: dto.document_number, + batchId: dto.batch_id, + statusCode: 500, + }); + await this.importTransactionRepo.save(failedTransaction).catch(() => {}); + + throw new InternalServerErrorException( + 'Migration import failed: ' + errorMessage + ); + } finally { + await queryRunner.release(); + } + } +} diff --git a/backend/src/modules/project/project.controller.spec.ts b/backend/src/modules/project/project.controller.spec.ts index e1983e9..791cf33 100644 --- a/backend/src/modules/project/project.controller.spec.ts +++ b/backend/src/modules/project/project.controller.spec.ts @@ -46,7 +46,7 @@ describe('ProjectController', () => { const mockResult = { data: [], meta: {} }; (mockProjectService.findAll as jest.Mock).mockResolvedValue(mockResult); - const result = await controller.findAll({}); + const result = await controller.findAll({ page: 1, limit: 10 }); expect(mockProjectService.findAll).toHaveBeenCalled(); }); diff --git a/backend/src/modules/workflow-engine/dsl/parser.service.spec.ts b/backend/src/modules/workflow-engine/dsl/parser.service.spec.ts index cf191e2..d784372 100644 --- a/backend/src/modules/workflow-engine/dsl/parser.service.spec.ts +++ b/backend/src/modules/workflow-engine/dsl/parser.service.spec.ts @@ -12,7 +12,7 @@ describe('WorkflowDslParser', () => { beforeEach(async () => { mockRepository = { - save: jest.fn((def) => Promise.resolve(def)), + save: jest.fn((def) => Promise.resolve(def)) as unknown as jest.Mock, findOne: jest.fn(), }; @@ -160,7 +160,7 @@ describe('WorkflowDslParser', () => { expect(result.valid).toBe(false); expect(result.errors).toBeDefined(); - expect(result.errors.length).toBeGreaterThan(0); + expect(result.errors?.length).toBeGreaterThan(0); }); }); @@ -184,7 +184,7 @@ describe('WorkflowDslParser', () => { it('should throw error if definition not found', async () => { mockRepository.findOne = jest.fn().mockResolvedValue(null); - await expect(parser.getParsedDsl(999)).rejects.toThrow( + await expect(parser.getParsedDsl('999')).rejects.toThrow( BadRequestException ); }); diff --git a/specs/03-Data-and-Storage/03-01-data-dictionary.md b/specs/03-Data-and-Storage/03-01-data-dictionary.md index 18906d4..c44eed1 100644 --- a/specs/03-Data-and-Storage/03-01-data-dictionary.md +++ b/specs/03-Data-and-Storage/03-01-data-dictionary.md @@ -1,2115 +1,2199 @@ ---- -title: 'Data & Storage: Data Dictionary and Data Model Architecture' -version: 1.8.0 -status: released -owner: Nattanin Peancharoen -last_updated: 2026-02-22 -related: - - specs/01-requirements/02-architecture.md - - specs/01-requirements/03-functional-requirements.md - - docs/4_Data_Dictionary_V1_4_5.md - - docs/8_lcbp3_v1_4_5.sql ---- - -# 1. Data Model Architecture Overview - -## 📋 1.1 Overview -เอกสารนี้อธิบายสถาปัตยกรรมของ Data Model สำหรับระบบ LCBP3-DMS โดยครอบคลุมโครงสร้างฐานข้อมูล, ความสัมพันธ์ระหว่างตาราง, และหลักการออกแบบที่สำคัญ - -## 🎯 1.2 Design Principles -### 1. Separation of Concerns - -- **Master-Revision Pattern**: แยกข้อมูลที่ไม่เปลี่ยนแปลง (Master) จากข้อมูลที่มีการแก้ไข (Revisions) - - `correspondences` (Master) ↔ `correspondence_revisions` (Revisions) - - `rfas` (Master) ↔ `rfa_revisions` (Revisions) - - `shop_drawings` (Master) ↔ `shop_drawing_revisions` (Revisions) - -### 2. Data Integrity - -- **Foreign Key Constraints**: ใช้ FK ทุกความสัมพันธ์เพื่อรักษาความสมบูรณ์ของข้อมูล -- **Soft Delete**: ใช้ `deleted_at` แทนการลบข้อมูลจริง เพื่อรักษาประวัติ -- **Optimistic Locking**: ใช้ `version` column ใน `document_number_counters` ป้องกัน Race Condition - -### 3. Flexibility & Extensibility - -- **JSON Details Field**: เก็บข้อมูลเฉพาะประเภทใน `correspondence_revisions.details` -- **Virtual Columns**: สร้าง Index จาก JSON fields สำหรับ Performance -- **Master Data Tables**: แยกข้อมูล Master (Types, Status, Codes) เพื่อความยืดหยุ่น - -### 4. Security & Audit - -- **RBAC (Role-Based Access Control)**: ระบบสิทธิ์แบบ Hierarchical Scope -- **Audit Trail**: บันทึกผู้สร้าง/แก้ไข และเวลาในทุกตาราง -- **Two-Phase File Upload**: ป้องกันไฟล์ขยะด้วย Temporary Storage - -# 2. Database Schema Overview (ERD) -### Entity Relationship Diagram - -```mermaid -erDiagram - %% Core Entities - organizations ||--o{ users : "employs" - projects ||--o{ contracts : "contains" - projects ||--o{ correspondences : "manages" - - %% RBAC - users ||--o{ user_assignments : "has" - roles ||--o{ user_assignments : "assigned_to" - roles ||--o{ role_permissions : "has" - permissions ||--o{ role_permissions : "granted_by" - - %% Correspondences - correspondences ||--o{ correspondence_revisions : "has_revisions" - correspondence_types ||--o{ correspondences : "categorizes" - correspondence_status ||--o{ correspondence_revisions : "defines_state" - disciplines ||--o{ correspondences : "classifies" - - %% RFAs - rfas ||--o{ rfa_revisions : "has_revisions" - rfa_types ||--o{ rfas : "categorizes" - rfa_status_codes ||--o{ rfa_revisions : "defines_state" - rfa_approve_codes ||--o{ rfa_revisions : "defines_result" - disciplines ||--o{ rfas : "classifies" - - %% Drawings - shop_drawings ||--o{ shop_drawing_revisions : "has_revisions" - shop_drawing_main_categories ||--o{ shop_drawings : "categorizes" - shop_drawing_sub_categories ||--o{ shop_drawings : "sub_categorizes" - - %% Attachments - attachments ||--o{ correspondence_attachments : "attached_to" - correspondences ||--o{ correspondence_attachments : "has" -``` - ---- - -# 3. Data Dictionary V1.8.0 - -> หมายเหตุ: PK = Primary Key, FK = Foreign Key, AI = AUTO_INCREMENT. รูปแบบ Soft Delete จะปรากฏ Column `deleted_at DATETIME NULL` เป็นมาตรฐาน - -## **1. 🏢 Core & Master Data Tables (องค์กร, โครงการ, สัญญา)** - -### 1.1 organization_roles - -* * Purpose **: MASTER TABLE FOR organization role TYPES IN the system | COLUMN Name | Data TYPE | Constraints | Description | | ----------- | ----------- | --------------------------- | ---------------------------------------------------------------- | -| id | INT | PRIMARY KEY, -AUTO_INCREMENT | UNIQUE identifier FOR organization role | | role_name | VARCHAR(20) | NOT NULL, -UNIQUE | Role name ( - CONTRACTOR, - THIRD PARTY -) | -| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | -| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp | -| deleted_at | DATETIME | NULL | Soft delete timestamp | ** INDEXES **: - PRIMARY KEY (id) - UNIQUE (role_name) ** Business Rules **: - Predefined system roles FOR organization TYPES - Cannot be deleted IF referenced by organizations --- - -### 1.2 organizations - -* * Purpose **: MASTER TABLE storing ALL organizations involved IN the system | COLUMN Name | Data TYPE | Constraints | Description | | ----------------- | ------------ | ----------------------------------- | ---------------------------------------- | -| id | INT | PRIMARY KEY, -AUTO_INCREMENT | UNIQUE identifier FOR organization | | organization_code | VARCHAR(20) | NOT NULL, -UNIQUE | Organization code (e.g., 'กทท.', 'TEAM') | | organization_name | VARCHAR(255) | NOT NULL | FULL organization name | | is_active | BOOLEAN | DEFAULT TRUE | Active STATUS (1 = active, 0 = inactive) | | created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | | updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last -UPDATE timestamp | -| deleted_at | DATETIME | NULL | Soft delete timestamp | ** INDEXES **: - PRIMARY KEY (id) - UNIQUE (organization_code) - INDEX (is_active) ** Relationships **: - Referenced by: users, - project_organizations, - contract_organizations, - correspondences, - circulations --- - - ### 1.3 projects - - * * Purpose **: MASTER TABLE FOR ALL projects IN the system | COLUMN Name | Data TYPE | Constraints | Description | | ------------ | ------------ | --------------------------- | ----------------------------- | - | id | INT | PRIMARY KEY, - AUTO_INCREMENT | UNIQUE identifier FOR project | | project_code | VARCHAR(50) | NOT NULL, - UNIQUE | Project code (e.g., 'LCBP3') | | project_name | VARCHAR(255) | NOT NULL | FULL project name | | is_active | TINYINT(1) | DEFAULT 1 | Active STATUS | -| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | -| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp | -| deleted_at | DATETIME | NULL | Soft delete timestamp | -** INDEXES **: - PRIMARY KEY (id) - UNIQUE (project_code) - INDEX (is_active) ** Relationships **: - Referenced by: contracts, - correspondences, - document_number_formats, - drawings --- - - ### 1.4 contracts - - * * Purpose **: MASTER TABLE FOR contracts within projects | COLUMN Name | Data TYPE | Constraints | Description | | ------------- | ------------ | ----------------------------------- | ------------------------------ | - | id | INT | PRIMARY KEY, - AUTO_INCREMENT | UNIQUE identifier FOR contract | | project_id | INT | NOT NULL, - FK | Reference TO projects TABLE | | contract_code | VARCHAR(50) | NOT NULL, - UNIQUE | Contract code | | contract_name | VARCHAR(255) | NOT NULL | FULL contract name | | description | TEXT | NULL | Contract description | | start_date | DATE | NULL | Contract START date | | end_date | DATE | NULL | Contract -END date | | is_active | BOOLEAN | DEFAULT TRUE | Active STATUS | | created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | | updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last -UPDATE timestamp | -| deleted_at | DATETIME | NULL | Soft delete timestamp | ** INDEXES **: - PRIMARY KEY (id) - UNIQUE (contract_code) - FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE - INDEX (project_id, is_active) ** Relationships **: - Parent: projects - Referenced by: contract_organizations, - user_assignments --- - - ### 1.5 disciplines (NEW v1.5.1) - - * * Purpose **: เก็บข้อมูลสาขางาน (Disciplines) แยกตามสัญญา (Req 6B) | COLUMN Name | Data TYPE | Constraints | Description | |: -------------- | :----------- | :----------- | :--------------------- | - | id | INT | PK, - AI | UNIQUE identifier | | contract_id | INT | FK, - NOT NULL | ผูกกับสัญญา | | discipline_code | VARCHAR(10) | NOT NULL | รหัสสาขา (เช่น GEN, STR) | | code_name_th | VARCHAR(255) | NULL | ชื่อไทย | | code_name_en | VARCHAR(255) | NULL | ชื่ออังกฤษ | | is_active | TINYINT(1) | DEFAULT 1 | สถานะการใช้งาน | ** INDEXES **: - UNIQUE (contract_id, discipline_code) --- - - ## **2. 👥 Users & RBAC Tables (ผู้ใช้, สิทธิ์, บทบาท)** - - ### 2.1 users - - * * Purpose **: MASTER TABLE storing ALL system users | COLUMN Name | Data TYPE | Constraints | Description | | ----------------------- | ------------ | ----------------------------------- | -------------------------------- | - | user_id | INT | PRIMARY KEY, - AUTO_INCREMENT | UNIQUE identifier FOR user | | username | VARCHAR(50) | NOT NULL, - UNIQUE | Login username | | password_hash | VARCHAR(255) | NOT NULL | Hashed PASSWORD (bcrypt) | | first_name | VARCHAR(50) | NULL | User 's first name | -| last_name | VARCHAR(50) | NULL | User' s last name | | email | VARCHAR(100) | NOT NULL, - UNIQUE | Email address | | line_id | VARCHAR(100) | NULL | LINE messenger ID | | primary_organization_id | INT | NULL, - FK | PRIMARY organization affiliation | | is_active | TINYINT(1) | DEFAULT 1 | Active STATUS | | failed_attempts | INT | DEFAULT 0 | Failed login attempts counter | | locked_until | DATETIME | NULL | Account LOCK expiration time | | last_login_at | TIMESTAMP | NULL | Last successful login timestamp | | created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | | updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last -UPDATE timestamp | | deleted_at | DATETIME | NULL | Deleted at | ** INDEXES **: - PRIMARY KEY (user_id) - UNIQUE (username) - UNIQUE (email) - FOREIGN KEY (primary_organization_id) REFERENCES organizations(id) ON DELETE -SET NULL - INDEX (is_active) - INDEX (email) ** Relationships **: - Parent: organizations (primary_organization_id) - Referenced by: user_assignments, - audit_logs, - notifications, - circulation_routings --- - - ### 2.2 roles - - * * Purpose **: MASTER TABLE defining system roles WITH scope levels | COLUMN Name | Data TYPE | Constraints | Description | | ----------- | ------------ | --------------------------- | ---------------------------------------------------- | - | role_id | INT | PRIMARY KEY, - AUTO_INCREMENT | UNIQUE identifier FOR role | | role_name | VARCHAR(100) | NOT NULL | Role name (e.g., 'Superadmin', 'Document Control') | | scope | ENUM | NOT NULL | Scope LEVEL: GLOBAL, - Organization, - Project, - Contract | | description | TEXT | NULL | Role description | | is_system | BOOLEAN | DEFAULT FALSE | System role flag (cannot be deleted) | -| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | -| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp | -| deleted_at | DATETIME | NULL | Soft delete timestamp | -** INDEXES **: - PRIMARY KEY (role_id) - INDEX (scope) ** Relationships **: - Referenced by: role_permissions, - user_assignments --- - - ### 2.3 permissions - - * * Purpose **: MASTER TABLE defining ALL system permissions | COLUMN Name | Data TYPE | Constraints | Description | | --------------- | ------------ | --------------------------- | ------------------------------------------------------ | - | permission_id | INT | PRIMARY KEY, - AUTO_INCREMENT | UNIQUE identifier FOR permission | | permission_name | VARCHAR(100) | NOT NULL, - UNIQUE | Permission code (e.g., 'rfas.create', 'document.view') | | description | TEXT | NULL | Permission description | | module | VARCHAR(50) | NULL | Related module name | | scope_level | ENUM | NULL | Scope: GLOBAL, - ORG, - PROJECT | | is_active | TINYINT(1) | DEFAULT 1 | Active STATUS | -| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | -| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp | -| deleted_at | DATETIME | NULL | Soft delete timestamp | -** INDEXES **: - PRIMARY KEY (permission_id) - UNIQUE (permission_name) - INDEX (module) - INDEX (scope_level) - INDEX (is_active) ** Relationships **: - Referenced by: role_permissions --- - - ### 2.4 role_permissions - - * * Purpose **: Junction TABLE mapping roles TO permissions (M :N) | COLUMN Name | Data TYPE | Constraints | Description | | ------------- | --------- | --------------- | ------------------------------ | - | role_id | INT | PRIMARY KEY, - FK | Reference TO roles TABLE | | permission_id | INT | PRIMARY KEY, - FK | Reference TO permissions TABLE | ** INDEXES **: - PRIMARY KEY (role_id, permission_id) - FOREIGN KEY (role_id) REFERENCES roles(role_id) ON DELETE CASCADE - FOREIGN KEY (permission_id) REFERENCES permissions(permission_id) ON DELETE CASCADE - INDEX (permission_id) ** Relationships **: - Parent: roles, - permissions --- - - ### 2.5 user_assignments - - * * Purpose **: Junction TABLE assigning users TO roles WITH scope context | COLUMN Name | Data TYPE | Constraints | Description | | ------------------- | --------- | --------------------------- | ---------------------------------- | - | id | INT | PRIMARY KEY, - AUTO_INCREMENT | UNIQUE identifier | | user_id | INT | NOT NULL, - FK | Reference TO users TABLE | | role_id | INT | NOT NULL, - FK | Reference TO roles TABLE | | organization_id | INT | NULL, - FK | Organization scope (IF applicable) | | project_id | INT | NULL, - FK | Project scope (IF applicable) | | contract_id | INT | NULL, - FK | Contract scope (IF applicable) | | assigned_by_user_id | INT | NULL, - FK | User who made the assignment | | assigned_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Assignment timestamp | ** INDEXES **: - PRIMARY KEY (id) - FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE - FOREIGN KEY (role_id) REFERENCES roles(role_id) ON DELETE CASCADE - FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE - FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE - FOREIGN KEY (contract_id) REFERENCES contracts(id) ON DELETE CASCADE - FOREIGN KEY (assigned_by_user_id) REFERENCES users(user_id) - INDEX (user_id, role_id) - INDEX (organization_id) - INDEX (project_id) - INDEX (contract_id) ** Relationships **: - Parent: users, - roles, - organizations, - projects, - contracts --- - - ### 2.6 project_organizations - - * * Purpose **: Junction TABLE linking projects TO participating organizations (M :N) | COLUMN Name | Data TYPE | Constraints | Description | | --------------- | --------- | --------------- | -------------------------------- | - | project_id | INT | PRIMARY KEY, - FK | Reference TO projects TABLE | | organization_id | INT | PRIMARY KEY, - FK | Reference TO organizations TABLE | ** INDEXES **: - PRIMARY KEY (project_id, organization_id) - FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE - FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE - INDEX (organization_id) ** Relationships **: - Parent: projects, - organizations --- - - ### 2.7 contract_organizations - - * * Purpose **: Junction TABLE linking contracts TO participating organizations WITH roles (M :N) | COLUMN Name | Data TYPE | Constraints | Description | | ---------------- | ------------ | --------------- | ------------------------------------------------------------------------- | - | contract_id | INT | PRIMARY KEY, - FK | Reference TO contracts TABLE | | organization_id | INT | PRIMARY KEY, - FK | Reference TO organizations TABLE | | role_in_contract | VARCHAR(100) | NULL | Organization 's role in contract (Owner, Designer, Consultant, Contractor) | - -**Indexes**: - -* PRIMARY KEY (contract_id, organization_id) -* FOREIGN KEY (contract_id) REFERENCES contracts(id) ON DELETE CASCADE -* FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE -* INDEX (organization_id) -* INDEX (role_in_contract) - -**Relationships**: - -* Parent: contracts, organizations - ---- - -### 2.8 user_preferences (NEW v1.5.1) - -**Purpose**: เก็บการตั้งค่าส่วนตัวของผู้ใช้ (Req 5.5, 6.8.3) - -| Column Name | Data Type | Constraints | Description | -| :----------- | :---------- | :---------------- | :-------------- | -| user_id | INT | PK, FK | User ID | -| notify_email | BOOLEAN | DEFAULT TRUE | รับอีเมลแจ้งเตือน | -| notify_line | BOOLEAN | DEFAULT TRUE | รับไลน์แจ้งเตือน | -| digest_mode | BOOLEAN | DEFAULT FALSE | รับแจ้งเตือนแบบรวม | -| ui_theme | VARCHAR(20) | DEFAULT ' light ' | UI Theme | - ---- - -### 2.9 refresh_tokens (NEW v1.5.1) - -**Purpose**: เก็บ Refresh Tokens สำหรับการทำ Authentication และ Token Rotation - -| Column Name | Data Type | Constraints | Description | -| :---------------- | :----------- | :------------------------ | :------------------------------------ | -| token_id | INT | PK, AI | Unique Token ID | -| user_id | INT | FK, NOT NULL | เจ้าของ Token | -| token_hash | VARCHAR(255) | NOT NULL | Hash ของ Refresh Token (Security) | -| expires_at | DATETIME | NOT NULL | วันหมดอายุของ Token | -| is_revoked | BOOLEAN | DEFAULT FALSE | สถานะถูกยกเลิก (True = ใช้งานไม่ได้) | -| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | เวลาที่สร้าง | -| replaced_by_token | VARCHAR(255) | NULL | Token ใหม่ที่มาแทนที่ (กรณี Token Rotation) | - -**Indexes**: - -* PRIMARY KEY (token_id) -* FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE -* INDEX (user_id) - -**Relationships**: - -* Parent: users - ---- - -## **3. ✉️ Correspondences Tables (เอกสารหลัก, Revisions, Workflows)** - -### 3.1 correspondence_types - -**Purpose**: Master table for correspondence document types - -| Column Name | Data Type | Constraints | Description | -| ----------- | ------------ | --------------------------- | --------------------------------------------------- | -| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique identifier | -| type_code | VARCHAR(50) | NOT NULL, UNIQUE | Type code (e.g., ' RFA ', ' RFI ', ' TRANSMITTAL ') | -| type_name | VARCHAR(255) | NOT NULL | Full type name | -| sort_order | INT | DEFAULT 0 | Display order | -| is_active | TINYINT(1) | DEFAULT 1 | Active status | -| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | -| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Last update timestamp | -| deleted_at | DATETIME | NULL | Soft delete timestamp | - -**Indexes**: - -* PRIMARY KEY (id) -* UNIQUE (type_code) -* INDEX (is_active) -* INDEX (sort_order) - -**Relationships**: - -* Referenced by: correspondences, document_number_formats, document_number_counters - ---- - -### 3.2 correspondence_sub_types (NEW v1.5.1) - -**Purpose**: เก็บประเภทหนังสือย่อย (Sub Types) สำหรับ Mapping เลขรหัส (Req 6B) - -| Column Name | Data Type | Constraints | Description | -| :--------------------- | :----------- | :----------- | :------------------------ | -| id | INT | PK, AI | Unique identifier | -| contract_id | INT | FK, NOT NULL | ผูกกับสัญญา | -| correspondence_type_id | INT | FK, NOT NULL | ผูกกับประเภทเอกสารหลัก | -| sub_type_code | VARCHAR(20) | NOT NULL | รหัสย่อย (เช่น MAT, SHP) | -| sub_type_name | VARCHAR(255) | NULL | ชื่อประเภทหนังสือย่อย | -| sub_type_number | VARCHAR(10) | NULL | เลขรหัสสำหรับ Running Number | - ---- - -### 3.3 correspondences (UPDATE v1.7.0) - -**Purpose**: Master table for correspondence documents (non-revisioned data) - -| Column Name | Data Type | Constraints | Description | -| ------------------------- | ------------ | --------------------------- | ------------------------------------------ | -| id | INT | PRIMARY KEY, AUTO_INCREMENT | Master correspondence ID | -| correspondence_number | VARCHAR(100) | NOT NULL | Document number (from numbering system) | -| correspondence_type_id | INT | NOT NULL, FK | Reference to correspondence_types | -| **discipline_id** | **INT** | **NULL, FK** | **[NEW] สาขางาน (ถ้ามี)** | -| is_internal_communication | TINYINT(1) | DEFAULT 0 | Internal (1) or external (0) communication | -| project_id | INT | NOT NULL, FK | Reference to projects table | -| originator_id | INT | NULL, FK | Originating organization | -| created_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | -| created_by | INT | NULL, FK | User who created the record | -| deleted_at | DATETIME | NULL | Soft delete timestamp | - -**Indexes**: - -* PRIMARY KEY (id) -* FOREIGN KEY (correspondence_type_id) REFERENCES correspondence_types(id) ON DELETE RESTRICT -* **FOREIGN KEY (discipline_id) REFERENCES disciplines(id) ON DELETE SET NULL** -* FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE -* FOREIGN KEY (originator_id) REFERENCES organizations(id) ON DELETE SET NULL -* FOREIGN KEY (created_by) REFERENCES users(user_id) ON DELETE SET NULL -* UNIQUE KEY (project_id, correspondence_number) -* INDEX (correspondence_type_id) -* INDEX (originator_id) -* INDEX (deleted_at) - -**Relationships**: - -* Parent: correspondence_types, **disciplines**, projects, organizations, users -* Children: correspondence_revisions, correspondence_recipients, correspondence_tags, correspondence_references, correspondence_attachments, circulations, transmittals - ---- - -### 3.4 correspondence_revisions (UPDATE v1.7.0) - -**Purpose**: Child table storing revision history of correspondences (1:N) - -| Column Name | Data Type | Constraints | Description | -| ------------------------ | ------------ | --------------------------------- | -------------------------------------------------------- | -| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique revision ID | -| correspondence_id | INT | NOT NULL, FK | Master correspondence ID | -| revision_number | INT | NOT NULL | Revision sequence (0, 1, 2...) | -| revision_label | VARCHAR(10) | NULL | Display revision (A, B, 1.1...) | -| is_current | BOOLEAN | DEFAULT FALSE | Current revision flag | -| correspondence_status_id | INT | NOT NULL, FK | Current status of this revision | -| title | VARCHAR(255) | NOT NULL | Document title | -| document_date | DATE | NULL | Document date | -| issued_date | DATETIME | NULL | Issue date | -| received_date | DATETIME | NULL | Received date | -| due_date | DATETIME | NULL | Due date for response | -| description | TEXT | NULL | Revision description | -| details | JSON | NULL | Type-specific details (e.g., RFI questions) | -| created_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | Revision creation timestamp | -| created_by | INT | NULL, FK | User who created revision | -| updated_by | INT | NULL, FK | User who last updated | -| v_ref_project_id | INT | GENERATED ALWAYS AS (...) VIRTUAL | Virtual Column ดึง Project ID จาก JSON details เพื่อทำ Index | - -| v_doc_subtype | VARCHAR(50) | GENERATED ALWAYS AS (...) VIRTUAL | Virtual Column ดึง Type จาก JSON details | -| schema_version | INT | DEFAULT 1 | Version of the schema used with this details | - -**Indexes**: - -* PRIMARY KEY (id) -* FOREIGN KEY (correspondence_id) REFERENCES correspondences(id) ON DELETE CASCADE -* FOREIGN KEY (correspondence_status_id) REFERENCES correspondence_status(id) ON DELETE RESTRICT -* FOREIGN KEY (created_by) REFERENCES users(user_id) ON DELETE SET NULL -* FOREIGN KEY (updated_by) REFERENCES users(user_id) ON DELETE SET NULL -* UNIQUE KEY (correspondence_id, revision_number) -* UNIQUE KEY (correspondence_id, is_current) -* INDEX (correspondence_status_id) -* INDEX (is_current) -* INDEX (document_date) -* INDEX (issued_date) -* INDEX (v_ref_project_id) -* INDEX (v_doc_subtype) - ---- - -### 3.5 correspondence_recipients - -**Purpose**: Junction table for correspondence recipients (TO/CC) (M:N) - -| Column Name | Data Type | Constraints | Description | -| ------------------------- | -------------------- | --------------- | ---------------------------- | -| correspondence_id | INT | PRIMARY KEY, FK | Reference to correspondences | -| recipient_organization_id | INT | PRIMARY KEY, FK | Recipient organization | -| recipient_type | ENUM(' TO ', ' CC ') | PRIMARY KEY | Recipient type | - -**Indexes**: - -* PRIMARY KEY (correspondence_id, recipient_organization_id, recipient_type) -* FOREIGN KEY (correspondence_id) REFERENCES correspondence_revisions(correspondence_id) ON DELETE CASCADE -* FOREIGN KEY (recipient_organization_id) REFERENCES organizations(id) ON DELETE RESTRICT -* INDEX (recipient_organization_id) -* INDEX (recipient_type) - -**Relationships**: - -* Parent: correspondences, organizations - ---- - -### 3.6 tags - -**Purpose**: Master table for document tagging system - -| Column Name | Data Type | Constraints | Description | -| ----------- | ------------ | ----------------------------------- | ------------------------- | -| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique tag ID | -| tag_name | VARCHAR(100) | NOT NULL, UNIQUE | Tag name | -| description | TEXT | NULL | Tag description | -| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | -| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp | - -**Indexes**: - -* PRIMARY KEY (id) -* UNIQUE (tag_name) -* INDEX (tag_name) - For autocomplete - -**Relationships**: - -* Referenced by: correspondence_tags - ---- - -### 3.7 correspondence_tags - -**Purpose**: Junction table linking correspondences to tags (M:N) - -| Column Name | Data Type | Constraints | Description | -| ----------------- | --------- | --------------- | ---------------------------- | -| correspondence_id | INT | PRIMARY KEY, FK | Reference to correspondences | -| tag_id | INT | PRIMARY KEY, FK | Reference to tags | - -**Indexes**: - -* PRIMARY KEY (correspondence_id, tag_id) -* FOREIGN KEY (correspondence_id) REFERENCES correspondences(id) ON DELETE CASCADE -* FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE -* INDEX (tag_id) - -**Relationships**: - -* Parent: correspondences, tags - ---- - -### 3.8 correspondence_references - -**Purpose**: Junction table for cross-referencing correspondences (M:N) - -| Column Name | Data Type | Constraints | Description | -| --------------------- | --------- | --------------- | ------------------------------------- | -| src_correspondence_id | INT | PRIMARY KEY, FK | Source correspondence ID | -| tgt_correspondence_id | INT | PRIMARY KEY, FK | Target (referenced) correspondence ID | - -**Indexes**: - -* PRIMARY KEY (src_correspondence_id, tgt_correspondence_id) -* FOREIGN KEY (src_correspondence_id) REFERENCES correspondences(id) ON DELETE CASCADE -* FOREIGN KEY (tgt_correspondence_id) REFERENCES correspondences(id) ON DELETE CASCADE -* INDEX (tgt_correspondence_id) - -**Relationships**: - -* Parent: correspondences (both sides) - ---- - -## **4. 📐 approval: RFA Tables (เอกสารขออนุมัติ, Workflows)** - -### 4.1 rfa_types (UPDATE v1.7.0) - -**Purpose**: Master table for RFA (Request for Approval) types - -| Column Name | Data Type | Constraints | Description | -| :----------- | :----------- | :-------------------------- | :------------------------------ | -| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique identifier | -| contract_id | INT | NOT NULL, FK | Contract reference | -| type_code | VARCHAR(20) | NOT NULL | Type code (DWG, DOC, MAT, etc.) | -| type_name_th | VARCHAR(100) | NOT NULL | Full type name (TH) | -| type_name_en | VARCHAR(100) | NOT NULL | Full type name (EN) | -| remark | TEXT | NULL | Remark | -| is_active | TINYINT(1) | DEFAULT 1 | Active status | - -**Indexes**: - -* PRIMARY KEY (id) -* UNIQUE (contract_id, type_code) -* FOREIGN KEY (contract_id) REFERENCES contracts(id) ON DELETE CASCADE -* INDEX (is_active) - -**Relationships**: - -* Referenced by: rfas - ---- - -### 4.2 rfa_status_codes - -**Purpose**: Master table for RFA status codes - -| Column Name | Data Type | Constraints | Description | -| ----------- | ------------ | --------------------------- | --------------------------------- | -| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique identifier | -| status_code | VARCHAR(20) | NOT NULL, UNIQUE | Status code (DFT, FAP, FRE, etc.) | -| status_name | VARCHAR(100) | NOT NULL | Full status name | -| description | TEXT | NULL | Status description | -| sort_order | INT | DEFAULT 0 | Display order | -| is_active | TINYINT(1) | DEFAULT 1 | Active status | - -**Indexes**: - -* PRIMARY KEY (id) -* UNIQUE (status_code) -* INDEX (is_active) -* INDEX (sort_order) - -**Relationships**: - -* Referenced by: rfa_revisions - ---- - -### 4.3 rfa_approve_codes - -**Purpose**: Master table for RFA approval result codes - -| Column Name | Data Type | Constraints | Description | -| ------------ | ------------ | --------------------------- | -------------------------------- | -| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique identifier | -| approve_code | VARCHAR(20) | NOT NULL, UNIQUE | Approval code (1A, 1C, 3R, etc.) | -| approve_name | VARCHAR(100) | NOT NULL | Full approval name | -| description | TEXT | NULL | Code description | -| sort_order | INT | DEFAULT 0 | Display order | -| is_active | TINYINT(1) | DEFAULT 1 | Active status | - -**Indexes**: - -* PRIMARY KEY (id) -* UNIQUE (approve_code) -* INDEX (is_active) -* INDEX (sort_order) - -**Relationships**: - -* Referenced by: rfa_revisions - ---- - -### 4.4 rfas (UPDATE v1.7.0) - -**Purpose**: Master table for RFA documents (non-revisioned data) - -| Column Name | Data Type | Constraints | Description | -| :---------- | :-------- | :------------------------ | :------------------------------------------ | -| id | INT | PK, FK | Master RFA ID (Shared with correspondences) | -| rfa_type_id | INT | NOT NULL, FK | Reference to rfa_types | -| created_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | -| created_by | INT | NULL, FK | User who created the record | -| deleted_at | DATETIME | NULL | Soft delete timestamp | - -**Indexes**: - -* PRIMARY KEY (id) -* FOREIGN KEY (id) REFERENCES correspondences(id) ON DELETE CASCADE -* FOREIGN KEY (rfa_type_id) REFERENCES rfa_types(id) -* FOREIGN KEY (created_by) REFERENCES users(user_id) ON DELETE SET NULL -* INDEX (rfa_type_id) -* INDEX (deleted_at) - -**Relationships**: - -* Parent: correspondences, rfa_types, users -* Children: rfa_revisions - ---- - -### 4.5 rfa_revisions (UPDATE v1.7.0) - -**Purpose**: Child table storing revision history of RFAs (1:N) - -| Column Name | Data Type | Constraints | Description | -| ----------- | --------- | --------------------------- | ------------------ | -| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique revision ID | - -| rfa_id | INT | NOT NULL, FK | Master RFA ID | -| revision_number | INT | NOT NULL | Revision sequence (0, 1, 2...) | -| revision_label | VARCHAR(10) | NULL | Display revision (A, B, 1.1...) | -| is_current | BOOLEAN | DEFAULT FALSE | Current revision flag | -| rfa_status_code_id | INT | NOT NULL, FK | Current RFA status | -| rfa_approve_code_id | INT | NULL, FK | Approval result code | -| title | VARCHAR(255) | NOT NULL | RFA title | -| document_date | DATE | NULL | Document date | -| issued_date | DATE | NULL | Issue date for approval | -| received_date | DATETIME | NULL | Received date | -| approved_date | DATE | NULL | Approval date | -| description | TEXT | NULL | Revision description | -| created_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | Revision creation timestamp | -| created_by | INT | NULL, FK | User who created revision | -| updated_by | INT | NULL, FK | User who last updated | -| details | JSON | NULL | Type-specific details (e.g., RFI questions) | -| v_ref_drawing_count | INT | GENERATED ALWAYS AS (...) VIRTUAL | Virtual Column ดึง Drawing Count จาก JSON details เพื่อทำ Index | -| schema_version | INT | DEFAULT 1 | Version of the schema used with this details | - -**Indexes**: - -* PRIMARY KEY (id) -* FOREIGN KEY (rfa_id) REFERENCES rfas(id) ON DELETE CASCADE -* FOREIGN KEY (rfa_status_code_id) REFERENCES rfa_status_codes(id) -* FOREIGN KEY (rfa_approve_code_id) REFERENCES rfa_approve_codes(id) ON DELETE SET NULL -* FOREIGN KEY (created_by) REFERENCES users(user_id) ON DELETE SET NULL -* FOREIGN KEY (updated_by) REFERENCES users(user_id) ON DELETE SET NULL -* UNIQUE KEY (rfa_id, revision_number) -* UNIQUE KEY (rfa_id, is_current) -* INDEX (rfa_status_code_id) -* INDEX (rfa_approve_code_id) -* INDEX (is_current) -* INDEX (v_ref_drawing_count): ตัวอย่างการ Index ข้อมูลตัวเลขใน JSON - -**Relationships**: - -* Parent: correspondences, rfas, rfa_status_codes, rfa_approve_codes, users -* Children: rfa_items - ---- - -### 4.6 rfa_items - -**Purpose**: Junction table linking RFA revisions to shop drawing revisions (M:N) - -| Column Name | Data Type | Constraints | Description | -| :----------------------- | :-------- | :-------------- | :----------------------- | -| rfa_revision_id | INT | PRIMARY KEY, FK | RFA Revision ID | -| shop_drawing_revision_id | INT | PRIMARY KEY, FK | Shop drawing revision ID | - -**Indexes**: - -* PRIMARY KEY (rfa_revision_id, shop_drawing_revision_id) -* FOREIGN KEY (rfa_revision_id) REFERENCES rfa_revisions(id) ON DELETE CASCADE -* FOREIGN KEY (shop_drawing_revision_id) REFERENCES shop_drawing_revisions(id) ON DELETE CASCADE -* INDEX (shop_drawing_revision_id) - -**Relationships**: - -* Parent: rfa_revisions, shop_drawing_revisions - -**Business Rules**: - -* Used primarily for RFA type = ' DWG ' (Shop Drawing) -* One RFA can contain multiple shop drawings -* One shop drawing can be referenced by multiple RFAs - ---- - - ---- - -## **5. 📐 Drawings Tables (แบบ, หมวดหมู่)** - -### 5.1 contract_drawing_volumes - -**Purpose**: Master table for contract drawing volume classification - -| Column Name | Data Type | Constraints | Description | -| ----------- | ------------ | ----------------------------------- | ------------------------- | -| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique volume ID | -| project_id | INT | NOT NULL, FK | Reference to projects | -| volume_code | VARCHAR(50) | NOT NULL | Volume code | -| volume_name | VARCHAR(255) | NOT NULL | Volume name | -| description | TEXT | NULL | Volume description | -| sort_order | INT | DEFAULT 0 | Display order | -| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | -| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp | - -**Indexes**: - -* PRIMARY KEY (id) -* FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE -* UNIQUE KEY (project_id, volume_code) -* INDEX (sort_order) - -**Relationships**: - -* Parent: projects -* Referenced by: contract_drawings - -**Business Rules**: - -* Volume codes must be unique within a project -* Used for organizing large sets of contract drawings - ---- - -### 5.2 contract_drawing_cats - -**Purpose**: Master table for contract drawing main categories - -| Column Name | Data Type | Constraints | Description | -| ----------- | ------------ | ----------------------------------- | ------------------------- | -| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique category ID | -| project_id | INT | NOT NULL, FK | Reference to projects | -| cat_code | VARCHAR(50) | NOT NULL | Category code | -| cat_name | VARCHAR(255) | NOT NULL | Category name | -| description | TEXT | NULL | Category description | -| sort_order | INT | DEFAULT 0 | Display order | -| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | -| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp | - -**Indexes**: - -* PRIMARY KEY (id) -* FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE -* UNIQUE KEY (project_id, cat_code) -* INDEX (sort_order) - -**Relationships**: - -* Parent: projects -* Referenced by: contract_drawing_subcat_cat_maps - -**Business Rules**: - -* Category codes must be unique within a project -* Hierarchical relationship with sub-categories via mapping table - ---- - -### 5.3 contract_drawing_sub_cats - -**Purpose**: Master table for contract drawing sub-categories - -| Column Name | Data Type | Constraints | Description | -| ------------ | ------------ | ----------------------------------- | ------------------------- | -| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique sub-category ID | -| project_id | INT | NOT NULL, FK | Reference to projects | -| sub_cat_code | VARCHAR(50) | NOT NULL | Sub-category code | -| sub_cat_name | VARCHAR(255) | NOT NULL | Sub-category name | -| description | TEXT | NULL | Sub-category description | -| sort_order | INT | DEFAULT 0 | Display order | -| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | -| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp | - -**Indexes**: - -* PRIMARY KEY (id) -* FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE -* UNIQUE KEY (project_id, sub_cat_code) -* INDEX (sort_order) - -**Relationships**: - -* Parent: projects -* Referenced by: contract_drawings, contract_drawing_subcat_cat_maps - -**Business Rules**: - -* Sub-category codes must be unique within a project -* Can be mapped to multiple main categories via mapping table - ---- - -### 5.4 contract_drawing_subcat_cat_maps (UPDATE v1.7.0) - -**Purpose**: Junction table mapping sub-categories to main categories (M:N) - -| Column Name | Data Type | Constraints | Description | -| ----------- | --------- | ------------------------------- | -------------------------- | -| **id** | **INT** | **PRIMARY KEY, AUTO_INCREMENT** | **Unique mapping ID** | -| project_id | INT | NOT NULL, FK | Reference to projects | -| sub_cat_id | INT | NOT NULL, FK | Reference to sub-category | -| cat_id | INT | NOT NULL, FK | Reference to main category | - -**Indexes**: - -* PRIMARY KEY (id) -* **UNIQUE KEY (project_id, sub_cat_id, cat_id)** -* FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE -* FOREIGN KEY (sub_cat_id) REFERENCES contract_drawing_sub_cats(id) ON DELETE CASCADE -* FOREIGN KEY (cat_id) REFERENCES contract_drawing_cats(id) ON DELETE CASCADE -* INDEX (sub_cat_id) -* INDEX (cat_id) - -**Relationships**: - -* Parent: projects, contract_drawing_sub_cats, contract_drawing_cats -* Referenced by: contract_drawings - -**Business Rules**: - -* Allows flexible categorization -* One sub-category can belong to multiple main categories -* Composite uniqueness enforced via UNIQUE constraint - ---- - -### 5.5 contract_drawings (UPDATE v1.7.0) - -**Purpose**: Master table for contract drawings (from contract specifications) - -| Column Name | Data Type | Constraints | Description | -| --------------- | ------------ | ----------------------------------- | ---------------------------------------- | -| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique drawing ID | -| project_id | INT | NOT NULL, FK | Reference to projects | -| condwg_no | VARCHAR(255) | NOT NULL | Contract drawing number | -| title | VARCHAR(255) | NOT NULL | Drawing title | -| **map_cat_id** | **INT** | **NULL, FK** | **[CHANGED] Reference to mapping table** | -| volume_id | INT | NULL, FK | Reference to volume | -| **volume_page** | **INT** | **NULL** | **[NEW] Page number within volume** | -| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | -| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp | -| deleted_at | DATETIME | NULL | Soft delete timestamp | -| updated_by | INT | NULL, FK | User who last updated | - -**Indexes**: - -* PRIMARY KEY (id) -* FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE -* **FOREIGN KEY (map_cat_id) REFERENCES contract_drawing_subcat_cat_maps(id) ON DELETE RESTRICT** -* FOREIGN KEY (volume_id) REFERENCES contract_drawing_volumes(id) ON DELETE RESTRICT -* FOREIGN KEY (updated_by) REFERENCES users(user_id) -* UNIQUE KEY (project_id, condwg_no) -* INDEX (map_cat_id) -* INDEX (volume_id) -* INDEX (deleted_at) - -**Relationships**: - -* Parent: projects, contract_drawing_subcat_cat_maps, contract_drawing_volumes, users -* Referenced by: shop_drawing_revision_contract_refs, contract_drawing_attachments - -**Business Rules**: - -* Drawing numbers must be unique within a project -* Represents baseline/contract drawings -* Referenced by shop drawings for compliance tracking -* Soft delete preserves history -* **map_cat_id references the mapping table for flexible categorization** - ---- - -### 5.6 shop_drawing_main_categories (UPDATE v1.7.0) - -**Purpose**: Master table for shop drawing main categories (discipline-level) - -| Column Name | Data Type | Constraints | Description | -| ------------------ | ------------ | ----------------------------------- | ------------------------------------ | -| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique category ID | -| **project_id** | **INT** | **NOT NULL, FK** | **[NEW] Reference to projects** | -| main_category_code | VARCHAR(50) | NOT NULL, UNIQUE | Category code (ARCH, STR, MEP, etc.) | -| main_category_name | VARCHAR(255) | NOT NULL | Category name | -| description | TEXT | NULL | Category description | -| sort_order | INT | DEFAULT 0 | Display order | -| is_active | TINYINT(1) | DEFAULT 1 | Active status | -| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | -| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp | - -**Indexes**: - -* PRIMARY KEY (id) -* **FOREIGN KEY (project_id) REFERENCES projects(id)** -* UNIQUE (main_category_code) -* INDEX (is_active) -* INDEX (sort_order) - -**Relationships**: - -* **Parent: projects** -* Referenced by: shop_drawings, asbuilt_drawings - -**Business Rules**: - -* **[CHANGED] Project-specific categories (was global)** -* Typically represents engineering disciplines - ---- - -### 5.7 shop_drawing_sub_categories (UPDATE v1.7.0) - -**Purpose**: Master table for shop drawing sub-categories (component-level) - -| Column Name | Data Type | Constraints | Description | -| ----------------- | ------------ | ----------------------------------- | ----------------------------------------------- | -| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique sub-category ID | -| **project_id** | **INT** | **NOT NULL, FK** | **[NEW] Reference to projects** | -| sub_category_code | VARCHAR(50) | NOT NULL, UNIQUE | Sub-category code (STR-COLUMN, ARCH-DOOR, etc.) | -| sub_category_name | VARCHAR(255) | NOT NULL | Sub-category name | -| description | TEXT | NULL | Sub-category description | -| sort_order | INT | DEFAULT 0 | Display order | -| is_active | TINYINT(1) | DEFAULT 1 | Active status | -| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | -| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp | - -**Indexes**: - -* PRIMARY KEY (id) -* **FOREIGN KEY (project_id) REFERENCES projects(id)** -* UNIQUE (sub_category_code) -* INDEX (is_active) -* INDEX (sort_order) - -**Relationships**: - -* **Parent: projects** -* Referenced by: shop_drawings, asbuilt_drawings - -**Business Rules**: - -* **[CHANGED] Project-specific sub-categories (was global)** -* **[REMOVED] No longer hierarchical under main categories** -* Represents specific drawing types or components - ---- - -### 5.8 shop_drawings (UPDATE v1.7.0) - -**Purpose**: Master table for shop drawings (contractor-submitted) - -| Column Name | Data Type | Constraints | Description | -| ---------------- | ------------ | ----------------------------------- | -------------------------- | -| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique drawing ID | -| project_id | INT | NOT NULL, FK | Reference to projects | -| drawing_number | VARCHAR(100) | NOT NULL, UNIQUE | Shop drawing number | -| main_category_id | INT | NOT NULL, FK | Reference to main category | -| sub_category_id | INT | NOT NULL, FK | Reference to sub-category | -| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | -| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp | -| deleted_at | DATETIME | NULL | Soft delete timestamp | -| updated_by | INT | NULL, FK | User who last updated | - -**Indexes**: - -* PRIMARY KEY (id) -* UNIQUE (drawing_number) -* FOREIGN KEY (project_id) REFERENCES projects(id) -* FOREIGN KEY (main_category_id) REFERENCES shop_drawing_main_categories(id) -* FOREIGN KEY (sub_category_id) REFERENCES shop_drawing_sub_categories(id) -* FOREIGN KEY (updated_by) REFERENCES users(user_id) -* INDEX (project_id) -* INDEX (main_category_id) -* INDEX (sub_category_id) -* INDEX (deleted_at) - -**Relationships**: - -* Parent: projects, shop_drawing_main_categories, shop_drawing_sub_categories, users -* Children: shop_drawing_revisions - -**Business Rules**: - -* Drawing numbers are globally unique across all projects -* Represents contractor shop drawings -* Can have multiple revisions -* Soft delete preserves history -* **[CHANGED] Title moved to shop_drawing_revisions table** - ---- - -### 5.9 shop_drawing_revisions (UPDATE v1.7.0) - -**Purpose**: Child table storing revision history of shop drawings (1:N) - -| Column Name | Data Type | Constraints | Description | -| ------------------------- | ---------------- | --------------------------- | ---------------------------------------- | -| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique revision ID | -| shop_drawing_id | INT | NOT NULL, FK | Master shop drawing ID | -| revision_number | INT | NOT NULL | Revision sequence (0, 1, 2...) | -| revision_label | VARCHAR(10) | NULL | Display revision (A, B, C...) | -| revision_date | DATE | NULL | Revision date | -| **title** | **VARCHAR(500)** | **NOT NULL** | **[NEW] Drawing title** | -| description | TEXT | NULL | Revision description/changes | -| **legacy_drawing_number** | **VARCHAR(100)** | **NULL** | **[NEW] Original/legacy drawing number** | -| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Revision creation timestamp | - -**Indexes**: - -* PRIMARY KEY (id) -* FOREIGN KEY (shop_drawing_id) REFERENCES shop_drawings(id) ON DELETE CASCADE -* UNIQUE KEY (shop_drawing_id, revision_number) -* INDEX (revision_date) - -**Relationships**: - -* Parent: shop_drawings -* Referenced by: rfa_items, shop_drawing_revision_contract_refs, shop_drawing_revision_attachments, asbuilt_revision_shop_revisions_refs - -**Business Rules**: - -* Revision numbers are sequential starting from 0 -* Each revision can reference multiple contract drawings -* Each revision can have multiple file attachments -* Linked to RFAs for approval tracking -* **[NEW] Title stored at revision level for version-specific naming** -* **[NEW] legacy_drawing_number supports data migration from old systems** - ---- - -### 5.10 shop_drawing_revision_contract_refs - -**Purpose**: Junction table linking shop drawing revisions to referenced contract drawings (M:N) - -| Column Name | Data Type | Constraints | Description | -| ------------------------ | --------- | --------------- | ---------------------------------- | -| shop_drawing_revision_id | INT | PRIMARY KEY, FK | Reference to shop drawing revision | -| contract_drawing_id | INT | PRIMARY KEY, FK | Reference to contract drawing | - -**Indexes**: - -* PRIMARY KEY (shop_drawing_revision_id, contract_drawing_id) -* FOREIGN KEY (shop_drawing_revision_id) REFERENCES shop_drawing_revisions(id) ON DELETE CASCADE -* FOREIGN KEY (contract_drawing_id) REFERENCES contract_drawings(id) ON DELETE CASCADE -* INDEX (contract_drawing_id) - -**Relationships**: - -* Parent: shop_drawing_revisions, contract_drawings - -**Business Rules**: - -* Tracks which contract drawings each shop drawing revision is based on -* Ensures compliance with contract specifications -* One shop drawing revision can reference multiple contract drawings - ---- - -### 5.11 asbuilt_drawings (NEW v1.7.0) - -**Purpose**: Master table for AS Built drawings (final construction records) - -| Column Name | Data Type | Constraints | Description | -| ---------------- | ------------ | ----------------------------------- | -------------------------- | -| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique drawing ID | -| project_id | INT | NOT NULL, FK | Reference to projects | -| drawing_number | VARCHAR(100) | NOT NULL, UNIQUE | AS Built drawing number | -| main_category_id | INT | NOT NULL, FK | Reference to main category | -| sub_category_id | INT | NOT NULL, FK | Reference to sub-category | -| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | -| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp | -| deleted_at | DATETIME | NULL | Soft delete timestamp | -| updated_by | INT | NULL, FK | User who last updated | - -**Indexes**: - -* PRIMARY KEY (id) -* UNIQUE (drawing_number) -* FOREIGN KEY (project_id) REFERENCES projects(id) -* FOREIGN KEY (main_category_id) REFERENCES shop_drawing_main_categories(id) -* FOREIGN KEY (sub_category_id) REFERENCES shop_drawing_sub_categories(id) -* FOREIGN KEY (updated_by) REFERENCES users(user_id) -* INDEX (project_id) -* INDEX (main_category_id) -* INDEX (sub_category_id) -* INDEX (deleted_at) - -**Relationships**: - -* Parent: projects, shop_drawing_main_categories, shop_drawing_sub_categories, users -* Children: asbuilt_drawing_revisions - -**Business Rules**: - -* Drawing numbers are globally unique across all projects -* Represents final as-built construction drawings -* Can have multiple revisions -* Soft delete preserves history -* Uses same category structure as shop drawings - ---- - -### 5.12 asbuilt_drawing_revisions (NEW v1.7.0) - -**Purpose**: Child table storing revision history of AS Built drawings (1:N) - -| Column Name | Data Type | Constraints | Description | -| --------------------- | ------------ | --------------------------- | ------------------------------ | -| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique revision ID | -| asbuilt_drawing_id | INT | NOT NULL, FK | Master AS Built drawing ID | -| revision_number | INT | NOT NULL | Revision sequence (0, 1, 2...) | -| revision_label | VARCHAR(10) | NULL | Display revision (A, B, C...) | -| revision_date | DATE | NULL | Revision date | -| title | VARCHAR(500) | NOT NULL | Drawing title | -| description | TEXT | NULL | Revision description/changes | -| legacy_drawing_number | VARCHAR(100) | NULL | Original/legacy drawing number | -| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Revision creation timestamp | - -**Indexes**: - -* PRIMARY KEY (id) -* FOREIGN KEY (asbuilt_drawing_id) REFERENCES asbuilt_drawings(id) ON DELETE CASCADE -* UNIQUE KEY (asbuilt_drawing_id, revision_number) -* INDEX (revision_date) - -**Relationships**: - -* Parent: asbuilt_drawings -* Referenced by: asbuilt_revision_shop_revisions_refs, asbuilt_drawing_revision_attachments - -**Business Rules**: - -* Revision numbers are sequential starting from 0 -* Each revision can reference multiple shop drawing revisions -* Each revision can have multiple file attachments -* Title stored at revision level for version-specific naming -* legacy_drawing_number supports data migration from old systems - ---- - -### 5.13 asbuilt_revision_shop_revisions_refs (NEW v1.7.0) - -**Purpose**: Junction table linking AS Built drawing revisions to shop drawing revisions (M:N) - -| Column Name | Data Type | Constraints | Description | -| --------------------------- | --------- | --------------- | ---------------------------------- | -| asbuilt_drawing_revision_id | INT | PRIMARY KEY, FK | Reference to AS Built revision | -| shop_drawing_revision_id | INT | PRIMARY KEY, FK | Reference to shop drawing revision | - -**Indexes**: - -* PRIMARY KEY (asbuilt_drawing_revision_id, shop_drawing_revision_id) -* FOREIGN KEY (asbuilt_drawing_revision_id) REFERENCES asbuilt_drawing_revisions(id) ON DELETE CASCADE -* FOREIGN KEY (shop_drawing_revision_id) REFERENCES shop_drawing_revisions(id) ON DELETE CASCADE -* INDEX (shop_drawing_revision_id) - -**Relationships**: - -* Parent: asbuilt_drawing_revisions, shop_drawing_revisions - -**Business Rules**: - -* Tracks which shop drawings each AS Built drawing revision is based on -* Maintains construction document lineage -* One AS Built revision can reference multiple shop drawing revisions -* Supports traceability from final construction to approved shop drawings - ---- - -### 5.14 asbuilt_drawing_revision_attachments (NEW v1.7.0) - -**Purpose**: Junction table linking AS Built drawing revisions to file attachments (M:N) - -| Column Name | Data Type | Constraints | Description | -| --------------------------- | ------------------------------------- | --------------- | ------------------------------------- | -| asbuilt_drawing_revision_id | INT | PRIMARY KEY, FK | Reference to AS Built revision | -| attachment_id | INT | PRIMARY KEY, FK | Reference to attachment file | -| file_type | ENUM('PDF', 'DWG', 'SOURCE', 'OTHER') | NULL | File type classification | -| is_main_document | BOOLEAN | DEFAULT FALSE | Main document flag (1 = primary file) | - -**Indexes**: - -* PRIMARY KEY (asbuilt_drawing_revision_id, attachment_id) -* FOREIGN KEY (asbuilt_drawing_revision_id) REFERENCES asbuilt_drawing_revisions(id) ON DELETE CASCADE -* FOREIGN KEY (attachment_id) REFERENCES attachments(id) ON DELETE CASCADE -* INDEX (attachment_id) - -**Relationships**: - -* Parent: asbuilt_drawing_revisions, attachments - -**Business Rules**: - -* Each AS Built revision can have multiple file attachments -* File types: PDF (documents), DWG (CAD files), SOURCE (source files), OTHER (miscellaneous) -* One attachment can be marked as main document per revision -* Cascade delete when revision is deleted - ---- - -## **6. 🔄 Circulations Tables (ใบเวียนภายใน)** - -### 6.1 circulation_status_codes - -**Purpose**: Master table for circulation workflow status codes - -| Column Name | Data Type | Constraints | Description | -| ----------- | ----------- | --------------------------- | --------------------------------------------------- | -| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique status ID | -| code | VARCHAR(20) | NOT NULL, UNIQUE | Status code (OPEN, IN_REVIEW, COMPLETED, CANCELLED) | -| description | VARCHAR(50) | NOT NULL | Status description | -| sort_order | INT | DEFAULT 0 | Display order | -| is_active | TINYINT(1) | DEFAULT 1 | Active status | - -**Indexes**: - -* PRIMARY KEY (id) -* UNIQUE (code) -* INDEX (is_active) -* INDEX (sort_order) - -**Relationships**: - -* Referenced by: circulations - -**Seed Data**: 4 status codes - -* OPEN: Initial status when created -* IN_REVIEW: Under review by recipients -* COMPLETED: All recipients have responded -* CANCELLED: Withdrawn/cancelled - ---- - -### 6.2 circulations - -**Purpose**: Master table for internal circulation sheets (document routing) - -| Column Name | Data Type | Constraints | Description | -| ----------------------- | ------------ | ----------------------------------- | ----------------------------------------- | -| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique circulation ID | -| correspondence_id | INT | UNIQUE, FK | Link to correspondence (1:1 relationship) | -| organization_id | INT | NOT NULL, FK | Organization that owns this circulation | -| circulation_no | VARCHAR(100) | NOT NULL | Circulation sheet number | -| circulation_subject | VARCHAR(500) | NOT NULL | Subject/title | -| circulation_status_code | VARCHAR(20) | NOT NULL, FK | Current status code | -| created_by_user_id | INT | NOT NULL, FK | User who created circulation | -| submitted_at | TIMESTAMP | NULL | Submission timestamp | -| closed_at | TIMESTAMP | NULL | Closure timestamp | -| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | -| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp | - -**Indexes**: - -* PRIMARY KEY (id) -* UNIQUE (correspondence_id) -* FOREIGN KEY (correspondence_id) REFERENCES correspondences(id) -* FOREIGN KEY (organization_id) REFERENCES organizations(id) -* FOREIGN KEY (circulation_status_code) REFERENCES circulation_status_codes(code) -* FOREIGN KEY (created_by_user_id) REFERENCES users(user_id) -* INDEX (organization_id) -* INDEX (circulation_status_code) -* INDEX (created_by_user_id) - -**Relationships**: - -* Parent: correspondences, organizations, circulation_status_codes, users -* Children: circulation_routings, circulation_attachments - -**Business Rules**: - -* Internal document routing within organization -* One-to-one relationship with correspondences -* Tracks document review/approval workflow -* Status progression: OPEN → IN_REVIEW → COMPLETED/CANCELLED - ---- - -## **7. 📤 Transmittals Tables (เอกสารนำส่ง)** - -### 7.1 transmittals - -**Purpose**: Child table for transmittal-specific data (1:1 with correspondences) - -| Column Name | Data Type | Constraints | Description | -| ----------------- | --------- | --------------- | --------------------------------------------------------- | -| correspondence_id | INT | PRIMARY KEY, FK | Reference to correspondences (1:1) | -| purpose | ENUM | NULL | Purpose: FOR_APPROVAL, FOR_INFORMATION, FOR_REVIEW, OTHER | -| remarks | TEXT | NULL | Additional remarks | - -**Indexes**: - -* PRIMARY KEY (correspondence_id) -* FOREIGN KEY (correspondence_id) REFERENCES correspondences(id) ON DELETE CASCADE -* INDEX (purpose) - -**Relationships**: - -* Parent: correspondences -* Children: transmittal_items - -**Business Rules**: - -* One-to-one relationship with correspondences -* Transmittal is a correspondence type for forwarding documents -* Contains metadata about the transmission - ---- - -### 7.2 transmittal_items - -**Purpose**: Junction table listing documents included in transmittal (M:N) - -| Column Name | Data Type | Constraints | Description | -| ---------------------- | ------------ | --------------------------- | --------------------------------------- | -| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique item ID | -| transmittal_id | INT | NOT NULL, FK | Reference to transmittal | -| item_correspondence_id | INT | NOT NULL, FK | Reference to document being transmitted | -| quantity | INT | DEFAULT 1 | Number of copies | -| remarks | VARCHAR(255) | NULL | Item-specific remarks | - -**Indexes**: - -* PRIMARY KEY (id) -* FOREIGN KEY (transmittal_id) REFERENCES transmittals(correspondence_id) ON DELETE CASCADE -* FOREIGN KEY (item_correspondence_id) REFERENCES correspondences(id) ON DELETE CASCADE -* UNIQUE KEY (transmittal_id, item_correspondence_id) -* INDEX (item_correspondence_id) - -**Relationships**: - -* Parent: transmittals, correspondences - -**Business Rules**: - -* One transmittal can contain multiple documents -* Tracks quantity of physical copies (if applicable) -* Links to any type of correspondence document - ---- - -## **8. 📎 File Management Tables (ไฟล์แนบ)** - -### 8.1 attachments - -**Purpose**: Central repository for all file attachments in the system - -| Column Name | Data Type | Constraints | Description | -| ------------------- | ------------ | --------------------------- | -------------------------------------------------------------- | -| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique attachment ID | -| original_filename | VARCHAR(255) | NOT NULL | Original filename from upload | -| stored_filename | VARCHAR(255) | NOT NULL | System-generated unique filename | -| file_path | VARCHAR(500) | NOT NULL | Full file path on server (/share/dms-data/) | -| mime_type | VARCHAR(100) | NOT NULL | MIME type (application/pdf, image/jpeg, etc.) | -| file_size | INT | NOT NULL | File size in bytes | -| uploaded_by_user_id | INT | NOT NULL, FK | User who uploaded file | -| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Upload timestamp | -| is_temporary | BOOLEAN | DEFAULT TRUE | ระบุว่าเป็นไฟล์ชั่วคราว (ยังไม่ได้ Commit) | -| temp_id\* | VARCHAR(100) | NULL | ID ชั่วคราวสำหรับอ้างอิงตอน Upload Phase 1 (อาจใช้ร่วมกับ id หรือแยกก็ได้) | -| expires_at | DATETIME | NULL | เวลาหมดอายุของไฟล์ Temp (เพื่อให้ Cron Job ลบออก) | -| checksum | VARCHAR(64) | NULL | SHA-256 Checksum สำหรับ Verify File Integrity [Req 3.9.3] | - -**Indexes**: - -* PRIMARY KEY (id) -* FOREIGN KEY (uploaded_by_user_id) REFERENCES users(user_id) ON DELETE CASCADE -* INDEX (stored_filename) -* INDEX (mime_type) -* INDEX (uploaded_by_user_id) -* INDEX (created_at) - -**Relationships**: - -* Parent: users -* Referenced by: correspondence_attachments, circulation_attachments, shop_drawing_revision_attachments, contract_drawing_attachments - -**Business Rules**: - -* Central storage prevents file duplication -* Stored filename prevents naming conflicts -* File path points to QNAP NAS storage -* Original filename preserved for download -* One file record can be linked to multiple documents - ---- - -### 8.2 correspondence_attachments - -**Purpose**: Junction table linking correspondences to file attachments (M:N) - -| Column Name | Data Type | Constraints | Description | -| ----------------- | --------- | --------------- | ---------------------------- | -| correspondence_id | INT | PRIMARY KEY, FK | Reference to correspondences | -| attachment_id | INT | PRIMARY KEY, FK | Reference to attachments | -| is_main_document | BOOLEAN | DEFAULT FALSE | Main/primary document flag | - -**Indexes**: - -* PRIMARY KEY (correspondence_id, attachment_id) -* FOREIGN KEY (correspondence_id) REFERENCES correspondences(id) ON DELETE CASCADE -* FOREIGN KEY (attachment_id) REFERENCES attachments(id) ON DELETE CASCADE -* INDEX (attachment_id) -* INDEX (is_main_document) - -**Relationships**: - -* Parent: correspondences, attachments - -**Business Rules**: - -* One correspondence can have multiple attachments -* One attachment can be linked to multiple correspondences -* is_main_document identifies primary file (typically PDF) - ---- - -### 8.3 circulation_attachments - -**Purpose**: Junction table linking circulations to file attachments (M:N) - -| Column Name | Data Type | Constraints | Description | -| ---------------- | --------- | --------------- | -------------------------- | -| circulation_id | INT | PRIMARY KEY, FK | Reference to circulations | -| attachment_id | INT | PRIMARY KEY, FK | Reference to attachments | -| is_main_document | BOOLEAN | DEFAULT FALSE | Main/primary document flag | - -**Indexes**: - -* PRIMARY KEY (circulation_id, attachment_id) -* FOREIGN KEY (circulation_id) REFERENCES circulations(id) ON DELETE CASCADE -* FOREIGN KEY (attachment_id) REFERENCES attachments(id) ON DELETE CASCADE -* INDEX (attachment_id) -* INDEX (is_main_document) - -**Relationships**: - -* Parent: circulations, attachments - ---- - -### 8.4 shop_drawing_revision_attachments - -**Purpose**: Junction table linking shop drawing revisions to file attachments (M:N) - -| Column Name | Data Type | Constraints | Description | -| ------------------------ | --------- | --------------- | ---------------------------------- | -| shop_drawing_revision_id | INT | PRIMARY KEY, FK | Reference to shop drawing revision | -| attachment_id | INT | PRIMARY KEY, FK | Reference to attachments | -| file_type | ENUM | NULL | File type: PDF, DWG, SOURCE, OTHER | -| is_main_document | BOOLEAN | DEFAULT FALSE | Main/primary document flag | - -**Indexes**: - -* PRIMARY KEY (shop_drawing_revision_id, attachment_id) -* FOREIGN KEY (shop_drawing_revision_id) REFERENCES shop_drawing_revisions(id) ON DELETE CASCADE -* FOREIGN KEY (attachment_id) REFERENCES attachments(id) ON DELETE CASCADE -* INDEX (attachment_id) -* INDEX (file_type) -* INDEX (is_main_document) - -**Relationships**: - -* Parent: shop_drawing_revisions, attachments - -**Business Rules**: - -* file_type categorizes drawing file formats -* Typically includes PDF for viewing and DWG for editing -* SOURCE may include native CAD files - ---- - -### 8.5 contract_drawing_attachments - -**Purpose**: Junction table linking contract drawings to file attachments (M:N) - -| Column Name | Data Type | Constraints | Description | -| ------------------- | --------- | --------------- | ---------------------------------- | -| contract_drawing_id | INT | PRIMARY KEY, FK | Reference to contract drawing | -| attachment_id | INT | PRIMARY KEY, FK | Reference to attachments | -| file_type | ENUM | NULL | File type: PDF, DWG, SOURCE, OTHER | -| is_main_document | BOOLEAN | DEFAULT FALSE | Main/primary document flag | - -**Indexes**: - -* PRIMARY KEY (contract_drawing_id, attachment_id) -* FOREIGN KEY (contract_drawing_id) REFERENCES contract_drawings(id) ON DELETE CASCADE -* FOREIGN KEY (attachment_id) REFERENCES attachments(id) ON DELETE CASCADE -* INDEX (attachment_id) -* INDEX (file_type) -* INDEX (is_main_document) - -**Relationships**: - -* Parent: contract_drawings, attachments - ---- - -## **9. 🔢 Document Numbering System Tables (ระบบเลขที่เอกสาร)** - -### 9.1 document_number_formats - -**Purpose**: Master table defining numbering formats for each document type - -| Column Name | Data Type | Constraints | Description | -| ---------------------- | ------------ | --------------------------- | -------------------------------------------- | -| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique format ID | -| project_id | INT | NOT NULL, FK | Reference to projects | -| correspondence_type_id | INT | NOT NULL, FK | Reference to correspondence_types | -| format_string | VARCHAR(100) | NOT NULL | Format pattern (e.g., {ORG}-{TYPE}-{YYYY}-#) | -| description | TEXT | NULL | Format description | -| reset_annually | BOOLEAN | DEFAULT TRUE | Start sequence new every year | -| is_active | TINYINT(1) | DEFAULT 1 | Active status | - -**Indexes**: - -* PRIMARY KEY (id) -* FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE -* FOREIGN KEY (correspondence_type_id) REFERENCES correspondence_types(id) ON DELETE CASCADE -* UNIQUE KEY (project_id, correspondence_type_id) -* INDEX (is_active) - -**Relationships**: - -* Parent: projects, correspondence_types - -**Business Rules**: - -* Defines how document numbers are constructed -* Supports placeholders: {PROJ}, {ORG}, {TYPE}, {YYYY}, {MM}, {#} - ---- - -### 9.2 document_number_counters (UPDATE v1.7.0) - -**Purpose**: Transaction table tracking running numbers (High Concurrency) - -| Column Name | Data Type | Constraints | Description | -| -------------------------- | ----------- | ------------- | ----------------------------------------------- | -| project_id | INT | PK, NOT NULL | โครงการ | -| originator_organization_id | INT | PK, NOT NULL | องค์กรผู้ส่ง | -| recipient_organization_id | INT | PK, NOT NULL | องค์กรผู้รับ (0 = no recipient / RFA) | -| correspondence_type_id | INT | PK, NULL | ประเภทเอกสาร (NULL = default) | -| sub_type_id | INT | PK, DEFAULT 0 | ประเภทย่อย สำหรับ TRANSMITTAL (0 = ไม่ระบุ) | -| rfa_type_id | INT | PK, DEFAULT 0 | ประเภท RFA (0 = ไม่ใช่ RFA) | -| discipline_id | INT | PK, DEFAULT 0 | สาขางาน (0 = ไม่ระบุ) | -| reset_scope | VARCHAR(20) | PK, NOT NULL | Scope of reset (YEAR_2024, MONTH_2024_01, NONE) | -| last_number | INT | DEFAULT 0 | เลขล่าสุดที่ถูกใช้งานไปแล้ว | -| version | INT | DEFAULT 0 | Optimistic Lock Version | -| updated_at | DATETIME(6) | ON UPDATE | เวลาที่อัปเดตล่าสุด | - -**Indexes**: - -* **PRIMARY KEY (project_id, originator_organization_id, recipient_organization_id, correspondence_type_id, sub_type_id, rfa_type_id, discipline_id, reset_scope)** -* INDEX idx_counter_lookup (project_id, correspondence_type_id, reset_scope) -* INDEX idx_counter_org (originator_organization_id, reset_scope) - -**Business Rules**: - -* **Composite Primary Key 8 Columns**: เพื่อรองรับการรันเลขที่ซับซ้อนและ Reset Scope ที่หลากหลาย -* **Concurrency Control**: ใช้ Redis Lock หรือ Optimistic Locking (version) -* **Reset Scope**: ใช้ Field `reset_scope` ควบคุมการ Reset แทน `current_year` แบบเดิม - ---- - -### 9.3 document_number_audit (UPDATE v1.7.0) - -**Purpose**: Audit log for document number generation (Debugging & Tracking) - -| Column Name | Data Type | Constraints | Description | -| :------------------------- | :----------- | :----------------- | :-------------------------------------- | -| id | INT | PK, AI | ID ของ audit record | -| document_id | INT | NULL, FK | ID ของเอกสารที่สร้างเลขที่ (NULL if failed) | -| document_type | VARCHAR(50) | NULL | ประเภทเอกสาร | -| document_number | VARCHAR(100) | NOT NULL | เลขที่เอกสารที่สร้าง (ผลลัพธ์) | -| operation | ENUM | DEFAULT 'CONFIRM' | RESERVE, CONFIRM, MANUAL_OVERRIDE, etc. | -| status | ENUM | DEFAULT 'RESERVED' | RESERVED, CONFIRMED, CANCELLED, VOID | -| counter_key | JSON | NOT NULL | Counter key ที่ใช้ (JSON 8 fields) | -| reservation_token | VARCHAR(36) | NULL | Token การจอง | -| idempotency_key | VARCHAR(128) | NULL | Idempotency Key from request | -| originator_organization_id | INT | NULL | องค์กรผู้ส่ง | -| recipient_organization_id | INT | NULL | องค์กรผู้รับ | -| template_used | VARCHAR(200) | NOT NULL | Template ที่ใช้ในการสร้าง | -| old_value | TEXT | NULL | Previous value | -| new_value | TEXT | NULL | New value | -| user_id | INT | NULL, FK | ผู้ขอสร้างเลขที่ | -| is_success | BOOLEAN | DEFAULT TRUE | สถานะความสำเร็จ | -| created_at | TIMESTAMP | DEFAULT NOW | วันที่/เวลาที่สร้าง | -| total_duration_ms | INT | NULL | เวลารวมทั้งหมดในการสร้าง (ms) | - -**Indexes**: - -* PRIMARY KEY (id) -* FOREIGN KEY (document_id) REFERENCES correspondences(id) ON DELETE CASCADE -* FOREIGN KEY (user_id) REFERENCES users(user_id) -* INDEX (document_id) -* INDEX (user_id) -* INDEX (status) -* INDEX (operation) -* INDEX (document_number) -* INDEX (reservation_token) -* INDEX (created_at) - ---- - -### 9.4 document_number_errors (UPDATE v1.7.0) - -**Purpose**: Error log for failed document number generation - -| Column Name | Data Type | Constraints | Description | -| :------------ | :-------- | :---------- | :--------------------------------------------- | -| id | INT | PK, AI | ID ของ error record | -| error_type | ENUM | NOT NULL | LOCK_TIMEOUT, VERSION_CONFLICT, DB_ERROR, etc. | -| error_message | TEXT | NULL | ข้อความ error | -| stack_trace | TEXT | NULL | Stack trace สำหรับ debugging | -| context_data | JSON | NULL | Context ของ request | -| user_id | INT | NULL | ผู้ที่เกิด error | -| created_at | TIMESTAMP | DEFAULT NOW | วันที่เกิด error | -| resolved_at | TIMESTAMP | NULL | วันที่แก้ไขแล้ว | - -**Indexes**: - -* PRIMARY KEY (id) -* INDEX (error_type) -* INDEX (created_at) -* INDEX (user_id) -* INDEX (resolved_at) - ---- - -### 9.5 document_number_reservations (NEW v1.7.0) - -**Purpose**: Two-Phase Commit table for document number reservation - -| Column Name | Data Type | Constraints | Description | -| :--------------------- | :----------- | :--------------- | :----------------------------------- | -| id | INT | PK, AI | Unique ID | -| token | VARCHAR(36) | UNIQUE, NOT NULL | UUID v4 Reservation Token | -| document_number | VARCHAR(100) | UNIQUE, NOT NULL | เลขที่เอกสารที่จอง | -| document_number_status | ENUM | DEFAULT RESERVED | RESERVED, CONFIRMED, CANCELLED, VOID | -| document_id | INT | NULL, FK | ID ของเอกสาร (เมื่อ Confirm แล้ว) | -| expires_at | DATETIME(6) | NOT NULL | เวลาหมดอายุการจอง | -| project_id | INT | NOT NULL, FK | Project Context | -| user_id | INT | NOT NULL, FK | User Context | - -**Indexes**: - -* PRIMARY KEY (id) -* FOREIGN KEY (document_id) REFERENCES correspondence_revisions(id) ON DELETE SET NULL -* FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE -* FOREIGN KEY (correspondence_type_id) REFERENCES correspondence_types(id) ON DELETE CASCADE -* FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE -* INDEX idx_token (token) -* INDEX idx_status (document_number_status) -* INDEX idx_status_expires (document_number_status, expires_at) -* INDEX idx_document_id (document_id) -* INDEX idx_user_id (user_id) -* INDEX idx_reserved_at (reserved_at) - ---- - -## **10. ⚙️ Unified Workflow Engine Tables (UPDATE v1.7.0)** - -### 10.1 workflow_definitions - -**Purpose**: เก็บแม่แบบ (Template) ของ Workflow (Definition / DSL) - -| Column Name | Data Type | Constraints | Description | -| :------------ | :---------- | :----------- | :------------------------------------- | -| id | CHAR(36) | PK, UUID | Unique Workflow Definition ID | -| workflow_code | VARCHAR(50) | NOT NULL | รหัส Workflow (เช่น RFA_FLOW_V1) | -| version | INT | DEFAULT 1 | หมายเลข Version | -| description | TEXT | NULL | คำอธิบาย Workflow | -| dsl | JSON | NOT NULL | นิยาม Workflow ต้นฉบับ (YAML/JSON Format) | -| compiled | JSON | NOT NULL | โครงสร้าง Execution Tree ที่ Compile แล้ว | -| is_active | BOOLEAN | DEFAULT TRUE | สถานะการใช้งาน | -| created_at | TIMESTAMP | DEFAULT NOW | วันที่สร้าง | -| updated_at | TIMESTAMP | ON UPDATE | วันที่แก้ไขล่าสุด | - -**Indexes**: - -* PRIMARY KEY (id) -* UNIQUE KEY (workflow_code, version) -* INDEX (is_active) - ---- - -### 10.2 workflow_instances - -**Purpose**: เก็บสถานะของ Workflow ที่กำลังรันอยู่จริง (Runtime) - -| Column Name | Data Type | Constraints | Description | -| :------------ | :---------- | :--------------- | :--------------------------------------------- | -| id | CHAR(36) | PK, UUID | Unique Instance ID | -| definition_id | CHAR(36) | FK, NOT NULL | อ้างอิง Definition ที่ใช้ | -| entity_type | VARCHAR(50) | NOT NULL | ประเภทเอกสาร (rfa_revision, correspondence...) | -| entity_id | VARCHAR(50) | NOT NULL | ID ของเอกสาร | -| current_state | VARCHAR(50) | NOT NULL | สถานะปัจจุบัน | -| status | ENUM | DEFAULT 'ACTIVE' | ACTIVE, COMPLETED, CANCELLED, TERMINATED | -| context | JSON | NULL | ตัวแปร Context สำหรับตัดสินใจ | -| created_at | TIMESTAMP | DEFAULT NOW | เวลาที่สร้าง | -| updated_at | TIMESTAMP | ON UPDATE | เวลาที่อัปเดตล่าสุด | - -**Indexes**: - -* PRIMARY KEY (id) -* FOREIGN KEY (definition_id) REFERENCES workflow_definitions(id) ON DELETE CASCADE -* INDEX (entity_type, entity_id) -* INDEX (current_state) - ---- - -### 10.3 workflow_histories - -**Purpose**: เก็บประวัติการดำเนินการในแต่ละ Step (Audit Trail) - -| Column Name | Data Type | Constraints | Description | -| :---------------- | :---------- | :----------- | :-------------------- | -| id | CHAR(36) | PK, UUID | Unique ID | -| instance_id | CHAR(36) | FK, NOT NULL | อ้างอิง Instance | -| from_state | VARCHAR(50) | NOT NULL | สถานะต้นทาง | -| to_state | VARCHAR(50) | NOT NULL | สถานะปลายทาง | -| action | VARCHAR(50) | NOT NULL | Action ที่กระทำ | -| action_by_user_id | INT | FK, NULL | User ID ผู้กระทำ | -| comment | TEXT | NULL | ความเห็น | -| metadata | JSON | NULL | Snapshot ข้อมูล ณ ขณะนั้น | -| created_at | TIMESTAMP | DEFAULT NOW | เวลาที่กระทำ | - -**Indexes**: - -* PRIMARY KEY (id) -* FOREIGN KEY (instance_id) REFERENCES workflow_instances(id) ON DELETE CASCADE -* INDEX (instance_id) -* INDEX (action_by_user_id) - ---- - -## **11. 🖥️ System & Logs Tables (ระบบ, บันทึก)** - -> **Audit Logging Architecture:** -### 1. Audit Logging - -**Table: `audit_logs`** - -บันทึกการเปลี่ยนแปลงสำคัญ: - -- User actions (CREATE, UPDATE, DELETE) -- Entity type และ Entity ID -- Old/New values (JSON) -- IP Address, User Agent - -### 2. User Preferences - -**Table: `user_preferences`** - -เก็บการตั้งค่าส่วนตัว: - -- Language preference -- Notification settings -- UI preferences (JSON) - -### 3. JSON Schema Validation - -**Table: `json_schemas`** - -เก็บ Schema สำหรับ Validate JSON fields: - -- `correspondence_revisions.details` -- `user_preferences.preferences` - ---- - - -### 11.1 json_schemas (UPDATE v1.7.0) - -**Purpose**: เก็บ Schema สำหรับ Validate JSON Columns (Req 3.12) - -| Column Name | Data Type | Constraints | Description | -| :---------------- | :----------- | :----------- | :------------------------------- | -| id | INT | PK, AI | Unique ID | -| schema_code | VARCHAR(100) | NOT NULL | รหัส Schema (เช่น RFA_DWG) | -| version | INT | DEFAULT 1 | เวอร์ชันของ Schema | -| table_name | VARCHAR(100) | NOT NULL | ชื่อตารางเป้าหมาย | -| schema_definition | JSON | NOT NULL | JSON Schema Definition | -| ui_schema | JSON | NULL | โครงสร้าง UI Schema สำหรับ Frontend | -| virtual_columns | JSON | NULL | Config สำหรับสร้าง Virtual Columns | -| migration_script | JSON | NULL | Script สำหรับแปลงข้อมูล | -| is_active | BOOLEAN | DEFAULT TRUE | สถานะการใช้งาน | - - ---- - -### 11.2 audit_logs (UPDATE v1.7.0) - -**Purpose**: Centralized audit logging for all system actions (Req 6.1) - -| Column Name | Data Type | Constraints | Description | -| :----------- | :----------- | :------------------------ | :------------------------------------------- | -| audit_id | BIGINT | PK, AI | Unique log ID | -| request_id | VARCHAR(100) | NULL | Trace ID linking to app logs | -| user_id | INT | NULL, FK | User who performed action | -| action | VARCHAR(100) | NOT NULL | Action name (e.g. rfa.create) | -| severity | ENUM | DEFAULT 'INFO' | INFO, WARN, ERROR, CRITICAL | -| entity_type | VARCHAR(50) | NULL | Module/Table name (e.g. rfa, correspondence) | -| entity_id | VARCHAR(50) | NULL | ID of affected entity | -| details_json | JSON | NULL | Context data / Old & New values | -| ip_address | VARCHAR(45) | NULL | User IP address | -| user_agent | VARCHAR(255) | NULL | User browser/client info | -| created_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | Log timestamp | - -**Indexes**: - -* PRIMARY KEY (audit_id, created_at) -- **Partition Key** -* INDEX idx_audit_user (user_id) -* INDEX idx_audit_action (action) -* INDEX idx_audit_entity (entity_type, entity_id) -* INDEX idx_audit_created (created_at) - -**Partitioning**: -* **PARTITION BY RANGE (YEAR(created_at))**: แบ่ง Partition รายปี เพื่อประสิทธิภาพในการเก็บข้อมูลระยะยาว - ---- - -### 11.3 notifications (UPDATE v1.7.0) - -**Purpose**: System notifications for users - -| Column Name | Data Type | Constraints | Description | -| :---------------- | :----------- | :-------------------------- | :------------------------ | -| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique notification ID | -| user_id | INT | NOT NULL, FK | Recipient user ID | -| title | VARCHAR(255) | NOT NULL | Notification title | -| message | TEXT | NOT NULL | Notification body | -| notification_type | ENUM | NOT NULL | Type: EMAIL, LINE, SYSTEM | -| is_read | BOOLEAN | DEFAULT FALSE | Read status | -| entity_type | VARCHAR(50) | NULL | Related Entity Type | -| entity_id | INT | NULL | Related Entity ID | -| created_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | Notification timestamp | - -**Indexes**: - -* PRIMARY KEY (id, created_at) -- **Partition Key** -* INDEX idx_notif_user (user_id) -* INDEX idx_notif_type (notification_type) -* INDEX idx_notif_read (is_read) -* INDEX idx_notif_created (created_at) - -**Partitioning**: -* **PARTITION BY RANGE (YEAR(created_at))**: แบ่ง Partition รายปี - ---- - -## **12. 🔍 Views (มุมมองข้อมูล)** - -### 12.1 v_current_correspondences - -**Purpose**: แสดงข้อมูล Correspondence Revision ล่าสุด (is_current = TRUE) - -### 12.2 v_current_rfas - -**Purpose**: แสดงข้อมูล RFA Revision ล่าสุด พร้อม Status และ Approve Code - -### 12.3 v_user_tasks (Unified Workflow) - -**Purpose**: รวมรายการงานที่ยังค้างอยู่ (Status = ACTIVE) จากทุกระบบ (RFA, Circulation, Correspondence) เพื่อนำไปแสดงใน Dashboard - -### 12.4 v_audit_log_details - -**Purpose**: แสดง audit_logs พร้อมข้อมูล username และ email ของผู้กระทำ - -### 12.5 v_user_all_permissions - -**Purpose**: รวมสิทธิ์ทั้งหมด (Global + Project + Organization) ของผู้ใช้ทุกคน - -### 12.6 v_documents_with_attachments - -**Purpose**: แสดงเอกสารทั้งหมดที่มีไฟล์แนบ (Correspondence, Circulation, Drawings) - -### 12.7 v_document_statistics - -**Purpose**: แสดงสถิติเอกสารตามประเภทและสถานะ - ---- - -## **13. 📊 Index Summaries (สรุป Index)** - -> **Performance Optimization Strategy:** -### 1. Indexing Strategy - -**Primary Indexes:** - -- Primary Keys (AUTO_INCREMENT) -- Foreign Keys (automatic in InnoDB) -- Unique Constraints (business keys) - -**Secondary Indexes:** - -```sql --- Correspondence search -CREATE INDEX idx_corr_type_status ON correspondence_revisions(correspondence_type_id, correspondence_status_id); -CREATE INDEX idx_corr_date ON correspondence_revisions(document_date); - --- Virtual columns for JSON -CREATE INDEX idx_v_ref_project ON correspondence_revisions(v_ref_project_id); -CREATE INDEX idx_v_doc_subtype ON correspondence_revisions(v_doc_subtype); - --- User lookup -CREATE INDEX idx_user_email ON users(email); -CREATE INDEX idx_user_org ON users(primary_organization_id, is_active); -``` - -### 2. Virtual Columns - -ใช้ Virtual Columns สำหรับ Index JSON fields: - -```sql -ALTER TABLE correspondence_revisions -ADD COLUMN v_ref_project_id INT GENERATED ALWAYS AS (JSON_UNQUOTE(JSON_EXTRACT(details, '$.ref_project_id'))) VIRTUAL, -ADD INDEX idx_v_ref_project(v_ref_project_id); -``` - -### 3. Partitioning (Future) - -พิจารณา Partition ตาราง `audit_logs` ตามปี: - -```sql -ALTER TABLE audit_logs -PARTITION BY RANGE (YEAR(created_at)) ( - PARTITION p2024 VALUES LESS THAN (2025), - PARTITION p2025 VALUES LESS THAN (2026), - PARTITION p_future VALUES LESS THAN MAXVALUE -); -``` - ---- - - -### 13.1 Performance Indexes - -| Table Name | Index Columns | Purpose | -| :----------------------- | :------------------------------------------------ | :----------------------------- | -| correspondences | (project_id, correspondence_number) | Fast lookup by document number | -| correspondences | (correspondence_type_id) | Filter by type | -| correspondence_revisions | (correspondence_id, is_current) | Get current revision | -| rfas | (rfa_type_id) | Filter by RFA type | -| rfa_revisions | (rfa_id, is_current) | Get current RFA revision | -| rfa_revisions | (rfa_status_code_id) | Filter by status | -| audit_logs | (created_at) | Date range queries | -| audit_logs | (user_id) | User activity history | -| audit_logs | (module, action) | Action type analysis | -| notifications | (user_id, is_read) | Unread notifications query | -| document_number_counters | (project_id, correspondence_type_id, reset_scope) | Running number generation | -| workflow_instances | (entity_type, entity_id) | Workflow lookup by document ID | -| workflow_instances | (current_state) | Monitor active workflows | - -### 13.2 Unique Constraints - -| Table Name | Columns | Description | -| :---------------------- | :----------------------------------- | :--------------------------------- | -| users | (username) | Unique login name | -| users | (email) | Unique email address | -| organizations | (organization_code) | Unique organization code | -| projects | (project_code) | Unique project code | -| contracts | (contract_code) | Unique contract code | -| correspondences | (project_id, correspondence_number) | Unique document number per project | -| shop_drawings | (drawing_number) | Unique shop drawing number | -| document_number_formats | (project_id, correspondence_type_id) | One format per type per project | -| workflow_definitions | (workflow_code, version) | Unique workflow code per version | - ---- - -## **14. 🛡️ Data Integrity Constraints (ความถูกต้องของข้อมูล)** - -### 14.1 Soft Delete Policy - -* **Tables with `deleted_at`**: - * users - * organizations - * projects - * contracts - * correspondences - * rfas - * shop_drawings - * contract_drawings -* **Rule**: Records are never physically deleted. `deleted_at` is set to timestamp. -* **Query Rule**: All standard queries MUST include `WHERE deleted_at IS NULL`. - -### 14.2 Foreign Key Cascades - -* **ON DELETE CASCADE**: - * Used for child tables that cannot exist without parent (e.g., `correspondence_revisions`, `rfa_revisions`, `correspondence_attachments`). -* **ON DELETE RESTRICT**: - * Used for master data references to prevent accidental deletion of used data (e.g., `correspondence_types`, `organizations`). -* **ON DELETE SET NULL**: - * Used for optional references (e.g., `created_by`, `originator_id`). - ---- - -## **15. 🔐 Security & Permissions Model (ความปลอดภัย)** - -### 15.1 Row-Level Security (RLS) Logic - -* **Organization Scope**: Users can only see documents where `originator_id` OR `recipient_organization_id` matches their organization. -* **Project Scope**: Users can only see documents within projects they are assigned to. -* **Confidentiality**: Documents marked `is_confidential` are visible ONLY to specific roles or users. - -### 15.2 Role-Based Access Control (RBAC) - -* **Permissions** are granular (e.g., `correspondence.view`, `correspondence.create`). -* **Roles** aggregate permissions (e.g., `Document Controller` = `view` + `create` + `edit`). -* **Assignments** link Users to Roles within a Context (Global, Project, or Organization). - ---- - -## **16. 🔄 Data Migration & Seeding (การย้ายข้อมูล)** - -### 16.1 Initial Seeding (V1.7.0) - -1. **Master Data**: - * `organizations`: Owner, Consultant, Contractor - * `projects`: LCBP3 - * `correspondence_types`: LETTER, MEMO, TRANSMITTAL, RFA - * `rfa_types`: DWG, MAT, DOC, RFI - * `rfa_status_codes`: DFT, PEND, APPR, REJ - * `disciplines`: GEN, STR, ARC, MEP -2. **System Users**: - * `admin`: Super Admin - * `system`: System Bot for automated tasks - -### 16.2 Migration Strategy - -* **Schema Migration**: Use TypeORM Migrations or raw SQL scripts (versioned). -* **Data Migration**: - * **V1.6.0 -> V1.7.0**: - * Run SQL script `9_lcbp3_v1_7_0.sql` - * Migrate `document_number_counters` to 8-col composite PK. - * Initialize `document_number_reservations`. - * Update `json_schemas` with new columns. - ---- - -## **17. 📈 Monitoring & Maintenance (การดูแลรักษา)** - -### 17.1 Database Maintenance - -* **Daily**: Incremental Backup. -* **Weekly**: Full Backup + `OPTIMIZE TABLE` for heavy tables (`audit_logs`, `notifications`). -* **Monthly**: Archive old `audit_logs` partitions to cold storage. - -### 17.2 Health Checks - -* Monitor `document_number_errors` for numbering failures. -* Monitor `workflow_instances` for stuck workflows (`status = ' IN_PROGRESS '` > 7 days). -* Check `document_number_counters` for gaps or resets. - ---- - -## **18. 📚 Best Practices** -### 1. Naming Conventions - -- **Tables**: `snake_case`, plural (e.g., `correspondences`, `users`) -- **Columns**: `snake_case` (e.g., `correspondence_number`, `created_at`) -- **Foreign Keys**: `{referenced_table_singular}_id` (e.g., `project_id`, `user_id`) -- **Junction Tables**: `{table1}_{table2}` (e.g., `correspondence_tags`) - -### 2. Timestamp Columns - -ทุกตารางควรมี: - -- `created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP` -- `updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP` - -### 3. Soft Delete - -ใช้ `deleted_at DATETIME NULL` แทนการลบจริง: - -```sql --- Soft delete -UPDATE correspondences SET deleted_at = NOW() WHERE id = 1; - --- Query active records -SELECT * FROM correspondences WHERE deleted_at IS NULL; -``` - -### 4. JSON Field Guidelines - -- ใช้สำหรับข้อมูลที่ไม่ต้อง Query บ่อย -- สร้าง Virtual Columns สำหรับ fields ที่ต้อง Index -- Validate ด้วย JSON Schema -- Document structure ใน Data Dictionary - ---- - ---- - -## **19. 📖 Glossary (คำศัพท์)** - -* **RFA**: Request for Approval (เอกสารขออนุมัติ) -* **Transmittal**: Document Transmittal Sheet (ใบนำส่งเอกสาร) -* **Shop Drawing**: แบบก่อสร้างที่ผู้รับเหมาจัดทำ -* **Contract Drawing**: แบบสัญญา (แบบตั้งต้น) -* **Revision**: ฉบับแก้ไข (0, 1, 2, A, B, C) -* **Originator**: ผู้จัดทำ/ผู้ส่งเอกสาร -* **Recipient**: ผู้รับเอกสาร -* **Workflow**: กระบวนการทำงาน/อนุมัติ -* **Discipline**: สาขางาน (เช่น โยธา, สถาปัตย์, ไฟฟ้า) - ---- - -**End of Data Dictionary V1.8.0** - +--- +title: 'Data & Storage: Data Dictionary and Data Model Architecture' +version: 1.8.0 +status: released +owner: Nattanin Peancharoen +last_updated: 2026-02-28 +related: + - specs/01-requirements/02-architecture.md + - specs/01-requirements/03-functional-requirements.md +--- + +# 1. Data Model Architecture Overview + +## 📋 1.1 Overview +เอกสารนี้อธิบายสถาปัตยกรรมของ Data Model สำหรับระบบ LCBP3-DMS โดยครอบคลุมโครงสร้างฐานข้อมูล, ความสัมพันธ์ระหว่างตาราง, และหลักการออกแบบที่สำคัญ + +## 🎯 1.2 Design Principles +### 1. Separation of Concerns + +- **Master-Revision Pattern**: แยกข้อมูลที่ไม่เปลี่ยนแปลง (Master) จากข้อมูลที่มีการแก้ไข (Revisions) + - `correspondences` (Master) ↔ `correspondence_revisions` (Revisions) + - `rfas` (Master) ↔ `rfa_revisions` (Revisions) + - `shop_drawings` (Master) ↔ `shop_drawing_revisions` (Revisions) + +### 2. Data Integrity + +- **Foreign Key Constraints**: ใช้ FK ทุกความสัมพันธ์เพื่อรักษาความสมบูรณ์ของข้อมูล +- **Soft Delete**: ใช้ `deleted_at` แทนการลบข้อมูลจริง เพื่อรักษาประวัติ +- **Optimistic Locking**: ใช้ `version` column ใน `document_number_counters` ป้องกัน Race Condition + +### 3. Flexibility & Extensibility + +- **JSON Details Field**: เก็บข้อมูลเฉพาะประเภทใน `correspondence_revisions.details` +- **Virtual Columns**: สร้าง Index จาก JSON fields สำหรับ Performance +- **Master Data Tables**: แยกข้อมูล Master (Types, Status, Codes) เพื่อความยืดหยุ่น + +### 4. Security & Audit + +- **RBAC (Role-Based Access Control)**: ระบบสิทธิ์แบบ Hierarchical Scope +- **Audit Trail**: บันทึกผู้สร้าง/แก้ไข และเวลาในทุกตาราง +- **Two-Phase File Upload**: ป้องกันไฟล์ขยะด้วย Temporary Storage + +# 2. Database Schema Overview (ERD) +### Entity Relationship Diagram + +```mermaid +erDiagram + %% Core Entities + organizations ||--o{ users : "employs" + projects ||--o{ contracts : "contains" + projects ||--o{ correspondences : "manages" + + %% RBAC + users ||--o{ user_assignments : "has" + roles ||--o{ user_assignments : "assigned_to" + roles ||--o{ role_permissions : "has" + permissions ||--o{ role_permissions : "granted_by" + + %% Correspondences + correspondences ||--o{ correspondence_revisions : "has_revisions" + correspondence_types ||--o{ correspondences : "categorizes" + correspondence_status ||--o{ correspondence_revisions : "defines_state" + disciplines ||--o{ correspondences : "classifies" + + %% RFAs + rfas ||--o{ rfa_revisions : "has_revisions" + rfa_types ||--o{ rfas : "categorizes" + rfa_status_codes ||--o{ rfa_revisions : "defines_state" + rfa_approve_codes ||--o{ rfa_revisions : "defines_result" + disciplines ||--o{ rfas : "classifies" + + %% Drawings + shop_drawings ||--o{ shop_drawing_revisions : "has_revisions" + shop_drawing_main_categories ||--o{ shop_drawings : "categorizes" + shop_drawing_sub_categories ||--o{ shop_drawings : "sub_categorizes" + + %% Attachments + attachments ||--o{ correspondence_attachments : "attached_to" + correspondences ||--o{ correspondence_attachments : "has" +``` + +--- + +# 3. Data Dictionary V1.8.0 + +> หมายเหตุ: PK = Primary Key, FK = Foreign Key, AI = AUTO_INCREMENT. รูปแบบ Soft Delete จะปรากฏ Column `deleted_at DATETIME NULL` เป็นมาตรฐาน + +## **1. 🏢 Core & Master Data Tables (องค์กร, โครงการ, สัญญา)** + +### 1.1 organization_roles + +* * Purpose **: MASTER TABLE FOR organization role TYPES IN the system | COLUMN Name | Data TYPE | Constraints | Description | | ----------- | ----------- | --------------------------- | ---------------------------------------------------------------- | +| id | INT | PRIMARY KEY, +AUTO_INCREMENT | UNIQUE identifier FOR organization role | | role_name | VARCHAR(20) | NOT NULL, +UNIQUE | Role name ( + CONTRACTOR, + THIRD PARTY +) | +| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | +| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp | +| deleted_at | DATETIME | NULL | Soft delete timestamp | ** INDEXES **: - PRIMARY KEY (id) - UNIQUE (role_name) ** Business Rules **: - Predefined system roles FOR organization TYPES - Cannot be deleted IF referenced by organizations --- + +### 1.2 organizations + +* * Purpose **: MASTER TABLE storing ALL organizations involved IN the system | COLUMN Name | Data TYPE | Constraints | Description | | ----------------- | ------------ | ----------------------------------- | ---------------------------------------- | +| id | INT | PRIMARY KEY, +AUTO_INCREMENT | UNIQUE identifier FOR organization | | organization_code | VARCHAR(20) | NOT NULL, +UNIQUE | Organization code (e.g., 'กทท.', 'TEAM') | | organization_name | VARCHAR(255) | NOT NULL | FULL organization name | | is_active | BOOLEAN | DEFAULT TRUE | Active STATUS (1 = active, 0 = inactive) | | created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | | updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last +UPDATE timestamp | +| deleted_at | DATETIME | NULL | Soft delete timestamp | ** INDEXES **: - PRIMARY KEY (id) - UNIQUE (organization_code) - INDEX (is_active) ** Relationships **: - Referenced by: users, + project_organizations, + contract_organizations, + correspondences, + circulations --- + + ### 1.3 projects + + * * Purpose **: MASTER TABLE FOR ALL projects IN the system | COLUMN Name | Data TYPE | Constraints | Description | | ------------ | ------------ | --------------------------- | ----------------------------- | + | id | INT | PRIMARY KEY, + AUTO_INCREMENT | UNIQUE identifier FOR project | | project_code | VARCHAR(50) | NOT NULL, + UNIQUE | Project code (e.g., 'LCBP3') | | project_name | VARCHAR(255) | NOT NULL | FULL project name | | is_active | TINYINT(1) | DEFAULT 1 | Active STATUS | +| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | +| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp | +| deleted_at | DATETIME | NULL | Soft delete timestamp | +** INDEXES **: - PRIMARY KEY (id) - UNIQUE (project_code) - INDEX (is_active) ** Relationships **: - Referenced by: contracts, + correspondences, + document_number_formats, + drawings --- + + ### 1.4 contracts + + * * Purpose **: MASTER TABLE FOR contracts within projects | COLUMN Name | Data TYPE | Constraints | Description | | ------------- | ------------ | ----------------------------------- | ------------------------------ | + | id | INT | PRIMARY KEY, + AUTO_INCREMENT | UNIQUE identifier FOR contract | | project_id | INT | NOT NULL, + FK | Reference TO projects TABLE | | contract_code | VARCHAR(50) | NOT NULL, + UNIQUE | Contract code | | contract_name | VARCHAR(255) | NOT NULL | FULL contract name | | description | TEXT | NULL | Contract description | | start_date | DATE | NULL | Contract START date | | end_date | DATE | NULL | Contract +END date | | is_active | BOOLEAN | DEFAULT TRUE | Active STATUS | | created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | | updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last +UPDATE timestamp | +| deleted_at | DATETIME | NULL | Soft delete timestamp | ** INDEXES **: - PRIMARY KEY (id) - UNIQUE (contract_code) - FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE - INDEX (project_id, is_active) ** Relationships **: - Parent: projects - Referenced by: contract_organizations, + user_assignments --- + + ### 1.5 disciplines (NEW v1.5.1) + + * * Purpose **: เก็บข้อมูลสาขางาน (Disciplines) แยกตามสัญญา (Req 6B) | COLUMN Name | Data TYPE | Constraints | Description | |: -------------- | :----------- | :----------- | :--------------------- | + | id | INT | PK, + AI | UNIQUE identifier | | contract_id | INT | FK, + NOT NULL | ผูกกับสัญญา | | discipline_code | VARCHAR(10) | NOT NULL | รหัสสาขา (เช่น GEN, STR) | | code_name_th | VARCHAR(255) | NULL | ชื่อไทย | | code_name_en | VARCHAR(255) | NULL | ชื่ออังกฤษ | | is_active | TINYINT(1) | DEFAULT 1 | สถานะการใช้งาน | ** INDEXES **: - UNIQUE (contract_id, discipline_code) --- + + ## **2. 👥 Users & RBAC Tables (ผู้ใช้, สิทธิ์, บทบาท)** + + ### 2.1 users + + * * Purpose **: MASTER TABLE storing ALL system users | COLUMN Name | Data TYPE | Constraints | Description | | ----------------------- | ------------ | ----------------------------------- | -------------------------------- | + | user_id | INT | PRIMARY KEY, + AUTO_INCREMENT | UNIQUE identifier FOR user | | username | VARCHAR(50) | NOT NULL, + UNIQUE | Login username | | password_hash | VARCHAR(255) | NOT NULL | Hashed PASSWORD (bcrypt) | | first_name | VARCHAR(50) | NULL | User 's first name | +| last_name | VARCHAR(50) | NULL | User' s last name | | email | VARCHAR(100) | NOT NULL, + UNIQUE | Email address | | line_id | VARCHAR(100) | NULL | LINE messenger ID | | primary_organization_id | INT | NULL, + FK | PRIMARY organization affiliation | | is_active | TINYINT(1) | DEFAULT 1 | Active STATUS | | failed_attempts | INT | DEFAULT 0 | Failed login attempts counter | | locked_until | DATETIME | NULL | Account LOCK expiration time | | last_login_at | TIMESTAMP | NULL | Last successful login timestamp | | created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | | updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last +UPDATE timestamp | | deleted_at | DATETIME | NULL | Deleted at | ** INDEXES **: - PRIMARY KEY (user_id) - UNIQUE (username) - UNIQUE (email) - FOREIGN KEY (primary_organization_id) REFERENCES organizations(id) ON DELETE +SET NULL - INDEX (is_active) - INDEX (email) ** Relationships **: - Parent: organizations (primary_organization_id) - Referenced by: user_assignments, + audit_logs, + notifications, + circulation_routings --- + + ### 2.2 roles + + * * Purpose **: MASTER TABLE defining system roles WITH scope levels | COLUMN Name | Data TYPE | Constraints | Description | | ----------- | ------------ | --------------------------- | ---------------------------------------------------- | + | role_id | INT | PRIMARY KEY, + AUTO_INCREMENT | UNIQUE identifier FOR role | | role_name | VARCHAR(100) | NOT NULL | Role name (e.g., 'Superadmin', 'Document Control') | | scope | ENUM | NOT NULL | Scope LEVEL: GLOBAL, + Organization, + Project, + Contract | | description | TEXT | NULL | Role description | | is_system | BOOLEAN | DEFAULT FALSE | System role flag (cannot be deleted) | +| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | +| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp | +| deleted_at | DATETIME | NULL | Soft delete timestamp | +** INDEXES **: - PRIMARY KEY (role_id) - INDEX (scope) ** Relationships **: - Referenced by: role_permissions, + user_assignments --- + + ### 2.3 permissions + + * * Purpose **: MASTER TABLE defining ALL system permissions | COLUMN Name | Data TYPE | Constraints | Description | | --------------- | ------------ | --------------------------- | ------------------------------------------------------ | + | permission_id | INT | PRIMARY KEY, + AUTO_INCREMENT | UNIQUE identifier FOR permission | | permission_name | VARCHAR(100) | NOT NULL, + UNIQUE | Permission code (e.g., 'rfas.create', 'document.view') | | description | TEXT | NULL | Permission description | | module | VARCHAR(50) | NULL | Related module name | | scope_level | ENUM | NULL | Scope: GLOBAL, + ORG, + PROJECT | | is_active | TINYINT(1) | DEFAULT 1 | Active STATUS | +| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | +| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp | +| deleted_at | DATETIME | NULL | Soft delete timestamp | +** INDEXES **: - PRIMARY KEY (permission_id) - UNIQUE (permission_name) - INDEX (module) - INDEX (scope_level) - INDEX (is_active) ** Relationships **: - Referenced by: role_permissions --- + + ### 2.4 role_permissions + + * * Purpose **: Junction TABLE mapping roles TO permissions (M :N) | COLUMN Name | Data TYPE | Constraints | Description | | ------------- | --------- | --------------- | ------------------------------ | + | role_id | INT | PRIMARY KEY, + FK | Reference TO roles TABLE | | permission_id | INT | PRIMARY KEY, + FK | Reference TO permissions TABLE | ** INDEXES **: - PRIMARY KEY (role_id, permission_id) - FOREIGN KEY (role_id) REFERENCES roles(role_id) ON DELETE CASCADE - FOREIGN KEY (permission_id) REFERENCES permissions(permission_id) ON DELETE CASCADE - INDEX (permission_id) ** Relationships **: - Parent: roles, + permissions --- + + ### 2.5 user_assignments + + * * Purpose **: Junction TABLE assigning users TO roles WITH scope context | COLUMN Name | Data TYPE | Constraints | Description | | ------------------- | --------- | --------------------------- | ---------------------------------- | + | id | INT | PRIMARY KEY, + AUTO_INCREMENT | UNIQUE identifier | | user_id | INT | NOT NULL, + FK | Reference TO users TABLE | | role_id | INT | NOT NULL, + FK | Reference TO roles TABLE | | organization_id | INT | NULL, + FK | Organization scope (IF applicable) | | project_id | INT | NULL, + FK | Project scope (IF applicable) | | contract_id | INT | NULL, + FK | Contract scope (IF applicable) | | assigned_by_user_id | INT | NULL, + FK | User who made the assignment | | assigned_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Assignment timestamp | ** INDEXES **: - PRIMARY KEY (id) - FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE - FOREIGN KEY (role_id) REFERENCES roles(role_id) ON DELETE CASCADE - FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE - FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE - FOREIGN KEY (contract_id) REFERENCES contracts(id) ON DELETE CASCADE - FOREIGN KEY (assigned_by_user_id) REFERENCES users(user_id) - INDEX (user_id, role_id) - INDEX (organization_id) - INDEX (project_id) - INDEX (contract_id) ** Relationships **: - Parent: users, + roles, + organizations, + projects, + contracts --- + + ### 2.6 project_organizations + + * * Purpose **: Junction TABLE linking projects TO participating organizations (M :N) | COLUMN Name | Data TYPE | Constraints | Description | | --------------- | --------- | --------------- | -------------------------------- | + | project_id | INT | PRIMARY KEY, + FK | Reference TO projects TABLE | | organization_id | INT | PRIMARY KEY, + FK | Reference TO organizations TABLE | ** INDEXES **: - PRIMARY KEY (project_id, organization_id) - FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE - FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE - INDEX (organization_id) ** Relationships **: - Parent: projects, + organizations --- + + ### 2.7 contract_organizations + + * * Purpose **: Junction TABLE linking contracts TO participating organizations WITH roles (M :N) | COLUMN Name | Data TYPE | Constraints | Description | | ---------------- | ------------ | --------------- | ------------------------------------------------------------------------- | + | contract_id | INT | PRIMARY KEY, + FK | Reference TO contracts TABLE | | organization_id | INT | PRIMARY KEY, + FK | Reference TO organizations TABLE | | role_in_contract | VARCHAR(100) | NULL | Organization 's role in contract (Owner, Designer, Consultant, Contractor) | + +**Indexes**: + +* PRIMARY KEY (contract_id, organization_id) +* FOREIGN KEY (contract_id) REFERENCES contracts(id) ON DELETE CASCADE +* FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE +* INDEX (organization_id) +* INDEX (role_in_contract) + +**Relationships**: + +* Parent: contracts, organizations + +--- + +### 2.8 user_preferences (NEW v1.5.1) + +**Purpose**: เก็บการตั้งค่าส่วนตัวของผู้ใช้ (Req 5.5, 6.8.3) + +| Column Name | Data Type | Constraints | Description | +| :----------- | :---------- | :---------------- | :-------------- | +| user_id | INT | PK, FK | User ID | +| notify_email | BOOLEAN | DEFAULT TRUE | รับอีเมลแจ้งเตือน | +| notify_line | BOOLEAN | DEFAULT TRUE | รับไลน์แจ้งเตือน | +| digest_mode | BOOLEAN | DEFAULT FALSE | รับแจ้งเตือนแบบรวม | +| ui_theme | VARCHAR(20) | DEFAULT ' light ' | UI Theme | + +--- + +### 2.9 refresh_tokens (NEW v1.5.1) + +**Purpose**: เก็บ Refresh Tokens สำหรับการทำ Authentication และ Token Rotation + +| Column Name | Data Type | Constraints | Description | +| :---------------- | :----------- | :------------------------ | :------------------------------------ | +| token_id | INT | PK, AI | Unique Token ID | +| user_id | INT | FK, NOT NULL | เจ้าของ Token | +| token_hash | VARCHAR(255) | NOT NULL | Hash ของ Refresh Token (Security) | +| expires_at | DATETIME | NOT NULL | วันหมดอายุของ Token | +| is_revoked | BOOLEAN | DEFAULT FALSE | สถานะถูกยกเลิก (True = ใช้งานไม่ได้) | +| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | เวลาที่สร้าง | +| replaced_by_token | VARCHAR(255) | NULL | Token ใหม่ที่มาแทนที่ (กรณี Token Rotation) | + +**Indexes**: + +* PRIMARY KEY (token_id) +* FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE +* INDEX (user_id) + +**Relationships**: + +* Parent: users + +--- + +## **3. ✉️ Correspondences Tables (เอกสารหลัก, Revisions, Workflows)** + +### 3.1 correspondence_types + +**Purpose**: Master table for correspondence document types + +| Column Name | Data Type | Constraints | Description | +| ----------- | ------------ | --------------------------- | --------------------------------------------------- | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique identifier | +| type_code | VARCHAR(50) | NOT NULL, UNIQUE | Type code (e.g., ' RFA ', ' RFI ', ' TRANSMITTAL ') | +| type_name | VARCHAR(255) | NOT NULL | Full type name | +| sort_order | INT | DEFAULT 0 | Display order | +| is_active | TINYINT(1) | DEFAULT 1 | Active status | +| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | +| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Last update timestamp | +| deleted_at | DATETIME | NULL | Soft delete timestamp | + +**Indexes**: + +* PRIMARY KEY (id) +* UNIQUE (type_code) +* INDEX (is_active) +* INDEX (sort_order) + +**Relationships**: + +* Referenced by: correspondences, document_number_formats, document_number_counters + +--- + +### 3.2 correspondence_sub_types (NEW v1.5.1) + +**Purpose**: เก็บประเภทหนังสือย่อย (Sub Types) สำหรับ Mapping เลขรหัส (Req 6B) + +| Column Name | Data Type | Constraints | Description | +| :--------------------- | :----------- | :----------- | :------------------------ | +| id | INT | PK, AI | Unique identifier | +| contract_id | INT | FK, NOT NULL | ผูกกับสัญญา | +| correspondence_type_id | INT | FK, NOT NULL | ผูกกับประเภทเอกสารหลัก | +| sub_type_code | VARCHAR(20) | NOT NULL | รหัสย่อย (เช่น MAT, SHP) | +| sub_type_name | VARCHAR(255) | NULL | ชื่อประเภทหนังสือย่อย | +| sub_type_number | VARCHAR(10) | NULL | เลขรหัสสำหรับ Running Number | + +--- + +### 3.3 correspondences (UPDATE v1.7.0) + +**Purpose**: Master table for correspondence documents (non-revisioned data) + +| Column Name | Data Type | Constraints | Description | +| ------------------------- | ------------ | --------------------------- | ------------------------------------------ | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Master correspondence ID | +| correspondence_number | VARCHAR(100) | NOT NULL | Document number (from numbering system) | +| correspondence_type_id | INT | NOT NULL, FK | Reference to correspondence_types | +| **discipline_id** | **INT** | **NULL, FK** | **[NEW] สาขางาน (ถ้ามี)** | +| is_internal_communication | TINYINT(1) | DEFAULT 0 | Internal (1) or external (0) communication | +| project_id | INT | NOT NULL, FK | Reference to projects table | +| originator_id | INT | NULL, FK | Originating organization | +| created_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | +| created_by | INT | NULL, FK | User who created the record | +| deleted_at | DATETIME | NULL | Soft delete timestamp | + +**Indexes**: + +* PRIMARY KEY (id) +* FOREIGN KEY (correspondence_type_id) REFERENCES correspondence_types(id) ON DELETE RESTRICT +* **FOREIGN KEY (discipline_id) REFERENCES disciplines(id) ON DELETE SET NULL** +* FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE +* FOREIGN KEY (originator_id) REFERENCES organizations(id) ON DELETE SET NULL +* FOREIGN KEY (created_by) REFERENCES users(user_id) ON DELETE SET NULL +* UNIQUE KEY (project_id, correspondence_number) +* INDEX (correspondence_type_id) +* INDEX (originator_id) +* INDEX (deleted_at) + +**Relationships**: + +* Parent: correspondence_types, **disciplines**, projects, organizations, users +* Children: correspondence_revisions, correspondence_recipients, correspondence_tags, correspondence_references, correspondence_attachments, circulations, transmittals + +--- + +### 3.4 correspondence_revisions (UPDATE v1.7.0) + +**Purpose**: Child table storing revision history of correspondences (1:N) + +| Column Name | Data Type | Constraints | Description | +| ------------------------ | ------------ | --------------------------------- | -------------------------------------------------------- | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique revision ID | +| correspondence_id | INT | NOT NULL, FK | Master correspondence ID | +| revision_number | INT | NOT NULL | Revision sequence (0, 1, 2...) | +| revision_label | VARCHAR(10) | NULL | Display revision (A, B, 1.1...) | +| is_current | BOOLEAN | DEFAULT FALSE | Current revision flag | +| correspondence_status_id | INT | NOT NULL, FK | Current status of this revision | +| title | VARCHAR(255) | NOT NULL | Document title | +| document_date | DATE | NULL | Document date | +| issued_date | DATETIME | NULL | Issue date | +| received_date | DATETIME | NULL | Received date | +| due_date | DATETIME | NULL | Due date for response | +| description | TEXT | NULL | Revision description | +| details | JSON | NULL | Type-specific details (e.g., RFI questions) | +| created_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | Revision creation timestamp | +| created_by | INT | NULL, FK | User who created revision | +| updated_by | INT | NULL, FK | User who last updated | +| v_ref_project_id | INT | GENERATED ALWAYS AS (...) VIRTUAL | Virtual Column ดึง Project ID จาก JSON details เพื่อทำ Index | + +| v_doc_subtype | VARCHAR(50) | GENERATED ALWAYS AS (...) VIRTUAL | Virtual Column ดึง Type จาก JSON details | +| schema_version | INT | DEFAULT 1 | Version of the schema used with this details | + +**Indexes**: + +* PRIMARY KEY (id) +* FOREIGN KEY (correspondence_id) REFERENCES correspondences(id) ON DELETE CASCADE +* FOREIGN KEY (correspondence_status_id) REFERENCES correspondence_status(id) ON DELETE RESTRICT +* FOREIGN KEY (created_by) REFERENCES users(user_id) ON DELETE SET NULL +* FOREIGN KEY (updated_by) REFERENCES users(user_id) ON DELETE SET NULL +* UNIQUE KEY (correspondence_id, revision_number) +* UNIQUE KEY (correspondence_id, is_current) +* INDEX (correspondence_status_id) +* INDEX (is_current) +* INDEX (document_date) +* INDEX (issued_date) +* INDEX (v_ref_project_id) +* INDEX (v_doc_subtype) + +--- + +### 3.5 correspondence_recipients + +**Purpose**: Junction table for correspondence recipients (TO/CC) (M:N) + +| Column Name | Data Type | Constraints | Description | +| ------------------------- | -------------------- | --------------- | ---------------------------- | +| correspondence_id | INT | PRIMARY KEY, FK | Reference to correspondences | +| recipient_organization_id | INT | PRIMARY KEY, FK | Recipient organization | +| recipient_type | ENUM(' TO ', ' CC ') | PRIMARY KEY | Recipient type | + +**Indexes**: + +* PRIMARY KEY (correspondence_id, recipient_organization_id, recipient_type) +* FOREIGN KEY (correspondence_id) REFERENCES correspondence_revisions(correspondence_id) ON DELETE CASCADE +* FOREIGN KEY (recipient_organization_id) REFERENCES organizations(id) ON DELETE RESTRICT +* INDEX (recipient_organization_id) +* INDEX (recipient_type) + +**Relationships**: + +* Parent: correspondences, organizations + +--- + +### 3.6 tags + +**Purpose**: Master table for document tagging system + +| Column Name | Data Type | Constraints | Description | +| ----------- | ------------ | ----------------------------------- | ------------------------- | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique tag ID | +| tag_name | VARCHAR(100) | NOT NULL, UNIQUE | Tag name | +| description | TEXT | NULL | Tag description | +| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | +| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp | + +**Indexes**: + +* PRIMARY KEY (id) +* UNIQUE (tag_name) +* INDEX (tag_name) - For autocomplete + +**Relationships**: + +* Referenced by: correspondence_tags + +--- + +### 3.7 correspondence_tags + +**Purpose**: Junction table linking correspondences to tags (M:N) + +| Column Name | Data Type | Constraints | Description | +| ----------------- | --------- | --------------- | ---------------------------- | +| correspondence_id | INT | PRIMARY KEY, FK | Reference to correspondences | +| tag_id | INT | PRIMARY KEY, FK | Reference to tags | + +**Indexes**: + +* PRIMARY KEY (correspondence_id, tag_id) +* FOREIGN KEY (correspondence_id) REFERENCES correspondences(id) ON DELETE CASCADE +* FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE +* INDEX (tag_id) + +**Relationships**: + +* Parent: correspondences, tags + +--- + +### 3.8 correspondence_references + +**Purpose**: Junction table for cross-referencing correspondences (M:N) + +| Column Name | Data Type | Constraints | Description | +| --------------------- | --------- | --------------- | ------------------------------------- | +| src_correspondence_id | INT | PRIMARY KEY, FK | Source correspondence ID | +| tgt_correspondence_id | INT | PRIMARY KEY, FK | Target (referenced) correspondence ID | + +**Indexes**: + +* PRIMARY KEY (src_correspondence_id, tgt_correspondence_id) +* FOREIGN KEY (src_correspondence_id) REFERENCES correspondences(id) ON DELETE CASCADE +* FOREIGN KEY (tgt_correspondence_id) REFERENCES correspondences(id) ON DELETE CASCADE +* INDEX (tgt_correspondence_id) + +**Relationships**: + +* Parent: correspondences (both sides) + +--- + +## **4. 📐 approval: RFA Tables (เอกสารขออนุมัติ, Workflows)** + +### 4.1 rfa_types (UPDATE v1.7.0) + +**Purpose**: Master table for RFA (Request for Approval) types + +| Column Name | Data Type | Constraints | Description | +| :----------- | :----------- | :-------------------------- | :------------------------------ | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique identifier | +| contract_id | INT | NOT NULL, FK | Contract reference | +| type_code | VARCHAR(20) | NOT NULL | Type code (DWG, DOC, MAT, etc.) | +| type_name_th | VARCHAR(100) | NOT NULL | Full type name (TH) | +| type_name_en | VARCHAR(100) | NOT NULL | Full type name (EN) | +| remark | TEXT | NULL | Remark | +| is_active | TINYINT(1) | DEFAULT 1 | Active status | + +**Indexes**: + +* PRIMARY KEY (id) +* UNIQUE (contract_id, type_code) +* FOREIGN KEY (contract_id) REFERENCES contracts(id) ON DELETE CASCADE +* INDEX (is_active) + +**Relationships**: + +* Referenced by: rfas + +--- + +### 4.2 rfa_status_codes + +**Purpose**: Master table for RFA status codes + +| Column Name | Data Type | Constraints | Description | +| ----------- | ------------ | --------------------------- | --------------------------------- | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique identifier | +| status_code | VARCHAR(20) | NOT NULL, UNIQUE | Status code (DFT, FAP, FRE, etc.) | +| status_name | VARCHAR(100) | NOT NULL | Full status name | +| description | TEXT | NULL | Status description | +| sort_order | INT | DEFAULT 0 | Display order | +| is_active | TINYINT(1) | DEFAULT 1 | Active status | + +**Indexes**: + +* PRIMARY KEY (id) +* UNIQUE (status_code) +* INDEX (is_active) +* INDEX (sort_order) + +**Relationships**: + +* Referenced by: rfa_revisions + +--- + +### 4.3 rfa_approve_codes + +**Purpose**: Master table for RFA approval result codes + +| Column Name | Data Type | Constraints | Description | +| ------------ | ------------ | --------------------------- | -------------------------------- | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique identifier | +| approve_code | VARCHAR(20) | NOT NULL, UNIQUE | Approval code (1A, 1C, 3R, etc.) | +| approve_name | VARCHAR(100) | NOT NULL | Full approval name | +| description | TEXT | NULL | Code description | +| sort_order | INT | DEFAULT 0 | Display order | +| is_active | TINYINT(1) | DEFAULT 1 | Active status | + +**Indexes**: + +* PRIMARY KEY (id) +* UNIQUE (approve_code) +* INDEX (is_active) +* INDEX (sort_order) + +**Relationships**: + +* Referenced by: rfa_revisions + +--- + +### 4.4 rfas (UPDATE v1.7.0) + +**Purpose**: Master table for RFA documents (non-revisioned data) + +| Column Name | Data Type | Constraints | Description | +| :---------- | :-------- | :------------------------ | :------------------------------------------ | +| id | INT | PK, FK | Master RFA ID (Shared with correspondences) | +| rfa_type_id | INT | NOT NULL, FK | Reference to rfa_types | +| created_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | +| created_by | INT | NULL, FK | User who created the record | +| deleted_at | DATETIME | NULL | Soft delete timestamp | + +**Indexes**: + +* PRIMARY KEY (id) +* FOREIGN KEY (id) REFERENCES correspondences(id) ON DELETE CASCADE +* FOREIGN KEY (rfa_type_id) REFERENCES rfa_types(id) +* FOREIGN KEY (created_by) REFERENCES users(user_id) ON DELETE SET NULL +* INDEX (rfa_type_id) +* INDEX (deleted_at) + +**Relationships**: + +* Parent: correspondences, rfa_types, users +* Children: rfa_revisions + +--- + +### 4.5 rfa_revisions (UPDATE v1.7.0) + +**Purpose**: Child table storing revision history of RFAs (1:N) + +| Column Name | Data Type | Constraints | Description | +| ----------- | --------- | --------------------------- | ------------------ | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique revision ID | + +| rfa_id | INT | NOT NULL, FK | Master RFA ID | +| revision_number | INT | NOT NULL | Revision sequence (0, 1, 2...) | +| revision_label | VARCHAR(10) | NULL | Display revision (A, B, 1.1...) | +| is_current | BOOLEAN | DEFAULT FALSE | Current revision flag | +| rfa_status_code_id | INT | NOT NULL, FK | Current RFA status | +| rfa_approve_code_id | INT | NULL, FK | Approval result code | +| title | VARCHAR(255) | NOT NULL | RFA title | +| document_date | DATE | NULL | Document date | +| issued_date | DATE | NULL | Issue date for approval | +| received_date | DATETIME | NULL | Received date | +| approved_date | DATE | NULL | Approval date | +| description | TEXT | NULL | Revision description | +| created_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | Revision creation timestamp | +| created_by | INT | NULL, FK | User who created revision | +| updated_by | INT | NULL, FK | User who last updated | +| details | JSON | NULL | Type-specific details (e.g., RFI questions) | +| v_ref_drawing_count | INT | GENERATED ALWAYS AS (...) VIRTUAL | Virtual Column ดึง Drawing Count จาก JSON details เพื่อทำ Index | +| schema_version | INT | DEFAULT 1 | Version of the schema used with this details | + +**Indexes**: + +* PRIMARY KEY (id) +* FOREIGN KEY (rfa_id) REFERENCES rfas(id) ON DELETE CASCADE +* FOREIGN KEY (rfa_status_code_id) REFERENCES rfa_status_codes(id) +* FOREIGN KEY (rfa_approve_code_id) REFERENCES rfa_approve_codes(id) ON DELETE SET NULL +* FOREIGN KEY (created_by) REFERENCES users(user_id) ON DELETE SET NULL +* FOREIGN KEY (updated_by) REFERENCES users(user_id) ON DELETE SET NULL +* UNIQUE KEY (rfa_id, revision_number) +* UNIQUE KEY (rfa_id, is_current) +* INDEX (rfa_status_code_id) +* INDEX (rfa_approve_code_id) +* INDEX (is_current) +* INDEX (v_ref_drawing_count): ตัวอย่างการ Index ข้อมูลตัวเลขใน JSON + +**Relationships**: + +* Parent: correspondences, rfas, rfa_status_codes, rfa_approve_codes, users +* Children: rfa_items + +--- + +### 4.6 rfa_items + +**Purpose**: Junction table linking RFA revisions to shop drawing revisions (M:N) + +| Column Name | Data Type | Constraints | Description | +| :----------------------- | :-------- | :-------------- | :----------------------- | +| rfa_revision_id | INT | PRIMARY KEY, FK | RFA Revision ID | +| shop_drawing_revision_id | INT | PRIMARY KEY, FK | Shop drawing revision ID | + +**Indexes**: + +* PRIMARY KEY (rfa_revision_id, shop_drawing_revision_id) +* FOREIGN KEY (rfa_revision_id) REFERENCES rfa_revisions(id) ON DELETE CASCADE +* FOREIGN KEY (shop_drawing_revision_id) REFERENCES shop_drawing_revisions(id) ON DELETE CASCADE +* INDEX (shop_drawing_revision_id) + +**Relationships**: + +* Parent: rfa_revisions, shop_drawing_revisions + +**Business Rules**: + +* Used primarily for RFA type = ' DWG ' (Shop Drawing) +* One RFA can contain multiple shop drawings +* One shop drawing can be referenced by multiple RFAs + +--- + + +--- + +## **5. 📐 Drawings Tables (แบบ, หมวดหมู่)** + +### 5.1 contract_drawing_volumes + +**Purpose**: Master table for contract drawing volume classification + +| Column Name | Data Type | Constraints | Description | +| ----------- | ------------ | ----------------------------------- | ------------------------- | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique volume ID | +| project_id | INT | NOT NULL, FK | Reference to projects | +| volume_code | VARCHAR(50) | NOT NULL | Volume code | +| volume_name | VARCHAR(255) | NOT NULL | Volume name | +| description | TEXT | NULL | Volume description | +| sort_order | INT | DEFAULT 0 | Display order | +| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | +| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp | + +**Indexes**: + +* PRIMARY KEY (id) +* FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE +* UNIQUE KEY (project_id, volume_code) +* INDEX (sort_order) + +**Relationships**: + +* Parent: projects +* Referenced by: contract_drawings + +**Business Rules**: + +* Volume codes must be unique within a project +* Used for organizing large sets of contract drawings + +--- + +### 5.2 contract_drawing_cats + +**Purpose**: Master table for contract drawing main categories + +| Column Name | Data Type | Constraints | Description | +| ----------- | ------------ | ----------------------------------- | ------------------------- | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique category ID | +| project_id | INT | NOT NULL, FK | Reference to projects | +| cat_code | VARCHAR(50) | NOT NULL | Category code | +| cat_name | VARCHAR(255) | NOT NULL | Category name | +| description | TEXT | NULL | Category description | +| sort_order | INT | DEFAULT 0 | Display order | +| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | +| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp | + +**Indexes**: + +* PRIMARY KEY (id) +* FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE +* UNIQUE KEY (project_id, cat_code) +* INDEX (sort_order) + +**Relationships**: + +* Parent: projects +* Referenced by: contract_drawing_subcat_cat_maps + +**Business Rules**: + +* Category codes must be unique within a project +* Hierarchical relationship with sub-categories via mapping table + +--- + +### 5.3 contract_drawing_sub_cats + +**Purpose**: Master table for contract drawing sub-categories + +| Column Name | Data Type | Constraints | Description | +| ------------ | ------------ | ----------------------------------- | ------------------------- | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique sub-category ID | +| project_id | INT | NOT NULL, FK | Reference to projects | +| sub_cat_code | VARCHAR(50) | NOT NULL | Sub-category code | +| sub_cat_name | VARCHAR(255) | NOT NULL | Sub-category name | +| description | TEXT | NULL | Sub-category description | +| sort_order | INT | DEFAULT 0 | Display order | +| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | +| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp | + +**Indexes**: + +* PRIMARY KEY (id) +* FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE +* UNIQUE KEY (project_id, sub_cat_code) +* INDEX (sort_order) + +**Relationships**: + +* Parent: projects +* Referenced by: contract_drawings, contract_drawing_subcat_cat_maps + +**Business Rules**: + +* Sub-category codes must be unique within a project +* Can be mapped to multiple main categories via mapping table + +--- + +### 5.4 contract_drawing_subcat_cat_maps (UPDATE v1.7.0) + +**Purpose**: Junction table mapping sub-categories to main categories (M:N) + +| Column Name | Data Type | Constraints | Description | +| ----------- | --------- | ------------------------------- | -------------------------- | +| **id** | **INT** | **PRIMARY KEY, AUTO_INCREMENT** | **Unique mapping ID** | +| project_id | INT | NOT NULL, FK | Reference to projects | +| sub_cat_id | INT | NOT NULL, FK | Reference to sub-category | +| cat_id | INT | NOT NULL, FK | Reference to main category | + +**Indexes**: + +* PRIMARY KEY (id) +* **UNIQUE KEY (project_id, sub_cat_id, cat_id)** +* FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE +* FOREIGN KEY (sub_cat_id) REFERENCES contract_drawing_sub_cats(id) ON DELETE CASCADE +* FOREIGN KEY (cat_id) REFERENCES contract_drawing_cats(id) ON DELETE CASCADE +* INDEX (sub_cat_id) +* INDEX (cat_id) + +**Relationships**: + +* Parent: projects, contract_drawing_sub_cats, contract_drawing_cats +* Referenced by: contract_drawings + +**Business Rules**: + +* Allows flexible categorization +* One sub-category can belong to multiple main categories +* Composite uniqueness enforced via UNIQUE constraint + +--- + +### 5.5 contract_drawings (UPDATE v1.7.0) + +**Purpose**: Master table for contract drawings (from contract specifications) + +| Column Name | Data Type | Constraints | Description | +| --------------- | ------------ | ----------------------------------- | ---------------------------------------- | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique drawing ID | +| project_id | INT | NOT NULL, FK | Reference to projects | +| condwg_no | VARCHAR(255) | NOT NULL | Contract drawing number | +| title | VARCHAR(255) | NOT NULL | Drawing title | +| **map_cat_id** | **INT** | **NULL, FK** | **[CHANGED] Reference to mapping table** | +| volume_id | INT | NULL, FK | Reference to volume | +| **volume_page** | **INT** | **NULL** | **[NEW] Page number within volume** | +| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | +| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp | +| deleted_at | DATETIME | NULL | Soft delete timestamp | +| updated_by | INT | NULL, FK | User who last updated | + +**Indexes**: + +* PRIMARY KEY (id) +* FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE +* **FOREIGN KEY (map_cat_id) REFERENCES contract_drawing_subcat_cat_maps(id) ON DELETE RESTRICT** +* FOREIGN KEY (volume_id) REFERENCES contract_drawing_volumes(id) ON DELETE RESTRICT +* FOREIGN KEY (updated_by) REFERENCES users(user_id) +* UNIQUE KEY (project_id, condwg_no) +* INDEX (map_cat_id) +* INDEX (volume_id) +* INDEX (deleted_at) + +**Relationships**: + +* Parent: projects, contract_drawing_subcat_cat_maps, contract_drawing_volumes, users +* Referenced by: shop_drawing_revision_contract_refs, contract_drawing_attachments + +**Business Rules**: + +* Drawing numbers must be unique within a project +* Represents baseline/contract drawings +* Referenced by shop drawings for compliance tracking +* Soft delete preserves history +* **map_cat_id references the mapping table for flexible categorization** + +--- + +### 5.6 shop_drawing_main_categories (UPDATE v1.7.0) + +**Purpose**: Master table for shop drawing main categories (discipline-level) + +| Column Name | Data Type | Constraints | Description | +| ------------------ | ------------ | ----------------------------------- | ------------------------------------ | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique category ID | +| **project_id** | **INT** | **NOT NULL, FK** | **[NEW] Reference to projects** | +| main_category_code | VARCHAR(50) | NOT NULL, UNIQUE | Category code (ARCH, STR, MEP, etc.) | +| main_category_name | VARCHAR(255) | NOT NULL | Category name | +| description | TEXT | NULL | Category description | +| sort_order | INT | DEFAULT 0 | Display order | +| is_active | TINYINT(1) | DEFAULT 1 | Active status | +| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | +| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp | + +**Indexes**: + +* PRIMARY KEY (id) +* **FOREIGN KEY (project_id) REFERENCES projects(id)** +* UNIQUE (main_category_code) +* INDEX (is_active) +* INDEX (sort_order) + +**Relationships**: + +* **Parent: projects** +* Referenced by: shop_drawings, asbuilt_drawings + +**Business Rules**: + +* **[CHANGED] Project-specific categories (was global)** +* Typically represents engineering disciplines + +--- + +### 5.7 shop_drawing_sub_categories (UPDATE v1.7.0) + +**Purpose**: Master table for shop drawing sub-categories (component-level) + +| Column Name | Data Type | Constraints | Description | +| ----------------- | ------------ | ----------------------------------- | ----------------------------------------------- | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique sub-category ID | +| **project_id** | **INT** | **NOT NULL, FK** | **[NEW] Reference to projects** | +| sub_category_code | VARCHAR(50) | NOT NULL, UNIQUE | Sub-category code (STR-COLUMN, ARCH-DOOR, etc.) | +| sub_category_name | VARCHAR(255) | NOT NULL | Sub-category name | +| description | TEXT | NULL | Sub-category description | +| sort_order | INT | DEFAULT 0 | Display order | +| is_active | TINYINT(1) | DEFAULT 1 | Active status | +| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | +| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp | + +**Indexes**: + +* PRIMARY KEY (id) +* **FOREIGN KEY (project_id) REFERENCES projects(id)** +* UNIQUE (sub_category_code) +* INDEX (is_active) +* INDEX (sort_order) + +**Relationships**: + +* **Parent: projects** +* Referenced by: shop_drawings, asbuilt_drawings + +**Business Rules**: + +* **[CHANGED] Project-specific sub-categories (was global)** +* **[REMOVED] No longer hierarchical under main categories** +* Represents specific drawing types or components + +--- + +### 5.8 shop_drawings (UPDATE v1.7.0) + +**Purpose**: Master table for shop drawings (contractor-submitted) + +| Column Name | Data Type | Constraints | Description | +| ---------------- | ------------ | ----------------------------------- | -------------------------- | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique drawing ID | +| project_id | INT | NOT NULL, FK | Reference to projects | +| drawing_number | VARCHAR(100) | NOT NULL, UNIQUE | Shop drawing number | +| main_category_id | INT | NOT NULL, FK | Reference to main category | +| sub_category_id | INT | NOT NULL, FK | Reference to sub-category | +| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | +| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp | +| deleted_at | DATETIME | NULL | Soft delete timestamp | +| updated_by | INT | NULL, FK | User who last updated | + +**Indexes**: + +* PRIMARY KEY (id) +* UNIQUE (drawing_number) +* FOREIGN KEY (project_id) REFERENCES projects(id) +* FOREIGN KEY (main_category_id) REFERENCES shop_drawing_main_categories(id) +* FOREIGN KEY (sub_category_id) REFERENCES shop_drawing_sub_categories(id) +* FOREIGN KEY (updated_by) REFERENCES users(user_id) +* INDEX (project_id) +* INDEX (main_category_id) +* INDEX (sub_category_id) +* INDEX (deleted_at) + +**Relationships**: + +* Parent: projects, shop_drawing_main_categories, shop_drawing_sub_categories, users +* Children: shop_drawing_revisions + +**Business Rules**: + +* Drawing numbers are globally unique across all projects +* Represents contractor shop drawings +* Can have multiple revisions +* Soft delete preserves history +* **[CHANGED] Title moved to shop_drawing_revisions table** + +--- + +### 5.9 shop_drawing_revisions (UPDATE v1.7.0) + +**Purpose**: Child table storing revision history of shop drawings (1:N) + +| Column Name | Data Type | Constraints | Description | +| ------------------------- | ---------------- | --------------------------- | ---------------------------------------- | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique revision ID | +| shop_drawing_id | INT | NOT NULL, FK | Master shop drawing ID | +| revision_number | INT | NOT NULL | Revision sequence (0, 1, 2...) | +| revision_label | VARCHAR(10) | NULL | Display revision (A, B, C...) | +| revision_date | DATE | NULL | Revision date | +| **title** | **VARCHAR(500)** | **NOT NULL** | **[NEW] Drawing title** | +| description | TEXT | NULL | Revision description/changes | +| **legacy_drawing_number** | **VARCHAR(100)** | **NULL** | **[NEW] Original/legacy drawing number** | +| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Revision creation timestamp | + +**Indexes**: + +* PRIMARY KEY (id) +* FOREIGN KEY (shop_drawing_id) REFERENCES shop_drawings(id) ON DELETE CASCADE +* UNIQUE KEY (shop_drawing_id, revision_number) +* INDEX (revision_date) + +**Relationships**: + +* Parent: shop_drawings +* Referenced by: rfa_items, shop_drawing_revision_contract_refs, shop_drawing_revision_attachments, asbuilt_revision_shop_revisions_refs + +**Business Rules**: + +* Revision numbers are sequential starting from 0 +* Each revision can reference multiple contract drawings +* Each revision can have multiple file attachments +* Linked to RFAs for approval tracking +* **[NEW] Title stored at revision level for version-specific naming** +* **[NEW] legacy_drawing_number supports data migration from old systems** + +--- + +### 5.10 shop_drawing_revision_contract_refs + +**Purpose**: Junction table linking shop drawing revisions to referenced contract drawings (M:N) + +| Column Name | Data Type | Constraints | Description | +| ------------------------ | --------- | --------------- | ---------------------------------- | +| shop_drawing_revision_id | INT | PRIMARY KEY, FK | Reference to shop drawing revision | +| contract_drawing_id | INT | PRIMARY KEY, FK | Reference to contract drawing | + +**Indexes**: + +* PRIMARY KEY (shop_drawing_revision_id, contract_drawing_id) +* FOREIGN KEY (shop_drawing_revision_id) REFERENCES shop_drawing_revisions(id) ON DELETE CASCADE +* FOREIGN KEY (contract_drawing_id) REFERENCES contract_drawings(id) ON DELETE CASCADE +* INDEX (contract_drawing_id) + +**Relationships**: + +* Parent: shop_drawing_revisions, contract_drawings + +**Business Rules**: + +* Tracks which contract drawings each shop drawing revision is based on +* Ensures compliance with contract specifications +* One shop drawing revision can reference multiple contract drawings + +--- + +### 5.11 asbuilt_drawings (NEW v1.7.0) + +**Purpose**: Master table for AS Built drawings (final construction records) + +| Column Name | Data Type | Constraints | Description | +| ---------------- | ------------ | ----------------------------------- | -------------------------- | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique drawing ID | +| project_id | INT | NOT NULL, FK | Reference to projects | +| drawing_number | VARCHAR(100) | NOT NULL, UNIQUE | AS Built drawing number | +| main_category_id | INT | NOT NULL, FK | Reference to main category | +| sub_category_id | INT | NOT NULL, FK | Reference to sub-category | +| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | +| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp | +| deleted_at | DATETIME | NULL | Soft delete timestamp | +| updated_by | INT | NULL, FK | User who last updated | + +**Indexes**: + +* PRIMARY KEY (id) +* UNIQUE (drawing_number) +* FOREIGN KEY (project_id) REFERENCES projects(id) +* FOREIGN KEY (main_category_id) REFERENCES shop_drawing_main_categories(id) +* FOREIGN KEY (sub_category_id) REFERENCES shop_drawing_sub_categories(id) +* FOREIGN KEY (updated_by) REFERENCES users(user_id) +* INDEX (project_id) +* INDEX (main_category_id) +* INDEX (sub_category_id) +* INDEX (deleted_at) + +**Relationships**: + +* Parent: projects, shop_drawing_main_categories, shop_drawing_sub_categories, users +* Children: asbuilt_drawing_revisions + +**Business Rules**: + +* Drawing numbers are globally unique across all projects +* Represents final as-built construction drawings +* Can have multiple revisions +* Soft delete preserves history +* Uses same category structure as shop drawings + +--- + +### 5.12 asbuilt_drawing_revisions (NEW v1.7.0) + +**Purpose**: Child table storing revision history of AS Built drawings (1:N) + +| Column Name | Data Type | Constraints | Description | +| --------------------- | ------------ | --------------------------- | ------------------------------ | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique revision ID | +| asbuilt_drawing_id | INT | NOT NULL, FK | Master AS Built drawing ID | +| revision_number | INT | NOT NULL | Revision sequence (0, 1, 2...) | +| revision_label | VARCHAR(10) | NULL | Display revision (A, B, C...) | +| revision_date | DATE | NULL | Revision date | +| title | VARCHAR(500) | NOT NULL | Drawing title | +| description | TEXT | NULL | Revision description/changes | +| legacy_drawing_number | VARCHAR(100) | NULL | Original/legacy drawing number | +| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Revision creation timestamp | + +**Indexes**: + +* PRIMARY KEY (id) +* FOREIGN KEY (asbuilt_drawing_id) REFERENCES asbuilt_drawings(id) ON DELETE CASCADE +* UNIQUE KEY (asbuilt_drawing_id, revision_number) +* INDEX (revision_date) + +**Relationships**: + +* Parent: asbuilt_drawings +* Referenced by: asbuilt_revision_shop_revisions_refs, asbuilt_drawing_revision_attachments + +**Business Rules**: + +* Revision numbers are sequential starting from 0 +* Each revision can reference multiple shop drawing revisions +* Each revision can have multiple file attachments +* Title stored at revision level for version-specific naming +* legacy_drawing_number supports data migration from old systems + +--- + +### 5.13 asbuilt_revision_shop_revisions_refs (NEW v1.7.0) + +**Purpose**: Junction table linking AS Built drawing revisions to shop drawing revisions (M:N) + +| Column Name | Data Type | Constraints | Description | +| --------------------------- | --------- | --------------- | ---------------------------------- | +| asbuilt_drawing_revision_id | INT | PRIMARY KEY, FK | Reference to AS Built revision | +| shop_drawing_revision_id | INT | PRIMARY KEY, FK | Reference to shop drawing revision | + +**Indexes**: + +* PRIMARY KEY (asbuilt_drawing_revision_id, shop_drawing_revision_id) +* FOREIGN KEY (asbuilt_drawing_revision_id) REFERENCES asbuilt_drawing_revisions(id) ON DELETE CASCADE +* FOREIGN KEY (shop_drawing_revision_id) REFERENCES shop_drawing_revisions(id) ON DELETE CASCADE +* INDEX (shop_drawing_revision_id) + +**Relationships**: + +* Parent: asbuilt_drawing_revisions, shop_drawing_revisions + +**Business Rules**: + +* Tracks which shop drawings each AS Built drawing revision is based on +* Maintains construction document lineage +* One AS Built revision can reference multiple shop drawing revisions +* Supports traceability from final construction to approved shop drawings + +--- + +### 5.14 asbuilt_drawing_revision_attachments (NEW v1.7.0) + +**Purpose**: Junction table linking AS Built drawing revisions to file attachments (M:N) + +| Column Name | Data Type | Constraints | Description | +| --------------------------- | ------------------------------------- | --------------- | ------------------------------------- | +| asbuilt_drawing_revision_id | INT | PRIMARY KEY, FK | Reference to AS Built revision | +| attachment_id | INT | PRIMARY KEY, FK | Reference to attachment file | +| file_type | ENUM('PDF', 'DWG', 'SOURCE', 'OTHER') | NULL | File type classification | +| is_main_document | BOOLEAN | DEFAULT FALSE | Main document flag (1 = primary file) | + +**Indexes**: + +* PRIMARY KEY (asbuilt_drawing_revision_id, attachment_id) +* FOREIGN KEY (asbuilt_drawing_revision_id) REFERENCES asbuilt_drawing_revisions(id) ON DELETE CASCADE +* FOREIGN KEY (attachment_id) REFERENCES attachments(id) ON DELETE CASCADE +* INDEX (attachment_id) + +**Relationships**: + +* Parent: asbuilt_drawing_revisions, attachments + +**Business Rules**: + +* Each AS Built revision can have multiple file attachments +* File types: PDF (documents), DWG (CAD files), SOURCE (source files), OTHER (miscellaneous) +* One attachment can be marked as main document per revision +* Cascade delete when revision is deleted + +--- + +## **6. 🔄 Circulations Tables (ใบเวียนภายใน)** + +### 6.1 circulation_status_codes + +**Purpose**: Master table for circulation workflow status codes + +| Column Name | Data Type | Constraints | Description | +| ----------- | ----------- | --------------------------- | --------------------------------------------------- | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique status ID | +| code | VARCHAR(20) | NOT NULL, UNIQUE | Status code (OPEN, IN_REVIEW, COMPLETED, CANCELLED) | +| description | VARCHAR(50) | NOT NULL | Status description | +| sort_order | INT | DEFAULT 0 | Display order | +| is_active | TINYINT(1) | DEFAULT 1 | Active status | + +**Indexes**: + +* PRIMARY KEY (id) +* UNIQUE (code) +* INDEX (is_active) +* INDEX (sort_order) + +**Relationships**: + +* Referenced by: circulations + +**Seed Data**: 4 status codes + +* OPEN: Initial status when created +* IN_REVIEW: Under review by recipients +* COMPLETED: All recipients have responded +* CANCELLED: Withdrawn/cancelled + +--- + +### 6.2 circulations + +**Purpose**: Master table for internal circulation sheets (document routing) + +| Column Name | Data Type | Constraints | Description | +| ----------------------- | ------------ | ----------------------------------- | ----------------------------------------- | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique circulation ID | +| correspondence_id | INT | UNIQUE, FK | Link to correspondence (1:1 relationship) | +| organization_id | INT | NOT NULL, FK | Organization that owns this circulation | +| circulation_no | VARCHAR(100) | NOT NULL | Circulation sheet number | +| circulation_subject | VARCHAR(500) | NOT NULL | Subject/title | +| circulation_status_code | VARCHAR(20) | NOT NULL, FK | Current status code | +| created_by_user_id | INT | NOT NULL, FK | User who created circulation | +| submitted_at | TIMESTAMP | NULL | Submission timestamp | +| closed_at | TIMESTAMP | NULL | Closure timestamp | +| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | +| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp | + +**Indexes**: + +* PRIMARY KEY (id) +* UNIQUE (correspondence_id) +* FOREIGN KEY (correspondence_id) REFERENCES correspondences(id) +* FOREIGN KEY (organization_id) REFERENCES organizations(id) +* FOREIGN KEY (circulation_status_code) REFERENCES circulation_status_codes(code) +* FOREIGN KEY (created_by_user_id) REFERENCES users(user_id) +* INDEX (organization_id) +* INDEX (circulation_status_code) +* INDEX (created_by_user_id) + +**Relationships**: + +* Parent: correspondences, organizations, circulation_status_codes, users +* Children: circulation_routings, circulation_attachments + +**Business Rules**: + +* Internal document routing within organization +* One-to-one relationship with correspondences +* Tracks document review/approval workflow +* Status progression: OPEN → IN_REVIEW → COMPLETED/CANCELLED + +--- + +## **7. 📤 Transmittals Tables (เอกสารนำส่ง)** + +### 7.1 transmittals + +**Purpose**: Child table for transmittal-specific data (1:1 with correspondences) + +| Column Name | Data Type | Constraints | Description | +| ----------------- | --------- | --------------- | --------------------------------------------------------- | +| correspondence_id | INT | PRIMARY KEY, FK | Reference to correspondences (1:1) | +| purpose | ENUM | NULL | Purpose: FOR_APPROVAL, FOR_INFORMATION, FOR_REVIEW, OTHER | +| remarks | TEXT | NULL | Additional remarks | + +**Indexes**: + +* PRIMARY KEY (correspondence_id) +* FOREIGN KEY (correspondence_id) REFERENCES correspondences(id) ON DELETE CASCADE +* INDEX (purpose) + +**Relationships**: + +* Parent: correspondences +* Children: transmittal_items + +**Business Rules**: + +* One-to-one relationship with correspondences +* Transmittal is a correspondence type for forwarding documents +* Contains metadata about the transmission + +--- + +### 7.2 transmittal_items + +**Purpose**: Junction table listing documents included in transmittal (M:N) + +| Column Name | Data Type | Constraints | Description | +| ---------------------- | ------------ | --------------------------- | --------------------------------------- | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique item ID | +| transmittal_id | INT | NOT NULL, FK | Reference to transmittal | +| item_correspondence_id | INT | NOT NULL, FK | Reference to document being transmitted | +| quantity | INT | DEFAULT 1 | Number of copies | +| remarks | VARCHAR(255) | NULL | Item-specific remarks | + +**Indexes**: + +* PRIMARY KEY (id) +* FOREIGN KEY (transmittal_id) REFERENCES transmittals(correspondence_id) ON DELETE CASCADE +* FOREIGN KEY (item_correspondence_id) REFERENCES correspondences(id) ON DELETE CASCADE +* UNIQUE KEY (transmittal_id, item_correspondence_id) +* INDEX (item_correspondence_id) + +**Relationships**: + +* Parent: transmittals, correspondences + +**Business Rules**: + +* One transmittal can contain multiple documents +* Tracks quantity of physical copies (if applicable) +* Links to any type of correspondence document + +--- + +## **8. 📎 File Management Tables (ไฟล์แนบ)** + +### 8.1 attachments + +**Purpose**: Central repository for all file attachments in the system + +| Column Name | Data Type | Constraints | Description | +| ------------------- | ------------ | --------------------------- | -------------------------------------------------------------- | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique attachment ID | +| original_filename | VARCHAR(255) | NOT NULL | Original filename from upload | +| stored_filename | VARCHAR(255) | NOT NULL | System-generated unique filename | +| file_path | VARCHAR(500) | NOT NULL | Full file path on server (/share/dms-data/) | +| mime_type | VARCHAR(100) | NOT NULL | MIME type (application/pdf, image/jpeg, etc.) | +| file_size | INT | NOT NULL | File size in bytes | +| uploaded_by_user_id | INT | NOT NULL, FK | User who uploaded file | +| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Upload timestamp | +| is_temporary | BOOLEAN | DEFAULT TRUE | ระบุว่าเป็นไฟล์ชั่วคราว (ยังไม่ได้ Commit) | +| temp_id\* | VARCHAR(100) | NULL | ID ชั่วคราวสำหรับอ้างอิงตอน Upload Phase 1 (อาจใช้ร่วมกับ id หรือแยกก็ได้) | +| expires_at | DATETIME | NULL | เวลาหมดอายุของไฟล์ Temp (เพื่อให้ Cron Job ลบออก) | +| checksum | VARCHAR(64) | NULL | SHA-256 Checksum สำหรับ Verify File Integrity [Req 3.9.3] | + +**Indexes**: + +* PRIMARY KEY (id) +* FOREIGN KEY (uploaded_by_user_id) REFERENCES users(user_id) ON DELETE CASCADE +* INDEX (stored_filename) +* INDEX (mime_type) +* INDEX (uploaded_by_user_id) +* INDEX (created_at) + +**Relationships**: + +* Parent: users +* Referenced by: correspondence_attachments, circulation_attachments, shop_drawing_revision_attachments, contract_drawing_attachments + +**Business Rules**: + +* Central storage prevents file duplication +* Stored filename prevents naming conflicts +* File path points to QNAP NAS storage +* Original filename preserved for download +* One file record can be linked to multiple documents + +--- + +### 8.2 correspondence_attachments + +**Purpose**: Junction table linking correspondences to file attachments (M:N) + +| Column Name | Data Type | Constraints | Description | +| ----------------- | --------- | --------------- | ---------------------------- | +| correspondence_id | INT | PRIMARY KEY, FK | Reference to correspondences | +| attachment_id | INT | PRIMARY KEY, FK | Reference to attachments | +| is_main_document | BOOLEAN | DEFAULT FALSE | Main/primary document flag | + +**Indexes**: + +* PRIMARY KEY (correspondence_id, attachment_id) +* FOREIGN KEY (correspondence_id) REFERENCES correspondences(id) ON DELETE CASCADE +* FOREIGN KEY (attachment_id) REFERENCES attachments(id) ON DELETE CASCADE +* INDEX (attachment_id) +* INDEX (is_main_document) + +**Relationships**: + +* Parent: correspondences, attachments + +**Business Rules**: + +* One correspondence can have multiple attachments +* One attachment can be linked to multiple correspondences +* is_main_document identifies primary file (typically PDF) + +--- + +### 8.3 circulation_attachments + +**Purpose**: Junction table linking circulations to file attachments (M:N) + +| Column Name | Data Type | Constraints | Description | +| ---------------- | --------- | --------------- | -------------------------- | +| circulation_id | INT | PRIMARY KEY, FK | Reference to circulations | +| attachment_id | INT | PRIMARY KEY, FK | Reference to attachments | +| is_main_document | BOOLEAN | DEFAULT FALSE | Main/primary document flag | + +**Indexes**: + +* PRIMARY KEY (circulation_id, attachment_id) +* FOREIGN KEY (circulation_id) REFERENCES circulations(id) ON DELETE CASCADE +* FOREIGN KEY (attachment_id) REFERENCES attachments(id) ON DELETE CASCADE +* INDEX (attachment_id) +* INDEX (is_main_document) + +**Relationships**: + +* Parent: circulations, attachments + +--- + +### 8.4 shop_drawing_revision_attachments + +**Purpose**: Junction table linking shop drawing revisions to file attachments (M:N) + +| Column Name | Data Type | Constraints | Description | +| ------------------------ | --------- | --------------- | ---------------------------------- | +| shop_drawing_revision_id | INT | PRIMARY KEY, FK | Reference to shop drawing revision | +| attachment_id | INT | PRIMARY KEY, FK | Reference to attachments | +| file_type | ENUM | NULL | File type: PDF, DWG, SOURCE, OTHER | +| is_main_document | BOOLEAN | DEFAULT FALSE | Main/primary document flag | + +**Indexes**: + +* PRIMARY KEY (shop_drawing_revision_id, attachment_id) +* FOREIGN KEY (shop_drawing_revision_id) REFERENCES shop_drawing_revisions(id) ON DELETE CASCADE +* FOREIGN KEY (attachment_id) REFERENCES attachments(id) ON DELETE CASCADE +* INDEX (attachment_id) +* INDEX (file_type) +* INDEX (is_main_document) + +**Relationships**: + +* Parent: shop_drawing_revisions, attachments + +**Business Rules**: + +* file_type categorizes drawing file formats +* Typically includes PDF for viewing and DWG for editing +* SOURCE may include native CAD files + +--- + +### 8.5 contract_drawing_attachments + +**Purpose**: Junction table linking contract drawings to file attachments (M:N) + +| Column Name | Data Type | Constraints | Description | +| ------------------- | --------- | --------------- | ---------------------------------- | +| contract_drawing_id | INT | PRIMARY KEY, FK | Reference to contract drawing | +| attachment_id | INT | PRIMARY KEY, FK | Reference to attachments | +| file_type | ENUM | NULL | File type: PDF, DWG, SOURCE, OTHER | +| is_main_document | BOOLEAN | DEFAULT FALSE | Main/primary document flag | + +**Indexes**: + +* PRIMARY KEY (contract_drawing_id, attachment_id) +* FOREIGN KEY (contract_drawing_id) REFERENCES contract_drawings(id) ON DELETE CASCADE +* FOREIGN KEY (attachment_id) REFERENCES attachments(id) ON DELETE CASCADE +* INDEX (attachment_id) +* INDEX (file_type) +* INDEX (is_main_document) + +**Relationships**: + +* Parent: contract_drawings, attachments + +--- + +## **9. 🔢 Document Numbering System Tables (ระบบเลขที่เอกสาร)** + +### 9.1 document_number_formats + +**Purpose**: Master table defining numbering formats for each document type + +| Column Name | Data Type | Constraints | Description | +| ---------------------- | ------------ | --------------------------- | -------------------------------------------- | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique format ID | +| project_id | INT | NOT NULL, FK | Reference to projects | +| correspondence_type_id | INT | NOT NULL, FK | Reference to correspondence_types | +| format_string | VARCHAR(100) | NOT NULL | Format pattern (e.g., {ORG}-{TYPE}-{YYYY}-#) | +| description | TEXT | NULL | Format description | +| reset_annually | BOOLEAN | DEFAULT TRUE | Start sequence new every year | +| is_active | TINYINT(1) | DEFAULT 1 | Active status | + +**Indexes**: + +* PRIMARY KEY (id) +* FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE +* FOREIGN KEY (correspondence_type_id) REFERENCES correspondence_types(id) ON DELETE CASCADE +* UNIQUE KEY (project_id, correspondence_type_id) +* INDEX (is_active) + +**Relationships**: + +* Parent: projects, correspondence_types + +**Business Rules**: + +* Defines how document numbers are constructed +* Supports placeholders: {PROJ}, {ORG}, {TYPE}, {YYYY}, {MM}, {#} + +--- + +### 9.2 document_number_counters (UPDATE v1.7.0) + +**Purpose**: Transaction table tracking running numbers (High Concurrency) + +| Column Name | Data Type | Constraints | Description | +| -------------------------- | ----------- | ------------- | ----------------------------------------------- | +| project_id | INT | PK, NOT NULL | โครงการ | +| originator_organization_id | INT | PK, NOT NULL | องค์กรผู้ส่ง | +| recipient_organization_id | INT | PK, NOT NULL | องค์กรผู้รับ (0 = no recipient / RFA) | +| correspondence_type_id | INT | PK, NULL | ประเภทเอกสาร (NULL = default) | +| sub_type_id | INT | PK, DEFAULT 0 | ประเภทย่อย สำหรับ TRANSMITTAL (0 = ไม่ระบุ) | +| rfa_type_id | INT | PK, DEFAULT 0 | ประเภท RFA (0 = ไม่ใช่ RFA) | +| discipline_id | INT | PK, DEFAULT 0 | สาขางาน (0 = ไม่ระบุ) | +| reset_scope | VARCHAR(20) | PK, NOT NULL | Scope of reset (YEAR_2024, MONTH_2024_01, NONE) | +| last_number | INT | DEFAULT 0 | เลขล่าสุดที่ถูกใช้งานไปแล้ว | +| version | INT | DEFAULT 0 | Optimistic Lock Version | +| updated_at | DATETIME(6) | ON UPDATE | เวลาที่อัปเดตล่าสุด | + +**Indexes**: + +* **PRIMARY KEY (project_id, originator_organization_id, recipient_organization_id, correspondence_type_id, sub_type_id, rfa_type_id, discipline_id, reset_scope)** +* INDEX idx_counter_lookup (project_id, correspondence_type_id, reset_scope) +* INDEX idx_counter_org (originator_organization_id, reset_scope) + +**Business Rules**: + +* **Composite Primary Key 8 Columns**: เพื่อรองรับการรันเลขที่ซับซ้อนและ Reset Scope ที่หลากหลาย +* **Concurrency Control**: ใช้ Redis Lock หรือ Optimistic Locking (version) +* **Reset Scope**: ใช้ Field `reset_scope` ควบคุมการ Reset แทน `current_year` แบบเดิม + +--- + +### 9.3 document_number_audit (UPDATE v1.7.0) + +**Purpose**: Audit log for document number generation (Debugging & Tracking) + +| Column Name | Data Type | Constraints | Description | +| :------------------------- | :----------- | :----------------- | :-------------------------------------- | +| id | INT | PK, AI | ID ของ audit record | +| document_id | INT | NULL, FK | ID ของเอกสารที่สร้างเลขที่ (NULL if failed) | +| document_type | VARCHAR(50) | NULL | ประเภทเอกสาร | +| document_number | VARCHAR(100) | NOT NULL | เลขที่เอกสารที่สร้าง (ผลลัพธ์) | +| operation | ENUM | DEFAULT 'CONFIRM' | RESERVE, CONFIRM, MANUAL_OVERRIDE, etc. | +| status | ENUM | DEFAULT 'RESERVED' | RESERVED, CONFIRMED, CANCELLED, VOID | +| counter_key | JSON | NOT NULL | Counter key ที่ใช้ (JSON 8 fields) | +| reservation_token | VARCHAR(36) | NULL | Token การจอง | +| idempotency_key | VARCHAR(128) | NULL | Idempotency Key from request | +| originator_organization_id | INT | NULL | องค์กรผู้ส่ง | +| recipient_organization_id | INT | NULL | องค์กรผู้รับ | +| template_used | VARCHAR(200) | NOT NULL | Template ที่ใช้ในการสร้าง | +| old_value | TEXT | NULL | Previous value | +| new_value | TEXT | NULL | New value | +| user_id | INT | NULL, FK | ผู้ขอสร้างเลขที่ | +| is_success | BOOLEAN | DEFAULT TRUE | สถานะความสำเร็จ | +| created_at | TIMESTAMP | DEFAULT NOW | วันที่/เวลาที่สร้าง | +| total_duration_ms | INT | NULL | เวลารวมทั้งหมดในการสร้าง (ms) | + +**Indexes**: + +* PRIMARY KEY (id) +* FOREIGN KEY (document_id) REFERENCES correspondences(id) ON DELETE CASCADE +* FOREIGN KEY (user_id) REFERENCES users(user_id) +* INDEX (document_id) +* INDEX (user_id) +* INDEX (status) +* INDEX (operation) +* INDEX (document_number) +* INDEX (reservation_token) +* INDEX (created_at) + +--- + +### 9.4 document_number_errors (UPDATE v1.7.0) + +**Purpose**: Error log for failed document number generation + +| Column Name | Data Type | Constraints | Description | +| :------------ | :-------- | :---------- | :--------------------------------------------- | +| id | INT | PK, AI | ID ของ error record | +| error_type | ENUM | NOT NULL | LOCK_TIMEOUT, VERSION_CONFLICT, DB_ERROR, etc. | +| error_message | TEXT | NULL | ข้อความ error | +| stack_trace | TEXT | NULL | Stack trace สำหรับ debugging | +| context_data | JSON | NULL | Context ของ request | +| user_id | INT | NULL | ผู้ที่เกิด error | +| created_at | TIMESTAMP | DEFAULT NOW | วันที่เกิด error | +| resolved_at | TIMESTAMP | NULL | วันที่แก้ไขแล้ว | + +**Indexes**: + +* PRIMARY KEY (id) +* INDEX (error_type) +* INDEX (created_at) +* INDEX (user_id) +* INDEX (resolved_at) + +--- + +### 9.5 document_number_reservations (NEW v1.7.0) + +**Purpose**: Two-Phase Commit table for document number reservation + +| Column Name | Data Type | Constraints | Description | +| :--------------------- | :----------- | :--------------- | :----------------------------------- | +| id | INT | PK, AI | Unique ID | +| token | VARCHAR(36) | UNIQUE, NOT NULL | UUID v4 Reservation Token | +| document_number | VARCHAR(100) | UNIQUE, NOT NULL | เลขที่เอกสารที่จอง | +| document_number_status | ENUM | DEFAULT RESERVED | RESERVED, CONFIRMED, CANCELLED, VOID | +| document_id | INT | NULL, FK | ID ของเอกสาร (เมื่อ Confirm แล้ว) | +| expires_at | DATETIME(6) | NOT NULL | เวลาหมดอายุการจอง | +| project_id | INT | NOT NULL, FK | Project Context | +| user_id | INT | NOT NULL, FK | User Context | + +**Indexes**: + +* PRIMARY KEY (id) +* FOREIGN KEY (document_id) REFERENCES correspondence_revisions(id) ON DELETE SET NULL +* FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE +* FOREIGN KEY (correspondence_type_id) REFERENCES correspondence_types(id) ON DELETE CASCADE +* FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE +* INDEX idx_token (token) +* INDEX idx_status (document_number_status) +* INDEX idx_status_expires (document_number_status, expires_at) +* INDEX idx_document_id (document_id) +* INDEX idx_user_id (user_id) +* INDEX idx_reserved_at (reserved_at) + +--- + +## **10. ⚙️ Unified Workflow Engine Tables (UPDATE v1.7.0)** + +### 10.1 workflow_definitions + +**Purpose**: เก็บแม่แบบ (Template) ของ Workflow (Definition / DSL) + +| Column Name | Data Type | Constraints | Description | +| :------------ | :---------- | :----------- | :------------------------------------- | +| id | CHAR(36) | PK, UUID | Unique Workflow Definition ID | +| workflow_code | VARCHAR(50) | NOT NULL | รหัส Workflow (เช่น RFA_FLOW_V1) | +| version | INT | DEFAULT 1 | หมายเลข Version | +| description | TEXT | NULL | คำอธิบาย Workflow | +| dsl | JSON | NOT NULL | นิยาม Workflow ต้นฉบับ (YAML/JSON Format) | +| compiled | JSON | NOT NULL | โครงสร้าง Execution Tree ที่ Compile แล้ว | +| is_active | BOOLEAN | DEFAULT TRUE | สถานะการใช้งาน | +| created_at | TIMESTAMP | DEFAULT NOW | วันที่สร้าง | +| updated_at | TIMESTAMP | ON UPDATE | วันที่แก้ไขล่าสุด | + +**Indexes**: + +* PRIMARY KEY (id) +* UNIQUE KEY (workflow_code, version) +* INDEX (is_active) + +--- + +### 10.2 workflow_instances + +**Purpose**: เก็บสถานะของ Workflow ที่กำลังรันอยู่จริง (Runtime) + +| Column Name | Data Type | Constraints | Description | +| :------------ | :---------- | :--------------- | :--------------------------------------------- | +| id | CHAR(36) | PK, UUID | Unique Instance ID | +| definition_id | CHAR(36) | FK, NOT NULL | อ้างอิง Definition ที่ใช้ | +| entity_type | VARCHAR(50) | NOT NULL | ประเภทเอกสาร (rfa_revision, correspondence...) | +| entity_id | VARCHAR(50) | NOT NULL | ID ของเอกสาร | +| current_state | VARCHAR(50) | NOT NULL | สถานะปัจจุบัน | +| status | ENUM | DEFAULT 'ACTIVE' | ACTIVE, COMPLETED, CANCELLED, TERMINATED | +| context | JSON | NULL | ตัวแปร Context สำหรับตัดสินใจ | +| created_at | TIMESTAMP | DEFAULT NOW | เวลาที่สร้าง | +| updated_at | TIMESTAMP | ON UPDATE | เวลาที่อัปเดตล่าสุด | + +**Indexes**: + +* PRIMARY KEY (id) +* FOREIGN KEY (definition_id) REFERENCES workflow_definitions(id) ON DELETE CASCADE +* INDEX (entity_type, entity_id) +* INDEX (current_state) + +--- + +### 10.3 workflow_histories + +**Purpose**: เก็บประวัติการดำเนินการในแต่ละ Step (Audit Trail) + +| Column Name | Data Type | Constraints | Description | +| :---------------- | :---------- | :----------- | :-------------------- | +| id | CHAR(36) | PK, UUID | Unique ID | +| instance_id | CHAR(36) | FK, NOT NULL | อ้างอิง Instance | +| from_state | VARCHAR(50) | NOT NULL | สถานะต้นทาง | +| to_state | VARCHAR(50) | NOT NULL | สถานะปลายทาง | +| action | VARCHAR(50) | NOT NULL | Action ที่กระทำ | +| action_by_user_id | INT | FK, NULL | User ID ผู้กระทำ | +| comment | TEXT | NULL | ความเห็น | +| metadata | JSON | NULL | Snapshot ข้อมูล ณ ขณะนั้น | +| created_at | TIMESTAMP | DEFAULT NOW | เวลาที่กระทำ | + +**Indexes**: + +* PRIMARY KEY (id) +* FOREIGN KEY (instance_id) REFERENCES workflow_instances(id) ON DELETE CASCADE +* INDEX (instance_id) +* INDEX (action_by_user_id) + +--- + +## **11. 🖥️ System & Logs Tables (ระบบ, บันทึก)** + +> **Audit Logging Architecture:** +### 1. Audit Logging + +**Table: `audit_logs`** + +บันทึกการเปลี่ยนแปลงสำคัญ: + +- User actions (CREATE, UPDATE, DELETE) +- Entity type และ Entity ID +- Old/New values (JSON) +- IP Address, User Agent + +### 2. User Preferences + +**Table: `user_preferences`** + +เก็บการตั้งค่าส่วนตัว: + +- Language preference +- Notification settings +- UI preferences (JSON) + +### 3. JSON Schema Validation + +**Table: `json_schemas`** + +เก็บ Schema สำหรับ Validate JSON fields: + +- `correspondence_revisions.details` +- `user_preferences.preferences` + +--- + + +### 11.1 json_schemas (UPDATE v1.7.0) + +**Purpose**: เก็บ Schema สำหรับ Validate JSON Columns (Req 3.12) + +| Column Name | Data Type | Constraints | Description | +| :---------------- | :----------- | :----------- | :------------------------------- | +| id | INT | PK, AI | Unique ID | +| schema_code | VARCHAR(100) | NOT NULL | รหัส Schema (เช่น RFA_DWG) | +| version | INT | DEFAULT 1 | เวอร์ชันของ Schema | +| table_name | VARCHAR(100) | NOT NULL | ชื่อตารางเป้าหมาย | +| schema_definition | JSON | NOT NULL | JSON Schema Definition | +| ui_schema | JSON | NULL | โครงสร้าง UI Schema สำหรับ Frontend | +| virtual_columns | JSON | NULL | Config สำหรับสร้าง Virtual Columns | +| migration_script | JSON | NULL | Script สำหรับแปลงข้อมูล | +| is_active | BOOLEAN | DEFAULT TRUE | สถานะการใช้งาน | + + +--- + +### 11.2 audit_logs (UPDATE v1.7.0) + +**Purpose**: Centralized audit logging for all system actions (Req 6.1) + +| Column Name | Data Type | Constraints | Description | +| :----------- | :----------- | :------------------------ | :------------------------------------------- | +| audit_id | BIGINT | PK, AI | Unique log ID | +| request_id | VARCHAR(100) | NULL | Trace ID linking to app logs | +| user_id | INT | NULL, FK | User who performed action | +| action | VARCHAR(100) | NOT NULL | Action name (e.g. rfa.create) | +| severity | ENUM | DEFAULT 'INFO' | INFO, WARN, ERROR, CRITICAL | +| entity_type | VARCHAR(50) | NULL | Module/Table name (e.g. rfa, correspondence) | +| entity_id | VARCHAR(50) | NULL | ID of affected entity | +| details_json | JSON | NULL | Context data / Old & New values | +| ip_address | VARCHAR(45) | NULL | User IP address | +| user_agent | VARCHAR(255) | NULL | User browser/client info | +| created_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | Log timestamp | + +**Indexes**: + +* PRIMARY KEY (audit_id, created_at) -- **Partition Key** +* INDEX idx_audit_user (user_id) +* INDEX idx_audit_action (action) +* INDEX idx_audit_entity (entity_type, entity_id) +* INDEX idx_audit_created (created_at) + +**Partitioning**: +* **PARTITION BY RANGE (YEAR(created_at))**: แบ่ง Partition รายปี เพื่อประสิทธิภาพในการเก็บข้อมูลระยะยาว + +--- + +### 11.3 notifications (UPDATE v1.7.0) + +**Purpose**: System notifications for users + +| Column Name | Data Type | Constraints | Description | +| :---------------- | :----------- | :-------------------------- | :------------------------ | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique notification ID | +| user_id | INT | NOT NULL, FK | Recipient user ID | +| title | VARCHAR(255) | NOT NULL | Notification title | +| message | TEXT | NOT NULL | Notification body | +| notification_type | ENUM | NOT NULL | Type: EMAIL, LINE, SYSTEM | +| is_read | BOOLEAN | DEFAULT FALSE | Read status | +| entity_type | VARCHAR(50) | NULL | Related Entity Type | +| entity_id | INT | NULL | Related Entity ID | +| created_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | Notification timestamp | + +**Indexes**: + +* PRIMARY KEY (id, created_at) -- **Partition Key** +* INDEX idx_notif_user (user_id) +* INDEX idx_notif_type (notification_type) +* INDEX idx_notif_read (is_read) +* INDEX idx_notif_created (created_at) + +**Partitioning**: +* **PARTITION BY RANGE (YEAR(created_at))**: แบ่ง Partition รายปี + +--- + +## **12. 🔍 Views (มุมมองข้อมูล)** + +### 12.1 v_current_correspondences + +**Purpose**: แสดงข้อมูล Correspondence Revision ล่าสุด (is_current = TRUE) + +### 12.2 v_current_rfas + +**Purpose**: แสดงข้อมูล RFA Revision ล่าสุด พร้อม Status และ Approve Code + +### 12.3 v_user_tasks (Unified Workflow) + +**Purpose**: รวมรายการงานที่ยังค้างอยู่ (Status = ACTIVE) จากทุกระบบ (RFA, Circulation, Correspondence) เพื่อนำไปแสดงใน Dashboard + +### 12.4 v_audit_log_details + +**Purpose**: แสดง audit_logs พร้อมข้อมูล username และ email ของผู้กระทำ + +### 12.5 v_user_all_permissions + +**Purpose**: รวมสิทธิ์ทั้งหมด (Global + Project + Organization) ของผู้ใช้ทุกคน + +### 12.6 v_documents_with_attachments + +**Purpose**: แสดงเอกสารทั้งหมดที่มีไฟล์แนบ (Correspondence, Circulation, Drawings) + +### 12.7 v_document_statistics + +**Purpose**: แสดงสถิติเอกสารตามประเภทและสถานะ + +--- + +## **13. 📊 Index Summaries (สรุป Index)** + +> **Performance Optimization Strategy:** +### 1. Indexing Strategy + +**Primary Indexes:** + +- Primary Keys (AUTO_INCREMENT) +- Foreign Keys (automatic in InnoDB) +- Unique Constraints (business keys) + +**Secondary Indexes:** + +```sql +-- Correspondence search +CREATE INDEX idx_corr_type_status ON correspondence_revisions(correspondence_type_id, correspondence_status_id); +CREATE INDEX idx_corr_date ON correspondence_revisions(document_date); + +-- Virtual columns for JSON +CREATE INDEX idx_v_ref_project ON correspondence_revisions(v_ref_project_id); +CREATE INDEX idx_v_doc_subtype ON correspondence_revisions(v_doc_subtype); + +-- User lookup +CREATE INDEX idx_user_email ON users(email); +CREATE INDEX idx_user_org ON users(primary_organization_id, is_active); +``` + +### 2. Virtual Columns + +ใช้ Virtual Columns สำหรับ Index JSON fields: + +```sql +ALTER TABLE correspondence_revisions +ADD COLUMN v_ref_project_id INT GENERATED ALWAYS AS (JSON_UNQUOTE(JSON_EXTRACT(details, '$.ref_project_id'))) VIRTUAL, +ADD INDEX idx_v_ref_project(v_ref_project_id); +``` + +### 3. Partitioning (Future) + +พิจารณา Partition ตาราง `audit_logs` ตามปี: + +```sql +ALTER TABLE audit_logs +PARTITION BY RANGE (YEAR(created_at)) ( + PARTITION p2024 VALUES LESS THAN (2025), + PARTITION p2025 VALUES LESS THAN (2026), + PARTITION p_future VALUES LESS THAN MAXVALUE +); +``` + +--- + + +### 13.1 Performance Indexes + +| Table Name | Index Columns | Purpose | +| :----------------------- | :------------------------------------------------ | :----------------------------- | +| correspondences | (project_id, correspondence_number) | Fast lookup by document number | +| correspondences | (correspondence_type_id) | Filter by type | +| correspondence_revisions | (correspondence_id, is_current) | Get current revision | +| rfas | (rfa_type_id) | Filter by RFA type | +| rfa_revisions | (rfa_id, is_current) | Get current RFA revision | +| rfa_revisions | (rfa_status_code_id) | Filter by status | +| audit_logs | (created_at) | Date range queries | +| audit_logs | (user_id) | User activity history | +| audit_logs | (module, action) | Action type analysis | +| notifications | (user_id, is_read) | Unread notifications query | +| document_number_counters | (project_id, correspondence_type_id, reset_scope) | Running number generation | +| workflow_instances | (entity_type, entity_id) | Workflow lookup by document ID | +| workflow_instances | (current_state) | Monitor active workflows | + +### 13.2 Unique Constraints + +| Table Name | Columns | Description | +| :---------------------- | :----------------------------------- | :--------------------------------- | +| users | (username) | Unique login name | +| users | (email) | Unique email address | +| organizations | (organization_code) | Unique organization code | +| projects | (project_code) | Unique project code | +| contracts | (contract_code) | Unique contract code | +| correspondences | (project_id, correspondence_number) | Unique document number per project | +| shop_drawings | (drawing_number) | Unique shop drawing number | +| document_number_formats | (project_id, correspondence_type_id) | One format per type per project | +| workflow_definitions | (workflow_code, version) | Unique workflow code per version | + +--- + +## **14. 🛡️ Data Integrity Constraints (ความถูกต้องของข้อมูล)** + +### 14.1 Soft Delete Policy + +* **Tables with `deleted_at`**: + * users + * organizations + * projects + * contracts + * correspondences + * rfas + * shop_drawings + * contract_drawings +* **Rule**: Records are never physically deleted. `deleted_at` is set to timestamp. +* **Query Rule**: All standard queries MUST include `WHERE deleted_at IS NULL`. + +### 14.2 Foreign Key Cascades + +* **ON DELETE CASCADE**: + * Used for child tables that cannot exist without parent (e.g., `correspondence_revisions`, `rfa_revisions`, `correspondence_attachments`). +* **ON DELETE RESTRICT**: + * Used for master data references to prevent accidental deletion of used data (e.g., `correspondence_types`, `organizations`). +* **ON DELETE SET NULL**: + * Used for optional references (e.g., `created_by`, `originator_id`). + +--- + +## **15. 🔐 Security & Permissions Model (ความปลอดภัย)** + +### 15.1 Row-Level Security (RLS) Logic + +* **Organization Scope**: Users can only see documents where `originator_id` OR `recipient_organization_id` matches their organization. +* **Project Scope**: Users can only see documents within projects they are assigned to. +* **Confidentiality**: Documents marked `is_confidential` are visible ONLY to specific roles or users. + +### 15.2 Role-Based Access Control (RBAC) + +* **Permissions** are granular (e.g., `correspondence.view`, `correspondence.create`). +* **Roles** aggregate permissions (e.g., `Document Controller` = `view` + `create` + `edit`). +* **Assignments** link Users to Roles within a Context (Global, Project, or Organization). + +--- + +## **16. 🔄 Data Migration & Seeding (การย้ายข้อมูล)** + +### 16.1 Initial Seeding (V1.7.0) + +1. **Master Data**: + * `organizations`: Owner, Consultant, Contractor + * `projects`: LCBP3 + * `correspondence_types`: LETTER, MEMO, TRANSMITTAL, RFA + * `rfa_types`: DWG, MAT, DOC, RFI + * `rfa_status_codes`: DFT, PEND, APPR, REJ + * `disciplines`: GEN, STR, ARC, MEP +2. **System Users**: + * `admin`: Super Admin + * `system`: System Bot for automated tasks + +### 16.2 Migration Strategy + +* **Schema Migration**: Use TypeORM Migrations or raw SQL scripts (versioned). +* **Data Migration**: + * **V1.6.0 -> V1.7.0**: + * Run SQL script `9_lcbp3_v1_7_0.sql` + * Migrate `document_number_counters` to 8-col composite PK. + * Initialize `document_number_reservations`. + * Update `json_schemas` with new columns. + +--- + + +### 16.3 Temporary Migration Tracking Tables (V1.8.0 n8n Migration) + +ตารางเหล่านี้ถูกใช้ชั่วคราวระหว่างกระบวนการ Migrate เอกสาร PDF 20,000 ฉบับด้วย n8n (ดูรายละเอียดใน 3-05-n8n-migration-setup-guide.md) และไม่ใช่ตาราง Business หลักของระบบ + +#### 16.3.1 migration_progress +**Purpose**: เก็บ Checkpoint สถานะการ Migrate + +| Column Name | Data Type | Constraints | Description | +| :--- | :--- | :--- | :--- | +| batch_id | VARCHAR(50) | PRIMARY KEY | รหัสชุดการ Migrate | +| last_processed_index | INT | DEFAULT 0 | ลำดับล่าสุดที่ประมวลผลผ่าน | +| status | ENUM | DEFAULT 'RUNNING' | สถานะ (RUNNING, COMPLETED, FAILED) | +| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | เวลาอัปเดตล่าสุด | + +#### 16.3.2 migration_review_queue +**Purpose**: คิวเอกสารที่ต้องการให้เจ้าหน้าที่ตรวจสอบ (Confidence ต่ำกว่าเกณฑ์) +*หมายเหตุ: เมื่อตรวจสอบผ่านและสร้าง Correspondence จริงแล้ว ข้อมูลในนี้อาจถูกลบหรือเก็บเป็น Log ได้* + +| Column Name | Data Type | Constraints | Description | +| :--- | :--- | :--- | :--- | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique ID | +| document_number | VARCHAR(100) | NOT NULL, UNIQUE | เลขที่เอกสาร (จาก OCR) | +| title | TEXT | | ชื่อเรื่อง | +| original_title | TEXT | | ชื่อเรื่องต้นฉบับก่อนตรวจสอบ | +| ai_suggested_category | VARCHAR(50) | | หมวดหมู่ที่ AI แนะนำ | +| ai_confidence | DECIMAL(4,3) | | ค่าความมั่นใจของ AI (0.000 - 1.000) | +| ai_issues | JSON | | รายละเอียดปัญหาที่ AI พบ | +| review_reason | VARCHAR(255) | | เหตุผลที่ต้องตรวจสอบ (เช่น Confidence ต่ำ) | +| status | ENUM | DEFAULT 'PENDING' | PENDING, APPROVED, REJECTED | +| reviewed_by | VARCHAR(100) | | ผู้ตรวจสอบ | +| reviewed_at | TIMESTAMP | NULL | เวลาที่ตรวจสอบ | +| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | วันที่บันทึกเข้าคิว | + +#### 16.3.3 migration_errors +**Purpose**: บันทึกข้อผิดพลาด (Errors) ระหว่างการทำงานของ n8n workflow + +| Column Name | Data Type | Constraints | Description | +| :--- | :--- | :--- | :--- | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique ID | +| batch_id | VARCHAR(50) | INDEX | รหัสชุดการ Migrate | +| document_number | VARCHAR(100) | | เลขที่เอกสาร | +| error_type | ENUM | INDEX | ประเภท Error (FILE_NOT_FOUND, AI_PARSE_ERROR, etc.) | +| error_message | TEXT | | รายละเอียด Error | +| raw_ai_response | TEXT | | Raw response จาก AI กรณีแปลผลไม่ได้ | +| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | วันที่บันทึก | + +#### 16.3.4 migration_fallback_state +**Purpose**: ติดตามสถานะ Fallback ของ AI (เช่น เปลี่ยน Model เมื่อ Error ถี่) + +| Column Name | Data Type | Constraints | Description | +| :--- | :--- | :--- | :--- | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique ID | +| batch_id | VARCHAR(50) | UNIQUE | รหัสชุดการ Migrate | +| recent_error_count | INT | DEFAULT 0 | จำนวน Error รวดล่าสุด | +| is_fallback_active | BOOLEAN | DEFAULT FALSE | สถานะการใช้งาน Fallback Model | +| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | เวลาอัปเดตล่าสุด | + +#### 16.3.5 import_transactions +**Purpose**: ป้องกันข้อมูลซ้ำ (Idempotency) ระหว่างการ Patch ข้อมูล + +| Column Name | Data Type | Constraints | Description | +| :--- | :--- | :--- | :--- | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique ID | +| idempotency_key | VARCHAR(255) | UNIQUE, NOT NULL | Key สำหรับเช็คซ้ำ | +| document_number | VARCHAR(100) | | เลขที่เอกสาร | +| batch_id | VARCHAR(100) | | รหัสชุดการ Migrate | +| status_code | INT | DEFAULT 201 | HTTP Status ของการ Import | +| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | วันที่บันทึก | + +#### 16.3.6 migration_daily_summary +**Purpose**: สรุปยอดการทำงานรายวันแยกตาม Batch + +| Column Name | Data Type | Constraints | Description | +| :--- | :--- | :--- | :--- | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique ID | +| batch_id | VARCHAR(50) | UNIQUE KEY PART 1 | รหัสชุดการ Migrate | +| summary_date | DATE | UNIQUE KEY PART 2 | วันที่สรุป | +| total_processed | INT | DEFAULT 0 | จำนวนที่ประมวลผลรวม | +| auto_ingested | INT | DEFAULT 0 | จำนวนที่เข้าสู่ระบบสำเร็จ | +| sent_to_review | INT | DEFAULT 0 | จำนวนที่ส่งคิวตรวจสอบ | +| rejected | INT | DEFAULT 0 | จำนวนที่ถูกปฏิเสธ | +| errors | INT | DEFAULT 0 | จำนวนที่เกิด Error | +| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | วันที่บันทึก | + +--- +## **17. 📈 Monitoring & Maintenance (การดูแลรักษา)** + +### 17.1 Database Maintenance + +* **Daily**: Incremental Backup. +* **Weekly**: Full Backup + `OPTIMIZE TABLE` for heavy tables (`audit_logs`, `notifications`). +* **Monthly**: Archive old `audit_logs` partitions to cold storage. + +### 17.2 Health Checks + +* Monitor `document_number_errors` for numbering failures. +* Monitor `workflow_instances` for stuck workflows (`status = ' IN_PROGRESS '` > 7 days). +* Check `document_number_counters` for gaps or resets. + +--- + +## **18. 📚 Best Practices** +### 1. Naming Conventions + +- **Tables**: `snake_case`, plural (e.g., `correspondences`, `users`) +- **Columns**: `snake_case` (e.g., `correspondence_number`, `created_at`) +- **Foreign Keys**: `{referenced_table_singular}_id` (e.g., `project_id`, `user_id`) +- **Junction Tables**: `{table1}_{table2}` (e.g., `correspondence_tags`) + +### 2. Timestamp Columns + +ทุกตารางควรมี: + +- `created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP` +- `updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP` + +### 3. Soft Delete + +ใช้ `deleted_at DATETIME NULL` แทนการลบจริง: + +```sql +-- Soft delete +UPDATE correspondences SET deleted_at = NOW() WHERE id = 1; + +-- Query active records +SELECT * FROM correspondences WHERE deleted_at IS NULL; +``` + +### 4. JSON Field Guidelines + +- ใช้สำหรับข้อมูลที่ไม่ต้อง Query บ่อย +- สร้าง Virtual Columns สำหรับ fields ที่ต้อง Index +- Validate ด้วย JSON Schema +- Document structure ใน Data Dictionary + +--- + +--- + +## **19. 📖 Glossary (คำศัพท์)** + +* **RFA**: Request for Approval (เอกสารขออนุมัติ) +* **Transmittal**: Document Transmittal Sheet (ใบนำส่งเอกสาร) +* **Shop Drawing**: แบบก่อสร้างที่ผู้รับเหมาจัดทำ +* **Contract Drawing**: แบบสัญญา (แบบตั้งต้น) +* **Revision**: ฉบับแก้ไข (0, 1, 2, A, B, C) +* **Originator**: ผู้จัดทำ/ผู้ส่งเอกสาร +* **Recipient**: ผู้รับเอกสาร +* **Workflow**: กระบวนการทำงาน/อนุมัติ +* **Discipline**: สาขางาน (เช่น โยธา, สถาปัตย์, ไฟฟ้า) + +--- + +**End of Data Dictionary V1.8.0** + diff --git a/specs/03-Data-and-Storage/03-04-legacy-data-migration.md b/specs/03-Data-and-Storage/03-04-legacy-data-migration.md index c6f25d6..de88cd1 100644 --- a/specs/03-Data-and-Storage/03-04-legacy-data-migration.md +++ b/specs/03-Data-and-Storage/03-04-legacy-data-migration.md @@ -4,7 +4,7 @@ | ------------------------------------------------------------------ | ------- | | legacy PDF document migration to system v1.8.0 uses n8n and Ollama | 1.8.0 | -> **Note:** Category Enum system-driven, Idempotency Contract, Duplicate Handling Clarification, Storage Enforcement, Audit Log Enhancement, Review Queue Integration, Revision Drift Protection, Execution Time, Encoding Normalization, Security Hardening, AI Physical Isolation (ASUSTOR), Folder Standard (/data/dms) +> **Note:** Category Enum system-driven, Idempotency Contract, Duplicate Handling Clarification, Storage Enforcement, Audit Log Enhancement, Review Queue Integration, Revision Drift Protection, Execution Time, Encoding Normalization, Security Hardening, Orchestrator on QNAP, AI Physical Isolation (Desktop Desk-5439), Folder Standard (/share/np-dms/n8n) --- @@ -21,8 +21,8 @@ ## 2. โครงสร้างพื้นฐาน (Migration Infrastructure) -- **Migration Orchestrator:** n8n (รันจาก Docker Container บน ASUSTOR NAS) -- **AI Validator:** Ollama (รันใน Internal Network บน ASUSTOR NAS) +- **Migration Orchestrator:** n8n (รันจาก Docker Container บน QNAP NAS) +- **AI Validator:** Ollama (รันใน Internal Network บน Desktop Desk-5439, RTX 2060 SUPER 8GB) - **Target Database:** MariaDB (`correspondences` table) บน QNAP NAS - **Target Storage:** QNAP File System — **ผ่าน Backend StorageService API เท่านั้น** (ห้าม move file โดยตรง) - **Connection:** 2.5G LAN + LACP / Internal VLAN @@ -35,18 +35,18 @@ **File Migration:** - ย้ายไฟล์ PDF ทั้งหมดจากแหล่งเก็บไปยัง Folder ชั่วคราวบน NAS (QNAP) -- Target Path: `/data/dms/staging_ai/` +- Target Path: `/share/np-dms/staging_ai/` **Mount Folder:** -- Bind Mount `/data/dms/staging_ai/` เข้ากับ n8n Container แบบ **read-only** -- สร้าง `/data/dms/migration_logs/` Volume แยกสำหรับเขียน Log แบบ **read-write** +- Bind Mount `/share/np-dms/staging_ai/` เข้ากับ n8n Container แบบ **read-only** +- สร้าง `/share/np-dms/n8n/migration_logs/` Volume แยกสำหรับเขียน Log แบบ **read-write** **Ollama Config:** -- ติดตั้ง Ollama บน ASUSTOR NAS +- ติดตั้ง Ollama บน Desktop (Desk-5439, RTX 2060 SUPER 8GB) - No DB credentials, Internal network only ```bash -# แนะนำ: llama3.2:3b (เร็ว, VRAM ~3GB, เหมาะ Classification) +# แนะนำ: llama3.2:3b (เร็ว, VRAM ~3GB, เหมาะ Classification) หรือ ollama run llama3.2:3b ollama pull llama3.2:3b # Fallback: mistral:7b-instruct-q4_K_M (แม่นกว่า, VRAM ~5GB) @@ -55,7 +55,7 @@ ollama pull llama3.2:3b **ทดสอบ Ollama:** ```bash -curl http://:11434/api/generate \ +curl http://192.168.20.100:11434/api/generate \ -d '{"model":"llama3.2:3b","prompt":"reply: ok","stream":false}' ``` @@ -165,7 +165,9 @@ return items.map(item => ({ json: { ...item.json, document_number: normalize(item.json.document_number), - title: normalize(item.json.title) + title: normalize(item.json.title), + // Mapping เลขอ้างอิงเก่า (Legacy Number) เพื่อนำไปเก็บใน details JSON + legacy_document_number: item.json.document_number } })); ``` @@ -174,7 +176,7 @@ return items.map(item => ({ - ตรวจสอบไฟล์ PDF มีอยู่จริงบน NAS - Normalize ชื่อไฟล์เป็น **UTF-8 NFC** -- Path Traversal Guard: resolved path ต้องอยู่ใน `/data/dms/staging_ai` เท่านั้น +- Path Traversal Guard: resolved path ต้องอยู่ใน `/share/np-dms/staging_ai` เท่านั้น - **Output 0** → valid → Node 3 - **Output 1** → error → Node 5D (ไม่หายเงียบ) @@ -246,7 +248,7 @@ if (item.json.excel_revision !== undefined) { #### Node 5A: Auto Ingest — Backend API -> ⚠️ **Storage Enforcement:** n8n ส่งแค่ `source_file_path` — Backend จะ generate UUID, enforce path strategy (`/data/dms/uploads/YYYY/MM/{uuid}.pdf`), และ move file atomically ผ่าน StorageService +> ⚠️ **Storage Enforcement:** n8n ส่งแค่ `source_file_path` — Backend จะ generate UUID, enforce path strategy (`/share/np-dms/staging_ai/...`), และ move file atomically ผ่าน StorageService ```http POST /api/correspondences/import @@ -303,9 +305,9 @@ Review → Admin Approve → POST /api/correspondences/import (เหมือ Admin Reject → ลบออกจาก queue ไม่สร้าง record ``` -#### Node 5C: Reject Log → `/data/dms/migration_logs/reject_log.csv` +#### Node 5C: Reject Log → `/share/np-dms/n8n/migration_logs/reject_log.csv` -#### Node 5D: Error Log → `/data/dms/migration_logs/error_log.csv` + MariaDB +#### Node 5D: Error Log → `/share/np-dms/n8n/migration_logs/error_log.csv` + MariaDB --- @@ -370,7 +372,7 @@ SELECT ROW_COUNT(); COMMIT; ``` -**Step 3:** ย้ายไฟล์กลับ `/data/dms/staging_ai/` ผ่าน Script แยก +**Step 3:** ย้ายไฟล์กลับ `/share/np-dms/staging_ai/` ผ่าน Script แยก **Step 4:** Reset State ```sql @@ -424,4 +426,4 @@ GROUP BY idempotency_key HAVING COUNT(*) > 1; --- -> **ข้อแนะนำด้าน Physical Storage:** ไฟล์ PDF ทั้ง 20,000 ไฟล์จะถูก move โดย Backend StorageService ไปยัง path ที่ถูกต้องโดยอัตโนมัติ ไม่ปล่อยค้างไว้ที่ `/data/dms/staging_ai/` +> **ข้อแนะนำด้าน Physical Storage:** ไฟล์ PDF ทั้ง 20,000 ไฟล์จะถูก move โดย Backend StorageService ไปยัง path ที่ถูกต้องโดยอัตโนมัติ ไม่ปล่อยค้างไว้ที่ `/share/np-dms/staging_ai/` diff --git a/specs/03-Data-and-Storage/03-05-n8n-migration-setup-guide.md b/specs/03-Data-and-Storage/03-05-n8n-migration-setup-guide.md index c4acbb3..f23845c 100644 --- a/specs/03-Data-and-Storage/03-05-n8n-migration-setup-guide.md +++ b/specs/03-Data-and-Storage/03-05-n8n-migration-setup-guide.md @@ -2,77 +2,103 @@ เอกสารนี้จัดทำขึ้นเพื่อรองรับการ Migration เอกสาร PDF 20,000 ฉบับ ตามแผนใน `03-04-legacy-data-migration.md` และ `ADR-017-ollama-data-migration.md` -> **Note:** Category Enum system-driven, Idempotency-Key Header, Storage Enforcement, Audit Log, Encoding Normalization, Security Hardening, Nginx Rate Limit, Docker Hardening, AI Physical Isolation (ASUSTOR), Folder Standard (/data/dms) +> **Note:** Category Enum system-driven, Idempotency-Key Header, Storage Enforcement, Audit Log, Encoding Normalization, Security Hardening, Nginx Rate Limit, Docker Hardening, Orchestrator on QNAP, AI Physical Isolation (Desktop Desk-5439), Folder Standard (/share/np-dms/n8n) --- ## 📌 ส่วนที่ 1: การติดตั้งและตั้งค่าเบื้องต้น -### 1.1 ติดตั้ง n8n บน ASUSTOR NAS (Docker) +### 1.1 ปรับปรุง n8n บน QNAP NAS (Docker) -```bash -mkdir -p /data/dms/n8n -cd /data/dms/n8n +คุณสามารถเพิ่ม PostgreSQL Service เข้าไปใน `docker-compose-lcbp3-n8n.yml` ปัจจุบันบน QNAP NAS ได้ดังนี้: -cat > docker-compose.yml << 'EOF' +```yaml version: '3.8' +x-restart: &restart_policy + restart: unless-stopped + +x-logging: &default_logging + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "5" + services: + n8n-db: + <<: [*restart_policy, *default_logging] + image: postgres:16-alpine + container_name: n8n-db + environment: + - POSTGRES_USER=n8n + - POSTGRES_PASSWORD= + - POSTGRES_DB=n8n + volumes: + - "/share/np-dms/n8n/postgres-data:/var/lib/postgresql/data" + networks: + lcbp3: {} + healthcheck: + test: ['CMD-SHELL', 'pg_isready -h localhost -U n8n -d n8n'] + interval: 10s + timeout: 5s + retries: 5 + n8n: - image: n8nio/n8n:latest - container_name: n8n-migration - restart: unless-stopped - # Docker Hardening (Patch) - mem_limit: 2g - logging: - driver: json-file - options: - max-size: "10m" - max-file: "3" + <<: [*restart_policy, *default_logging] + image: n8nio/n8n:1.78.0 + container_name: n8n + depends_on: + n8n-db: + condition: service_healthy + deploy: + resources: + limits: + cpus: "1.5" + memory: 2G + environment: + TZ: "Asia/Bangkok" + NODE_ENV: "production" + N8N_PUBLIC_URL: "https://n8n.np-dms.work/" + WEBHOOK_URL: "https://n8n.np-dms.work/" + N8N_EDITOR_BASE_URL: "https://n8n.np-dms.work/" + N8N_PROTOCOL: "https" + N8N_HOST: "n8n.np-dms.work" + N8N_PORT: 5678 + N8N_PROXY_HOPS: "1" + N8N_DIAGNOSTICS_ENABLED: 'false' + N8N_SECURE_COOKIE: 'true' + N8N_ENCRYPTION_KEY: "9AAIB7Da9DW1qAhJE5/Bz4SnbQjeAngI" + N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS: 'true' + GENERIC_TIMEZONE: "Asia/Bangkok" + # DB Setup + DB_TYPE: postgresdb + DB_POSTGRESDB_DATABASE: n8n + DB_POSTGRESDB_HOST: n8n-db + DB_POSTGRESDB_PORT: 5432 + DB_POSTGRESDB_USER: n8n + DB_POSTGRESDB_PASSWORD: + # Data Prune + EXECUTIONS_DATA_PRUNE: 'true' + EXECUTIONS_DATA_MAX_AGE: 168 + EXECUTIONS_DATA_PRUNE_TIMEOUT: 60 ports: - "5678:5678" - environment: - - N8N_HOST=0.0.0.0 - - N8N_PORT=5678 - - N8N_PROTOCOL=http - - NODE_ENV=production - - WEBHOOK_URL=http://:5678/ - - GENERIC_TIMEZONE=Asia/Bangkok - - TZ=Asia/Bangkok - - N8N_SECURE_COOKIE=false - - N8N_USER_FOLDER=/home/node/.n8n - - N8N_PUBLIC_API_DISABLED=true - - N8N_BASIC_AUTH_ACTIVE=true - - N8N_BASIC_AUTH_USER=admin - - N8N_BASIC_AUTH_PASSWORD= - - N8N_PAYLOAD_SIZE_MAX=10485760 - - EXECUTIONS_DATA_PRUNE=true - - EXECUTIONS_DATA_MAX_AGE=168 - - EXECUTIONS_DATA_PRUNE_TIMEOUT=60 - - DB_TYPE=postgresdb - - DB_POSTGRESDB_HOST= - - DB_POSTGRESDB_PORT=5432 - - DB_POSTGRESDB_DATABASE=n8n - - DB_POSTGRESDB_USER=n8n - - DB_POSTGRESDB_PASSWORD= - volumes: - - ./n8n_data:/home/node/.n8n - # read-only: อ่านไฟล์ PDF ต้นฉบับเท่านั้น - - /data/dms/staging_ai:/data/dms/staging_ai:ro - # read-write: เขียน Log และ CSV ทั้งหมด - - /data/dms/migration_logs:/data/dms/migration_logs:rw networks: - - n8n-network - -networks: - n8n-network: - driver: bridge -EOF - -docker-compose up -d + lcbp3: {} + volumes: + - "/share/np-dms/n8n:/home/node/.n8n" + - "/share/np-dms/n8n/cache:/home/node/.cache" + - "/share/np-dms/n8n/scripts:/scripts" + - "/share/np-dms/n8n/data:/data" + - "/var/run/docker.sock:/var/run/docker.sock" + # read-only: อ่านไฟล์ PDF ต้นฉบับเท่านั้น + - "/share/np-dms/staging_ai:/share/np-dms/staging_ai:ro" + # read-write: เขียน Log และ CSV ทั้งหมด + - "/share/np-dms/n8n/migration_logs:/share/np-dms/n8n/migration_logs:rw" ``` -> ⚠️ **Volume หมายเหตุ:** `/data/dms/staging_ai` = **read-only** (อ่านไฟล์ต้นฉบับ) และ `/data/dms/migration_logs` = **read-write** (เขียน Log/CSV) — ห้ามเขียน CSV ลง `staging_ai` เพราะจะ Error ทันที +> ⚠️ **Volume หมายเหตุ:** `/share/np-dms/staging_ai` = **read-only** (อ่านไฟล์ต้นฉบับ) และ `/share/np-dms/n8n/migration_logs` = **read-write** (เขียน Log/CSV) — ห้ามเขียน CSV ลง `staging_ai` เพราะจะ Error ทันที ### 1.2 Nginx Rate Limit @@ -92,19 +118,19 @@ location /api/correspondences/import { **Settings → Environment Variables ใน n8n UI:** -| Variable | ค่าที่แนะนำ | คำอธิบาย | -| --------------------------- | ---------------------------- | ------------------------------------ | -| `OLLAMA_HOST` | `http://:11434` | URL ของ Ollama (ใน internal network) | -| `OLLAMA_MODEL_PRIMARY` | `llama3.2:3b` | Model หลัก | -| `OLLAMA_MODEL_FALLBACK` | `mistral:7b-instruct-q4_K_M` | Model สำรอง | -| `MIGRATION_BATCH_SIZE` | `10` | จำนวน Record ต่อ Batch | -| `MIGRATION_DELAY_MS` | `2000` | Delay ระหว่าง Request (ms) | -| `CONFIDENCE_THRESHOLD_HIGH` | `0.85` | Threshold Auto Ingest | -| `CONFIDENCE_THRESHOLD_LOW` | `0.60` | Threshold Review Queue | -| `MAX_RETRY_COUNT` | `3` | จำนวนครั้ง Retry | -| `FALLBACK_ERROR_THRESHOLD` | `5` | Error ที่ trigger Fallback | -| `BACKEND_URL` | `https://` | URL ของ LCBP3 Backend | -| `MIGRATION_BATCH_ID` | `migration_20260226` | ID ของ Batch | +| Variable | ค่าที่แนะนำ | คำอธิบาย | +| --------------------------- | ----------------------------- | ---------------------------------- | +| `OLLAMA_HOST` | `http://192.168.20.100:11434` | URL ของ Ollama (Desktop Desk-5439) | +| `OLLAMA_MODEL_PRIMARY` | `llama3.2:3b` | Model หลัก | +| `OLLAMA_MODEL_FALLBACK` | `mistral:7b-instruct-q4_K_M` | Model สำรอง | +| `MIGRATION_BATCH_SIZE` | `10` | จำนวน Record ต่อ Batch | +| `MIGRATION_DELAY_MS` | `2000` | Delay ระหว่าง Request (ms) | +| `CONFIDENCE_THRESHOLD_HIGH` | `0.85` | Threshold Auto Ingest | +| `CONFIDENCE_THRESHOLD_LOW` | `0.60` | Threshold Review Queue | +| `MAX_RETRY_COUNT` | `3` | จำนวนครั้ง Retry | +| `FALLBACK_ERROR_THRESHOLD` | `5` | Error ที่ trigger Fallback | +| `BACKEND_URL` | `https://` | URL ของ LCBP3 Backend | +| `MIGRATION_BATCH_ID` | `migration_20260226` | ID ของ Batch | --- @@ -193,12 +219,12 @@ CREATE TABLE IF NOT EXISTS migration_daily_summary ( **Credentials → Add New:** #### 🔐 Ollama API -| Field | ค่า | -| -------------- | --------------------------- | -| Name | `Ollama Local API` | -| Type | `HTTP Request` | -| Base URL | `http://:11434` | -| Authentication | `None` | +| Field | ค่า | +| -------------- | ----------------------------- | +| Name | `Ollama Local API` | +| Type | `HTTP Request` | +| Base URL | `http://192.168.20.100:11434` | +| Authentication | `None` | #### 🔐 LCBP3 Backend API | Field | ค่า | @@ -306,9 +332,9 @@ $workflow.variables.system_categories = categories; // ตรวจ File Mount try { - const files = fs.readdirSync('/data/dms/staging_ai'); + const files = fs.readdirSync('/share/np-dms/staging_ai'); if (files.length === 0) throw new Error('staging_ai is empty'); - fs.writeFileSync('/data/dms/migration_logs/.preflight_ok', new Date().toISOString()); + fs.writeFileSync('/share/np-dms/n8n/migration_logs/.preflight_ok', new Date().toISOString()); } catch (err) { throw new Error(`File mount check failed: ${err.message}`); } @@ -382,9 +408,9 @@ for (const item of items) { const safeName = path.basename( String(docNumber).replace(/[^a-zA-Z0-9\-_.]/g, '_') ).normalize('NFC'); - const filePath = path.resolve('/data/dms/staging_ai', `${safeName}.pdf`); + const filePath = path.resolve('/share/np-dms/staging_ai', `${safeName}.pdf`); - if (!filePath.startsWith('/data/dms/staging_ai/')) { + if (!filePath.startsWith('/share/np-dms/staging_ai/')) { errorItems.push({ ...item, json: { ...item.json, error: 'Path traversal detected', error_type: 'FILE_NOT_FOUND' } }); continue; } @@ -612,7 +638,10 @@ return [autoIngest, reviewQueue, rejectLog, errorLog]; "ai_confidence": "={{ $json.ai_result.confidence }}", "ai_issues": "={{ $json.ai_result.detected_issues }}", "migrated_by": "SYSTEM_IMPORT", - "batch_id": "={{ $env.MIGRATION_BATCH_ID }}" + "batch_id": "={{ $env.MIGRATION_BATCH_ID }}", + "details": { + "legacy_number": "={{ $json.legacy_document_number }}" + } }, "options": { "timeout": 30000, "retry": { "count": 3, "delay": 5000 } } } @@ -662,12 +691,12 @@ ON DUPLICATE KEY UPDATE status = 'PENDING', review_reason = '{{ $json.review_rea --- -### 4.10 Node 5C: Reject Log → `/data/migration_logs/` +#### 4.10 Node 5C: Reject Log → `/share/np-dms/n8n/migration_logs/` ```javascript const fs = require('fs'); const item = $input.first(); -const csvPath = '/data/dms/migration_logs/reject_log.csv'; +const csvPath = '/share/np-dms/n8n/migration_logs/reject_log.csv'; const header = 'timestamp,document_number,title,reject_reason,ai_confidence,ai_issues\n'; const esc = (s) => `"${String(s||'').replace(/"/g,'""')}"`; @@ -687,12 +716,12 @@ return [$input.first()]; --- -### 4.11 Node 5D: Error Log → `/data/migration_logs/` + MariaDB +#### 4.11 Node 5D: Error Log → `/share/np-dms/n8n/migration_logs/` + MariaDB ```javascript const fs = require('fs'); const item = $input.first(); -const csvPath = '/data/dms/migration_logs/error_log.csv'; +const csvPath = '/share/np-dms/n8n/migration_logs/error_log.csv'; const header = 'timestamp,document_number,error_type,error_message,raw_ai_response\n'; const esc = (s) => `"${String(s||'').replace(/"/g,'""')}"`; diff --git a/specs/03-Data-and-Storage/fix-project-permissions.sql b/specs/03-Data-and-Storage/fix-project-permissions.sql deleted file mode 100644 index f2fc6eb..0000000 --- a/specs/03-Data-and-Storage/fix-project-permissions.sql +++ /dev/null @@ -1,29 +0,0 @@ --- Fix Project Permissions --- File: specs/07-database/fix-project-permissions.sql --- 1. Ensure project.view permission exists -INSERT IGNORE INTO permissions ( - permission_id, - permission_name, - description, - module, - is_active - ) -VALUES ( - 202, - 'project.view', - 'ดูรายการโครงการ', - 'project', - 1 - ); --- 2. Grant project.view to Superadmin (Role 1) -INSERT IGNORE INTO role_permissions (role_id, permission_id) -VALUES (1, 202); --- 3. Grant project.view to Organization Admin (Role 2) -INSERT IGNORE INTO role_permissions (role_id, permission_id) -VALUES (2, 202); --- 4. Grant project.view to Project Manager (Role 6) -INSERT IGNORE INTO role_permissions (role_id, permission_id) -VALUES (6, 202); --- 5. Grant project.view to Viewer (Role 5) -INSERT IGNORE INTO role_permissions (role_id, permission_id) -VALUES (5, 202); \ No newline at end of file diff --git a/specs/03-Data-and-Storage/lcbp3-v1.8.0-schema.sql b/specs/03-Data-and-Storage/lcbp3-v1.8.0-schema.sql index 3302229..3447e64 100644 --- a/specs/03-Data-and-Storage/lcbp3-v1.8.0-schema.sql +++ b/specs/03-Data-and-Storage/lcbp3-v1.8.0-schema.sql @@ -16,12 +16,16 @@ -- Major Changes: -- 1. ปรับปรุง: -- 1.1 TABLE correspondences --- - INDEX idx_doc_number (document_number), +-- - INDEX idx_doc_number (correspondence_number), -- - INDEX idx_deleted_at (deleted_at), -- - INDEX idx_created_by (created_by), -- 2. เพิ่ม: -- 2.1 TABLE migration_progress -- 2.2 TABLE import_transactions +-- 2.3 TABLE migration_review_queue +-- 2.4 TABLE migration_errors +-- 2.5 TABLE migration_fallback_state +-- 2.6 TABLE migration_daily_summary -- ========================================================== SET NAMES utf8mb4; @@ -50,8 +54,19 @@ DROP VIEW IF EXISTS v_current_correspondences; -- 🗑️ DROP TABLE SCRIPT: LCBP3-DMS v1.4.2 -- คำเตือน: ข้อมูลทั้งหมดจะหายไป กรุณา Backup ก่อนรันบน Production SET FOREIGN_KEY_CHECKS = 0; + DROP TABLE IF EXISTS migration_progress; + DROP TABLE IF EXISTS import_transactions; + +DROP TABLE IF EXISTS migration_review_queue; + +DROP TABLE IF EXISTS migration_errors; + +DROP TABLE IF EXISTS migration_fallback_state; + +DROP TABLE IF EXISTS migration_daily_summary; + -- ============================================================ -- ส่วนที่ 1: ตาราง System, Logs & Preferences (ตารางปลายทาง/ส่วนเสริม) -- ============================================================ @@ -472,16 +487,19 @@ CREATE TABLE correspondences ( created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', created_by INT COMMENT 'ผู้สร้าง', deleted_at DATETIME NULL COMMENT 'สำหรับ Soft Delete', - INDEX idx_doc_number (document_number), + INDEX idx_corr_number (correspondence_number), INDEX idx_deleted_at (deleted_at), INDEX idx_created_by (created_by), FOREIGN KEY (correspondence_type_id) REFERENCES correspondence_types (id) ON DELETE RESTRICT, FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE, - FOREIGN KEY (originator_id) REFERENCES organizations (id) ON DELETE SET NULL, - FOREIGN KEY (created_by) REFERENCES users (user_id) ON DELETE SET NULL, + FOREIGN KEY (originator_id) REFERENCES organizations (id) ON DELETE + SET NULL, + FOREIGN KEY (created_by) REFERENCES users (user_id) ON DELETE + SET NULL, -- Foreign Key ที่รวมเข้ามาจาก ALTER (ระบุชื่อ Constraint ตามที่ต้องการ) - CONSTRAINT fk_corr_discipline FOREIGN KEY (discipline_id) REFERENCES disciplines (id) ON DELETE SET NULL, - UNIQUE KEY uq_corr_no_per_project (project_id, correspondence_number) + CONSTRAINT fk_corr_discipline FOREIGN KEY (discipline_id) REFERENCES disciplines (id) ON DELETE + SET NULL, + UNIQUE KEY uq_corr_no_per_project (project_id, correspondence_number) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง "แม่" ของเอกสารโต้ตอบ เก็บข้อมูลที่ไม่เปลี่ยนตาม Revision'; -- ตารางเชื่อมผู้รับ (TO/CC) สำหรับเอกสารแต่ละฉบับ (M:N) @@ -1545,11 +1563,12 @@ CREATE INDEX idx_wf_hist_user ON workflow_histories (action_by_user_id); -- Checkpoint Table: CREATE TABLE IF NOT EXISTS migration_progress ( - batch_id VARCHAR(50) PRIMARY KEY, - last_processed_index INT DEFAULT 0, - status ENUM('RUNNING','COMPLETED','FAILED') DEFAULT 'RUNNING', - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + batch_id VARCHAR(50) PRIMARY KEY, + last_processed_index INT DEFAULT 0, + STATUS ENUM('RUNNING', 'COMPLETED', 'FAILED') DEFAULT 'RUNNING', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); + -- Idempotency Table : CREATE TABLE IF NOT EXISTS import_transactions ( id INT AUTO_INCREMENT PRIMARY KEY, @@ -1560,6 +1579,7 @@ CREATE TABLE IF NOT EXISTS import_transactions ( created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_idem_key (idempotency_key) ); + -- ============================================================ -- 5. PARTITIONING PREPARATION (Advance - Optional) -- ============================================================ @@ -2049,6 +2069,87 @@ CREATE INDEX idx_correspondences_type_project ON correspondences (correspondence CREATE INDEX idx_corr_revisions_current_status ON correspondence_revisions (is_current, correspondence_status_id); +-- ===================================================== +-- Migration Tracking Tables (Temporary) +-- ===================================================== +-- Checkpoint +CREATE TABLE IF NOT EXISTS migration_progress ( + batch_id VARCHAR(50) PRIMARY KEY, + last_processed_index INT DEFAULT 0, + STATUS ENUM('RUNNING', 'COMPLETED', 'FAILED') DEFAULT 'RUNNING', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +-- Review Queue (Temporary — ไม่ใช่ Business Schema) +CREATE TABLE IF NOT EXISTS migration_review_queue ( + id INT AUTO_INCREMENT PRIMARY KEY, + document_number VARCHAR(100) NOT NULL, + title TEXT, + original_title TEXT, + ai_suggested_category VARCHAR(50), + ai_confidence DECIMAL(4, 3), + ai_issues JSON, + review_reason VARCHAR(255), + STATUS ENUM('PENDING', 'APPROVED', 'REJECTED') DEFAULT 'PENDING', + reviewed_by VARCHAR(100), + reviewed_at TIMESTAMP NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY uq_doc_number (document_number) +); + +-- Error Log +CREATE TABLE IF NOT EXISTS migration_errors ( + id INT AUTO_INCREMENT PRIMARY KEY, + batch_id VARCHAR(50), + document_number VARCHAR(100), + error_type ENUM( + 'FILE_NOT_FOUND', + 'AI_PARSE_ERROR', + 'API_ERROR', + 'DB_ERROR', + 'UNKNOWN' + ), + error_message TEXT, + raw_ai_response TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX idx_batch_id (batch_id), + INDEX idx_error_type (error_type) +); + +-- Fallback State +CREATE TABLE IF NOT EXISTS migration_fallback_state ( + id INT AUTO_INCREMENT PRIMARY KEY, + batch_id VARCHAR(50) UNIQUE, + recent_error_count INT DEFAULT 0, + is_fallback_active BOOLEAN DEFAULT FALSE, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +-- Idempotency (Patch) +CREATE TABLE IF NOT EXISTS import_transactions ( + id INT AUTO_INCREMENT PRIMARY KEY, + idempotency_key VARCHAR(255) UNIQUE NOT NULL, + document_number VARCHAR(100), + batch_id VARCHAR(100), + status_code INT DEFAULT 201, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX idx_idem_key (idempotency_key) +); + +-- Daily Summary +CREATE TABLE IF NOT EXISTS migration_daily_summary ( + id INT AUTO_INCREMENT PRIMARY KEY, + batch_id VARCHAR(50), + summary_date DATE, + total_processed INT DEFAULT 0, + auto_ingested INT DEFAULT 0, + sent_to_review INT DEFAULT 0, + rejected INT DEFAULT 0, + errors INT DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY uq_batch_date (batch_id, summary_date) +); + CREATE INDEX idx_corr_revisions_correspondence_current ON correspondence_revisions (correspondence_id, is_current); -- Indexes for v_current_rfas performance diff --git a/specs/03-Data-and-Storage/lcbp3-v1.8.0-seed-basic.sql b/specs/03-Data-and-Storage/lcbp3-v1.8.0-seed-basic.sql index 31c6cbe..e05d1e0 100644 --- a/specs/03-Data-and-Storage/lcbp3-v1.8.0-seed-basic.sql +++ b/specs/03-Data-and-Storage/lcbp3-v1.8.0-seed-basic.sql @@ -18,7 +18,6 @@ -- 2.1 username = migration_bot -- 2.2 -- ========================================================== - INSERT INTO organization_roles (id, role_name) VALUES (1, 'OWNER'), (2, 'DESIGNER'), @@ -26,6 +25,7 @@ VALUES (1, 'OWNER'), (4, 'CONTRACTOR'), (5, 'THIRD PARTY'), (6, 'GUEST'); + INSERT INTO organizations ( id, organization_code, @@ -100,6 +100,7 @@ VALUES (1, 'กทท.', 'การท่าเรือแห่งประเ ), (31, 'EN', 'Third Party Environment', 5), (32, 'CAR', 'Third Party Fishery Care', 5); + -- Seed project INSERT INTO projects (project_code, project_name) VALUES ( @@ -126,6 +127,7 @@ VALUES ( 'LCBP3-EN', 'โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 4) งานก่อสร้าง' ); + -- Seed contract -- ใช้ Subquery เพื่อดึง project_id มาเชื่อมโยง ทำให้ไม่ต้องมานั่งจัดการ ID ด้วยตัวเอง INSERT INTO contracts ( @@ -204,6 +206,7 @@ VALUES ( ), TRUE ); + -- Seed user -- Initial SUPER_ADMIN user INSERT INTO users ( @@ -252,18 +255,32 @@ VALUES ( '$2b$10$MpKnf1UEvlu8hZcqMkhMsuWG3gYD/priWTUr71GpF/uuroaGxtose', 'Viewer', 'สคฉ.03', - 'viewer01 @example.com', + 'viewer01@example.com', NULL, 10 ); - INSERT INTO users (username, email, role, is_active) - VALUES ( +INSERT INTO users ( + user_id, + username, + password_hash, + first_name, + last_name, + email, + line_id, + primary_organization_id + ) +VALUES ( + 5, 'migration_bot', + '$2b$10$MpKnf1UEvlu8hZcqMkhMsuWG3gYD/priWTUr71GpF/uuroaGxtose', + 'Migration', + 'Bot', 'migration@system.internal', - 'SYSTEM_ADMIN', - TRUE + NULL, + 1 ); + -- ========================================================== -- Seed Roles (บทบาทพื้นฐาน 5 บทบาท ตาม Req 4.3) -- ========================================================== @@ -317,6 +334,7 @@ VALUES ( 'Contract', 'ผู้ดูแลสัญญา: จัดการสมาชิกในสัญญา, สร้าง / จัดการข้อมูลหลักเฉพาะสัญญา, และอนุมัติเอกสารในสัญญา' ); + -- ========================================================== -- Seed Role-Permissions Mapping (จับคู่สิทธิ์เริ่มต้น) -- ========================================================== @@ -343,8 +361,11 @@ VALUES (1, 1, 1, NULL, NULL, NULL, NULL), -- admin: Organization scope (org_id=1 = กทท.) (3, 3, 4, 41, NULL, NULL, 1), -- editor01: Editor role (role_id=4) at organization 41 (คคง.), assigned by superadmin - (4, 4, 5, 10, NULL, NULL, 1); --- viewer01: Viewer role (role_id=5) at organization 10 (สคฉ.03), assigned by superadmin + (4, 4, 5, 10, NULL, NULL, 1), + -- viewer01: Viewer role (role_id=5) at organization 10 (สคฉ.03), assigned by superadmin + (5, 5, 1, NULL, NULL, NULL, 1); + +-- migration_bot: Superadmin role (role_id=1) for migration scripts, assigned by superadmin -- ===================================================== -- == 4. การเชื่อมโยงโครงการกับองค์กร (project_organizations) == -- ===================================================== @@ -369,6 +390,7 @@ WHERE organization_code IN ( 'EN', 'CAR' ); + -- โครงการย่อย (LCBP3C1) จะมีเฉพาะองค์กรที่เกี่ยวข้อง INSERT INTO project_organizations (project_id, organization_id) SELECT ( @@ -385,6 +407,7 @@ WHERE organization_code IN ( 'คคง.', 'ผรม.1 ' ); + -- ทำเช่นเดียวกันสำหรับโครงการอื่นๆ (ตัวอย่าง) INSERT INTO project_organizations (project_id, organization_id) SELECT ( @@ -401,6 +424,7 @@ WHERE organization_code IN ( 'คคง.', 'ผรม.2' ); + -- ===================================================== -- == 5. การเชื่อมโยงสัญญากับองค์กร (contract_organizations) == -- ===================================================== @@ -432,6 +456,7 @@ VALUES ( ), 'Designer' ); + -- สัญญาที่ปรึกษาควบคุมงาน (PSLCBP3) INSERT INTO contract_organizations (contract_id, organization_id, role_in_contract) VALUES ( @@ -460,6 +485,7 @@ VALUES ( ), 'Consultant' ); + -- สัญญางานก่อสร้าง ส่วนที่ 1 (LCBP3-C1) INSERT INTO contract_organizations (contract_id, organization_id, role_in_contract) VALUES ( @@ -488,6 +514,7 @@ VALUES ( ), 'Contractor' ); + -- สัญญางานก่อสร้าง ส่วนที่ 2 (LCBP3-C2) INSERT INTO contract_organizations (contract_id, organization_id, role_in_contract) VALUES ( @@ -516,6 +543,7 @@ VALUES ( ), 'Contractor' ); + -- สัญญาตรวจสอบสิ่งแวดล้อม (LCBP3-EN) INSERT INTO contract_organizations (contract_id, organization_id, role_in_contract) VALUES ( @@ -544,6 +572,7 @@ VALUES ( ), 'Consultant' ); + -- Seed correspondence_status INSERT INTO correspondence_status ( status_code, @@ -574,6 +603,7 @@ VALUES ('DRAFT', 'Draft', 10, 1), ('CCBDSN', 'Canceled by Designer', 92, 1), ('CCBCSC', 'Canceled by CSC', 93, 1), ('CCBCON', 'Canceled by Contractor', 94, 1); + -- Seed correspondence_types INSERT INTO correspondence_types (type_code, type_name, sort_order, is_active) VALUES ('RFA', 'Request for Approval', 1, 1), @@ -586,6 +616,7 @@ VALUES ('RFA', 'Request for Approval', 1, 1), ('MOM', 'Minutes of Meeting', 8, 1), ('NOTICE', 'Notice', 9, 1), ('OTHER', 'Other', 10, 1); + -- Seed rfa_types INSERT INTO rfa_types ( contract_id, @@ -1075,6 +1106,7 @@ SELECT id, 'รายงานการฝึกปฏิบัติ' FROM contracts WHERE contract_code = 'LCBP3-C2'; + -- Seed rfa_status_codes INSERT INTO rfa_status_codes ( status_code, @@ -1089,6 +1121,7 @@ VALUES ('DFT', 'Draft', 'ฉบับร่าง', 1), ('ASB', 'AS - Built', 'แบบก่อสร้างจริง', 30), ('OBS', 'Obsolete', 'ไม่ใช้งาน', 80), ('CC', 'Canceled', 'ยกเลิก', 99); + INSERT INTO rfa_approve_codes ( approve_code, approve_name, @@ -1103,12 +1136,14 @@ VALUES ('1A', 'Approved by Authority', 10, 1), ('3R', 'Revise and Resubmit', 32, 1), ('4X', 'Reject', 40, 1), ('5N', 'No Further Action', 50, 1); + -- Seed circulation_status_codes INSERT INTO circulation_status_codes (code, description, sort_order) VALUES ('OPEN', 'Open', 1), ('IN_REVIEW', 'In Review', 2), ('COMPLETED', 'ปCompleted', 3), ('CANCELLED', 'Cancelled / Withdrawn', 9); + -- ตาราง "แม่" ของ RFA (มีความสัมพันธ์ 1:N กับ rfa_revisions) -- ========================================================== -- SEED DATA 6B.md (Disciplines, RFA Types, Sub Types) @@ -1372,6 +1407,7 @@ SELECT id, 'Other' FROM contracts WHERE contract_code = 'LCBP3-C1'; + -- LCBP3-C2 INSERT INTO disciplines ( contract_id, @@ -1616,6 +1652,7 @@ SELECT id, 'Others' FROM contracts WHERE contract_code = 'LCBP3-C2'; + -- 2. Seed ข้อมูล Correspondence Sub Types (Mapping RFA Types กับ Number) -- เนื่องจาก sub_type_code ตรงกับ RFA Type Code แต่ Req ต้องการ Mapping เป็น Number -- LCBP3-C1 @@ -1666,6 +1703,7 @@ FROM contracts c, correspondence_types ct WHERE c.contract_code = 'LCBP3-C1' AND ct.type_code = 'RFA'; + -- LCBP3-C2 INSERT INTO correspondence_sub_types ( contract_id, @@ -1713,6 +1751,7 @@ FROM contracts c, correspondence_types ct WHERE c.contract_code = 'LCBP3-C2' AND ct.type_code = 'RFA'; + -- LCBP3-C3 INSERT INTO correspondence_sub_types ( contract_id, @@ -1760,6 +1799,7 @@ FROM contracts c, correspondence_types ct WHERE c.contract_code = 'LCBP3-C3' AND ct.type_code = 'RFA'; + -- LCBP3-C4 INSERT INTO correspondence_sub_types ( contract_id, @@ -1807,6 +1847,7 @@ FROM contracts c, correspondence_types ct WHERE c.contract_code = 'LCBP3-C4' AND ct.type_code = 'RFA'; + INSERT INTO `correspondences` ( `id`, `correspondence_number`, @@ -1843,6 +1884,7 @@ VALUES ( 1, NULL ); + INSERT INTO `correspondence_revisions` ( `id`, `correspondence_id`, @@ -1881,6 +1923,7 @@ VALUES ( 1, NULL ); + INSERT INTO `rfas` ( `id`, `rfa_type_id`, @@ -1889,6 +1932,7 @@ INSERT INTO `rfas` ( `deleted_at` ) VALUES (2, 68, '2025-12-06 05:40:02', 1, NULL); + INSERT INTO `rfa_revisions` ( `id`, `rfa_id`, @@ -1929,6 +1973,7 @@ VALUES ( NULL, NULL ); + -- ========================================================== -- 20. Workflow Definitions (Unified Workflow Engine) -- ========================================================== @@ -2165,6 +2210,7 @@ VALUES ( NOW(), NOW() ); + INSERT INTO `document_number_formats` ( `id`, `project_id`, diff --git a/specs/03-Data-and-Storage/lcbp3-v1.7.0-seed-contractdrawing.sql b/specs/03-Data-and-Storage/lcbp3-v1.8.0-seed-contractdrawing.sql similarity index 99% rename from specs/03-Data-and-Storage/lcbp3-v1.7.0-seed-contractdrawing.sql rename to specs/03-Data-and-Storage/lcbp3-v1.8.0-seed-contractdrawing.sql index b0fa5f6..4edd6cc 100644 --- a/specs/03-Data-and-Storage/lcbp3-v1.7.0-seed-contractdrawing.sql +++ b/specs/03-Data-and-Storage/lcbp3-v1.8.0-seed-contractdrawing.sql @@ -1,15 +1,14 @@ -- ========================================================== --- DMS DMS v0.5.0 --- Database v5.1 - Seed contract_dwg data --- Server: Container Station on QNAPQNAP TS-473A --- Database service: MariaDB 10.11 --- database ui: phpmyadmin 5-apache --- backend sevice: node.js --- frontend sevice: next.js --- reverse proxy: nginx 1.27-alpine +-- DMS v1.8.0 Document Management System Database +-- Seed Contract Drawing data +-- Server: Container Station on QNAP TS-473A +-- Database service: MariaDB 11.8 +-- database web ui: phpmyadmin 5-apache +-- database development ui: DBeaver +-- backend service: NestJS +-- frontend service: next.js +-- reverse proxy: jc21/nginx-proxy-manager:latest -- cron service: n8n --- scripts: alpine:3.20 --- Notes: -- ========================================================== INSERT INTO contract_drawing_volumes (project_id, volume_code, volume_name, description) diff --git a/specs/03-Data-and-Storage/lcbp3-v1.7.0-seed-permissions.sql b/specs/03-Data-and-Storage/lcbp3-v1.8.0-seed-permissions.sql similarity index 94% rename from specs/03-Data-and-Storage/lcbp3-v1.7.0-seed-permissions.sql rename to specs/03-Data-and-Storage/lcbp3-v1.8.0-seed-permissions.sql index 0def6ae..353fbc9 100644 --- a/specs/03-Data-and-Storage/lcbp3-v1.7.0-seed-permissions.sql +++ b/specs/03-Data-and-Storage/lcbp3-v1.8.0-seed-permissions.sql @@ -1,8 +1,9 @@ -- ========================================================== --- DMS v1.6.0 - Permissions Seed Data (REORGANIZED) --- File: specs/07-database/permissions-seed-data.sql +-- DMS v1.8.0 - Permissions Seed Data (REORGANIZED) +-- File: specs/07-database/lcbp3-v1.8.0-seed-permissions.sql -- Total Permissions: 85 (Reorganized with systematic ID allocation) -- Created: 2025-12-13 +-- Updated: 2026-02-28 (v1.8.0 merge) -- ========================================================== -- Clear existing data TRUNCATE TABLE role_permissions; @@ -1065,3 +1066,37 @@ VALUES -- Contract Management -- ========================================================== -- VERIFICATION: Run permissions-verification.sql after this -- ========================================================== + + +-- ========================================================== +-- MERGED FROM fix-project-permissions.sql (v1.8.0 Update) +-- ========================================================== +-- Fix Project Permissions +-- File: specs/07-database/fix-project-permissions.sql +-- 1. Ensure project.view permission exists +INSERT IGNORE INTO permissions ( + permission_id, + permission_name, + description, + module, + is_active + ) +VALUES ( + 202, + 'project.view', + 'ดูรายการโครงการ', + 'project', + 1 + ); +-- 2. Grant project.view to Superadmin (Role 1) +INSERT IGNORE INTO role_permissions (role_id, permission_id) +VALUES (1, 202); +-- 3. Grant project.view to Organization Admin (Role 2) +INSERT IGNORE INTO role_permissions (role_id, permission_id) +VALUES (2, 202); +-- 4. Grant project.view to Project Manager (Role 6) +INSERT IGNORE INTO role_permissions (role_id, permission_id) +VALUES (6, 202); +-- 5. Grant project.view to Viewer (Role 5) +INSERT IGNORE INTO role_permissions (role_id, permission_id) +VALUES (5, 202); \ No newline at end of file diff --git a/specs/03-Data-and-Storage/lcbp3-v1.7.0-seed-shopdrawing.sql b/specs/03-Data-and-Storage/lcbp3-v1.8.0-seed-shopdrawing.sql similarity index 99% rename from specs/03-Data-and-Storage/lcbp3-v1.7.0-seed-shopdrawing.sql rename to specs/03-Data-and-Storage/lcbp3-v1.8.0-seed-shopdrawing.sql index e51dfdc..c25a71d 100644 --- a/specs/03-Data-and-Storage/lcbp3-v1.7.0-seed-shopdrawing.sql +++ b/specs/03-Data-and-Storage/lcbp3-v1.8.0-seed-shopdrawing.sql @@ -1,3 +1,15 @@ +-- ========================================================== +-- DMS v1.8.0 Document Management System Database +-- Seed Shop Drawing data +-- Server: Container Station on QNAP TS-473A +-- Database service: MariaDB 11.8 +-- database web ui: phpmyadmin 5-apache +-- database development ui: DBeaver +-- backend service: NestJS +-- frontend service: next.js +-- reverse proxy: jc21/nginx-proxy-manager:latest +-- cron service: n8n +-- ========================================================== INSERT INTO shop_drawing_sub_categories( project_id, sub_category_code, diff --git a/specs/03-Data-and-Storage/n8n.workflow b/specs/03-Data-and-Storage/n8n.workflow new file mode 100644 index 0000000..dba3879 --- /dev/null +++ b/specs/03-Data-and-Storage/n8n.workflow @@ -0,0 +1,216 @@ +{ + "meta": { + "instanceId": "lcbp3-migration" + }, + "nodes": [ + { + "parameters": {}, + "id": "trigger-1", + "name": "When clicking ‘Execute Workflow’", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [0, 0] + }, + { + "parameters": { + "operation": "read", + "fileFormat": "xlsx", + "options": {} + }, + "id": "spreadsheet-1", + "name": "Read Excel Data", + "type": "n8n-nodes-base.spreadsheetFile", + "typeVersion": 2, + "position": [200, 0] + }, + { + "parameters": { + "batchSize": 10, + "options": {} + }, + "id": "split-in-batches-1", + "name": "Split In Batches", + "type": "n8n-nodes-base.splitInBatches", + "typeVersion": 3, + "position": [400, 0] + }, + { + "parameters": { + "jsCode": "const item = $input.first();\n\nconst prompt = `You are a Document Controller for a large construction project.\nYour task is to validate document metadata.\nYou MUST respond ONLY with valid JSON. No explanation, no markdown, no extra text.\n\nDocument Number: ${item.json.document_number}\nTitle: ${item.json.title}\nCategory List: [\"Correspondence\",\"RFA\",\"Drawing\",\"Transmittal\",\"Report\",\"Other\"]\n\nRespond ONLY with this exact JSON structure:\n{\n \"is_valid\": true,\n \"confidence\": 0.95,\n \"suggested_category\": \"Correspondence\",\n \"detected_issues\": [],\n \"suggested_title\": null\n}`;\n\nreturn [{\n json: {\n ...item.json,\n ollama_payload: {\n model: \"llama3.2:3b\",\n format: \"json\",\n stream: false,\n prompt: prompt\n }\n }\n}];" + }, + "id": "code-1", + "name": "Build Prompt", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [620, 0] + }, + { + "parameters": { + "method": "POST", + "url": "http://192.168.20.100:11434/api/generate", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ $json.ollama_payload }}", + "options": { + "timeout": 30000 + } + }, + "id": "http-1", + "name": "Ollama Local API", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.1, + "position": [840, 0] + }, + { + "parameters": { + "jsCode": "const items = $input.all();\nconst parsed = [];\n\nfor (const item of items) {\n try {\n let raw = item.json.response || '';\n raw = raw.replace(/```json/gi, '').replace(/```/g, '').trim();\n const aiResult = JSON.parse(raw);\n parsed.push({ json: { ...item.json, ai_result: aiResult } });\n } catch (err) {\n parsed.push({ json: { ...item.json, ai_result: { confidence: 0, is_valid: false, error: err.message } } });\n }\n}\nreturn parsed;" + }, + "id": "code-2", + "name": "Parse JSON", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [1040, 0] + }, + { + "parameters": { + "conditions": { + "boolean": [ + { + "value1": "={{ $json.ai_result.confidence >= 0.85 && $json.ai_result.is_valid }}", + "value2": true + } + ] + } + }, + "id": "if-1", + "name": "Confidence >= 0.85?", + "type": "n8n-nodes-base.if", + "typeVersion": 1, + "position": [1240, 0] + }, + { + "parameters": { + "method": "POST", + "url": "http://:3000/api/migration/import", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Idempotency-Key", + "value": "={{ $json.document_number }}:BATCH-001" + }, + { + "name": "Authorization", + "value": "Bearer " + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"source_file_path\": \"/share/np-dms/staging_ai/{{$json.document_number}}.pdf\",\n \"document_number\": \"{{$json.document_number}}\",\n \"title\": \"{{$json.ai_result.suggested_title || $json.title}}\",\n \"category\": \"{{$json.ai_result.suggested_category}}\",\n \"revision\": 1, \n \"batch_id\": \"BATCH_001\",\n \"ai_confidence\": {{$json.ai_result.confidence}},\n \"ai_issues\": {{$json.ai_result.detected_issues}},\n \"legacy_document_number\": \"{{$json.legacy_number}}\"\n}", + "options": {} + }, + "id": "http-2", + "name": "LCBP3 Backend (Auto Ingest)", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.1, + "position": [1460, -100] + }, + { + "parameters": { + "jsCode": "return [{ json: { message: \"Sent to Human Review Queue OR Check AI Error Log\", data: $input.first().json } }];" + }, + "id": "code-3", + "name": "Review Queue / Reject Log", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [1460, 100] + } + ], + "connections": { + "When clicking ‘Execute Workflow’": { + "main": [ + [ + { + "node": "Read Excel Data", + "type": "main", + "index": 0 + } + ] + ] + }, + "Read Excel Data": { + "main": [ + [ + { + "node": "Split In Batches", + "type": "main", + "index": 0 + } + ] + ] + }, + "Split In Batches": { + "main": [ + [ + { + "node": "Build Prompt", + "type": "main", + "index": 0 + } + ] + ] + }, + "Build Prompt": { + "main": [ + [ + { + "node": "Ollama Local API", + "type": "main", + "index": 0 + } + ] + ] + }, + "Ollama Local API": { + "main": [ + [ + { + "node": "Parse JSON", + "type": "main", + "index": 0 + } + ] + ] + }, + "Parse JSON": { + "main": [ + [ + { + "node": "Confidence >= 0.85?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Confidence >= 0.85?": { + "main": [ + [ + { + "node": "LCBP3 Backend (Auto Ingest)", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Review Queue / Reject Log", + "type": "main", + "index": 0 + } + ] + ] + } + } +} diff --git a/specs/03-Data-and-Storage/permissions-verification.sql b/specs/03-Data-and-Storage/permissions-verification.sql index 80c7c8d..697337e 100644 --- a/specs/03-Data-and-Storage/permissions-verification.sql +++ b/specs/03-Data-and-Storage/permissions-verification.sql @@ -1,6 +1,6 @@ -- ========================================================== --- Permission System Verification Queries --- File: specs/07-database/permissions-verification.sql +-- Permission System Verification Queries (v1.8.0) +-- File: specs/03-Data-and-Storage/permissions-verification.sql -- Purpose: Verify permissions setup after seed data deployment -- ========================================================== -- ========================================================== @@ -271,6 +271,8 @@ FROM ( SELECT 'drawing.view' UNION SELECT 'workflow.action_review' + UNION + SELECT 'project.view' ) required_perms LEFT JOIN permissions p USING (permission_name) ORDER BY permission_name; diff --git a/specs/04-Infrastructure-OPS/04-00-docker-compose/docker-compose-lcbp3-n8n.yml b/specs/04-Infrastructure-OPS/04-00-docker-compose/docker-compose-lcbp3-n8n.yml index 78585a4..2821d71 100644 --- a/specs/04-Infrastructure-OPS/04-00-docker-compose/docker-compose-lcbp3-n8n.yml +++ b/specs/04-Infrastructure-OPS/04-00-docker-compose/docker-compose-lcbp3-n8n.yml @@ -10,12 +10,33 @@ x-logging: &default_logging max-size: "10m" max-file: "5" services: + n8n-db: + <<: [*restart_policy, *default_logging] + image: postgres:16-alpine + container_name: n8n-db + environment: + - POSTGRES_USER=n8n + - POSTGRES_PASSWORD=Np721220$ + - POSTGRES_DB=n8n + volumes: + - "/share/np-dms/n8n/postgres-data:/var/lib/postgresql/data" + networks: + lcbp3: {} + healthcheck: + test: ['CMD-SHELL', 'pg_isready -h localhost -U n8n -d n8n'] + interval: 10s + timeout: 5s + retries: 5 + n8n: <<: [*restart_policy, *default_logging] - image: n8nio/n8n:1.78.0 + image: n8nio/n8n:latest container_name: n8n stdin_open: true tty: true + depends_on: + n8n-db: + condition: service_healthy deploy: resources: limits: @@ -38,14 +59,19 @@ services: N8N_DIAGNOSTICS_ENABLED: 'false' N8N_SECURE_COOKIE: 'true' N8N_ENCRYPTION_KEY: "9AAIB7Da9DW1qAhJE5/Bz4SnbQjeAngI" - N8N_BASIC_AUTH_ACTIVE: 'true' - N8N_BASIC_AUTH_USER: admin - N8N_BASIC_AUTH_PASSWORD: Center#2025 N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS: 'true' GENERIC_TIMEZONE: "Asia/Bangkok" - # DB: MySQL/MariaDB removed in n8n v1.x — now using SQLite (default) - # Data is persisted in /home/node/.n8n (mounted volume below) - DB_TYPE: sqlite + # DB Setup + DB_TYPE: postgresdb + DB_POSTGRESDB_DATABASE: n8n + DB_POSTGRESDB_HOST: n8n-db + DB_POSTGRESDB_PORT: 5432 + DB_POSTGRESDB_USER: n8n + DB_POSTGRESDB_PASSWORD: Np721220$ + # Data Prune + EXECUTIONS_DATA_PRUNE: 'true' + EXECUTIONS_DATA_MAX_AGE: 168 + EXECUTIONS_DATA_PRUNE_TIMEOUT: 60 ports: - "5678:5678" @@ -57,6 +83,10 @@ services: - "/share/np-dms/n8n/scripts:/scripts" - "/share/np-dms/n8n/data:/data" - "/var/run/docker.sock:/var/run/docker.sock" + # read-only: อ่านไฟล์ PDF ต้นฉบับเท่านั้น + - "/share/np-dms-as/Legacy:/share/np-dms/staging_ai:ro" + # read-write: เขียน Log และ CSV ทั้งหมด + - "/share/np-dms/n8n/migration_logs:/share/np-dms/n8n/migration_logs:rw" healthcheck: test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:5678/healthz || exit 1"] @@ -70,6 +100,8 @@ networks: external: true # สำหรับ n8n volumes -# chown -R 1000:1000 /share/Container/n8n -# chmod -R 755 /share/Container/n8n +# chown -R 1000:1000 /share/np-dms/n8n +# chmod -R 755 /share/np-dms/n8n3 +# chown -R 999:999 /share/np-dms/n8n/postgres-data +# chmod -R 700 /share/np-dms/n8n/postgres-data diff --git a/specs/06-Decision-Records/ADR-017-ollama-data-migration.md b/specs/06-Decision-Records/ADR-017-ollama-data-migration.md index 7563fdd..00a21cc 100644 --- a/specs/06-Decision-Records/ADR-017-ollama-data-migration.md +++ b/specs/06-Decision-Records/ADR-017-ollama-data-migration.md @@ -9,9 +9,7 @@ - [n8n Migration Setup Guide](../03-Data-and-Storage/03-05-n8n-migration-setup-guide.md) - [Software Architecture](../02-Architecture/02-02-software-architecture.md) - [Data Dictionary](../03-Data-and-Storage/03-01-data-dictionary.md) - -> **Note:** ADR-017 is clarified and hardened by ADR-018 regarding AI physical isolation. Category Enum system-driven, Idempotency Contract, Duplicate Handling Clarification, Storage Enforcement, Audit Log Enhancement, Review Queue Integration, Revision Drift Protection, Execution Time, Encoding Normalization, Security Hardening, AI Physical Isolation (ASUSTOR). - +> **Note:** ADR-017 is clarified and hardened by ADR-018 regarding AI physical isolation. Category Enum system-driven, Idempotency Contract, Duplicate Handling Clarification, Storage Enforcement, Audit Log Enhancement, Review Queue Integration, Revision Drift Protection, Execution Time, Encoding Normalization, Security Hardening, Orchestrator on QNAP, AI Physical Isolation (Desktop Desk-5439). --- ## Context and Problem Statement @@ -84,18 +82,18 @@ ## Implementation Summary -| Component | รายละเอียด | -| ---------------------- | ------------------------------------------------------------- | -| Migration Orchestrator | n8n (Docker บน ASUSTOR NAS) | -| AI Model Primary | Ollama `llama3.2:3b` | -| AI Model Fallback | Ollama `mistral:7b-instruct-q4_K_M` | -| Hardware | ASUSTOR NAS (AI Processing Only) | -| Data Ingestion | RESTful API + Migration Token (7 วัน) + Idempotency-Key Header | -| Concurrency | Sequential — 1 Request/ครั้ง, Delay 2 วินาที | -| Checkpoint | MariaDB `migration_progress` | -| Fallback | Auto-switch Model เมื่อ Error ≥ Threshold | -| Storage | Backend StorageService เท่านั้น — ห้าม move file โดยตรง | -| Expected Runtime | ~16.6 ชั่วโมง (~3–4 คืน) สำหรับ 20,000 records | +| Component | รายละเอียด | +| ---------------------- | ------------------------------------------------------------------------------- | +| Migration Orchestrator | n8n (Docker บน QNAP NAS) | +| AI Model Primary | Ollama `llama3.2:3b` | +| AI Model Fallback | Ollama `mistral:7b-instruct-q4_K_M` | +| Hardware | QNAP NAS (Orchestrator) + Desktop Desk-5439 (AI Processing, RTX 2060 SUPER 8GB) | +| Data Ingestion | RESTful API + Migration Token (7 วัน) + Idempotency-Key Header | +| Concurrency | Sequential — 1 Request/ครั้ง, Delay 2 วินาที | +| Checkpoint | MariaDB `migration_progress` | +| Fallback | Auto-switch Model เมื่อ Error ≥ Threshold | +| Storage | Backend StorageService เท่านั้น — ห้าม move file โดยตรง | +| Expected Runtime | ~16.6 ชั่วโมง (~3–4 คืน) สำหรับ 20,000 records | ---