From d74218bb2ac75d7846d597c5d1f31f0742bcfa28 Mon Sep 17 00:00:00 2001 From: admin Date: Wed, 3 Dec 2025 01:16:27 +0700 Subject: [PATCH] 251202:2300 Prepare 1.5.1 --- docs/8_lcbp3_v1_5_1.sql | 1582 +++++++++++++++ docs/8_lcbp3_v1_5_1_seed.sql | 1748 +++++++++++++++++ lcbp3.code-workspace | 7 +- specs/00-overview/README.md | 54 +- specs/00-overview/glossary.md | 8 +- specs/00-overview/quick-start.md | 14 +- .../03.11-document-numbering.md | 1513 +++++++++++++- specs/01-requirements/README.md | 181 +- specs/02-architecture/README.md | 57 +- specs/02-architecture/api-design.md | 12 +- specs/02-architecture/system-architecture.md | 8 +- specs/03-implementation/document-numbering.md | 627 ++++++ specs/04-operations/README.md | 6 +- specs/04-operations/backup-recovery.md | 6 +- specs/04-operations/deployment-guide.md | 937 +++++++++ specs/04-operations/deployment.md | 0 specs/04-operations/disaster-recovery.md | 0 .../document-numbering-operations.md | 684 +++++++ specs/04-operations/environment-setup.md | 6 +- specs/04-operations/incident-response.md | 6 +- specs/04-operations/maintenance-procedures.md | 6 +- specs/04-operations/monitoring-alerting.md | 6 +- specs/04-operations/monitoring.md | 0 specs/04-operations/security-operations.md | 6 +- .../ADR-002-document-numbering-strategy.md | 9 + specs/05-decisions/README.md | 12 +- specs/06-tasks/README.md | 21 +- .../TASK-BE-004-document-numbering.md | 114 +- 28 files changed, 7362 insertions(+), 268 deletions(-) create mode 100644 docs/8_lcbp3_v1_5_1.sql create mode 100644 docs/8_lcbp3_v1_5_1_seed.sql create mode 100644 specs/03-implementation/document-numbering.md create mode 100644 specs/04-operations/deployment-guide.md delete mode 100644 specs/04-operations/deployment.md delete mode 100644 specs/04-operations/disaster-recovery.md create mode 100644 specs/04-operations/document-numbering-operations.md delete mode 100644 specs/04-operations/monitoring.md diff --git a/docs/8_lcbp3_v1_5_1.sql b/docs/8_lcbp3_v1_5_1.sql new file mode 100644 index 0000000..23bf0e4 --- /dev/null +++ b/docs/8_lcbp3_v1_5_1.sql @@ -0,0 +1,1582 @@ +-- ========================================================== +-- DMS v1.5.1 Document Management System Database +-- Deploy Script Schema +-- Server: Container Station on QNAP TS-473A +-- Database service: MariaDB 10.11 +-- 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 +-- ========================================================== +-- [v1.5.1 UPDATE] Enhanced Document Numbering System +-- Update: Upgraded from v1.4.5 +-- Last Updated: 2025-12-02 +-- Major Changes: +-- 1. Document Numbering: 8-column composite PK (was 5 columns) +-- 2. New Tables: document_number_audit, document_number_errors +-- 3. Enhanced indexes and constraints for document_number_counters +-- 4. Based on specs v1.5.1 (refs: 03.11-document-numbering.md) +-- ========================================================== +SET NAMES utf8mb4; +SET time_zone = '+07:00'; +-- ปิดการตรวจสอบ Foreign Key ชั่วคราวเพื่อให้สามารถลบตารางได้ทั้งหมด +SET FOREIGN_KEY_CHECKS = 0; +DROP VIEW IF EXISTS v_document_statistics; +DROP VIEW IF EXISTS v_documents_with_attachments; +DROP VIEW IF EXISTS v_user_all_permissions; +DROP VIEW IF EXISTS v_audit_log_details; +DROP VIEW IF EXISTS v_user_tasks; +DROP VIEW IF EXISTS v_contract_parties_all; +DROP VIEW IF EXISTS v_current_rfas; +DROP VIEW IF EXISTS v_current_correspondences; +-- DROP PROCEDURE IF EXISTS sp_get_next_document_number; +-- 🗑️ DROP TABLE SCRIPT: LCBP3-DMS v1.4.2 +-- คำเตือน: ข้อมูลทั้งหมดจะหายไป กรุณา Backup ก่อนรันบน Production +SET FOREIGN_KEY_CHECKS = 0; +-- ============================================================ +-- ส่วนที่ 1: ตาราง System, Logs & Preferences (ตารางปลายทาง/ส่วนเสริม) +-- ============================================================ +DROP TABLE IF EXISTS backup_logs; +DROP TABLE IF EXISTS search_indices; +DROP TABLE IF EXISTS notifications; +DROP TABLE IF EXISTS audit_logs; +-- [NEW v1.4.2] ตารางการตั้งค่าส่วนตัวของผู้ใช้ (FK -> users) +DROP TABLE IF EXISTS user_preferences; +-- [NEW v1.4.2] ตารางเก็บ Schema สำหรับ Validate JSON (Stand-alone) +DROP TABLE IF EXISTS json_schemas; +-- [v1.5.1 NEW] ตาราง Audit และ Error Log สำหรับ Document Numbering +DROP TABLE IF EXISTS document_number_errors; +DROP TABLE IF EXISTS document_number_audit; +-- ============================================================ +-- ส่วนที่ 2: ตาราง Junction (เชื่อมโยงข้อมูล M:N) +-- ============================================================ +DROP TABLE IF EXISTS correspondence_tags; +DROP TABLE IF EXISTS shop_drawing_revision_contract_refs; +DROP TABLE IF EXISTS contract_drawing_subcat_cat_maps; +-- ============================================================ +-- ส่วนที่ 3: ตารางไฟล์แนบและการเชื่อมโยง (Attachments) +-- ============================================================ +DROP TABLE IF EXISTS contract_drawing_attachments; +DROP TABLE IF EXISTS circulation_attachments; +DROP TABLE IF EXISTS shop_drawing_revision_attachments; +DROP TABLE IF EXISTS correspondence_attachments; +DROP TABLE IF EXISTS attachments; +-- ตารางหลักเก็บ path ไฟล์ +-- ============================================================ +-- ส่วนที่ 4: ตาราง Workflow & Routing (Process Logic) +-- ============================================================ +-- Correspondence Workflow +-- ============================================================ +-- ส่วนที่ 5: ตาราง Mapping สิทธิ์และโครงสร้าง (Access Control) +-- ============================================================ +DROP TABLE IF EXISTS role_permissions; +DROP TABLE IF EXISTS user_assignments; +DROP TABLE IF EXISTS contract_organizations; +DROP TABLE IF EXISTS project_organizations; +-- ============================================================ +-- ส่วนที่ 6: ตารางรายละเอียดของเอกสาร (Revisions & Items) +-- ============================================================ +DROP TABLE IF EXISTS transmittal_items; +DROP TABLE IF EXISTS shop_drawing_revisions; +DROP TABLE IF EXISTS rfa_items; +DROP TABLE IF EXISTS rfa_revisions; +DROP TABLE IF EXISTS correspondence_references; +DROP TABLE IF EXISTS correspondence_recipients; +DROP TABLE IF EXISTS correspondence_revisions; +-- [Modified v1.4.2] มี Virtual Columns +-- ============================================================ +-- ส่วนที่ 7: ตารางเอกสารหลัก (Core Documents) +-- ============================================================ +DROP TABLE IF EXISTS circulations; +DROP TABLE IF EXISTS transmittals; +DROP TABLE IF EXISTS contract_drawings; +DROP TABLE IF EXISTS shop_drawings; +DROP TABLE IF EXISTS rfas; +DROP TABLE IF EXISTS correspondences; +-- ============================================================ +-- ส่วนที่ 8: ตารางหมวดหมู่และข้อมูลหลัก (Master Data) +-- ============================================================ +-- [NEW 6B] ลบตารางใหม่ที่เพิ่มเข้ามาเพื่อป้องกัน Error เวลา Re-deploy +DROP TABLE IF EXISTS correspondence_sub_types; +DROP TABLE IF EXISTS disciplines; +DROP TABLE IF EXISTS shop_drawing_sub_categories; +DROP TABLE IF EXISTS shop_drawing_main_categories; +DROP TABLE IF EXISTS contract_drawing_sub_cats; +DROP TABLE IF EXISTS contract_drawing_cats; +DROP TABLE IF EXISTS contract_drawing_volumes; +DROP TABLE IF EXISTS circulation_status_codes; +DROP TABLE IF EXISTS rfa_approve_codes; +DROP TABLE IF EXISTS rfa_status_codes; +DROP TABLE IF EXISTS rfa_types; +DROP TABLE IF EXISTS correspondence_status; +DROP TABLE IF EXISTS correspondence_types; +DROP TABLE IF EXISTS document_number_counters; +-- [Modified v1.4.2] มี version column +DROP TABLE IF EXISTS document_number_formats; +DROP TABLE IF EXISTS tags; +-- ============================================================ +-- ส่วนที่ 9: ตารางผู้ใช้ บทบาท และโครงสร้างรากฐาน (Root Tables) +-- ============================================================ +DROP TABLE IF EXISTS organization_roles; +DROP TABLE IF EXISTS roles; +DROP TABLE IF EXISTS permissions; +DROP TABLE IF EXISTS contracts; +DROP TABLE IF EXISTS projects; +DROP TABLE IF EXISTS users; +-- Referenced by user_preferences, audit_logs, etc. +DROP TABLE IF EXISTS organizations; +-- Referenced by users, projects, etc. +-- ===================================================== +-- 1. 🏢 Core & Master Data (องค์กร, โครงการ, สัญญา) +-- ===================================================== +-- ตาราง Master เก็บประเภทบทบาทขององค์กร +CREATE TABLE organization_roles ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + role_name VARCHAR(20) NOT NULL UNIQUE COMMENT 'ชื่อบทบาท (OWNER, DESIGNER, CONSULTANT, CONTRACTOR, THIRD PARTY)' +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บประเภทบทบาทขององค์กร'; +-- ตาราง Master เก็บข้อมูลองค์กรทั้งหมดที่เกี่ยวข้องในระบบ +CREATE TABLE organizations ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + organization_code VARCHAR(20) NOT NULL UNIQUE COMMENT 'รหัสองค์กร', + organization_name VARCHAR(255) NOT NULL COMMENT 'ชื่อองค์กร', + -- role_id INT COMMENT 'บทบาทขององค์กร', + is_active BOOLEAN DEFAULT TRUE COMMENT 'สถานะการใช้งาน', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด' -- FOREIGN KEY (role_id) REFERENCES organization_roles(id) ON DELETE SET NULL +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บข้อมูลองค์กรทั้งหมดที่เกี่ยวข้องในระบบ'; +-- ตาราง Master เก็บข้อมูลโครงการ +CREATE TABLE projects ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + project_code VARCHAR(50) NOT NULL UNIQUE COMMENT 'รหัสโครงการ', + project_name VARCHAR(255) NOT NULL COMMENT 'ชื่อโครงการ', + -- parent_project_id INT COMMENT 'รหัสโครงการหลัก (ถ้ามี)', + -- contractor_organization_id INT COMMENT 'รหัสองค์กรผู้รับเหมา (ถ้ามี)', + is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน' -- FOREIGN KEY (parent_project_id) REFERENCES projects(id) ON DELETE SET NULL, + -- FOREIGN KEY (contractor_organization_id) REFERENCES organizations(id) ON DELETE SET NULL +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บข้อมูลโครงการ'; +-- ตาราง Master เก็บข้อมูลสัญญา +CREATE TABLE contracts ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + project_id INT NOT NULL, + contract_code VARCHAR(50) NOT NULL UNIQUE COMMENT 'รหัสสัญญา', + contract_name VARCHAR(255) NOT NULL COMMENT 'ชื่อสัญญา', + description TEXT COMMENT 'คำอธิบายสัญญา', + start_date DATE COMMENT 'วันที่เริ่มสัญญา', + end_date DATE COMMENT 'วันที่สิ้นสุดสัญญา', + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', + FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บข้อมูลสัญญา'; +-- ===================================================== +-- 2. 👥 Users & RBAC (ผู้ใช้, สิทธิ์, บทบาท) +-- ===================================================== +-- ตาราง Master เก็บข้อมูลผู้ใช้งาน (User) +CREATE TABLE users ( + user_id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + username VARCHAR(50) NOT NULL UNIQUE COMMENT 'ชื่อผู้ใช้งาน', + password_hash VARCHAR(255) NOT NULL COMMENT 'รหัสผ่าน (Hashed)', + first_name VARCHAR(50) COMMENT 'ชื่อจริง', + last_name VARCHAR(50) COMMENT 'นามสกุล', + email VARCHAR(100) NOT NULL UNIQUE COMMENT 'อีเมล', + line_id VARCHAR(100) COMMENT 'LINE ID', + primary_organization_id INT COMMENT 'สังกัดองค์กร', + is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน', + failed_attempts INT DEFAULT 0 COMMENT 'จำนวนครั้งที่ล็อกอินล้มเหลว', + locked_until DATETIME COMMENT 'ล็อกอินไม่ได้จนถึงเวลา', + last_login_at TIMESTAMP NULL COMMENT 'วันที่และเวลาที่ล็อกอินล่าสุด', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', + deleted_at DATETIME NULL DEFAULT NULL COMMENT 'วันที่ลบ', + FOREIGN KEY (primary_organization_id) REFERENCES organizations(id) ON DELETE + SET NULL +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บข้อมูลผู้ใช้งาน (User)'; +-- ตาราง Master เก็บ "บทบาท" ของผู้ใช้ในระบบ +CREATE TABLE roles ( + role_id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + -- role_code VARCHAR(50) NOT NULL UNIQUE COMMENT 'รหัสบทบาท (เช่น SUPER_ADMIN, ADMIN, EDITOR, VIEWER)', + role_name VARCHAR(100) NOT NULL COMMENT 'ชื่อบทบาท', + scope ENUM('Global', 'Organization', 'Project', 'Contract') NOT NULL, + -- ขอบเขตของบทบาท (จากข้อ 4.3) + description TEXT COMMENT 'คำอธิบายบทบาท', + is_system BOOLEAN DEFAULT FALSE COMMENT '(1 = บทบาทของระบบ ลบไม่ได้)' +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บ "บทบาท" ของผู้ใช้ในระบบ'; +-- ตาราง Master เก็บ "สิทธิ์" (Permission) หรือ "การกระทำ" ทั้งหมดในระบบ +CREATE TABLE permissions ( + permission_id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + permission_name VARCHAR(100) NOT NULL UNIQUE COMMENT 'รหัสสิทธิ์ (เช่น rfas.create, rfas.view)', + description TEXT COMMENT 'คำอธิบายสิทธิ์', + module VARCHAR(50) COMMENT 'โมดูลที่เกี่ยวข้อง', + scope_level ENUM('GLOBAL', 'ORG', 'PROJECT') COMMENT 'ระดับขอบเขตของสิทธิ์', + is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน' +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บ "สิทธิ์" (Permission) หรือ "การกระทำ" ทั้งหมดในระบบ'; +-- ตารางเชื่อมระหว่าง roles และ permissions (M:N) +CREATE TABLE role_permissions ( + role_id INT COMMENT 'ID ของบทบาท', + permission_id INT COMMENT 'ID ของสิทธิ์', + 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 +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเชื่อมระหว่าง roles และ permissions (M :N)'; +-- search.advanced +-- ตารางเชื่อมผู้ใช้ (users) +CREATE TABLE user_assignments ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NOT NULL, + role_id INT NOT NULL, + -- คอลัมน์สำหรับกำหนดขอบเขต (จะใช้เพียงอันเดียวต่อแถว) + organization_id INT NULL, + project_id INT NULL, + contract_id INT NULL, + assigned_by_user_id INT, + -- ผู้ที่มอบหมายบทบาทนี้ + assigned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + 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), + -- Constraint เพื่อให้แน่ใจว่ามีเพียงขอบเขตเดียวที่ถูกกำหนดในแต่ละแถว + CONSTRAINT chk_scope CHECK ( + ( + organization_id IS NOT NULL + AND project_id IS NULL + AND contract_id IS NULL + ) + OR ( + organization_id IS NULL + AND project_id IS NOT NULL + AND contract_id IS NULL + ) + OR ( + organization_id IS NULL + AND project_id IS NULL + AND contract_id IS NOT NULL + ) + OR ( + organization_id IS NULL + AND project_id IS NULL + AND contract_id IS NULL + ) -- สำหรับ Global scope + ) +); +CREATE TABLE project_organizations ( + project_id INT NOT NULL, + organization_id INT NOT NULL, + 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 +); +CREATE TABLE contract_organizations ( + contract_id INT NOT NULL, + organization_id INT NOT NULL, + role_in_contract VARCHAR(100), + -- เช่น 'Owner', 'Designer', 'Consultant', 'Contractor ' + 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 +); +-- ===================================================== +-- 3. ✉️ Correspondences (เอกสารหลัก, Revisions) +-- ===================================================== +-- ตาราง Master เก็บประเภทเอกสารโต้ตอบ +CREATE TABLE correspondence_types ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + type_code VARCHAR(50) NOT NULL UNIQUE COMMENT 'รหัสประเภท (เช่น RFA, RFI)', + type_name VARCHAR(255) NOT NULL COMMENT 'ชื่อประเภท', + sort_order INT DEFAULT 0 COMMENT 'ลำดับการแสดงผล', + is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน ' +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บประเภทเอกสารโต้ตอบ'; +-- ตาราง Master เก็บสถานะของเอกสาร +CREATE TABLE correspondence_status ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + status_code VARCHAR(50) NOT NULL UNIQUE COMMENT 'รหัสสถานะหนังสือ (เช่น DRAFT, SUBOWN)', + status_name VARCHAR(255) NOT NULL COMMENT 'ชื่อสถานะหนังสือ', + sort_order INT DEFAULT 0 COMMENT 'ลำดับการแสดงผล', + is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน ' +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บสถานะของเอกสาร'; +-- ตาราง "แม่" ของเอกสารโต้ตอบ เก็บข้อมูลที่ไม่เปลี่ยนตาม Revision +CREATE TABLE correspondences ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง (นี่คือ "Master ID" ที่ใช้เชื่อมโยง)', + correspondence_number VARCHAR(100) NOT NULL COMMENT 'เลขที่เอกสาร (สร้างจาก DocumentNumberingModule)', + correspondence_type_id INT NOT NULL COMMENT 'ประเภทเอกสาร', + is_internal_communication TINYINT(1) DEFAULT 0 COMMENT '(1 = ภายใน, 0 = ภายนอก)', + project_id INT NOT NULL COMMENT 'อยู่ในโครงการ', + originator_id INT COMMENT 'องค์กรผู้ส่ง', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + created_by INT COMMENT 'ผู้สร้าง', + deleted_at DATETIME NULL COMMENT 'สำหรับ Soft Delete', + 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, + UNIQUE KEY uq_corr_no_per_project (project_id, correspondence_number) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง "แม่" ของเอกสารโต้ตอบ เก็บข้อมูลที่ไม่เปลี่ยนตาม Revision'; +-- ตาราง "ลูก" เก็บประวัติการแก้ไข (Revisions) ของ correspondences (1:N) +CREATE TABLE correspondence_revisions ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของ Revision', + correspondence_id INT NOT NULL COMMENT 'Master ID', + revision_number INT NOT NULL COMMENT 'หมายเลข Revision (0, 1, 2...)', + revision_label VARCHAR(10) COMMENT 'Revision ที่แสดง (เช่น A, B, 1.1)', + is_current BOOLEAN DEFAULT FALSE COMMENT '(1 = Revision ปัจจุบัน)', + correspondence_status_id INT NOT NULL COMMENT 'สถานะของ Revision นี้', + title VARCHAR(255) NOT NULL COMMENT 'เรื่อง', + document_date DATE COMMENT 'วันที่ในเอกสาร', + issued_date DATETIME COMMENT 'วันที่ออกเอกสาร', + received_date DATETIME COMMENT 'วันที่ลงรับเอกสาร', + due_date DATETIME COMMENT 'วันที่ครบกำหนด', + description TEXT COMMENT 'คำอธิบายการแก้ไขใน Revision นี้', + details JSON COMMENT 'ข้อมูลเฉพาะ (เช่น RFI details)', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้างเอกสาร', + created_by INT COMMENT 'ผู้สร้าง', + updated_by INT COMMENT 'ผู้แก้ไขล่าสุด', + 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 uq_master_revision_number (correspondence_id, revision_number), + UNIQUE KEY uq_master_current (correspondence_id, is_current) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง "ลูก" เก็บประวัติการแก้ไข (Revisions) ของ correspondences (1 :N)'; +-- ตารางเชื่อมผู้รับ (TO/CC) สำหรับเอกสารแต่ละฉบับ (M:N) +CREATE TABLE correspondence_recipients ( + correspondence_id INT COMMENT 'ID ของเอกสาร', + recipient_organization_id INT COMMENT 'ID องค์กรผู้รับ', + recipient_type ENUM('TO', 'CC ') COMMENT 'ประเภทผู้รับ (TO หรือ CC)', + 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 +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเชื่อมผู้รับ (TO / CC) สำหรับเอกสารแต่ละฉบับ (M :N)'; +-- ตาราง Master เก็บ Tags ทั้งหมดที่ใช้ในระบบ +CREATE TABLE tags ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + tag_name VARCHAR(100) NOT NULL UNIQUE COMMENT 'ชื่อ Tag', + description TEXT COMMENT 'คำอธิบายแท็ก', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด ' +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บ Tags ทั้งหมดที่ใช้ในระบบ'; +-- ตารางเชื่อมระหว่าง correspondences และ tags (M:N) +CREATE TABLE correspondence_tags ( + correspondence_id INT COMMENT 'ID ของเอกสาร', + tag_id INT COMMENT 'ID ของ Tag', + 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 +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเชื่อมระหว่าง correspondences และ tags (M :N)'; +-- ตารางเชื่อมการอ้างอิงระหว่างเอกสาร (M:N) +CREATE TABLE correspondence_references ( + src_correspondence_id INT COMMENT 'ID เอกสารต้นทาง', + tgt_correspondence_id INT COMMENT 'ID เอกสารเป้าหมาย', + 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 +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเชื่อมการอ้างอิงระหว่างเอกสาร (M :N)'; +-- ===================================================== +-- 4. 📐 approval: RFA (เอกสารขออนุมัติ, Workflows) +-- ===================================================== +-- ตาราง Master สำหรับประเภท RFA +CREATE TABLE rfa_types ( + id INT PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT 'ID ของตาราง', + contract_id INT NOT NULL COMMENT 'ผูกกับสัญญา', + type_code VARCHAR(20) NOT NULL COMMENT 'รหัสประเภท RFA (เช่น DWG, DOC, MAT)', + type_name_th VARCHAR(100) NOT NULL COMMENT 'ชื่อประเภท RFA th', + type_name_en VARCHAR(100) NOT NULL COMMENT 'ชื่อประเภท RFA en', + remark TEXT COMMENT 'หมายเหตุ', + -- sort_order INT DEFAULT 0 COMMENT 'ลำดับการแสดงผล', + is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน ', + UNIQUE KEY uk_rfa_types_contract_code (contract_id, type_code), + FOREIGN KEY (contract_id) REFERENCES contracts(id) ON DELETE CASCADE +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master สำหรับประเภท RFA'; +-- [NEW 6B] ตารางเก็บข้อมูลสาขางาน (Disciplines) แยกตามสัญญา +CREATE TABLE disciplines ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + contract_id INT NOT NULL COMMENT 'ผูกกับสัญญา', + discipline_code VARCHAR(10) NOT NULL COMMENT 'รหัสสาขา (เช่น GEN, STR)', + code_name_th VARCHAR(255) COMMENT 'ชื่อไทย', + code_name_en VARCHAR(255) COMMENT 'ชื่ออังกฤษ', + is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (contract_id) REFERENCES contracts(id) ON DELETE CASCADE, + UNIQUE KEY uk_discipline_contract (contract_id, discipline_code) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเก็บข้อมูลสาขางาน (Disciplines) ตาม Req 6B'; +-- [NEW 6B] ตารางเก็บประเภทหนังสือย่อย (Sub Types) สำหรับ Mapping เลขรหัส +CREATE TABLE correspondence_sub_types ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + contract_id INT NOT NULL COMMENT 'ผูกกับสัญญา', + correspondence_type_id INT NOT NULL COMMENT 'ผูกกับประเภทเอกสารหลัก (เช่น RFA)', + sub_type_code VARCHAR(20) NOT NULL COMMENT 'รหัสย่อย (เช่น MAT, SHP)', + sub_type_name VARCHAR(255) COMMENT 'ชื่อประเภทหนังสือย่อย', + sub_type_number VARCHAR(10) COMMENT 'เลขรหัสสำหรับ Running Number (เช่น 11, 22)', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (contract_id) REFERENCES contracts(id) ON DELETE CASCADE, + FOREIGN KEY (correspondence_type_id) REFERENCES correspondence_types(id) ON DELETE CASCADE +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเก็บประเภทหนังสือย่อย (Sub Types) ตาม Req 6B'; +-- หรือใช้ ALTER TABLE (แนะนำให้รันหลังสร้างตาราง disciplines เสร็จ) +ALTER TABLE correspondences +ADD COLUMN discipline_id INT NULL COMMENT 'สาขางาน (ถ้ามี)' +AFTER correspondence_type_id; +ALTER TABLE correspondences +ADD CONSTRAINT fk_corr_discipline FOREIGN KEY (discipline_id) REFERENCES disciplines(id) ON DELETE +SET NULL; +-- ตาราง Master สำหรับสถานะ RFA +CREATE TABLE rfa_status_codes ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + status_code VARCHAR(20) NOT NULL UNIQUE COMMENT 'รหัสสถานะ RFA (เช่น DFT - Draft, FAP - For Approve)', + status_name VARCHAR(100) NOT NULL COMMENT 'ชื่อสถานะ', + description TEXT COMMENT 'คำอธิบาย', + sort_order INT DEFAULT 0 COMMENT 'ลำดับการแสดงผล', + is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน ' +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master สำหรับสถานะ RFA'; +-- ตาราง Master สำหรับรหัสผลการอนุมัติ RFA +CREATE TABLE rfa_approve_codes ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + approve_code VARCHAR(20) NOT NULL UNIQUE COMMENT 'รหัสผลการอนุมัติ ( + เช่น 1A - Approved, + 3R - Revise + and Resubmit + )', + approve_name VARCHAR(100) NOT NULL COMMENT 'ชื่อผลการอนุมัติ', + description TEXT COMMENT 'คำอธิบาย', + sort_order INT DEFAULT 0 COMMENT 'ลำดับการแสดงผล', + is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน ' +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master สำหรับรหัสผลการอนุมัติ RFA'; +CREATE TABLE rfas ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง (RFA Master ID)', + rfa_type_id INT NOT NULL COMMENT 'ประเภท RFA', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + created_by INT COMMENT 'ผู้สร้าง', + deleted_at DATETIME NULL COMMENT 'สำหรับ Soft Delete', + FOREIGN KEY (rfa_type_id) REFERENCES rfa_types(id), + FOREIGN KEY (created_by) REFERENCES users(user_id) ON DELETE + SET NULL +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง "แม่" ของ RFA (มีความสัมพันธ์ 1 :N กับ rfa_revisions)'; +ALTER TABLE rfas +ADD COLUMN discipline_id INT NULL COMMENT 'สาขางาน (ถ้ามี)' +AFTER rfa_type_id; +ALTER TABLE rfas +ADD CONSTRAINT fk_rfa_discipline FOREIGN KEY (discipline_id) REFERENCES disciplines(id) ON DELETE +SET NULL; +-- ตาราง "ลูก" เก็บประวัติ (Revisions) ของ rfas (1:N) +CREATE TABLE rfa_revisions ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของ Revision', + correspondence_id INT NOT NULL COMMENT 'Master ID ของ Correspondence', + rfa_id INT NOT NULL COMMENT 'Master ID ของ RFA', + revision_number INT NOT NULL COMMENT 'หมายเลข Revision (0, 1, 2...)', + revision_label VARCHAR(10) COMMENT 'Revision ที่แสดง (เช่น A, B, 1.1)', + is_current BOOLEAN DEFAULT FALSE COMMENT '(1 = Revision ปัจจุบัน)', + rfa_status_code_id INT NOT NULL COMMENT 'สถานะ RFA', + rfa_approve_code_id INT COMMENT 'ผลการอนุมัติ', + title VARCHAR(255) NOT NULL COMMENT 'เรื่อง', + document_date DATE COMMENT 'วันที่ในเอกสาร', + issued_date DATE COMMENT 'วันที่ส่งขออนุมัติ', + received_date DATETIME COMMENT 'วันที่ลงรับเอกสาร', + approved_date DATE COMMENT 'วันที่อนุมัติ', + description TEXT COMMENT 'คำอธิบายการแก้ไขใน Revision นี้', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้างเอกสาร', + created_by INT COMMENT 'ผู้สร้าง', + updated_by INT COMMENT 'ผู้แก้ไขล่าสุด', + FOREIGN KEY (correspondence_id) REFERENCES correspondences(id) ON DELETE CASCADE, + 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 uq_rr_rev_number (rfa_id, revision_number), + UNIQUE KEY uq_rr_current (rfa_id, is_current) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง "ลูก" เก็บประวัติ (Revisions) ของ rfas (1 :N)'; +-- ตารางเชื่อมระหว่าง rfa_revisions (ที่เป็นประเภท DWG) กับ shop_drawing_revisions (M:N) +CREATE TABLE rfa_items ( + rfarev_correspondence_id INT COMMENT 'ID ของ RFA Revision', + shop_drawing_revision_id INT COMMENT 'ID ของ Shop Drawing Revision', + PRIMARY KEY ( + rfarev_correspondence_id, + shop_drawing_revision_id + ), + FOREIGN KEY (rfarev_correspondence_id) REFERENCES rfa_revisions(correspondence_id) ON DELETE CASCADE, + FOREIGN KEY (shop_drawing_revision_id) REFERENCES shop_drawing_revisions(id) ON DELETE CASCADE +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเชื่อมระหว่าง rfa_revisions (ที่เป็นประเภท DWG) กับ shop_drawing_revisions (M :N)'; +-- ===================================================== +-- 5. 📐 Drawings (แบบ, หมวดหมู่) +-- ===================================================== +-- ตาราง Master สำหรับ "เล่ม" ของแบบคู่สัญญา +CREATE TABLE contract_drawing_volumes ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + project_id INT NOT NULL COMMENT 'โครงการ', + volume_code VARCHAR(50) NOT NULL COMMENT 'รหัสเล่ม', + volume_name VARCHAR(255) NOT NULL COMMENT 'ชื่อเล่ม', + description TEXT COMMENT 'คำอธิบาย', + sort_order INT DEFAULT 0 COMMENT 'ลำดับการแสดงผล', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', + FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE, + UNIQUE KEY ux_volume_project (project_id, volume_code) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master สำหรับ "เล่ม" ของแบบคู่สัญญา'; +-- ตาราง Master สำหรับ "หมวดหมู่หลัก" ของแบบคู่สัญญา +CREATE TABLE contract_drawing_cats ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + project_id INT NOT NULL COMMENT 'โครงการ', + cat_code VARCHAR(50) NOT NULL COMMENT 'รหัสหมวดหมู่หลัก', + cat_name VARCHAR(255) NOT NULL COMMENT 'ชื่อหมวดหมู่หลัก', + description TEXT COMMENT 'คำอธิบาย', + sort_order INT DEFAULT 0 COMMENT 'ลำดับการแสดงผล', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', + FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE, + UNIQUE KEY ux_cat_project (project_id, cat_code) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master สำหรับ "หมวดหมู่หลัก" ของแบบคู่สัญญา'; +-- ตาราง Master สำหรับ "หมวดหมู่ย่อย" ของแบบคู่สัญญา +CREATE TABLE contract_drawing_sub_cats ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + project_id INT NOT NULL COMMENT 'โครงการ', + sub_cat_code VARCHAR(50) NOT NULL COMMENT 'รหัสหมวดหมู่ย่อย', + sub_cat_name VARCHAR(255) NOT NULL COMMENT 'ชื่อหมวดหมู่ย่อย', + description TEXT COMMENT 'คำอธิบาย', + sort_order INT DEFAULT 0 COMMENT 'ลำดับการแสดงผล', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', + FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE, + UNIQUE KEY ux_subcat_project (project_id, sub_cat_code) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master สำหรับ "หมวดหมู่ย่อย" ของแบบคู่สัญญา'; +-- ตารางเชื่อมระหว่าง หมวดหมู่หลัก-ย่อย (M:N) +CREATE TABLE contract_drawing_subcat_cat_maps ( + project_id INT COMMENT 'ID ของโครงการ', + sub_cat_id INT COMMENT 'ID ของหมวดหมู่ย่อย', + cat_id INT COMMENT 'ID ของหมวดหมู่หลัก', + PRIMARY 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 +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเชื่อมระหว่าง หมวดหมู่หลัก - ย่อย (M :N)'; +-- ตาราง Master เก็บข้อมูล "แบบคู่สัญญา" +CREATE TABLE contract_drawings ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + project_id INT NOT NULL COMMENT 'โครงการ', + condwg_no VARCHAR(255) NOT NULL COMMENT 'เลขที่แบบสัญญา', + title VARCHAR(255) NOT NULL COMMENT 'ชื่อแบบสัญญา', + sub_cat_id INT COMMENT 'หมวดหมู่ย่อย', + volume_id INT COMMENT 'เล่ม', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', + deleted_at DATETIME NULL COMMENT 'วันที่ลบ', + updated_by INT COMMENT 'ผู้แก้ไขล่าสุด', + FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE, + FOREIGN KEY (sub_cat_id) REFERENCES contract_drawing_sub_cats(id) ON DELETE RESTRICT, + FOREIGN KEY (volume_id) REFERENCES contract_drawing_volumes(id) ON DELETE RESTRICT, + UNIQUE KEY ux_condwg_no_project (project_id, condwg_no) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บข้อมูล "แบบคู่สัญญา"'; +-- ตาราง Master สำหรับ "หมวดหมู่หลัก" ของแบบก่อสร้าง +CREATE TABLE shop_drawing_main_categories ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + main_category_code VARCHAR(50) NOT NULL UNIQUE COMMENT 'รหัสหมวดหมู่หลัก (เช่น ARCH, STR)', + main_category_name VARCHAR(255) NOT NULL COMMENT 'ชื่อหมวดหมู่หลัก', + description TEXT COMMENT 'คำอธิบาย', + sort_order INT DEFAULT 0 COMMENT 'ลำดับการแสดงผล', + is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด ' +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master สำหรับ "หมวดหมู่หลัก" ของแบบก่อสร้าง'; +-- ตาราง Master สำหรับ "หมวดหมู่ย่อย" ของแบบก่อสร้าง +CREATE TABLE shop_drawing_sub_categories ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + sub_category_code VARCHAR(50) NOT NULL UNIQUE COMMENT 'รหัสหมวดหมู่ย่อย (เช่น STR - COLUMN)', + sub_category_name VARCHAR(255) NOT NULL COMMENT 'ชื่อหมวดหมู่ย่อย', + main_category_id INT NOT NULL COMMENT 'หมวดหมู่หลัก', + description TEXT COMMENT 'คำอธิบาย', + sort_order INT DEFAULT 0 COMMENT 'ลำดับการแสดงผล', + is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', + FOREIGN KEY (main_category_id) REFERENCES shop_drawing_main_categories(id) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master สำหรับ "หมวดหมู่ย่อย" ของแบบก่อสร้าง'; +-- ตาราง Master เก็บข้อมูล "แบบก่อสร้าง" +CREATE TABLE shop_drawings ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + project_id INT NOT NULL COMMENT 'โครงการ', + drawing_number VARCHAR(100) NOT NULL UNIQUE COMMENT 'เลขที่ Shop Drawing', + title VARCHAR(500) NOT NULL COMMENT 'ชื่อแบบ', + main_category_id INT NOT NULL COMMENT 'หมวดหมู่หลัก', + sub_category_id INT NOT NULL COMMENT 'หมวดหมู่ย่อย', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', + deleted_at DATETIME NULL COMMENT 'วันที่ลบ', + updated_by INT COMMENT 'ผู้แก้ไขล่าสุด', + 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) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บข้อมูล "แบบก่อสร้าง"'; +-- ตาราง "ลูก" เก็บประวัติ (Revisions) ของ shop_drawings (1:N) +CREATE TABLE shop_drawing_revisions ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของ Revision', + shop_drawing_id INT NOT NULL COMMENT 'Master ID', + revision_number INT NOT NULL COMMENT 'หมายเลข Revision (เช่น 0, 1, 2...)', + revision_label VARCHAR(10) COMMENT 'Revision ที่แสดง (เช่น A, B, 1.1)', + revision_date DATE COMMENT 'วันที่ของ Revision', + description TEXT COMMENT 'คำอธิบายการแก้ไข', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + FOREIGN KEY (shop_drawing_id) REFERENCES shop_drawings(id) ON DELETE CASCADE, + UNIQUE KEY ux_sd_rev_drawing_revision (shop_drawing_id, revision_number) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง "ลูก" เก็บประวัติ (Revisions) ของ shop_drawings (1 :N)'; +-- ตารางเชื่อมระหว่าง shop_drawing_revisions กับ contract_drawings (M:N) +CREATE TABLE shop_drawing_revision_contract_refs ( + shop_drawing_revision_id INT COMMENT 'ID ของ Shop Drawing Revision', + contract_drawing_id INT COMMENT 'ID ของ Contract Drawing', + 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 +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเชื่อมระหว่าง shop_drawing_revisions กับ contract_drawings (M :N)'; +-- ===================================================== +-- 6. 🔄 Circulations (ใบเวียนภายใน) +-- ===================================================== +-- ตาราง Master เก็บสถานะใบเวียน +CREATE TABLE circulation_status_codes ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + code VARCHAR(20) NOT NULL UNIQUE COMMENT 'รหัสสถานะการดำเนินงาน', + description VARCHAR(50) NOT NULL COMMENT 'คำอธิบายสถานะการดำเนินงาน', + sort_order INT DEFAULT 0 COMMENT 'ลำดับการแสดงผล', + is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน ' +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บสถานะใบเวียน'; +-- ตาราง "แม่" ของใบเวียนเอกสารภายใน +CREATE TABLE circulations ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตารางใบเวียน', + correspondence_id INT UNIQUE COMMENT 'ID ของเอกสาร (จากตาราง correspondences)', + organization_id INT NOT NULL COMMENT 'ID ขององค์กรณ์ที่เป็นเจ้าของใบเวียนนี้', + circulation_no VARCHAR(100) NOT NULL COMMENT 'เลขที่ใบเวียน', + circulation_subject VARCHAR(500) NOT NULL COMMENT 'เรื่องใบเวียน', + circulation_status_code VARCHAR(20) NOT NULL COMMENT 'รหัสสถานะใบเวียน', + created_by_user_id INT NOT NULL COMMENT 'ID ของผู้สร้างใบเวียน', + submitted_at TIMESTAMP NULL COMMENT 'วันที่ส่งใบเวียน', + closed_at TIMESTAMP NULL COMMENT 'วันที่ปิดใบเวียน', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', + 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) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง "แม่" ของใบเวียนเอกสารภายใน'; +-- ===================================================== +-- 7. 📤 Transmittals (เอกสารนำส่ง) +-- ===================================================== +-- ตารางข้อมูลเฉพาะของเอกสารนำส่ง (เป็นตารางลูก 1:1 ของ correspondences) +CREATE TABLE transmittals ( + correspondence_id INT PRIMARY KEY COMMENT 'ID ของเอกสาร', + purpose ENUM( + 'FOR_APPROVAL', + 'FOR_INFORMATION', + 'FOR_REVIEW', + 'OTHER ' + ) COMMENT 'วัตถุประสงค์', + remarks TEXT COMMENT 'หมายเหตุ', + FOREIGN KEY (correspondence_id) REFERENCES correspondences(id) ON DELETE CASCADE +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางข้อมูลเฉพาะของเอกสารนำส่ง (เป็นตารางลูก 1 :1 ของ correspondences)'; +-- ตารางเชื่อมระหว่าง transmittals และเอกสารที่นำส่ง (M:N) +CREATE TABLE transmittal_items ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของรายการ', + transmittal_id INT NOT NULL COMMENT 'ID ของ Transmittal', + item_correspondence_id INT NOT NULL COMMENT 'ID ของเอกสารที่แนบไป', + quantity INT DEFAULT 1 COMMENT 'จำนวน', + remarks VARCHAR(255) COMMENT 'หมายเหตุสำหรับรายการนี้', + FOREIGN KEY (transmittal_id) REFERENCES transmittals(correspondence_id) ON DELETE CASCADE, + FOREIGN KEY (item_correspondence_id) REFERENCES correspondences(id) ON DELETE CASCADE, + UNIQUE KEY ux_transmittal_item (transmittal_id, item_correspondence_id) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเชื่อมระหว่าง transmittals และเอกสารที่นำส่ง (M :N)'; +-- ===================================================== +-- 8. 📎 File Management (ไฟล์แนบ) +-- ===================================================== +-- ตาราง "กลาง" เก็บไฟล์แนบทั้งหมดของระบบ +-- 2.2 Attachments - Two-Phase Storage & Security +-- รองรับ: Backend Plan T2.2, Req 3.9.1 +-- เหตุผล: จัดการไฟล์ขยะ (Orphan Files) และตรวจสอบความถูกต้องไฟล์ +CREATE TABLE attachments ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของไฟล์แนบ', + original_filename VARCHAR(255) NOT NULL COMMENT 'ชื่อไฟล์ดั้งเดิมตอนอัปโหลด', + stored_filename VARCHAR(255) NOT NULL COMMENT 'ชื่อไฟล์ที่เก็บจริงบน Server (ป้องกันชื่อซ้ำ)', + file_path VARCHAR(500) NOT NULL COMMENT 'Path ที่เก็บไฟล์ (บน QNAP / share / dms - data /)', + mime_type VARCHAR(100) NOT NULL COMMENT 'ประเภทไฟล์ (เช่น application / pdf)', + file_size INT NOT NULL COMMENT 'ขนาดไฟล์ (bytes)', + is_temporary BOOLEAN DEFAULT TRUE COMMENT 'True = ยังไม่ Commit ลง DB จริง', + temp_id VARCHAR(100) NULL COMMENT 'ID ชั่วคราวสำหรับอ้างอิงตอน Upload Phase 1', + uploaded_by_user_id INT NOT NULL COMMENT 'ผู้อัปโหลดไฟล์', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่อัปโหลด', + expires_at DATETIME NULL COMMENT 'เวลาหมดอายุของไฟล์ Temp', + checksum VARCHAR(64) NULL COMMENT 'SHA -256 Checksum', + FOREIGN KEY (uploaded_by_user_id) REFERENCES users(user_id) ON DELETE CASCADE +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง "กลาง" เก็บไฟล์แนบทั้งหมดของระบบ'; +-- ตารางเชื่อม correspondences กับ attachments (M:N) +CREATE TABLE correspondence_attachments ( + correspondence_id INT COMMENT 'ID ของเอกสาร', + attachment_id INT COMMENT 'ID ของไฟล์แนบ', + is_main_document BOOLEAN DEFAULT FALSE COMMENT '(1 = ไฟล์หลัก)', + 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 +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเชื่อม correspondences กับ attachments (M :N)'; +-- ตารางเชื่อม circulations กับ attachments (M:N) +CREATE TABLE circulation_attachments ( + circulation_id INT COMMENT 'ID ของใบเวียน', + attachment_id INT COMMENT 'ID ของไฟล์แนบ', + is_main_document BOOLEAN DEFAULT FALSE COMMENT '(1 = ไฟล์หลักของใบเวียน)', + 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 +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเชื่อม circulations กับ attachments (M :N)'; +-- ตารางเชื่อม shop_drawing_revisions กับ attachments (M:N) +CREATE TABLE shop_drawing_revision_attachments ( + shop_drawing_revision_id INT COMMENT 'ID ของ Shop Drawing Revision', + attachment_id INT COMMENT 'ID ของไฟล์แนบ', + file_type ENUM('PDF', 'DWG', 'SOURCE', 'OTHER ') COMMENT 'ประเภทไฟล์', + is_main_document BOOLEAN DEFAULT FALSE COMMENT '(1 = ไฟล์หลัก)', + 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 +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเชื่อม shop_drawing_revisions กับ attachments (M :N)'; +-- ตารางเชื่อม contract_drawings กับ attachments (M:N) +CREATE TABLE contract_drawing_attachments ( + contract_drawing_id INT COMMENT 'ID ของ Contract Drawing', + attachment_id INT COMMENT 'ID ของไฟล์แนบ', + file_type ENUM('PDF', 'DWG', 'SOURCE', 'OTHER ') COMMENT 'ประเภทไฟล์', + is_main_document BOOLEAN DEFAULT FALSE COMMENT '(1 = ไฟล์หลัก)', + 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 +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเชื่อม contract_drawings กับ attachments (M :N)'; +-- ===================================================== +-- 9. 🔢 Document Numbering (การสร้างเลขที่เอกสาร) +-- ===================================================== +-- ตาราง Master เก็บ "รูปแบบ" Template ของเลขที่เอกสาร +CREATE TABLE document_number_formats ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + project_id INT NOT NULL COMMENT 'โครงการ', + correspondence_type_id INT NOT NULL COMMENT 'ประเภทเอกสาร', + format_template VARCHAR(255) NOT NULL COMMENT 'รูปแบบ Template (เช่น { ORG_CODE } - { TYPE_CODE } - { SEQ :4 })', + description TEXT COMMENT 'คำอธิบายรูปแบบนี้', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', + FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE, + FOREIGN KEY (correspondence_type_id) REFERENCES correspondence_types(id) ON DELETE CASCADE, + UNIQUE KEY uk_project_type (project_id, correspondence_type_id) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บ "รูปแบบ" Template ของเลขที่เอกสาร'; +-- ========================================================== +-- [v1.5.1 UPDATE] ตารางเก็บ "ตัวนับ" (Running Number) ล่าสุด +-- เปลี่ยนแปลงหลัก: +-- - PRIMARY KEY: เปลี่ยนจาก 5 คอลัมน์เป็น 8 คอลัมน์ +-- - เพิ่มคอลัมน์: recipient_organization_id, sub_type_id, rfa_type_id +-- - เพิ่ม INDEXES สำหรับ performance +-- - เพิ่ม CONSTRAINTS สำหรับ data validation +-- เหตุผล: รองรับ 10 token types และ granular counter management +-- รองรับ: Backend Plan T2.3, Req 3.11.5, specs v1.5.1 +-- ========================================================== +CREATE TABLE document_number_counters ( + -- [v1.5.1] Composite Primary Key Columns (8 columns total) + project_id INT NOT NULL COMMENT 'โครงการ', + originator_organization_id INT NOT NULL COMMENT 'องค์กรผู้ส่ง', + recipient_organization_id INT NULL COMMENT '[v1.5.1 NEW] องค์กรผู้รับ (NULL = ทุกองค์กร)', + correspondence_type_id INT NOT NULL COMMENT 'ประเภทเอกสาร (LETTER, RFA, TRANSMITTAL, etc.)', + sub_type_id INT DEFAULT 0 COMMENT '[v1.5.1 NEW] ประเภทย่อย สำหรับ TRANSMITTAL (0 = ไม่ระบุ)', + rfa_type_id INT DEFAULT 0 COMMENT '[v1.5.1 NEW] ประเภท RFA เช่น SHD, RPT, MAT (0 = ไม่ใช่ RFA)', + discipline_id INT DEFAULT 0 COMMENT 'สาขางาน เช่น TER, STR, GEO (0 = ไม่ระบุ)', + current_year INT NOT NULL COMMENT 'ปี ค.ศ. ของตัวนับ (auto-reset ทุกปี)', + + -- Counter Data + version INT DEFAULT 0 NOT NULL COMMENT 'Optimistic Lock Version (TypeORM @VersionColumn)', + last_number INT DEFAULT 0 COMMENT 'เลขที่ล่าสุดที่ใช้ไปแล้ว (auto-increment)', + + -- [v1.5.1 UPDATE] Primary Key: 5 columns -> 8 columns + -- ใช้ COALESCE เพื่อรองรับ NULL ใน recipient_organization_id + PRIMARY KEY ( + project_id, + originator_organization_id, + COALESCE(recipient_organization_id, 0), -- [v1.5.1 NEW] Handle NULL values + correspondence_type_id, + sub_type_id, -- [v1.5.1 NEW] + rfa_type_id, -- [v1.5.1 NEW] + discipline_id, + current_year + ), + + -- Foreign Keys + FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE, + FOREIGN KEY (originator_organization_id) REFERENCES organizations(id) ON DELETE CASCADE, + FOREIGN KEY (recipient_organization_id) REFERENCES organizations(id) ON DELETE CASCADE, + FOREIGN KEY (correspondence_type_id) REFERENCES correspondence_types(id) ON DELETE CASCADE, + + -- [v1.5.1 NEW] Performance Indexes + INDEX idx_counter_lookup (project_id, correspondence_type_id, current_year), + INDEX idx_counter_org (originator_organization_id, current_year), + + -- [v1.5.1 NEW] Data Validation Constraints + CONSTRAINT chk_last_number_positive CHECK (last_number >= 0), + CONSTRAINT chk_current_year_valid CHECK (current_year BETWEEN 2020 AND 2100) + +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci + COMMENT = '[v1.5.1 UPDATE] ตารางเก็บ Running Number Counters - รองรับ 8-column composite PK'; + +-- ========================================================== +-- [v1.5.1 NEW] ตารางเก็บ Audit Trail สำหรับการสร้างเลขที่เอกสาร +-- เพิ่มตาราง: document_number_audit +-- เหตุผล: บันทึกประวัติการสร้างเลขที่ รองรับ audit requirement ≥ 7 ปี +-- รองรับ: Req 3.11.8, Backend Plan T2.8 +-- ========================================================== +CREATE TABLE document_number_audit ( + id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 'ID ของ audit record', + + -- Document Info + document_id INT NOT NULL COMMENT 'ID ของเอกสารที่สร้างเลขที่ (correspondences.id)', + generated_number VARCHAR(100) NOT NULL COMMENT 'เลขที่เอกสารที่สร้าง (ผลลัพธ์)', + counter_key JSON NOT NULL COMMENT 'Counter key ที่ใช้ (JSON format) - 8 fields', + template_used VARCHAR(200) NOT NULL COMMENT 'Template ที่ใช้ในการสร้าง', + + -- User Info + user_id INT NOT NULL COMMENT 'ผู้ขอสร้างเลขที่', + ip_address VARCHAR(45) COMMENT 'IP address ของผู้ขอ (IPv4/IPv6)', + user_agent TEXT COMMENT 'User agent string (browser info)', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่/เวลาที่สร้าง', + + -- Performance & Error Tracking + retry_count INT DEFAULT 0 COMMENT 'จำนวนครั้งที่ retry ก่อนสำเร็จ', + lock_wait_ms INT COMMENT 'เวลารอ Redis lock (milliseconds)', + total_duration_ms INT COMMENT 'เวลารวมทั้งหมดในการสร้าง (milliseconds)', + fallback_used ENUM('NONE', 'DB_LOCK', 'RETRY') DEFAULT 'NONE' + COMMENT 'Fallback strategy ที่ถูกใช้ (NONE=normal, DB_LOCK=Redis down, RETRY=conflict)', + + -- Indexes for performance + INDEX idx_document_id (document_id), + INDEX idx_user_id (user_id), + INDEX idx_created_at (created_at), + INDEX idx_generated_number (generated_number), + + -- Foreign Keys + FOREIGN KEY (document_id) REFERENCES correspondences(id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users(user_id) + +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci + COMMENT='[v1.5.1 NEW] Audit Trail สำหรับการสร้างเลขที่เอกสาร - เก็บ ≥ 7 ปี'; + +-- ========================================================== +-- [v1.5.1 NEW] ตารางเก็บ Error Logs สำหรับ Document Numbering +-- เพิ่มตาราง: document_number_errors +-- เหตุผล: ติดตาม errors, troubleshooting, monitoring +-- รองรับ: Req 3.11.6, Ops monitoring requirements +-- ========================================================== +CREATE TABLE document_number_errors ( + id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 'ID ของ error record', + + -- Error Classification + error_type ENUM( + 'LOCK_TIMEOUT', -- Redis lock timeout + 'VERSION_CONFLICT', -- Optimistic lock version mismatch + 'DB_ERROR', -- Database connection/query error + 'REDIS_ERROR', -- Redis connection error + 'VALIDATION_ERROR' -- Template/input validation error + ) NOT NULL COMMENT 'ประเภท error (5 types)', + + -- Error Details + error_message TEXT COMMENT 'ข้อความ error (stack top)', + stack_trace TEXT COMMENT 'Stack trace แบบเต็ม (สำหรับ debugging)', + context_data JSON COMMENT 'Context ของ request (user, project, counter_key, etc.)', + + -- User Info + user_id INT COMMENT 'ผู้ที่เกิด error', + ip_address VARCHAR(45) COMMENT 'IP address', + + -- Timestamps + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่เกิด error', + resolved_at TIMESTAMP NULL COMMENT 'วันที่แก้ไขแล้ว (NULL = ยังไม่แก้)', + + -- Indexes for troubleshooting + INDEX idx_error_type (error_type), + INDEX idx_created_at (created_at), + INDEX idx_user_id (user_id), + INDEX idx_unresolved (resolved_at) -- Find unresolved errors + +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci + COMMENT='[v1.5.1 NEW] Error Log สำหรับ Document Numbering System'; + +-- ===================================================== +-- 10. ⚙️ System & Logs (ระบบและ Log) +-- ===================================================== +-- 1.1 JSON Schemas Registry +-- รองรับ: Backend Plan T2.5.1, Req 6.11.1 +-- เหตุผล: เพื่อ Validate โครงสร้าง JSON Details ของเอกสารแต่ละประเภทแบบ Centralized +CREATE TABLE json_schemas ( + id INT AUTO_INCREMENT PRIMARY KEY, + schema_code VARCHAR(100) NOT NULL COMMENT 'รหัส Schema (เช่น RFA_DWG)', + version INT NOT NULL DEFAULT 1 COMMENT 'เวอร์ชันของ Schema', + table_name VARCHAR(100) NOT NULL COMMENT 'ชื่อตารางเป้าหมาย (เช่น rfa_revisions)', + schema_definition JSON NOT NULL COMMENT 'โครงสร้าง Data Schema (AJV Standard)', + ui_schema JSON NULL COMMENT 'โครงสร้าง UI Schema สำหรับ Frontend', + virtual_columns JSON NULL COMMENT 'Config สำหรับสร้าง Virtual Columns', + migration_script JSON NULL COMMENT 'Script สำหรับแปลงข้อมูลจากเวอร์ชันก่อนหน้า', + is_active BOOLEAN DEFAULT TRUE COMMENT 'สถานะการใช้งาน', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + -- ป้องกัน Schema Code ซ้ำกันใน Version เดียวกัน + UNIQUE KEY uk_schema_version (schema_code, version) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'ตารางเก็บ JSON Schema และ Configuration'; +-- 1.2 User Preferences +-- รองรับ: Req 5.5, 6.8.3 +-- เหตุผล: แยกการตั้งค่า Notification และ UI ออกจากตาราง Users หลัก +CREATE TABLE user_preferences ( + user_id INT PRIMARY KEY, + notify_email BOOLEAN DEFAULT TRUE, + notify_line BOOLEAN DEFAULT TRUE, + digest_mode BOOLEAN DEFAULT FALSE COMMENT 'รับแจ้งเตือนแบบรวม (Digest) แทน Real - time', + ui_theme VARCHAR(20) DEFAULT 'light', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT fk_user_prefs_user FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci; +-- ตารางเก็บบันทึกการกระทำของผู้ใช้ +-- 4.1 Audit Logs Enhancements +-- รองรับ: Req 6.1 +-- เหตุผล: รองรับ Distributed Tracing และระบุความรุนแรง +CREATE TABLE audit_logs ( + audit_id BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ID ของ Log', + request_id VARCHAR(100) NULL COMMENT 'Trace ID linking to app logs', + user_id INT COMMENT 'ผู้กระทำ', + action VARCHAR(100) NOT NULL COMMENT 'การกระทำ ( + เช่น rfa.create, + correspondence.update, + login.success + )', + severity ENUM('INFO', 'WARN', 'ERROR', 'CRITICAL ') DEFAULT 'INFO', + entity_type VARCHAR(50) COMMENT 'ตาราง / โมดูล (เช่น ''rfa '', ''correspondence '')', + entity_id VARCHAR(50) COMMENT 'Primary ID ของระเบียนที่ได้รับผลกระทำ', + details_json JSON COMMENT 'ข้อมูลบริบท', + ip_address VARCHAR(45) COMMENT 'IP Address', + user_agent VARCHAR(255) COMMENT 'User Agent', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'เวลาที่กระทำ', + -- [แก้ไข] รวม created_at เข้ามาใน Primary Key เพื่อรองรับ Partition + PRIMARY KEY (audit_id, created_at), + -- [แก้ไข] ใช้ Index ธรรมดาแทน Foreign Key เพื่อไม่ให้ติดข้อจำกัดของ Partition Table + 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) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเก็บบันทึกการกระทำของผู้ใช้' -- [เพิ่ม] คำสั่ง Partition +PARTITION BY RANGE (YEAR(created_at)) ( + PARTITION p_old + VALUES LESS THAN (2024), + PARTITION p2024 + VALUES LESS THAN (2025), + PARTITION p2025 + VALUES LESS THAN (2026), + PARTITION p2026 + VALUES LESS THAN (2027), + PARTITION p2027 + VALUES LESS THAN (2028), + PARTITION p2028 + VALUES LESS THAN (2029), + PARTITION p2029 + VALUES LESS THAN (2030), + PARTITION p2030 + VALUES LESS THAN (2031), + PARTITION p_future + VALUES LESS THAN MAXVALUE +); +-- ตารางสำหรับจัดการการแจ้งเตือน (Email/Line/System) +CREATE TABLE notifications ( + id INT NOT NULL AUTO_INCREMENT COMMENT 'ID ของการแจ้งเตือน', + user_id INT NOT NULL COMMENT 'ID ผู้ใช้', + title VARCHAR(255) NOT NULL COMMENT 'หัวข้อการแจ้งเตือน', + message TEXT NOT NULL COMMENT 'รายละเอียดการแจ้งเตือน', + notification_type ENUM('EMAIL', 'LINE', 'SYSTEM ') NOT NULL COMMENT 'ประเภท (EMAIL, LINE, SYSTEM)', + is_read BOOLEAN DEFAULT FALSE COMMENT 'สถานะการอ่าน', + entity_type VARCHAR(50) COMMENT 'เช่น ''rfa '', + ''circulation ''', + entity_id INT COMMENT 'ID ของเอนทิตีที่เกี่ยวข้อง', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + -- [แก้ไข] รวม created_at เข้ามาใน Primary Key + PRIMARY KEY (id, created_at), + -- [แก้ไข] ใช้ Index ธรรมดาแทน Foreign 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) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางสำหรับจัดการการแจ้งเตือน (Email / Line / System)' -- [เพิ่ม] คำสั่ง Partition +PARTITION BY RANGE (YEAR(created_at)) ( + PARTITION p_old + VALUES LESS THAN (2024), + PARTITION p2024 + VALUES LESS THAN (2025), + PARTITION p2025 + VALUES LESS THAN (2026), + PARTITION p2026 + VALUES LESS THAN (2027), + PARTITION p2027 + VALUES LESS THAN (2028), + PARTITION p2028 + VALUES LESS THAN (2029), + PARTITION p2029 + VALUES LESS THAN (2030), + PARTITION p2030 + VALUES LESS THAN (2031), + PARTITION p_future + VALUES LESS THAN MAXVALUE +); +-- ตารางสำหรับจัดการดัชนีการค้นหาขั้นสูง (Full-text Search) +CREATE TABLE search_indices ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของดัชนี', + entity_type VARCHAR(50) NOT NULL COMMENT 'ชนิดเอนทิตี (เช่น ''correspondence '', ''rfa '')', + entity_id INT NOT NULL COMMENT 'ID ของเอนทิตี', + content TEXT NOT NULL COMMENT 'เนื้อหาที่จะค้นหา', + indexed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง / อัปเดตัชนี ' +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางสำหรับจัดการดัชนีการค้นหาขั้นสูง (Full - text Search)'; +-- ตารางสำหรับบันทึกประวัติการสำรองข้อมูล +CREATE TABLE backup_logs ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของการสำรอง', + backup_type ENUM('DATABASE', 'FILES', 'FULL') NOT NULL COMMENT 'ประเภท (DATABASE, FILES, FULL)', + backup_path VARCHAR(500) NOT NULL COMMENT 'ตำแหน่งไฟล์สำรอง', + file_size BIGINT COMMENT 'ขนาดไฟล์', + status ENUM('STARTED', 'COMPLETED', 'FAILED') NOT NULL COMMENT 'สถานะ', + started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'เวลาเริ่มต้น', + completed_at TIMESTAMP NULL COMMENT 'เวลาเสร็จสิ้น', + error_message TEXT COMMENT 'ข้อความผิดพลาด (ถ้ามี)' +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางสำหรับบันทึกประวัติการสำรองข้อมูล'; +-- 4.2 Virtual Columns for JSON Search (ตัวอย่างสำหรับ Correspondence) +-- รองรับ: Backend Plan T2.1, Req 3.11.3 +-- เหตุผล: เพิ่มความเร็วในการ Search/Sort ข้อมูลที่อยู่ใน JSON details +-- หมายเหตุ: ต้องมั่นใจว่า MariaDB เวอร์ชัน 10.11+ รองรับ Syntax นี้ +-- ตัวอย่าง: ดึง Project ID ที่อ้างอิงใน details ออกมาทำ Index +ALTER TABLE correspondence_revisions +ADD COLUMN v_ref_project_id INT GENERATED ALWAYS AS ( + JSON_UNQUOTE(JSON_EXTRACT(details, '$.projectId')) + ) VIRTUAL, + ADD INDEX idx_corr_rev_v_project (v_ref_project_id); +-- ตัวอย่าง: ดึง Document Type ย่อยจาก details +ALTER TABLE correspondence_revisions +ADD COLUMN v_doc_subtype VARCHAR(50) GENERATED ALWAYS AS (JSON_UNQUOTE(JSON_EXTRACT(details, '$.subType'))) VIRTUAL, + ADD INDEX idx_corr_rev_v_subtype (v_doc_subtype); +-- 2. ปรับปรุงตาราง correspondence_revisions +-- เพิ่ม Virtual Columns และ Schema Version +ALTER TABLE correspondence_revisions +ADD COLUMN schema_version INT DEFAULT 1 COMMENT 'เวอร์ชันของ Schema ที่ใช้กับ details' +AFTER details; +-- ทำแบบเดียวกันกับ RFA Revisions หากมีการเก็บ JSON details +ALTER TABLE rfa_revisions +ADD COLUMN details JSON NULL COMMENT 'RFA Specific Details' +AFTER description; +ALTER TABLE rfa_revisions +ADD COLUMN v_ref_drawing_count INT GENERATED ALWAYS AS ( + JSON_UNQUOTE(JSON_EXTRACT(details, '$.drawingCount')) + ) VIRTUAL; +ALTER TABLE rfa_revisions +ADD COLUMN schema_version INT DEFAULT 1 COMMENT 'Version ของ JSON Schema' +AFTER details; +CREATE INDEX idx_rfa_rev_v_drawing_count ON rfa_revisions(v_ref_drawing_count); +-- ... (ต่อท้ายไฟล์เดิม) +-- ============================================================ +-- ส่วนที่ 11: Unified Workflow Engine (Phase 6A/Phase 3) +-- ============================================================ +DROP TABLE IF EXISTS workflow_histories; +DROP TABLE IF EXISTS workflow_instances; +DROP TABLE IF EXISTS workflow_definitions; +-- 1. ตารางเก็บนิยาม Workflow (Definition / DSL) +CREATE TABLE workflow_definitions ( + id CHAR(36) NOT NULL PRIMARY KEY COMMENT 'UUID ของ Workflow Definition', + workflow_code VARCHAR(50) NOT NULL COMMENT 'รหัส Workflow เช่น RFA_FLOW_V1, CORRESPONDENCE_FLOW_V1', + version INT NOT NULL DEFAULT 1 COMMENT 'หมายเลข Version', + description TEXT NULL COMMENT 'คำอธิบาย Workflow', + dsl JSON NOT NULL COMMENT 'นิยาม Workflow ต้นฉบับ (YAML/JSON Format)', + compiled JSON NOT NULL COMMENT 'โครงสร้าง Execution Tree ที่ Compile แล้ว', + is_active BOOLEAN DEFAULT TRUE COMMENT 'สถานะการใช้งาน', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', + -- ป้องกันการมี Workflow Code และ Version ซ้ำกัน + UNIQUE KEY uq_workflow_version (workflow_code, version) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'ตารางเก็บนิยามกฎการเดินเอกสาร (Workflow DSL)'; +-- สร้าง Index สำหรับการค้นหา Workflow ที่ Active ล่าสุดได้เร็วขึ้น +CREATE INDEX idx_workflow_active ON workflow_definitions(workflow_code, is_active, version); +-- 2. ตารางเก็บ Workflow Instance (สถานะเอกสารจริง) +CREATE TABLE workflow_instances ( + id CHAR(36) NOT NULL PRIMARY KEY COMMENT 'UUID ของ Instance', + definition_id CHAR(36) NOT NULL COMMENT 'อ้างอิง Definition ที่ใช้', + entity_type VARCHAR(50) NOT NULL COMMENT 'ประเภทเอกสาร (rfa_revision, correspondence_revision, circulation)', + entity_id VARCHAR(50) NOT NULL COMMENT 'ID ของเอกสาร (String/Int)', + current_state VARCHAR(50) NOT NULL COMMENT 'สถานะปัจจุบัน', + status ENUM('ACTIVE', 'COMPLETED', 'CANCELLED', 'TERMINATED') DEFAULT 'ACTIVE' COMMENT 'สถานะภาพรวม', + context JSON NULL COMMENT 'ตัวแปร Context สำหรับตัดสินใจ', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT fk_wf_inst_def FOREIGN KEY (definition_id) REFERENCES workflow_definitions(id) ON DELETE CASCADE +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'ตารางเก็บสถานะการเดินเรื่องของเอกสาร'; +CREATE INDEX idx_wf_inst_entity ON workflow_instances(entity_type, entity_id); +CREATE INDEX idx_wf_inst_state ON workflow_instances(current_state); +-- 3. ตารางเก็บประวัติ (Audit Log / History) +CREATE TABLE workflow_histories ( + id CHAR(36) NOT NULL PRIMARY KEY COMMENT 'UUID', + instance_id CHAR(36) NOT NULL COMMENT 'อ้างอิง Instance', + from_state VARCHAR(50) NOT NULL COMMENT 'สถานะต้นทาง', + to_state VARCHAR(50) NOT NULL COMMENT 'สถานะปลายทาง', + action VARCHAR(50) NOT NULL COMMENT 'Action ที่กระทำ', + action_by_user_id INT NULL COMMENT 'User ID ผู้กระทำ', + comment TEXT NULL COMMENT 'ความเห็น', + metadata JSON NULL COMMENT 'Snapshot ข้อมูล ณ ขณะนั้น', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT fk_wf_hist_inst FOREIGN KEY (instance_id) REFERENCES workflow_instances(id) ON DELETE CASCADE +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'ตารางประวัติการเปลี่ยนสถานะ Workflow'; +CREATE INDEX idx_wf_hist_instance ON workflow_histories(instance_id); +CREATE INDEX idx_wf_hist_user ON workflow_histories(action_by_user_id); +-- ============================================================ +-- 5. PARTITIONING PREPARATION (Advance - Optional) +-- ============================================================ +-- หมายเหตุ: การทำ Partitioning บนตารางที่มีอยู่แล้ว (audit_logs, notifications) +-- มักจะต้อง Drop Primary Key เดิม แล้วสร้างใหม่โดยรวม Partition Key (created_at) เข้าไป +-- ขั้นตอนนี้ควรทำแยกต่างหากเมื่อระบบเริ่มมีข้อมูลเยอะ หรือทำใน Maintenance Window +-- +-- ตัวอย่าง SQL สำหรับ Audit Logs (Reference Only): +-- ALTER TABLE audit_logs DROP PRIMARY KEY, ADD PRIMARY KEY (audit_id, created_at); +-- 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 +-- ); +-- ===================================================== +-- CREATE INDEXES +-- ===================================================== +-- Indexes for document_number_formats +CREATE INDEX idx_document_number_formats_project ON document_number_formats(project_id); +CREATE INDEX idx_document_number_formats_type ON document_number_formats(correspondence_type_id); +CREATE INDEX idx_document_number_formats_project_type ON document_number_formats(project_id, correspondence_type_id); +-- Indexes for document_number_counters +CREATE INDEX idx_document_number_counters_project ON document_number_counters(project_id); +CREATE INDEX idx_document_number_counters_org ON document_number_counters(originator_organization_id); +CREATE INDEX idx_document_number_counters_type ON document_number_counters(correspondence_type_id); +CREATE INDEX idx_document_number_counters_year ON document_number_counters(current_year); +-- Indexes for tags +CREATE INDEX idx_tags_name ON tags(tag_name); +CREATE INDEX idx_tags_created_at ON tags(created_at); +-- Indexes for correspondence_tags +CREATE INDEX idx_correspondence_tags_correspondence ON correspondence_tags(correspondence_id); +CREATE INDEX idx_correspondence_tags_tag ON correspondence_tags(tag_id); +-- Indexes for audit_logs +CREATE INDEX idx_audit_logs_user ON audit_logs(user_id); +CREATE INDEX idx_audit_logs_action ON audit_logs(action); +CREATE INDEX idx_audit_logs_entity ON audit_logs(entity_type, entity_id); +CREATE INDEX idx_audit_logs_created_at ON audit_logs(created_at); +CREATE INDEX idx_audit_logs_ip ON audit_logs(ip_address); +-- Indexes for notifications +CREATE INDEX idx_notifications_user ON notifications(user_id); +CREATE INDEX idx_notifications_type ON notifications(notification_type); +CREATE INDEX idx_notifications_read ON notifications(is_read); +CREATE INDEX idx_notifications_entity ON notifications(entity_type, entity_id); +CREATE INDEX idx_notifications_created_at ON notifications(created_at); +-- Indexes for search_indices +CREATE INDEX idx_search_indices_entity ON search_indices(entity_type, entity_id); +CREATE INDEX idx_search_indices_indexed_at ON search_indices(indexed_at); +-- Indexes for backup_logs +CREATE INDEX idx_backup_logs_type ON backup_logs(backup_type); +CREATE INDEX idx_backup_logs_status ON backup_logs(status); +CREATE INDEX idx_backup_logs_started_at ON backup_logs(started_at); +CREATE INDEX idx_backup_logs_completed_at ON backup_logs(completed_at); +-- ===================================================== +-- Additional Composite Indexes for Performance +-- ===================================================== +-- Composite index for document_number_counters for faster lookups +CREATE INDEX idx_doc_counter_composite ON document_number_counters( + project_id, + originator_organization_id, + correspondence_type_id, + current_year +); +-- Composite index for notifications for user-specific queries +CREATE INDEX idx_notifications_user_unread ON notifications(user_id, is_read, created_at); +-- Composite index for audit_logs for reporting +CREATE INDEX idx_audit_logs_reporting ON audit_logs(created_at, entity_type, action); +-- Composite index for search_indices for entity-based queries +CREATE INDEX idx_search_entities ON search_indices(entity_type, entity_id, indexed_at); +-- สร้าง Index สำหรับ Cleanup Job +CREATE INDEX idx_attachments_temp_cleanup ON attachments(is_temporary, expires_at); +CREATE INDEX idx_attachments_temp_id ON attachments(temp_id); +CREATE INDEX idx_audit_request_id ON audit_logs(request_id); +-- ===================================================== +-- SQL Script for LCBP3-DMS (V1.4.0) - MariaDB +-- Generated from Data Dictionary +-- ===================================================== +-- ===================================================== +-- 11. 📊 Views & Procedures (วิว และ โปรซีเดอร์) +-- ===================================================== +-- View แสดง Revision "ปัจจุบัน" ของ correspondences ทั้งหมด (ที่ไม่ใช่ RFA) +CREATE VIEW v_current_correspondences AS +SELECT c.id AS correspondence_id, + c.correspondence_number, + c.correspondence_type_id, + ct.type_code AS correspondence_type_code, + ct.type_name AS correspondence_type_name, + c.project_id, + p.project_code, + p.project_name, + c.originator_id, + org.organization_code AS originator_code, + org.organization_name AS originator_name, + cr.id AS revision_id, + cr.revision_number, + cr.revision_label, + cr.title, + cr.document_date, + cr.issued_date, + cr.received_date, + cr.due_date, + cr.correspondence_status_id, + cs.status_code, + cs.status_name, + cr.created_by, + u.username AS created_by_username, + cr.created_at AS revision_created_at +FROM correspondences c + INNER JOIN correspondence_types ct ON c.correspondence_type_id = ct.id + INNER JOIN projects p ON c.project_id = p.id + LEFT JOIN organizations org ON c.originator_id = org.id + INNER JOIN correspondence_revisions cr ON c.id = cr.correspondence_id + INNER JOIN correspondence_status cs ON cr.correspondence_status_id = cs.id + LEFT JOIN users u ON cr.created_by = u.user_id +WHERE cr.is_current = TRUE + AND c.correspondence_type_id NOT IN ( + SELECT id + FROM correspondence_types + WHERE type_code = 'RFA' + ) + AND c.deleted_at IS NULL; +-- View แสดง Revision "ปัจจุบัน" ของ rfa_revisions ทั้งหมด +CREATE VIEW v_current_rfas AS +SELECT r.id AS rfa_id, + r.rfa_type_id, + rt.type_code AS rfa_type_code, + rt.type_name_th AS rfa_type_name_th, + rt.type_name_en AS rfa_type_name_en, + rr.correspondence_id, + c.correspondence_number, + c.project_id, + p.project_code, + p.project_name, + c.originator_id, + org.organization_name AS originator_name, + rr.id AS revision_id, + rr.revision_number, + rr.revision_label, + rr.title, + rr.document_date, + rr.issued_date, + rr.received_date, + rr.approved_date, + rr.rfa_status_code_id, + rsc.status_code AS rfa_status_code, + rsc.status_name AS rfa_status_name, + rr.rfa_approve_code_id, + rac.approve_code AS rfa_approve_code, + rac.approve_name AS rfa_approve_name, + rr.created_by, + u.username AS created_by_username, + rr.created_at AS revision_created_at +FROM rfas r + INNER JOIN rfa_types rt ON r.rfa_type_id = rt.id + INNER JOIN rfa_revisions rr ON r.id = rr.rfa_id + INNER JOIN correspondences c ON rr.correspondence_id = c.id + INNER JOIN projects p ON c.project_id = p.id + INNER JOIN organizations org ON c.originator_id = org.id + INNER JOIN rfa_status_codes rsc ON rr.rfa_status_code_id = rsc.id + LEFT JOIN rfa_approve_codes rac ON rr.rfa_approve_code_id = rac.id + LEFT JOIN users u ON rr.created_by = u.user_id +WHERE rr.is_current = TRUE + AND r.deleted_at IS NULL + AND c.deleted_at IS NULL; +-- View แสดงความสัมพันธ์ทั้งหมดระหว่าง Contract, Project, และ Organization +CREATE VIEW v_contract_parties_all AS +SELECT c.id AS contract_id, + c.contract_code, + c.contract_name, + p.id AS project_id, + p.project_code, + p.project_name, + o.id AS organization_id, + o.organization_code, + o.organization_name, + co.role_in_contract +FROM contracts c + INNER JOIN projects p ON c.project_id = p.id + INNER JOIN contract_organizations co ON c.id = co.contract_id + INNER JOIN organizations o ON co.organization_id = o.id +WHERE c.is_active = TRUE; +-- ============================================================ +-- View: v_user_tasks (Unified Workflow Engine Edition) +-- ============================================================ +-- หน้าที่: รวมรายการงานที่ยังค้างอยู่ (Status = ACTIVE) จากทุกระบบ (RFA, Circulation, Correspondence) +-- เพื่อนำไปแสดงในหน้า Dashboard "My Tasks" +-- ============================================================ +CREATE OR REPLACE VIEW v_user_tasks AS +SELECT -- 1. Workflow Instance Info + wi.id AS instance_id, + wd.workflow_code, + wi.current_state, + wi.status AS workflow_status, + wi.created_at AS assigned_at, + -- 2. Entity Info (Polymorphic Identity) + wi.entity_type, + wi.entity_id, + -- 3. Normalized Document Info (ดึงข้อมูลจริงจากตารางลูกตามประเภท) + -- ใช้ CASE WHEN เพื่อรวมคอลัมน์ที่ชื่อต่างกันให้เป็นชื่อกลาง (document_number, subject) + CASE + WHEN wi.entity_type = 'rfa_revision' THEN rfa_corr.correspondence_number + WHEN wi.entity_type = 'circulation' THEN circ.circulation_no + WHEN wi.entity_type = 'correspondence_revision' THEN corr_corr.correspondence_number + ELSE 'N/A' + END AS document_number, + CASE + WHEN wi.entity_type = 'rfa_revision' THEN rfa_rev.title + WHEN wi.entity_type = 'circulation' THEN circ.circulation_subject + WHEN wi.entity_type = 'correspondence_revision' THEN corr_rev.title + ELSE 'Unknown Document' + END AS subject, + -- 4. Context Info (สำหรับ Filter สิทธิ์การมองเห็นที่ Backend) + -- ดึงเป็น JSON String เพื่อให้ Backend ไป Parse หรือใช้ JSON_CONTAINS + JSON_UNQUOTE(JSON_EXTRACT(wi.context, '$.ownerId')) AS owner_id, + JSON_EXTRACT(wi.context, '$.assigneeIds') AS assignee_ids_json +FROM workflow_instances wi + JOIN workflow_definitions wd ON wi.definition_id = wd.id -- 5. Joins for RFA (ซับซ้อนหน่อยเพราะ RFA ผูกกับ Correspondence อีกที) + LEFT JOIN rfa_revisions rfa_rev ON wi.entity_type = 'rfa_revision' + AND wi.entity_id = CAST(rfa_rev.id AS CHAR) + LEFT JOIN correspondences rfa_corr ON rfa_rev.correspondence_id = rfa_corr.id -- 6. Joins for Circulation + LEFT JOIN circulations circ ON wi.entity_type = 'circulation' + AND wi.entity_id = CAST(circ.id AS CHAR) -- 7. Joins for Correspondence + LEFT JOIN correspondence_revisions corr_rev ON wi.entity_type = 'correspondence_revision' + AND wi.entity_id = CAST(corr_rev.id AS CHAR) + LEFT JOIN correspondences corr_corr ON corr_rev.correspondence_id = corr_corr.id -- 8. Filter เฉพาะงานที่ยัง Active อยู่ +WHERE wi.status = 'ACTIVE'; +-- View แสดง audit_logs พร้อมข้อมูล username และ email ของผู้กระทำ +CREATE VIEW v_audit_log_details AS +SELECT al.audit_id, + al.user_id, + u.username, + u.email, + u.first_name, + u.last_name, + al.action, + al.entity_type, + al.entity_id, + al.details_json, + al.ip_address, + al.user_agent, + al.created_at +FROM audit_logs al + LEFT JOIN users u ON al.user_id = u.user_id; +-- View รวมสิทธิ์ทั้งหมด (Global + Project) ของผู้ใช้ทุกคน +CREATE VIEW v_user_all_permissions AS -- Global Permissions +SELECT ua.user_id, + ua.role_id, + r.role_name, + rp.permission_id, + p.permission_name, + p.module, + p.scope_level, + ua.organization_id, + NULL AS project_id, + NULL AS contract_id, + 'GLOBAL' AS permission_scope +FROM user_assignments ua + INNER JOIN roles r ON ua.role_id = r.role_id + INNER JOIN role_permissions rp ON ua.role_id = rp.role_id + INNER JOIN permissions p ON rp.permission_id = p.permission_id -- Global scope +WHERE p.is_active = 1 + AND ua.organization_id IS NULL + AND ua.project_id IS NULL + AND ua.contract_id IS NULL +UNION ALL +-- Organization-specific Permissions +SELECT ua.user_id, + ua.role_id, + r.role_name, + rp.permission_id, + p.permission_name, + p.module, + p.scope_level, + ua.organization_id, + NULL AS project_id, + NULL AS contract_id, + 'ORGANIZATION' AS permission_scope +FROM user_assignments ua + INNER JOIN roles r ON ua.role_id = r.role_id + INNER JOIN role_permissions rp ON ua.role_id = rp.role_id + INNER JOIN permissions p ON rp.permission_id = p.permission_id -- Organization scope +WHERE p.is_active = 1 + AND ua.organization_id IS NOT NULL + AND ua.project_id IS NULL + AND ua.contract_id IS NULL +UNION ALL +-- Project-specific Permissions +SELECT ua.user_id, + ua.role_id, + r.role_name, + rp.permission_id, + p.permission_name, + p.module, + p.scope_level, + ua.organization_id, + ua.project_id, + NULL AS contract_id, + 'PROJECT' AS permission_scope +FROM user_assignments ua + INNER JOIN roles r ON ua.role_id = r.role_id + INNER JOIN role_permissions rp ON ua.role_id = rp.role_id + INNER JOIN permissions p ON rp.permission_id = p.permission_id -- Project scope +WHERE p.is_active = 1 + AND ua.project_id IS NOT NULL + AND ua.contract_id IS NULL +UNION ALL +-- Contract-specific Permissions +SELECT ua.user_id, + ua.role_id, + r.role_name, + rp.permission_id, + p.permission_name, + p.module, + p.scope_level, + ua.organization_id, + ua.project_id, + ua.contract_id, + 'CONTRACT' AS permission_scope +FROM user_assignments ua + INNER JOIN roles r ON ua.role_id = r.role_id + INNER JOIN role_permissions rp ON ua.role_id = rp.role_id + INNER JOIN permissions p ON rp.permission_id = p.permission_id -- Contract scope +WHERE p.is_active = 1 + AND ua.contract_id IS NOT NULL; +-- ===================================================== +-- Additional Useful Views +-- ===================================================== +-- View แสดงเอกสารทั้งหมดที่มีไฟล์แนบ +CREATE VIEW v_documents_with_attachments AS +SELECT 'CORRESPONDENCE' AS document_type, + c.id AS document_id, + c.correspondence_number AS document_number, + c.project_id, + p.project_code, + p.project_name, + COUNT(ca.attachment_id) AS attachment_count, + MAX(a.created_at) AS latest_attachment_date +FROM correspondences c + INNER JOIN projects p ON c.project_id = p.id + LEFT JOIN correspondence_attachments ca ON c.id = ca.correspondence_id + LEFT JOIN attachments a ON ca.attachment_id = a.id +WHERE c.deleted_at IS NULL +GROUP BY c.id, + c.correspondence_number, + c.project_id, + p.project_code, + p.project_name +UNION ALL +SELECT 'CIRCULATION' AS document_type, + circ.id AS document_id, + circ.circulation_no AS document_number, + corr.project_id, + p.project_code, + p.project_name, + COUNT(ca.attachment_id) AS attachment_count, + MAX(a.created_at) AS latest_attachment_date +FROM circulations circ + INNER JOIN correspondences corr ON circ.correspondence_id = corr.id + INNER JOIN projects p ON corr.project_id = p.id + LEFT JOIN circulation_attachments ca ON circ.id = ca.circulation_id + LEFT JOIN attachments a ON ca.attachment_id = a.id +GROUP BY circ.id, + circ.circulation_no, + corr.project_id, + p.project_code, + p.project_name +UNION ALL +SELECT 'SHOP_DRAWING' AS document_type, + sdr.id AS document_id, + sd.drawing_number AS document_number, + sd.project_id, + p.project_code, + p.project_name, + COUNT(sdra.attachment_id) AS attachment_count, + MAX(a.created_at) AS latest_attachment_date +FROM shop_drawing_revisions sdr + INNER JOIN shop_drawings sd ON sdr.shop_drawing_id = sd.id + INNER JOIN projects p ON sd.project_id = p.id + LEFT JOIN shop_drawing_revision_attachments sdra ON sdr.id = sdra.shop_drawing_revision_id + LEFT JOIN attachments a ON sdra.attachment_id = a.id +WHERE sd.deleted_at IS NULL +GROUP BY sdr.id, + sd.drawing_number, + sd.project_id, + p.project_code, + p.project_name +UNION ALL +SELECT 'CONTRACT_DRAWING' AS document_type, + cd.id AS document_id, + cd.condwg_no AS document_number, + cd.project_id, + p.project_code, + p.project_name, + COUNT(cda.attachment_id) AS attachment_count, + MAX(a.created_at) AS latest_attachment_date +FROM contract_drawings cd + INNER JOIN projects p ON cd.project_id = p.id + LEFT JOIN contract_drawing_attachments cda ON cd.id = cda.contract_drawing_id + LEFT JOIN attachments a ON cda.attachment_id = a.id +WHERE cd.deleted_at IS NULL +GROUP BY cd.id, + cd.condwg_no, + cd.project_id, + p.project_code, + p.project_name; +-- View แสดงสถิติเอกสารตามประเภทและสถานะ +CREATE VIEW v_document_statistics AS +SELECT p.id AS project_id, + p.project_code, + p.project_name, + ct.id AS correspondence_type_id, + ct.type_code, + ct.type_name, + cs.id AS status_id, + cs.status_code, + cs.status_name, + COUNT(DISTINCT c.id) AS document_count, + COUNT(DISTINCT cr.id) AS revision_count +FROM projects p + CROSS JOIN correspondence_types ct + CROSS JOIN correspondence_status cs + LEFT JOIN correspondences c ON p.id = c.project_id + AND ct.id = c.correspondence_type_id + LEFT JOIN correspondence_revisions cr ON c.id = cr.correspondence_id + AND cs.id = cr.correspondence_status_id + AND cr.is_current = TRUE +WHERE p.is_active = 1 + AND ct.is_active = 1 + AND cs.is_active = 1 +GROUP BY p.id, + p.project_code, + p.project_name, + ct.id, + ct.type_code, + ct.type_name, + cs.id, + cs.status_code, + cs.status_name; +-- ===================================================== +-- Indexes for View Performance Optimization +-- ===================================================== +-- Indexes for v_current_correspondences performance +CREATE INDEX idx_correspondences_type_project ON correspondences(correspondence_type_id, project_id); +CREATE INDEX idx_corr_revisions_current_status ON correspondence_revisions(is_current, correspondence_status_id); +CREATE INDEX idx_corr_revisions_correspondence_current ON correspondence_revisions(correspondence_id, is_current); +-- Indexes for v_current_rfas performance +CREATE INDEX idx_rfa_revisions_current_status ON rfa_revisions(is_current, rfa_status_code_id); +CREATE INDEX idx_rfa_revisions_rfa_current ON rfa_revisions(rfa_id, is_current); +-- Indexes for document statistics performance +CREATE INDEX idx_correspondences_project_type ON correspondences(project_id, correspondence_type_id); +CREATE INDEX idx_corr_revisions_status_current ON correspondence_revisions(correspondence_status_id, is_current); +SET FOREIGN_KEY_CHECKS = 1; diff --git a/docs/8_lcbp3_v1_5_1_seed.sql b/docs/8_lcbp3_v1_5_1_seed.sql new file mode 100644 index 0000000..1f967d1 --- /dev/null +++ b/docs/8_lcbp3_v1_5_1_seed.sql @@ -0,0 +1,1748 @@ +INSERT INTO organizations (id, organization_code, organization_name) +VALUES + ( 1, 'กทท.' , 'การท่าเรือแห่งประเทศไทย'), + ( 10, 'สคฉ.3' , 'โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3' ), + ( 11, 'สคฉ.3-01' , 'ตรวจรับพัสดุ ที่ปรึกษาควบคุมงาน'), + ( 12, 'สคฉ.3-02' , 'ตรวจรับพัสดุ งานทางทะเล'), + ( 13, 'สคฉ.3-03' , 'ตรวจรับพัสดุ อาคารและระบบสาธารณูปโภค'), + ( 14, 'สคฉ.3-04' , 'ตรวจรับพัสดุ ตรวจสอบผลกระทบสิ่งแวดล้อม'), + ( 15, 'สคฉ.3-05' , 'ตรวจรับพัสดุ เยียวยาการประมง'), + ( 16, 'สคฉ.3-06' , 'ตรวจรับพัสดุ งานก่อสร้าง ส่วนที่ 3'), + ( 17, 'สคฉ.3-07' , 'ตรวจรับพัสดุ งานก่อสร้าง ส่วนที่ 4'), + ( 18, 'สคฉ.3-xx' , 'ตรวจรับพัสดุ ที่ปรึกษาออกแบบ ส่วนที่ 4'), + ( 21, 'TEAM' , 'Designer Consulting Ltd.'), + ( 22, 'คคง.' , 'Construction Supervision Ltd.'), + ( 41, 'ผรม.1' , 'Contractor งานทางทะเล'), + ( 42, 'ผรม.2' , 'Contractor อาคารและระบบ'), + ( 43, 'ผรม.3' , 'Contractor #3 Ltd.'), + ( 44, 'ผรม.4' , 'Contractor #4 Ltd.'), + ( 31, 'EN' , 'Third Party Environment'), + ( 32, 'CAR' , 'Third Party Fishery Care'); +-- Seed project +INSERT INTO projects (project_code, project_name) +VALUES + ( 'LCBP3' , 'โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 1-4)' ), + ( 'LCBP3-C1' , 'โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 1) งานก่อสร้างงานทางทะเล' ), + ( 'LCBP3-C2' , 'โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 2) งานก่อสร้างอาคาร ท่าเทียบเรือ ระบบถนน และระบบสาธารณูปโภค' ), + ( 'LCBP3-C3' , 'โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 3) งานก่อสร้าง' ), + ( 'LCBP3-C4' , 'โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 4) งานก่อสร้าง' ), + ( 'LCBP3-EN' , 'โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 4) งานก่อสร้าง' ); +-- Seed contract +-- ใช้ Subquery เพื่อดึง project_id มาเชื่อมโยง ทำให้ไม่ต้องมานั่งจัดการ ID ด้วยตัวเอง +INSERT INTO contracts ( contract_code , contract_name , project_id , is_active ) VALUES + ( 'LCBP3-DS' , 'งานจ้างที่ปรีกษาออกแบบ โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 1-4)' , + ( SELECT id + FROM projects + WHERE project_code = 'LCBP3' + ), true ), + ( 'LCBP3-PS' , 'งานจ้างที่ปรีกษาควบคุมงาน โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 1-4)' , + ( SELECT id + FROM projects + WHERE project_code = 'LCBP3' + ), true ), + ( 'LCBP3-C1' , 'งานก่อสร้าง โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 1) งานก่อสร้างงานทางทะเล' , + ( SELECT id + FROM projects + WHERE project_code = 'LCBP3-C1' + ), true ), + ( 'LCBP3-C2' , 'งานก่อสร้าง โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 2) งานก่อสร้างอาคาร ท่าเทียบเรือ ระบบถนน และระบบสาธารณูปโภค' , + ( SELECT id + FROM projects + WHERE project_code = 'LCBP3-C2' + ), true ), + ( 'LCBP3-C3' , 'งานก่อสร้าง โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 3) งานก่อสร้าง' , + ( SELECT id + FROM projects + WHERE project_code = 'LCBP3-C3' + ), true ), + ( 'LCBP3-C4' , 'งานก่อสร้าง โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 4) งานก่อสร้าง' , + ( SELECT id + FROM projects + WHERE project_code = 'LCBP3-C4' + ), true ), + ( 'LCBP3-EN' , 'งานจ้างเหมาตรวจสอบผลกระทบสิ่งแวดล้อมนะหว่างงานก่อสร้างโครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 1-4)' , + ( SELECT id + FROM projects + WHERE project_code = 'LCBP3' + ), true ); +-- Seed user +-- Initial SUPER_ADMIN user +INSERT INTO users ( `user_id`, `username`, `password_hash`, `first_name`, `last_name`, `email`, `line_id`, `primary_organization_id` ) VALUES + ( 1, 'superadmin' , '$2b$10$E6d5k.f46jr.POGWKHhiQ.X1ZsFrMpZox//sCxeOiLUULGuAHO0NW' , 'Super' , 'Admin' , 'superadmin @example.com' , null, null ), + ( 2, 'admin' , '$2b$10$E6d5k.f46jr.POGWKHhiQ.X1ZsFrMpZox//sCxeOiLUULGuAHO0NW' , 'Admin' , 'คคง.' , 'admin@example.com' , null, 1 ), + ( 3, 'editor01' , '$2b$10$E6d5k.f46jr.POGWKHhiQ.X1ZsFrMpZox//sCxeOiLUULGuAHO0NW' , 'DC' , 'C1' , 'editor01 @example.com' , null, 41 ), + ( 4, 'viewer01' , '$2b$10$E6d5k.f46jr.POGWKHhiQ.X1ZsFrMpZox//sCxeOiLUULGuAHO0NW' , 'Viewer' , 'สคฉ.03' , 'viewer01 @example.com' , null, 10 ); +-- ========================================================== +-- Seed Roles (บทบาทพื้นฐาน 5 บทบาท ตาม Req 4.3) +-- ========================================================== +-- 1. Superadmin (Global) +INSERT INTO roles ( role_id, role_name, scope, description ) VALUES + ( 1, 'Superadmin' , 'Global' , 'ผู้ดูแลระบบสูงสุด: สามารถทำทุกอย่างในระบบ, จัดการองค์กร, และจัดการข้อมูลหลักระดับ Global' ). +-- 2. Org Admin (Organization) + ( 2, 'Org Admin' , 'Organization' , 'ผู้ดูแลองค์กร: จัดการผู้ใช้ในองค์กร, จัดการบทบาท / สิทธิ์ภายในองค์กร, และดูรายงานขององค์กร' ), +-- 3. Document Control (Organization) + ( 3, 'Document Control' , 'Organization' , 'ควบคุมเอกสารขององค์กร: เพิ่ม / แก้ไข / ลบเอกสาร, และกำหนดสิทธิ์เอกสารภายในองค์กร' ), +-- 4. Editor (Organization) + ( 4, 'Editor' , 'Organization' , 'ผู้แก้ไขเอกสารขององค์กร: เพิ่ม / แก้ไขเอกสารที่ได้รับมอบหมาย' ), +-- 5. Viewer (Organization) + ( 5, 'Viewer' , 'Organization' , 'ผู้ดูเอกสารขององค์กร: ดูเอกสารที่มีสิทธิ์เข้าถึงเท่านั้น' ), +-- 6. Project Manager (Project) + ( 6, 'Project Manager' , 'Project' , 'ผู้จัดการโครงการ: จัดการสมาชิกในโครงการ, สร้าง / จัดการสัญญาในโครงการ, และดูรายงานโครงการ' ), +-- 7. Contract Admin (Contract) + ( 7, 'Contract Admin' , 'Contract' , 'ผู้ดูแลสัญญา: จัดการสมาชิกในสัญญา, สร้าง / จัดการข้อมูลหลักเฉพาะสัญญา, และอนุมัติเอกสารในสัญญา' ); + +-- ===================================================== +-- 2. Seed Permissions (สิทธิ์การใช้งานทั้งหมด) +-- สิทธิ์ระดับระบบและการจัดการหลัก (System & Master Data) +-- ===================================================== +INSERT INTO permissions ( permission_id, permission_name, description ) VALUES + ( 1, 'system.manage_all' , 'ทำทุกอย่างในระบบ (Superadmin Power)' ), + -- การจัดการองค์กร + ( 2, 'organization.create' , 'สร้างองค์กรใหม่' ), + ( 3, 'organization.edit' , 'แก้ไขข้อมูลองค์กร' ), + ( 4, 'organization.delete' , 'ลบองค์กร' ), + ( 5, 'organization.view' , 'ดูรายการองค์กร' ), + -- การจัดการโครงการ + ( 6, 'project.create' , 'สร้างโครงการใหม่' ), + ( 7, 'project.edit' , 'แก้ไขข้อมูลโครงการ' ), + ( 8, 'project.delete' , 'ลบโครงการ' ), + ( 9, 'project.view' , 'ดูรายการโครงการ' ), + -- การจัดการบทบาทและสิทธิ์ (Roles & Permissions) + ( 10, 'role.create' , 'สร้างบทบาท (Role) ใหม่' ), + ( 11, 'role.edit' , 'แก้ไขบทบาท (Role)' ), + ( 12, 'role.delete' , 'ลบบทบาท (Role)' ), + ( 13, 'permission.assign' , 'มอบสิทธิ์ให้กับบทบาท (Role)' ), + -- การจัดการข้อมูลหลัก (Master Data) + ( 14, 'master_data.document_type.manage' , 'จัดการประเภทเอกสาร (Document Types)' ), + ( 15, 'master_data.document_status.manage' , 'จัดการสถานะเอกสาร (Document Statuses)' ), + ( 16, 'master_data.drawing_category.manage' , 'จัดการหมวดหมู่แบบ (Drawing Categories)' ), + ( 17, 'master_data.tag.manage' , 'จัดการ Tags' ), + -- การจัดการผู้ใช้งาน + ( 18, 'user.create' , 'สร้างผู้ใช้งานใหม่' ), + ( 19, 'user.edit' , 'แก้ไขข้อมูลผู้ใช้งาน' ), + ( 20, 'user.delete' , 'ลบ / ปิดการใช้งานผู้ใช้' ), + ( 21, 'user.view' , 'ดูข้อมูลผู้ใช้งาน' ), + ( 22, 'user.assign_organization' , 'มอบผู้ใช้งานให้กับองค์กร' ); +-- ===================================================== +-- == 2. สิทธิ์การจัดการโครงการและสัญญา (Project & Contract) == +-- ===================================================== +INSERT INTO permissions ( permission_id, permission_name, description ) VALUES + ( 23, 'project.manage_members' , 'จัดการสมาชิกในโครงการ (เชิญ / ถอดสมาชิก)' ), + ( 24, 'project.create_contracts' , 'สร้างสัญญาในโครงการ' ), + ( 25, 'project.manage_contracts' , 'จัดการสัญญาในโครงการ' ), + ( 26, 'project.view_reports' , 'ดูรายงานระดับโครงการ' ), + ( 27, 'contract.manage_members' , 'จัดการสมาชิกในสัญญา' ), + ( 28, 'contract.view' , 'ดูข้อมูลสัญญา' ); +-- ===================================================== +-- == 3. สิทธิ์การจัดการเอกสาร (Document Management) == +-- ===================================================== +-- สิทธิ์ทั่วไปสำหรับเอกสารทุกประเภท +INSERT INTO permissions ( permission_id, permission_name, description ) VALUES + ( 29, 'document.create_draft' , 'สร้างเอกสารในสถานะฉบับร่าง (Draft) ' ), + ( 30, 'document.submit' , 'ส่งเอกสาร (Submitted)' ), + ( 31, 'document.view' , 'ดูเอกสาร' ), + ( 32, 'document.edit' , 'แก้ไขเอกสาร (ทั่วไป)' ), + ( 33, 'document.admin_edit' , 'แก้ไข / ถอน / ยกเลิกเอกสารที่ส่งแล้ว (Admin Power) ' ), + ( 34, 'document.delete' , 'ลบเอกสาร' ), + ( 35, 'document.attach' , 'จัดการไฟล์แนบ (อัปโหลด / ลบ) ' ), + -- สิทธิ์เฉพาะสำหรับ Correspondence + ( 36, 'correspondence.create' , 'สร้างเอกสารโต้ตอบ (Correspondence) ' ), + -- สิทธิ์เฉพาะสำหรับ Request for Approval (RFA) + ( 37, 'rfa.create' , 'สร้างเอกสารขออนุมัติ (RFA)' ), + ( 38, 'rfa.manage_shop_drawings' , 'จัดการข้อมูล Shop Drawing และ Contract Drawing ที่เกี่ยวข้อง' ), + -- สิทธิ์เฉพาะสำหรับ Shop Drawing & Contract Drawing + ( 39, 'drawing.create' , 'สร้าง / แก้ไขข้อมูลแบบ (Shop / Contract Drawing)' ), + -- สิทธิ์เฉพาะสำหรับ Transmittal + ( 40, 'transmittal.create' , 'สร้างเอกสารนำส่ง (Transmittal)' ), + -- สิทธิ์เฉพาะสำหรับ Circulation Sheet (ใบเวียน) + ( 41, 'circulation.create' , 'สร้างใบเวียนเอกสาร (Circulation)' ), + ( 42, 'circulation.respond' , 'ตอบกลับใบเวียน (Main / Action)' ), + ( 43, 'circulation.acknowledge' , 'รับทราบใบเวียน (Information)' ), + ( 44, 'circulation.close' , 'ปิดใบเวียน' ); +-- ===================================================== +-- == 4. สิทธิ์การจัดการ Workflow == +-- ===================================================== +INSERT INTO permissions ( permission_id, permission_name, description ) VALUES + ( 45, 'workflow.action_review' , 'ดำเนินการในขั้นตอนปัจจุบัน (เช่น ตรวจสอบแล้ว)' ), + ( 46, 'workflow.force_proceed' , 'บังคับไปยังขั้นตอนถัดไป (Document Control Power)' ), + ( 47, 'workflow.revert' , 'ย้อนกลับไปยังขั้นตอนก่อนหน้า (Document Control Power)' ); +-- ===================================================== +-- == 5. สิทธิ์ด้านการค้นหาและรายงาน (Search & Reporting) == +-- ===================================================== +INSERT INTO permissions ( permission_id, permission_name, description ) VALUES + ( 48, 'search.advanced' , 'ใช้งานการค้นหาขั้นสูง' ), + ( 49, 'report.generate' , 'สร้างรายงานสรุป (รายวัน / สัปดาห์ / เดือน / ปี)' ); +-- ========================================================== +-- Seed Role-Permissions Mapping (จับคู่สิทธิ์เริ่มต้น) +-- ========================================================== +-- Seed data for the 'role_permissions 'table +-- This table links roles to their specific permissions. +-- NOTE: This assumes the role_id and permission_id FROM the previous seed data files. +-- Superadmin (role_id = 1), Org Admin (role_id = 2), Document Control (role_id = 3), etc. +-- ===================================================== +-- == 1. Superadmin (role_id = 1) - Gets ALL permissions == +-- ===================================================== +-- Superadmin can do everything. We can dynamically link all permissions to this role. +-- This is a robust way to ensure Superadmin always has full power. +INSERT INTO role_permissions (role_id, permission_id) +SELECT 1, + permission_id +FROM permissions; +-- ===================================================== +-- == 2. Org Admin (role_id = 2) == +-- ===================================================== +INSERT INTO role_permissions (role_id, permission_id) +VALUES -- จัดการผู้ใช้ในองค์กร + (2, 18), + -- user.create + (2, 19), + -- user.edit + (2, 20), + -- user.delete + (2, 21), + -- user.view + (2, 22), + -- user.assign_organization + -- จัดการองค์กร + (2, 3), + -- organization.edit + (2, 5), + -- organization.view + -- จัดการข้อมูลหลักที่อนุญาต (เฉพาะ Tags) + (2, 17), + -- master_data.tag.manage + -- ดูข้อมูลต่างๆ ในองค์กร + (2, 31), + -- document.view + (2, 9), + -- project.view + (2, 28), + -- contract.view + -- การค้นหาและรายงาน + (2, 48), + -- search.advanced + (2, 49); +-- report.generate +-- ===================================================== +-- == 3. Document Control (role_id = 3) == +-- ===================================================== +INSERT INTO role_permissions (role_id, permission_id) +VALUES -- สิทธิ์จัดการเอกสารทั้งหมด + (3, 29), + -- document.create_draft + (3, 30), + -- document.submit + (3, 31), + -- document.view + (3, 32), + -- document.edit + (3, 33), + -- document.admin_edit + (3, 34), + -- document.delete + (3, 35), + -- document.attach + -- สิทธิ์สร้างเอกสารแต่ละประเภท + (3, 36), + -- correspondence.create + (3, 37), + -- rfa.create + (3, 39), + -- drawing.create + (3, 40), + -- transmittal.create + (3, 41), + -- circulation.create + -- สิทธิ์จัดการ Workflow + (3, 45), + -- workflow.action_review + (3, 46), + -- workflow.force_proceed + (3, 47), + -- workflow.revert + -- สิทธิ์จัดการ Circulation + (3, 42), + -- circulation.respond + (3, 43), + -- circulation.acknowledge + (3, 44), + -- circulation.close + -- สิทธิ์อื่นๆ ที่จำเป็น + (3, 38), + -- rfa.manage_shop_drawings + (3, 48), + -- search.advanced + (3, 49); +-- report.generate +-- ===================================================== +-- == 4. Editor (role_id = 4) == +-- ===================================================== +INSERT INTO role_permissions (role_id, permission_id) +VALUES -- สิทธิ์แก้ไขเอกสาร (แต่ไม่ใช่สิทธิ์ Admin) + (4, 29), + -- document.create_draft + (4, 30), + -- document.submit + (4, 31), + -- document.view + (4, 32), + -- document.edit + (4, 35), + -- document.attach + -- สิทธิ์สร้างเอกสารแต่ละประเภท + (4, 36), + -- correspondence.create + (4, 37), + -- rfa.create + (4, 39), + -- drawing.create + (4, 40), + -- transmittal.create + (4, 41), + -- circulation.create + -- สิทธิ์อื่นๆ ที่จำเป็น + (4, 38), + -- rfa.manage_shop_drawings + (4, 48); +-- search.advanced +-- ===================================================== +-- == 5. Viewer (role_id = 5) == +-- ===================================================== +INSERT INTO role_permissions (role_id, permission_id) +VALUES -- สิทธิ์ดูเท่านั้น + (5, 31), + -- document.view + (5, 48); +-- search.advanced +-- ===================================================== +-- == 6. Project Manager (role_id = 6) == +-- ===================================================== +INSERT INTO role_permissions (role_id, permission_id) +VALUES -- สิทธิ์จัดการโครงการ + (6, 23), + -- project.manage_members + (6, 24), + -- project.create_contracts + (6, 25), + -- project.manage_contracts + (6, 26), + -- project.view_reports + (6, 9), + -- project.view + -- สิทธิ์จัดการข้อมูลหลักระดับโครงการ + (6, 16), + -- master_data.drawing_category.manage + -- สิทธิ์ดูข้อมูลในสัญญา + (6, 28), + -- contract.view + -- สิทธิ์ในการจัดการเอกสาร (ระดับ Editor) + (6, 29), + -- document.create_draft + (6, 30), + -- document.submit + (6, 31), + -- document.view + (6, 32), + -- document.edit + (6, 35), + -- document.attach + (6, 36), + -- correspondence.create + (6, 37), + -- rfa.create + (6, 39), + -- drawing.create + (6, 40), + -- transmittal.create + (6, 41), + -- circulation.create + (6, 38), + -- rfa.manage_shop_drawings + (6, 48), + -- search.advanced + (6, 49); +-- report.generate +-- ===================================================== +-- == 7. Contract Admin (role_id = 7) == +-- ===================================================== +INSERT INTO role_permissions (role_id, permission_id) +VALUES -- สิทธิ์จัดการสัญญา + (7, 27), + -- contract.manage_members + (7, 28), + -- contract.view + -- สิทธิ์ในการอนุมัติ (ส่วนหนึ่งของ Workflow) + (7, 45), + -- workflow.action_review + -- สิทธิ์จัดการข้อมูลเฉพาะสัญญา + (7, 38), + -- rfa.manage_shop_drawings + (7, 39), + -- drawing.create + -- สิทธิ์ในการจัดการเอกสาร (ระดับ Editor) + (7, 29), + -- document.create_draft + (7, 30), + -- document.submit + (7, 31), + -- document.view + (7, 32), + -- document.edit + (7, 35), + -- document.attach + (7, 36), + -- correspondence.create + (7, 37), + -- rfa.create + (7, 40), + -- transmittal.create + (7, 41), + -- circulation.create + (7, 48); +-- Seed data for the 'user_assignments' table +INSERT INTO `user_assignments` ( `id`, `user_id`, `role_id`, `organization_id`, `project_id`, `contract_id`, `assigned_by_user_id` ) VALUES + ( 1, 1, 1, null, null, null, null ), ( 2, 2, 2, 1, null, null, null ); +-- ===================================================== +-- == 4. การเชื่อมโยงโครงการกับองค์กร (project_organizations) == +-- ===================================================== +-- โครงการหลัก (LCBP3) จะมีองค์กรหลักๆ เข้ามาเกี่ยวข้องทั้งหมด +INSERT INTO project_organizations (project_id, organization_id) +SELECT ( + SELECT id + FROM projects + WHERE project_code = 'LCBP3 ' ), + id +FROM organizations +WHERE organization_code in ( 'กทท.' , 'สคฉ.3' , 'TEAM' , 'คคง.' , 'ผรม.1' , 'ผรม.2' , 'ผรม.3' , 'ผรม.4' , 'EN' , 'CAR ' ); +-- โครงการย่อย (LCBP3C1) จะมีเฉพาะองค์กรที่เกี่ยวข้อง +INSERT INTO project_organizations (project_id, organization_id) +SELECT ( + SELECT id + FROM projects + WHERE project_code = 'LCBP3-C1 ' ), + id +FROM organizations +WHERE organization_code in ( 'กทท.' , 'สคฉ.3' , 'สคฉ.3 -02' , 'คคง.' , 'ผรม.1 ' ); +-- ทำเช่นเดียวกันสำหรับโครงการอื่นๆ (ตัวอย่าง) +INSERT INTO project_organizations (project_id, organization_id) +SELECT ( + SELECT id + FROM projects + WHERE project_code = 'LCBP3-C2 ' ), + id +FROM organizations +WHERE organization_code in ( 'กทท.' , 'สคฉ.3' , 'สคฉ.3 -03' , 'คคง.' , 'ผรม.2 ' ); +-- ===================================================== +-- == 5. การเชื่อมโยงสัญญากับองค์กร (contract_organizations) == +-- ===================================================== +-- สัญญาที่ปรึกษาออกแบบ (DSLCBP3) +INSERT INTO contract_organizations ( contract_id, organization_id, role_in_contract ) VALUES + ( + ( SELECT id + FROM contracts + WHERE contract_code = 'LCBP3-DS' + ), + ( SELECT id + FROM organizations + WHERE organization_code = 'กทท.' + ), 'Owner' ), + (( + SELECT id + FROM contracts + WHERE contract_code = 'LCBP3-DS' + ), + ( SELECT id + FROM organizations + WHERE organization_code = 'TEAM' + ), 'Designer' ); +-- สัญญาที่ปรึกษาควบคุมงาน (PSLCBP3) +INSERT INTO contract_organizations ( contract_id, organization_id, role_in_contract ) VALUES + ( + ( SELECT id + FROM contracts + WHERE contract_code = 'LCBP3-PS' + ), + ( SELECT id + FROM organizations + WHERE organization_code = 'กทท.' + ), 'Owner' ), + (( + SELECT id + FROM contracts + WHERE contract_code = 'LCBP3-PS' + ), + ( SELECT id + FROM organizations + WHERE organization_code = 'คคง.' + ), 'Consultant' ); +-- สัญญางานก่อสร้าง ส่วนที่ 1 (LCBP3-C1) +INSERT INTO contract_organizations ( contract_id, organization_id, role_in_contract ) VALUES + ( + ( SELECT id + FROM contracts + WHERE contract_code = 'LCBP3-C1' + ), + ( SELECT id + FROM organizations + WHERE organization_code = 'กทท.' + ), 'Owner' ), + (( + SELECT id + FROM contracts + WHERE contract_code = 'LCBP3-C1' + ), + ( SELECT id + FROM organizations + WHERE organization_code = 'ผรม.1' + ), 'Contractor' ); +-- สัญญางานก่อสร้าง ส่วนที่ 2 (LCBP3-C2) +INSERT INTO contract_organizations ( contract_id, organization_id, role_in_contract ) VALUES + ( + ( SELECT id + FROM contracts + WHERE contract_code = 'LCBP3-C2' + ), + ( SELECT id + FROM organizations + WHERE organization_code = 'กทท.' + ), 'Owner' ), + (( + SELECT id + FROM contracts + WHERE contract_code = 'LCBP3-C2' + ), + ( SELECT id + FROM organizations + WHERE organization_code = 'ผรม.2' + ), 'Contractor' ); +-- สัญญาตรวจสอบสิ่งแวดล้อม (LCBP3-EN) +INSERT INTO contract_organizations ( contract_id, organization_id, role_in_contract ) VALUES + ( + ( SELECT id + FROM contracts + WHERE contract_code = 'LCBP3-EN' + ), + ( SELECT id + FROM organizations + WHERE organization_code = 'กทท.' + ), 'Owner' ), + ( + ( + SELECT id + FROM contracts + WHERE contract_code = 'LCBP3-EN' + ), + ( SELECT id + FROM organizations + WHERE organization_code = 'EN' + ), 'Consultant' ); +-- Seed correspondence_status +INSERT INTO correspondence_status ( status_code, status_name, sort_order, is_active ) VALUES + ( 'DRAFT' , 'Draft' , 10, 1), + ( 'SUBOWN' , 'Submitted to Owner' , 21, 1), + ( 'SUBDSN' , 'Submitted to Designer' , 22, 1), + ( 'SUBCSC' , 'Submitted to CSC' , 23, 1), + ( 'SUBCON' , 'Submitted to Contractor' , 24, 1), + ( 'SUBOTH' , 'Submitted to Others' , 25, 1), + ( 'REPOWN' , 'Reply by Owner' , 31, 1), + ( 'REPDSN' , 'Reply by Designer' , 32, 1 ), + ( 'REPCSC' , 'Reply by CSC' , 33, 1 ), + ( 'REPCON' , 'Reply by Contractor' , 34, 1 ), + ( 'REPOTH' , 'Reply by Others' , 35, 1 ), + ( 'RSBOWN' , 'Resubmited by Owner' , 41, 1 ), + ( 'RSBDSN' , 'Resubmited by Designer' , 42, 1 ), + ( 'RSBCSC' , 'Resubmited by CSC' , 43, 1 ), + ( 'RSBCON' , 'Resubmited by Contractor' , 44, 1 ), + ( 'CLBOWN' , 'Closed by Owner' , 51, 1 ), + ( 'CLBDSN' , 'Closed by Designer' , 52, 1 ), + ( 'CLBCSC' , 'Closed by CSC' , 53, 1 ), + ( 'CLBCON' , 'Closed by Contractor' , 54, 1 ), + ( 'CCBOWN' , 'Canceled by Owner' , 91, 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 ), + ( 'RFI' , 'Request for Information' , 2, 1 ), + ( 'TRANSMITTAL' , 'Transmittal' , 3, 1 ), + ( 'EMAIL' , 'Email' , 4, 1 ), + ( 'INSTRUCTION' , 'Instruction' , 5, 1 ), + ( 'LETTER' , 'Letter' , 6, 1 ), + ( 'MEMO' , 'Memorandum' , 7, 1 ), + ( 'MOM' , 'Minutes of Meeting' , 8, 1 ), + ( 'NOTICE' , 'Notice' , 9, 1 ), + ( 'OTHER' , 'Other' , 10, 1 ); +-- Seed rfa_types +INSERT INTO rfa_types ( contract_id, type_code, type_name_en, type_name_th ) +SELECT id, + 'ADW' , + 'As Built Drawing' , + 'แบบร่างหลังการก่อสร้าง' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'BC' , + 'Box Culvert' , + 'ท่อระบายน้ำรูปกล่อง' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'BM' , + 'Benchmark' , + 'หมุดหลักฐาน' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'CER' , + 'Certificates' , + 'ใบรับรอง' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'CN' , + 'Canal Drainage' , + 'ระบบระบายน้ำในคลอง' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'CON' , + 'Contract' , + 'สัญญา' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'DDS' , + 'Design Data Submission' , + 'นำส่งข้อมูลการออกแบบ' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'DDW' , + 'Draft Drawing' , + 'แบบร่าง' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'DRW' , + 'Drawings (All Types)' , + 'แบบก่อสร้าง' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'DSN' , + 'Design/Calculation/Manual (All Stages)' , + 'ออกแบบ / คำนวณ / คู่มือ' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'GEN' , + 'General' , + 'ทั่วไป' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'ICR' , + 'Incident Report' , + 'รายงานการเกิดอุบัติเหตุและการบาดเจ็บ' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'INS' , + 'Insurances/Bond/Guarantee' , + 'การประกัน / พันธบัตร / การค้ำประกัน' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'INR' , + 'Inspection/Audit/Surveillance Report' , + 'รายงานการตรวจสอบ / การตรวจสอบ / รายงานการเฝ้าระวัง' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'ITP' , + 'Inspection and Test Plan' , + 'แผนการตรวจสอบและทดสอบ' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'JSA' , + 'Jobs Analysis' , + 'รายงานการวิเคราะห์ความปลอดภัย' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'MAN' , + 'Manual' , + 'คู่มือ' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'MAT' , + 'Materials/Equipment/Plant' , + 'วัสดุ / อุปกรณ์ / โรงงาน' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'MOM' , + 'Minutes of Meeting' , + 'รายงานการประชุม' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'MPR' , + 'Monthly Progress Report' , + 'รายงานความคืบหน้าประจำเดือน' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'MST' , + 'Method Statement for Construction/Installation' , + 'ขั้นตอนการก่อสร้าง / ติดตั้ง' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'NDS' , + 'Non-Design Data Submission' , + 'นำส่งข้อมูลที่ไม่เกี่ยวข้องกับการออกแบบ' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'PMA' , + 'Payment/Invoice/Retention/Estimate' , + 'การชำระเงิน / ใบแจ้งหนี้ / ประกันผลงาน / ประมาณการ' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'PRD' , + 'Procedure' , + 'ระเบียบปฏิบัติ' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'PRG' , + 'Progress of Construction' , + 'ความคืบหน้าของการก่อสร้าง / ภาพถ่าย / วิดีโอ' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'QMS' , + 'Quality Document (Plan/Work Instruction)' , + 'เอกสารด้านคุณภาพ (แผนงาน / ข้อแนะนำในการทำงาน)' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'RPT' , + 'Report' , + 'รายงาน' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'SAR' , + 'Semi Annual Report' , + 'รายงานประจำหกเดือน' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'SCH' , + 'Schedule and Program' , + 'แผนงาน' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'SDW' , + 'Shop Drawing' , + 'แบบขยายรายละเอียด' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'SI' , + 'Soil Investigation' , + 'การตรวจสอบดิน' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'SPE' , + 'Specification' , + 'ข้อกำหนด' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'TNR' , + 'Training Report' , + 'รายงานการฝึกปฏิบัติ' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'UC' , + 'Underground Construction' , + 'โครงสร้างใต้ดิน' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'VEN' , + 'Vendor' , + 'ผู้ขาย' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'VRO' , + 'Variation Request/Instruction/Order' , + 'คำขอเปลี่ยนแปลง / ข้อเสนอแนะ / ข้อเรียกร้อง' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'WTY' , + 'Warranty' , + 'การประกัน' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'GEN' , + 'General' , + 'ทั่วไป' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'CON' , + 'Contract' , + 'สัญญา' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'INS' , + 'Insurances/Bond/Guarantee' , + 'การประกัน / พันธบัตร / การค้ำประกัน' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'SCH' , + 'Schedule and Program' , + 'แผนงาน' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'PMA' , + 'Payment/Invoice/Retention/Estimate' , + 'การชำระเงิน / ใบแจ้งหนี้ / ประกันผลงาน / ประมาณการ' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'VRO' , + 'Variation Request/Instruction/Order' , + 'คำขอเปลี่ยนแปลง / ข้อเสนอแนะ / ข้อเรียกร้อง' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'VEN' , + 'Vendor' , + 'ผู้ขาย' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'WTY' , + 'Warranty' , + 'การประกัน' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'DRW' , + 'Drawings (All Types)' , + 'แบบก่อสร้าง' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'DDW' , + 'Draft Drawing' , + 'แบบร่าง' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'SDW' , + 'Shop Drawing' , + 'แบบขยายรายละเอียด' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'ADW' , + 'As Built Drawing' , + 'แบบร่างหลังการก่อสร้าง' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'DDS' , + 'Design Data Submission' , + 'นำส่งข้อมูลการออกแบบ' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'DSN' , + 'Design/Calculation/Manual (All Stages)' , + 'ออกแบบ / คำนวณ / คู่มือ' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'NDS' , + 'Non-Design Data Submission' , + 'นำส่งข้อมูลที่ไม่เกี่ยวข้องกับการออกแบบ' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'PRD' , + 'Procedure' , + 'ระเบียบปฏิบัติ' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'MST' , + 'Method Statement for Construction/Installation' , + 'ขั้นตอนการก่อสร้าง / ติดตั้ง' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'QMS' , + 'Quality Document (Plan/Work Instruction)' , + 'เอกสารด้านคุณภาพ (แผนงาน / ข้อแนะนำในการทำงาน)' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'INR' , + 'Inspection/Audit/Surveillance Report' , + 'รายงานการตรวจสอบ / การตรวจสอบ / รายงานการเฝ้าระวัง' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'ITP' , + 'Inspection and Test Plan' , + 'แผนการตรวจสอบและทดสอบ' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'MAT' , + 'Materials/Equipment/Plant' , + 'วัสดุ / อุปกรณ์ / โรงงาน' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'SPE' , + 'Specification' , + 'ข้อกำหนด' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'MAN' , + 'Manual' , + 'คู่มือ' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'CER' , + 'Certificates' , + 'ใบรับรอง' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'SAR' , + 'Semi Annual Report' , + 'รายงานประจำหกเดือน' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'JSA' , + 'Jobs Analysis' , + 'รายงานการวิเคราะห์ความปลอดภัย' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'MOM' , + 'Minutes of Meeting' , + 'รายงานการประชุม' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'MPR' , + 'Monthly Progress Report' , + 'รายงานความคืบหน้าประจำเดือน' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'ICR' , + 'Incident Report' , + 'รายงานการเกิดอุบัติเหตุและการบาดเจ็บ' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'PRG' , + 'Progress of Construction' , + 'ความคืบหน้าของการก่อสร้าง / ภาพถ่าย / วิดีโอ' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'RPT' , + 'Report' , + 'รายงาน' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'TNR' , + 'Training Report' , + 'รายงานการฝึกปฏิบัติ' +FROM contracts +WHERE contract_code = 'LCBP3-C2'; +-- Seed rfa_status_codes +INSERT INTO rfa_status_codes ( status_code, status_name, description, sort_order ) VALUES + ( 'DFT' , 'Draft' , 'ฉบับร่าง' , 1), + ( 'FAP' , 'For Approve' , 'เพื่อขออนุมัติ' , 11 ), + ( 'FRE' , 'For Review' , 'เพื่อตรวจสอบ' , 12 ), + ( 'FCO' , 'For Construction' , 'เพื่อก่อสร้าง' , 20 ), + ( 'ASB' , 'AS - Built' , 'แบบก่อสร้างจริง' , 30 ), + ( 'OBS' , 'Obsolete' , 'ไม่ใช้งาน' , 80 ), + ( 'CC' , 'Canceled' , 'ยกเลิก' , 99 ); +INSERT INTO rfa_approve_codes ( approve_code, approve_name, sort_order, is_active ) VALUES + ( '1A' , 'Approved by Authority' , 10, 1 ), + ( '1C' , 'Approved by CSC' , 11, 1 ), + ( '1N' , 'Approved As Note' , 12, 1 ), + ( '1R' , 'Approved with Remarks' , 13, 1 ), + ( '3C' , 'Consultant Comments' , 31, 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) +-- ========================================================== +-- 1. Seed ข้อมูล Disciplines (สาขางาน) +-- LCBP3-C1 +INSERT INTO disciplines ( contract_id, discipline_code, code_name_th, code_name_en ) +SELECT id, + 'GEN' , + 'งานบริหารโครงการ' , + 'General Management' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'COD' , + 'สัญญาและข้อโต้แย้ง' , + 'Contracting' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'QSB' , + 'สำรวจปริมาณและควบคุมงบประมาณ' , + 'Quantity Survey and Budget Control' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'PPG' , + 'บริหารแผนและความก้าวหน้า' , + 'Plan and Progress Management' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'PRC' , + 'งานจัดซื้อ' , + 'Procurement' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'SUB' , + 'ผู้รับเหมาช่วง' , + 'Subcontractor' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'ODC' , + 'สำนักงาน-ควบคุมเอกสาร' , + 'Operation Docment Control' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'LAW' , + 'กฎหมาย' , + 'Law' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'TRF' , + 'จราจร' , + 'Traffic' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'BIM' , + 'BIM' , + 'Building information modeling' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'SRV' , + 'งานสำรวจ' , + 'Survey' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'SFT' , + 'ความปลอดภัย' , + 'Safety' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'BST' , + 'งานโครงสร้างอาคาร' , + 'Building Structure Work' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'TEM' , + 'งานชั่วคราว' , + 'Temporary Work' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'UTL' , + 'งานระบบสาธารณูปโภค' , + 'Utility' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'EPW' , + 'งานระบบไฟฟ้า' , + 'Electrical Power Work' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'ECM' , + 'งานระบบไฟฟ้าสื่อสาร' , + 'Electrical Communication Work' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'ENV' , + 'สิ่งแวดล้อม' , + 'Environment' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'AQV' , + 'คุณภาพอากาศและความสั่นสะเทือน' , + 'Air quality and vibration' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'WAB' , + 'คุณภาพน้ำและชีววิทยาทางน้ำ' , + 'Water quality and Aquatic biology' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'ONS' , + 'วิศวกรรมชายฝั่ง' , + 'Onshore Engineer Work' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'PPR' , + 'มวลชนสัมพันธ์และการประชาสัมพันธ์' , + 'Public Relations' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'OSW' , + 'งานก่อสร้างงานทางทะเล' , + 'Offshore Work' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'DRE' , + 'งานขุดและถมทะเล' , + 'Dredging and Reclamation' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'REV' , + 'งานคันหินล้อมพื้นที่ถมทะเล' , + 'Revetment' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'BRW' , + 'งานเขื่อนกันคลื่น' , + 'Breakwater' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'SOI' , + 'ปรับปรุงคุณภาพดิน' , + 'Soil Improvement' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'BLC' , + 'งานปรับปรุงคลองบางละมุง' , + 'Bang Lamung Canal Bank Protection' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'FUP' , + 'งานประตูระบายน้ำและท่อลอด' , + 'Floodgate & Under Ground Piping Works' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'SWP' , + 'งานอาคารควบคุมสถานีสูบน้ำทะเล' , + 'Sea Water Pumping Station Control BuilDing' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'NAV' , + 'งานติดตั้งเครื่องหมายช่วงการเดินเรือ' , + 'Navigations Aids' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'GEO' , + 'งานด้านธรณีเทคนิค' , + 'Geotechnical' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'CRW' , + 'งานด้านโยธา - Rock Works' , + 'Civil-Rock work' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'DVR' , + 'ทีมนักประดาน้ำ' , + 'Dive Work' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'MTS' , + 'งานทดสอบวัสดุและธรณีเทคนิค' , + 'Materials and Geotechnical Testing' +FROM contracts +WHERE contract_code = 'LCBP3-C1' +union all +SELECT id, + 'OTH' , + 'อื่นๆ' , + 'Other' +FROM contracts +WHERE contract_code = 'LCBP3-C1'; +-- LCBP3-C2 +INSERT INTO disciplines ( contract_id, discipline_code, code_name_th, code_name_en ) +SELECT id, + 'GEN' , + 'งานบริหารโครงการ' , + 'Project Management' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'COD' , + 'สัญญาและข้อโต้แย้ง' , + 'Contracts and arguments' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'QSB' , + 'สำรวจปริมาณและควบคุมงบประมาณ' , + 'Survey the quantity and control the budget' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'PPM' , + 'บริหารแผนและความก้าวหน้า' , + 'Plan Management & Progress' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'ODC' , + 'สำนักงาน-ควบคุมเอกสาร' , + 'Document Control Office' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'LAW' , + 'กฎหมาย' , + 'Law' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'TRF' , + 'จราจร' , + 'Traffic' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'BIM' , + 'Building Information Modeling' , + 'Building Information Modeling' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'SRV' , + 'งานสำรวจ' , + 'Survey' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'SFT' , + 'ความปลอดภัย' , + 'Safety' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'BST' , + 'งานโครงสร้างอาคาร' , + 'Building Structure' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'UTL' , + 'งานะบบสาธารณูปโภค' , + 'Public Utilities' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'EPW' , + 'งานระบบไฟฟ้า' , + 'Electrical Systems' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'ECM' , + 'งานระบบไฟฟ้าสื่อสาร' , + 'Electrical Communication System' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'ENV' , + 'สิ่งแวดล้อม' , + 'Environment' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'AQV' , + 'คุณภาพอากาศและความสั่นสะเทือน' , + 'Air Quality and Vibration' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'WAB' , + 'คุณภาพน้ำและชีววิทยาทางน้ำ' , + 'Water Quality and Aquatic Biology' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'ONS' , + 'วิศวกรรมชายฝั่ง' , + 'Coastal Engineering' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'PPR' , + 'มวลชนสัมพันธ์และประชาสัมพันธ์' , + 'Mass Relations and Public Relations' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'OFW' , + 'งานก่อสร้างทางทะเล' , + 'Marine Construction' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'EXR' , + 'งานขุดและถมทะเล' , + 'Excavation and reclamation' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'GEO' , + 'งานด้านธรณีเทคนิค' , + 'Geotechnical work' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'CRW' , + 'งานด้านโยธา - Rock Works' , + 'Civil Works - Rock Works' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'DVW' , + 'ทีมนักประดาน้ำ' , + 'Team of Divers' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'MTT' , + 'งานทดสอบวัสดุ' , + 'Materials Testing' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'ARC' , + 'งานสถาปัตยกรรม' , + 'Architecture' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'STR' , + 'งานโครงสร้าง' , + 'Structural work' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'SAN' , + 'งานระบบสุขาภิบาล' , + 'Sanitation System' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'DRA' , + 'งานระบบระบายน้ำ' , + 'Drainage system work' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'TER' , + 'งานท่าเทียบเรือ' , + 'Terminal Work work' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'BUD' , + 'งานอาคาร' , + 'Building' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'ROW' , + 'งานถนนและสะพาน' , + 'Road and Bridge Work' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'MEC' , + 'งานเคริองกล' , + 'Mechanical work' +FROM contracts +WHERE contract_code = 'LCBP3-C2' +union all +SELECT id, + 'OTH' , + 'อื่น ๆ' , + '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 +INSERT INTO correspondence_sub_types ( contract_id, correspondence_type_id, sub_type_code, sub_type_name, ) +SELECT c.id, + ct.id, + 'MAT' , + 'Material Approval' , + '11' +FROM contracts c, + correspondence_types ct +WHERE c.contract_code = 'LCBP3-C1' + and ct.type_code = 'RFA' +union all +SELECT c.id, + ct.id, + 'SHP' , + 'Shop Drawing Submittal' , + '12' +FROM contracts c, + correspondence_types ct +WHERE c.contract_code = 'LCBP3-C1' + and ct.type_code = 'RFA' +union all +SELECT c.id, + ct.id, + 'DWG' , + 'Document Approval' , + '13' +FROM contracts c, + correspondence_types ct +WHERE c.contract_code = 'LCBP3-C1' + and ct.type_code = 'RFA' +union all +SELECT c.id, + ct.id, + 'MET' , + 'Engineering Document Submittal' , + '14' +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, correspondence_type_id, sub_type_code, sub_type_name, sub_type_number ) +SELECT c.id, + ct.id, + 'MAT' , + 'Material Approval' , + '21' +FROM contracts c, + correspondence_types ct +WHERE c.contract_code = 'LCBP3-C2' + and ct.type_code = 'RFA' +union all +SELECT c.id, + ct.id, + 'SHP' , + 'Shop Drawing Submittal' , + '22' +FROM contracts c, + correspondence_types ct +WHERE c.contract_code = 'LCBP3-C2' + and ct.type_code = 'RFA' +union all +SELECT c.id, + ct.id, + 'DWG' , + 'Document Approval' , + '23' +FROM contracts c, + correspondence_types ct +WHERE c.contract_code = 'LCBP3-C2' + and ct.type_code = 'RFA' +union all +SELECT c.id, + ct.id, + 'MET' , + 'Engineering Document Submittal' , + '24' +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, correspondence_type_id, sub_type_code, sub_type_name, sub_type_number ) +SELECT c.id, + ct.id, + 'MAT' , + 'Material Approval' , + '31' +FROM contracts c, + correspondence_types ct +WHERE c.contract_code = 'LCBP3-C3' + and ct.type_code = 'RFA' +union all +SELECT c.id, + ct.id, + 'SHP' , + 'Shop Drawing Submittal' , + '32' +FROM contracts c, + correspondence_types ct +WHERE c.contract_code = 'LCBP3-C3' + and ct.type_code = 'RFA' +union all +SELECT c.id, + ct.id, + 'DWG' , + 'Document Approval' , + '33' +FROM contracts c, + correspondence_types ct +WHERE c.contract_code = 'LCBP3-C3' + and ct.type_code = 'RFA' +union all +SELECT c.id, + ct.id, + 'MET' , + 'Engineering Document Submittal' , + '34' +FROM contracts c, + correspondence_types ct +WHERE c.contract_code = 'LCBP3-C4' + and ct.type_code = 'RFA'; +-- Note: 6B data has C4 on the right column for MET but C3 on left, checking logic... MD says C3 for first 3 rows, then C4 mixed. I will assume C4 starts at row 12 in the MD table. +-- LCBP3-C4 +INSERT INTO correspondence_sub_types ( contract_id, correspondence_type_id, sub_type_code, sub_type_name, sub_type_number ) +SELECT c.id, + ct.id, + 'MAT' , + 'Material Approval' , + '41' +FROM contracts c, + correspondence_types ct +WHERE c.contract_code = 'LCBP3-C4' + and ct.type_code = 'RFA' +union all +SELECT c.id, + ct.id, + 'SHP' , + 'Shop Drawing Submittal' , + '42' +FROM contracts c, + correspondence_types ct +WHERE c.contract_code = 'LCBP3-C4' + and ct.type_code = 'RFA' +union all +SELECT c.id, + ct.id, + 'DWG' , + 'Document Approval' , + '43' +FROM contracts c, + correspondence_types ct +WHERE c.contract_code = 'LCBP3-C4' + and ct.type_code = 'RFA' +union all +SELECT c.id, + ct.id, + 'MET' , + 'Engineering Document Submittal' , + '44' +FROM contracts c, + correspondence_types ct +WHERE c.contract_code = 'LCBP3-C4' + and ct.type_code = 'RFA'; diff --git a/lcbp3.code-workspace b/lcbp3.code-workspace index 3aa79d8..df634f4 100644 --- a/lcbp3.code-workspace +++ b/lcbp3.code-workspace @@ -664,7 +664,9 @@ "password": "", "database": "lcbp3" } - ] + ], + "terminal.integrated.copyOnSelection": true, + "terminal.integrated.tabs.defaultColor": "terminal.ansiBlue" }, // ======================================== @@ -927,8 +929,7 @@ "chakrounanas.turbo-console-log", "wallabyjs.console-ninja", "pkief.material-icon-theme", - "github.copilot", - "inferrinizzard.prettier-sql-vscode" + "github.copilot" ] } } diff --git a/specs/00-overview/README.md b/specs/00-overview/README.md index 22889d8..85f8ea1 100644 --- a/specs/00-overview/README.md +++ b/specs/00-overview/README.md @@ -1,9 +1,9 @@ # LCBP3-DMS - Project Overview **Project Name:** Laem Chabang Port Phase 3 - Document Management System (LCBP3-DMS) -**Version:** 1.5.0 +**Version:** 1.5.1 **Status:** Planning & Specification Phase -**Last Updated:** 2025-12-01 +**Last Updated:** 2025-12-02 --- @@ -199,21 +199,24 @@ lcbp3/ ### Documentation -| Category | Document | Description | -| ------------------ | --------------------------------------------------------------------------- | ------------------------------------- | -| **Overview** | [Glossary](./glossary.md) | Technical terminology & abbreviations | -| **Overview** | [Quick Start](./quick-start.md) | 5-minute getting started guide | -| **Requirements** | [Functional Requirements](../01-requirements/03-functional-requirements.md) | Feature specifications | -| **Architecture** | [System Architecture](../02-architecture/system-architecture.md) | Overall system design | -| **Architecture** | [Data Model](../02-architecture/data-model.md) | Database schema | -| **Architecture** | [API Design](../02-architecture/api-design.md) | REST API specifications | -| **Implementation** | [Backend Guidelines](../03-implementation/backend-guidelines.md) | Backend coding standards | -| **Implementation** | [Frontend Guidelines](../03-implementation/frontend-guidelines.md) | Frontend coding standards | -| **Implementation** | [Testing Strategy](../03-implementation/testing-strategy.md) | Testing approach | -| **Operations** | [Deployment Guide](../04-operations/deployment-guide.md) | How to deploy | -| **Operations** | [Monitoring](../04-operations/monitoring-alerting.md) | Monitoring & alerts | -| **Decisions** | [ADR Index](../05-decisions/README.md) | Architecture decisions | -| **Tasks** | [Backend Tasks](../06-tasks/README.md) | Development tasks | +| Category | Document | Description | +| ------------------ | ------------------------------------------------------------------------------------ | ------------------------------------- | +| **Overview** | [Glossary](./glossary.md) | Technical terminology & abbreviations | +| **Overview** | [Quick Start](./quick-start.md) | 5-minute getting started guide | +| **Requirements** | [Functional Requirements](../01-requirements/03-functional-requirements.md) | Feature specifications | +| **Requirements** | [Document Numbering](../01-requirements/03.11-document-numbering.md) | Document numbering requirements | +| **Architecture** | [System Architecture](../02-architecture/system-architecture.md) | Overall system design | +| **Architecture** | [Data Model](../02-architecture/data-model.md) | Database schema | +| **Architecture** | [API Design](../02-architecture/api-design.md) | REST API specifications | +| **Implementation** | [Backend Guidelines](../03-implementation/backend-guidelines.md) | Backend coding standards | +| **Implementation** | [Frontend Guidelines](../03-implementation/frontend-guidelines.md) | Frontend coding standards | +| **Implementation** | [Document Numbering Implementation](../03-implementation/document-numbering.md) | Document numbering implementation | +| **Implementation** | [Testing Strategy](../03-implementation/testing-strategy.md) | Testing approach | +| **Operations** | [Deployment Guide](../04-operations/deployment-guide.md) | How to deploy | +| **Operations** | [Monitoring](../04-operations/monitoring-alerting.md) | Monitoring & alerts | +| **Operations** | [Document Numbering Operations](../04-operations/document-numbering-operations.md) | Doc numbering ops guide | +| **Decisions** | [ADR Index](../05-decisions/README.md) | Architecture decisions | +| **Tasks** | [Backend Tasks](../06-tasks/README.md) | Development tasks | ### Key ADRs @@ -371,7 +374,7 @@ lcbp3/ ### Operations Support -- **Email:** ops-team@example.com +- **Email:** - **Phone:** [Phone Number] - **On-Call:** [On-Call Schedule] @@ -379,9 +382,9 @@ lcbp3/ ## 📝 Document Control -- **Version:** 1.5.0 +- **Version:** 1.5.1 - **Status:** Active -- **Last Updated:** 2025-12-01 +- **Last Updated:** 2025-12-02 - **Next Review:** 2026-01-01 - **Owner:** System Architect - **Classification:** Internal Use Only @@ -390,12 +393,13 @@ lcbp3/ ## 🔄 Version History -| Version | Date | Description | -| ------- | ---------- | ------------------------------------------ | +| Version | Date | Description | +| ------- | ---------- | ----------------------------------------- | +| 1.6.0 | 2025-12-02 | Reorganized documentation structure | | 1.5.0 | 2025-12-01 | Complete specification with ADRs and tasks | -| 1.4.5 | 2025-11-30 | Updated architecture documents | -| 1.4.4 | 2025-11-29 | Initial backend/frontend plans | -| 1.0.0 | 2025-11-01 | Initial requirements | +| 1.4.5 | 2025-11-30 | Updated architecture documents | +| 1.4.4 | 2025-11-29 | Initial backend/frontend plans | +| 1.0.0 | 2025-11-01 | Initial requirements | --- diff --git a/specs/00-overview/glossary.md b/specs/00-overview/glossary.md index 1f5d858..8ba1639 100644 --- a/specs/00-overview/glossary.md +++ b/specs/00-overview/glossary.md @@ -1,8 +1,8 @@ # Glossary - คำศัพท์และคำย่อทางเทคนิค **Project:** LCBP3-DMS -**Version:** 1.5.0 -**Last Updated:** 2025-12-01 +**Version:** 1.5.1 +**Last Updated:** 2025-12-02 --- @@ -491,6 +491,6 @@ Logging library สำหรับ Node.js --- -**Version:** 1.5.0 -**Last Updated:** 2025-12-01 +**Version:** 1.5.1 +**Last Updated:** 2025-12-02 **Next Review:** 2026-03-01 diff --git a/specs/00-overview/quick-start.md b/specs/00-overview/quick-start.md index 4bac079..b53b6ec 100644 --- a/specs/00-overview/quick-start.md +++ b/specs/00-overview/quick-start.md @@ -1,8 +1,8 @@ # Quick Start Guide **Project:** LCBP3-DMS -**Version:** 1.5.0 -**Last Updated:** 2025-12-01 +**Version:** 1.5.1 +**Last Updated:** 2025-12-02 --- @@ -355,7 +355,7 @@ git push origin feature/my-feature ### Resources - **Documentation:** `/specs` directory -- **API Docs:** http://localhost:3000/api/docs +- **API Docs:** - **Issue Tracker:** [Link to issue tracker] ### Contact @@ -373,8 +373,8 @@ git push origin feature/my-feature - [ ] Setup environment variables - [ ] Start Docker services - [ ] Run migrations -- [ ] Access backend (http://localhost:3000/health) -- [ ] Access frontend (http://localhost:3001) +- [ ] Access backend () +- [ ] Access frontend () - [ ] Login with default credentials - [ ] Run tests - [ ] Read [System Architecture](../02-architecture/system-architecture.md) @@ -385,5 +385,5 @@ git push origin feature/my-feature **Welcome aboard! 🎉** -**Version:** 1.5.0 -**Last Updated:** 2025-12-01 +**Version:** 1.5.1 +**Last Updated:** 2025-12-02 diff --git a/specs/01-requirements/03.11-document-numbering.md b/specs/01-requirements/03.11-document-numbering.md index 51f243c..5960291 100644 --- a/specs/01-requirements/03.11-document-numbering.md +++ b/specs/01-requirements/03.11-document-numbering.md @@ -7,12 +7,21 @@ status: draft owner: Nattanin Peancharoen last_updated: 2025-12-02 related: - - specs/01-requirements/01-objectives.md - - specs/01-requirements/02-architecture.md - - specs/01-requirements/03-functional-requirements.md - - specs/04-data-dictionary/4_Data_Dictionary_V1_4_4.md + +- specs/01-requirements/01-objectives.md +- specs/01-requirements/02-architecture.md +- specs/01-requirements/03-functional-requirements.md +- specs/03-implementation/document-numbering.md +- specs/04-operations/document-numbering-operations.md +- specs/04-data-dictionary/4_Data_Dictionary_V1_4_4.md + --- +> **📖 เอกสารที่เกี่ยวข้อง** +> +> - **Implementation Guide**: [document-numbering.md](file:///e:/np-dms/lcbp3/specs/03-implementation/document-numbering.md) - รายละเอียดการ implement ด้วย NestJS, TypeORM, Redis +> - **Operations Guide**: [document-numbering-operations.md](file:///e:/np-dms/lcbp3/specs/04-operations/document-numbering-operations.md) - Monitoring, Troubleshooting, Maintenance Procedures + ## 3.11.1. วัตถุประสงค์ - ระบบต้องสามารถสร้างเลขที่เอกสาร (Running Number) ได้โดยอัตโนมัติและยืดหยุ่นสูง @@ -40,24 +49,30 @@ related: ### Counter Key แยกตามประเภทเอกสาร **LETTER / RFI / MEMO / EMAIL / MOM / INSTRUCTION / NOTICE / OTHER**: + ``` (project_id, originator_organization_id, recipient_organization_id, correspondence_type_id, 0, 0, 0, current_year) ``` + *หมายเหตุ*: ไม่ใช้ `discipline_id`, `sub_type_id`, `rfa_type_id` **TRANSMITTAL**: + ``` (project_id, originator_organization_id, recipient_organization_id, correspondence_type_id, sub_type_id, 0, 0, current_year) ``` + *หมายเหตุ*: ใช้ `sub_type_id` เพิ่มเติม **RFA**: + ``` (project_id, originator_organization_id, NULL, correspondence_type_id, 0, rfa_type_id, discipline_id, current_year) ``` + *หมายเหตุ*: RFA ไม่ใช้ `recipient_organization_id` เพราะเป็นเอกสารโครงการ (CONTRACTOR → CONSULTANT → OWNER) ### วิธีการหา project_id @@ -84,10 +99,10 @@ related: - `rfa_type_id`: ใช้ `0` (ไม่ระบุประเภท RFA) - `recipient_organization_id`: ใช้ `NULL` สำหรับ RFA, Required สำหรับ LETTER/TRANSMITTAL - ## 3.11.3. Format Templates by Correspondence Type > **📝 หมายเหตุสำคัญ** +> > - Templates ด้านล่างเป็น **ตัวอย่าง** สำหรับประเภทเอกสารหลัก > - ระบบรองรับ **ทุกประเภทเอกสาร** ที่อยู่ใน `correspondence_types` table > - หากมีการเพิ่มประเภทใหม่ในอนาคต สามารถใช้งานได้โดยอัตโนมัติ @@ -96,6 +111,7 @@ related: ### 3.11.3.1. Letter (TYPE = LETTER) **Template**: + ``` {ORIGINATOR}-{RECIPIENT}-{SEQ:4}-{YEAR:B.E.} ``` @@ -103,12 +119,14 @@ related: **Example**: `คคง.-สคฉ.3-0001-2568` **Token Breakdown**: + - `คคง.` = {ORIGINATOR} = รหัสองค์กรผู้ส่ง - `สคฉ.3` = {RECIPIENT} = รหัสองค์กรผู้รับหลัก (TO) - `0001` = {SEQ:4} = Running number (เริ่ม 0001, 0002, ...) - `2568` = {YEAR:B.E.} = ปี พ.ศ. > **⚠️ Template vs Counter Separation** +> > - {CORR_TYPE} **ไม่แสดง**ใน template เพื่อความกระชับ > - แต่ระบบ**ยังใช้ correspondence_type_id ใน Counter Key** เพื่อแยก counter > - LETTER, MEMO, RFI **มี counter แยกกัน** แม้ template format เหมือนกัน @@ -120,6 +138,7 @@ related: ### 3.11.3.2. Transmittal (TYPE = TRANSMITTAL) **Template**: + ``` {ORIGINATOR}-{RECIPIENT}-{SUB_TYPE}-{SEQ:4}-{YEAR:B.E.} ``` @@ -127,6 +146,7 @@ related: **Example**: `คคง.-สคฉ.3-21-0117-2568` **Token Breakdown**: + - `คคง.` = {ORIGINATOR} - `สคฉ.3` = {RECIPIENT} - `21` = {SUB_TYPE} = หมายเลขประเภทย่อย (11=MAT, 12=SHP, 13=DWG, 14=MET, ...) @@ -134,6 +154,7 @@ related: - `2568` = {YEAR:B.E.} > **⚠️ Template vs Counter Separation** +> > - {CORR_TYPE} **ไม่แสดง**ใน template (เหมือน LETTER) > - TRANSMITTAL มี counter แยกจาก LETTER @@ -144,6 +165,7 @@ related: ### 3.11.3.3. RFA (Request for Approval) **Template**: + ``` {PROJECT}-{CORR_TYPE}-{DISCIPLINE}-{RFA_TYPE}-{SEQ:4}-{REV} ``` @@ -151,6 +173,7 @@ related: **Example**: `LCBP3-C2-RFA-TER-RPT-0001-A` **Token Breakdown**: + - `LCBP3-C2` = {PROJECT} = รหัสโครงการ - `RFA` = {CORR_TYPE} = ประเภทเอกสาร (**แสดง**ใน RFA template) - `TER` = {DISCIPLINE} = รหัสสาขางาน (TER=Terminal, STR=Structure, ...) @@ -159,6 +182,7 @@ related: - `A` = {REV} = Revision code > **📋 RFA Workflow** +> > - RFA เป็น **เอกสารโครงการ** (Project-level document) > - Workflow: **CONTRACTOR → CONSULTANT → OWNER** > - ไม่มี specific `recipient_id` เพราะเป็น workflow ที่กำหนดไว้แล้ว @@ -172,6 +196,7 @@ related: **Status**: 🚧 **To Be Determined** Drawing Numbering ยังไม่ได้กำหนด Template เนื่องจาก: + - มีความซับซ้อนสูง (Contract Drawing และ Shop Drawing มีกฎต่างกัน) - อาจต้องใช้ระบบ Numbering แยกต่างหาก - ต้องพิจารณาร่วมกับ RFA ที่เกี่ยวข้อง @@ -183,6 +208,7 @@ Drawing Numbering ยังไม่ได้กำหนด Template เนื **Applicable to**: RFI, MEMO, EMAIL, MOM, INSTRUCTION, NOTICE, OTHER **Template**: + ``` {ORIGINATOR}-{RECIPIENT}-{SEQ:4}-{YEAR:B.E.} ``` @@ -191,6 +217,7 @@ Drawing Numbering ยังไม่ได้กำหนด Template เนื **Example (MEMO)**: `คคง.-ผรม.1-0001-2568` > **🔑 Counter Separation** +> > - แม้ template format **เหมือนกับ LETTER** > - แต่แต่ละ type มี **counter แยกกัน** ผ่าน `correspondence_type_id` > - RFI counter ≠ MEMO counter ≠ LETTER counter @@ -199,7 +226,6 @@ Drawing Numbering ยังไม่ได้กำหนด Template เนื **หมายเหตุ**: ทุกประเภทที่ไม่ได้ระบุเฉพาะจะใช้ Template นี้ ถ้ามีการเพิ่ม correspondence type ใหม่ใน `correspondence_types` table จะใช้ Template นี้โดยอัตโนมัติ - ## 3.11.4. Supported Token Types | Token | Description | Example | Database Source | @@ -219,168 +245,1469 @@ Drawing Numbering ยังไม่ได้กำหนด Template เนื ### Token Usage Notes **{SEQ:n}**: + - `n` = จำนวนหลักที่ต้องการ (typically 4) - Counter **เริ่มจาก 0001** และเพิ่มทีละ 1 (0001, 0002, 0003, ...) - Padding ด้วย 0 ทางซ้าย - Reset ทุกปี (ตาม `current_year` ใน Counter Key) **{RECIPIENT}**: + - ใช้เฉพาะผู้รับที่มี `recipient_type = 'TO'` เท่านั้น - ถ้ามีหลาย TO ให้ใช้คนแรก (ตาม sort order) - **ไม่ใช้สำหรับ RFA** (RFA ไม่มี {RECIPIENT} ใน template) **{CORR_TYPE}**: + - รองรับทุกค่าจาก `correspondence_types.type_code` - ถ้าม�การเพิ่มประเภทใหม่ จะใช้งานได้ทันที - **แสดงใน template**: RFA only - **ไม่แสดงแต่ใช้ใน counter**: LETTER, TRANSMITTAL, และ Other types **Deprecated Tokens** (ไม่ควรใช้): + - ~~`{ORG}`~~ → ใช้ `{ORIGINATOR}` หรือ `{RECIPIENT}` แทน - ~~`{TYPE}`~~ → ใช้ `{CORR_TYPE}`, `{SUB_TYPE}`, หรือ `{RFA_TYPE}` แทน (ตามบริบท) - ~~`{CATEGORY}`~~ → ไม่ได้ใช้งานในระบบปัจจุบัน +## 3.11.5. Security & Data Integrity Requirements +### 3.11.5.1. Concurrency Control -## 3.11.5. กลไกความปลอดภัย (Concurrency Control) +**Requirements:** -### 3.11.6.1. Redis Distributed Lock +- ระบบ**ต้อง**ป้องกัน race condition เมื่อมีการสร้างเลขที่เอกสารพร้อมกัน +- ระบบ**ต้อง**รับประกัน uniqueness ของเลขที่เอกสารในทุกสถานการณ์ +- ระบบ**ควร**ใช้ Distributed Lock (Redis) เป็นกลไก primary +- ระบบ**ต้อง**มี fallback mechanism เมื่อ Redis ไม่พร้อมใช้งาน -- ใช้ Redis Distributed Lock เพื่อป้องกัน race condition -- Lock key format: `lock:docnum:{project_id}:{doc_type_id}:{...counter_key_parts}` -- Lock TTL: 5 วินาที (auto-release เมื่อ timeout) -- Lock acquisition timeout: 10 วินาที +**Implementation Details:** ดู [Implementation Guide - Section 2.3](file:///e:/np-dms/lcbp3/specs/03-implementation/document-numbering.md#23-redis-lock-service) -### 3.11.6.2. Optimistic Locking +### 3.11.5.2. Data Integrity -- ใช้ `version` column ในตาราง `document_number_configs` -- ตรวจสอบ version ก่อน update counter -- หาก version conflict เกิดขึ้น → retry transaction +**Requirements:** -### 3.11.6.3. Database Constraints +- ระบบ**ต้อง**ใช้ Optimistic Locking เพื่อตรวจจับ concurrent updates +- ระบบ**ต้อง**มี database constraints เพื่อป้องกันข้อมูลผิดพลาด: + - Unique constraint บน `document_number` + - Foreign key constraints ทุก relationship + - Check constraints สำหรับ business rules -- Unique constraint บน `document_number` column -- Foreign key constraints เพื่อความสัมพันธ์ข้อมูล -- Check constraints สำหรับ business rules +### 3.11.5.3. Authorization -## 3.11.7. Retry Mechanism & Error Handling +**Requirements:** -### 3.11.7.1. Scenario 1: Redis Unavailable +- เฉพาะ **authenticated users** เท่านั้นที่สามารถ request document number +- เฉพาะ **Project Admin** เท่านั้นที่แก้ไข template ได้ +- เฉพาะ **Super Admin** เท่านั้นที่ reset counter ได้ (requires approval) -- **Fallback**: ใช้ database-only locking (pessimistic lock) -- **Action**: - - ใช้ `SELECT ... FOR UPDATE` แทน Redis lock - - Log warning พร้อม alert ops team - - ระบบยังใช้งานได้แต่ performance ลดลง +## 3.11.6. Error Handling Requirements -### 3.11.7.2. Scenario 2: Lock Acquisition Timeout +### 3.11.6.1. Retry Mechanism -- **Retry**: 5 ครั้งด้วย exponential backoff - - Attempt 1: wait 1s - - Attempt 2: wait 2s - - Attempt 3: wait 4s - - Attempt 4: wait 8s - - Attempt 5: wait 16s (รวม ~31 วินาที) -- **Failure**: Return HTTP 503 "Service Temporarily Unavailable" -- **Frontend**: แสดงข้อความ "ระบบกำลังยุ่ง กรุณาลองใหม่ภายหลัง" +**Requirements:** -### 3.11.7.3. Scenario 3: Version Conflict After Lock +ระบบ**ต้อง**จัดการ error scenarios ต่อไปนี้: -- **Retry**: 2 ครั้ง (reload counter + retry transaction) -- **Failure**: Log error พร้อม context และ return HTTP 409 Conflict -- **Frontend**: แสดงข้อความ "เลขที่เอกสารถูกเปลี่ยน กรุณาลองใหม่" +| Scenario | Strategy | Max Retries | Expected Response | +|----------|----------|-------------|-------------------| +| Redis Unavailable | Fallback to DB Lock | 0 | Continue (degraded performance) | +| Lock Timeout | Exponential Backoff | 5 | HTTP 503 after final retry | +| Version Conflict | Immediate Retry | 2 | HTTP 409 after final retry | +| DB Connection Error | Exponential Backoff | 3 | HTTP 500 after final retry | -### 3.11.7.4. Scenario 4: Database Connection Error +**Implementation Details:** ดู [Implementation Guide - Section 2.5](file:///e:/np-dms/lcbp3/specs/03-implementation/document-numbering.md#25-main-service-with-retry-logic) -- **Retry**: 3 ครั้งด้วย exponential backoff (1s, 2s, 4s) -- **Failure**: Return HTTP 500 "Internal Server Error" -- **Frontend**: แสดงข้อความ "เกิดข้อผิดพลาดในระบบ กรุณาติดต่อผู้ดูแลระบบ" +### 3.11.6.2. User Experience -## 3.11.8. Configuration Management +**Requirements:** -### 3.11.8.1. Admin Panel Configuration +- Error messages **ต้อง**เป็นภาษาไทย และเข้าใจง่าย +- HTTP status codes **ต้อง**สื่อความหมายที่ถูกต้อง +- Frontend **ควร**แสดง retry option สำหรับ transient errors -- Project Admin สามารถกำหนด/แก้ไข template ผ่าน Admin Panel -- การเปลี่ยนแปลง template จะไม่ส่งผลต่อเอกสารที่สร้างไว้แล้ว -- ต้องมีการ validate template ก่อนบันทึก (ตรวจสอบ token ที่ใช้ถูกต้อง) +## 3.11.7. Configuration Management Requirements -### 3.11.8.2. Template Versioning +### 3.11.7.1. Template Management -- เก็บ history ของ template changes -- บันทึก user, timestamp, และเหตุผลในการเปลี่ยนแปลง -- สามารถ rollback ไปเวอร์ชันก่อนหน้าได้ +**Requirements:** -### 3.11.8.3. Counter Reset Policy +- Project Admin **ต้อง**สามารถกำหนด/แก้ไข template ผ่าน Admin Panel +- ระบบ**ต้อง**validate template ก่อนบันทึก +- การเปลี่ยนแปลง template **ต้องไม่**ส่งผลต่อเอกสารที่สร้างไว้แล้ว -- Counter reset ตามปี (yearly reset) -- Counter reset ตาม project phase (optional) -- Admin สามารถ manual reset counter ได้ (require approval + audit log) +### 3.11.7.2. Template Versioning -## 3.11.9. Audit Trail +**Requirements:** -### 3.11.9.1. การบันทึก Audit Log +- ระบบ**ต้อง**เก็บ history ของ template changes +- ระบบ**ต้อง**บันทึก user, timestamp, และเหตุผลในการเปลี่ยนแปลง +- ระบบ**ควร**สามารถ rollback ไปเวอร์ชันก่อนหน้าได้ -บันทึกทุกการ generate เลขที่เอกสารใน `document_number_audit` table: +### 3.11.7.3. Counter Reset Policy + +**Requirements:** + +- Counter **ต้อง**reset ตามปี (อัตโนมัติ) +- Admin **ต้อง**สามารถ manual reset counter ได้ (require approval + audit log) + +**Implementation Details:** ดู [Implementation Guide - Section 4](file:///e:/np-dms/lcbp3/specs/03-implementation/document-numbering.md#4-bullmq-job-for-counter-reset) + +## 3.11.8. Audit Trail Requirements + +### 3.11.8.1. Audit Logging + +**Requirements:** + +ระบบ**ต้อง**บันทึกข้อมูลต่อไปนี้สำหรับทุก document number generation: - `document_id` - เอกสารที่ถูกสร้าง - `generated_number` - เลขที่ถูกสร้าง -- `counter_key` - key ที่ใช้ในการนับ +- `counter_key` - key ที่ใช้ในการนับ (JSON format) - `template_used` - template ที่ใช้ - `user_id` - ผู้ที่ request - `ip_address` - IP address ของผู้ request - `timestamp` - เวลาที่สร้าง -- `retry_count` - จำนวนครั้งที่ retry (ถ้ามี) +- `retry_count` - จำนวนครั้งที่ retry +- `performance_metrics` - Lock wait time, total duration -### 3.11.9.2. Conflict & Error Logging +### 3.11.8.2. Error Logging -- บันทึก version conflicts และ กลไก retry ที่ใช้ -- บันทึก lock timeouts และ failure reasons -- บันทึก fallback scenarios (เช่น Redis unavailable) +**Requirements:** -## 3.11.10. Performance Requirements +- ระบบ**ต้อง**บันทึก error แยกต่างหาก พร้อม error type classification +- ระบบ**ควร**alert ops team สำหรับ critical errors -### 3.11.10.1. Response Time +### 3.11.8.3. Retention Policy -- Document number generation ต้องเสร็จภายใน **2 วินาที** (95th percentile) -- Document number generation ต้องเสร็จภายใน **5 วินาที** (99th percentile) -- ในกรณี normal operation (ไม่มี retry) ควรเสร็จภายใน **500ms** +**Requirements:** -### 3.11.10.2. Throughput +- Audit log **ต้อง**เก็บอย่างน้อย **7 ปี** (ตาม พ.ร.บ. ข้อมูลอิเล็กทรอนิกส์) -- ระบบรองรับ concurrent requests อย่างน้อย **50 requests/second** -- Peak load รองรับได้ถึง **100 requests/second** (ช่วงเวลาเร่งงาน) +## 3.11.9. Performance Requirements -### 3.11.10.3. Availability +### 3.11.9.1. Response Time -- Uptime ≥ 99.5% (exclude planned maintenance) -- Maximum downtime ต่อเดือน ≤ 3.6 ชั่วโมง +**SLA Targets:** -## 3.11.11. Monitoring & Alerting +| Metric | Target | Notes | +|--------|--------|-------| +| 95th percentile | ≤ 2 วินาที | ตั้งแต่ request ถึง response | +| 99th percentile | ≤ 5 วินาที | รวม retry attempts | +| Normal operation | ≤ 500ms | ไม่มี retry | -### 3.11.11.1. Metrics to Monitor +### 3.11.9.2. Throughput + +**Capacity Targets:** + +| Load Level | Target | Notes | +|------------|--------|-------| +| Normal load | ≥ 50 req/s | ใช้งานปกติ | +| Peak load | ≥ 100 req/s | ช่วงเร่งงาน | + +### 3.11.9.3. Availability + +**SLA Targets:** + +- **Uptime**: ≥ 99.5% (excluding planned maintenance) +- **Maximum downtime**: ≤ 3.6 ชั่วโมง/เดือน +- **RTO**: ≤ 30 นาที +- **RPO**: ≤ 5 นาที + +**Operations Details:** ดู [Operations Guide - Section 1](file:///e:/np-dms/lcbp3/specs/04-operations/document-numbering-operations.md#1-performance-requirements) + +## 3.11.10. Monitoring & Alerting Requirements + +### 3.11.10.1. Metrics + +**Requirements:** + +ระบบ**ต้อง**collect metrics ต่อไปนี้: - Lock acquisition time (p50, p95, p99) -- Lock acquisition failure rate +- Lock acquisition success/failure rate - Counter generation latency - Retry count distribution - Redis connection status - Database connection pool usage -### 3.11.11.2. Alert Conditions +### 3.11.10.2. Alerts -- 🔴 **Critical**: Redis unavailable > 1 minute -- 🔴 **Critical**: Lock acquisition failures > 10% in 5 minutes -- 🟡 **Warning**: Lock acquisition failures > 5% in 5 minutes -- 🟡 **Warning**: Average lock wait time > 1 second -- 🟡 **Warning**: Retry count > 100 per hour +**Requirements:** -### 3.11.11.3. Dashboard +ระบบ**ต้อง**alert สำหรับ conditions ต่อไปนี้: -- Real-time lock acquisition success rate -- Lock wait time percentiles (p50, p95, p99) -- Counter generation rate (per minute) -- Error rate breakdown (by error type) -- Redis/Database health status +| Severity | Condition | Action | +|----------|-----------|--------| +| 🔴 Critical | Redis unavailable > 1 minute | PagerDuty + Slack | +| 🔴 Critical | Lock failures > 10% in 5 min | PagerDuty + Slack | +| 🟡 Warning | Lock failures > 5% in 5 min | Slack | +| 🟡 Warning | Avg lock wait time > 1 sec | Slack | +| 🟡 Warning | Retry count > 100/hour | Slack | + +### 3.11.10.3. Dashboard + +**Requirements:** + +- Ops team **ต้อง**มี real-time dashboard แสดง: + - Lock acquisition success rate + - Lock wait time percentiles + - Generation rate (per minute) + - Error rate by type + - Connection health status + +**Operations Details:** ดู [Operations Guide - Section 3](file:///e:/np-dms/lcbp3/specs/04-operations/document-numbering-operations.md#3-monitoring--metrics) + + +## 3.11.11. API Reference + +เอกสารนี้อ้างอิงถึง API endpoints ต่อไปนี้: + +### Document Number Generation + +```http +POST /api/v1/documents/{documentId}/generate-number +``` + +สร้างเลขที่เอกสารสำหรับ document ที่ระบุ + +**Request Body:** + +```json +{ + "counterKey": { + "projectId": 2, + "originatorOrgId": 22, + "recipientOrgId": 10, + "correspondenceTypeId": 6, + "subTypeId": 0, + "rfaTypeId": 0, + "disciplineId": 0, + "year": 2025 + } +} +``` + +**Response:** + +```json +{ + "documentNumber": "คคง.-สคฉ.3-0001-2568", + "generatedAt": "2025-12-02T15:30:00Z" +} +``` + +### Template Management + +```http +GET /api/v1/document-numbering/configs +``` + +ดูรายการ template configuration ทั้งหมด + +```http +PUT /api/v1/document-numbering/configs/{configId} +``` + +แก้ไข template (Project Admin only) + +```http +POST /api/v1/document-numbering/configs/{configId}/reset-counter +``` + +Reset counter (Super Admin only, requires approval) + +**รายละเอียดเพิ่มเติม:** ดู [API Design](file:///e:/np-dms/lcbp3/specs/02-architecture/api-design.md) + +## 3.11.12. Database Schema Reference + +เอกสารนี้อ้างอิงถึง database tables ต่อไปนี้: + +### Core Tables + +- `document_number_counters` - เก็บ counter values และ template configuration +- `document_number_audit` - เก็บ audit trail ของการ generate เลขที่ +- `document_number_errors` - เก็บ error logs + +### Related Tables + +- `documents` - เก็บ document number ที่ถูกสร้าง (column: `document_number` UNIQUE) +- `correspondence_types` - ประเภทเอกสาร (LETTER, RFA, TRANSMITTAL, etc.) +- `correspondence_sub_types` - ประเภทย่อย (สำหรับ TRANSMITTAL) +- `rfa_types` - ประเภท RFA (SHD, RPT, MAT, etc.) +- `disciplines` - สาขาวิชา (TER, STR, GEO, etc.) +- `projects` - โครงการ +- `organizations` - องค์กร + +**Schema Details:** ดู [Implementation Guide - Section 1](file:///e:/np-dms/lcbp3/specs/03-implementation/document-numbering.md#1-database-implementation) + +## 3.11.13. Database Schema Requirements + +### 3.11.13.1. Counter Table Schema Requirements + +**Primary Table**: `document_number_counters` + +**Required Columns:** +- Composite primary key: `(project_id, originator_organization_id, recipient_organization_id, correspondence_type_id, sub_type_id, rfa_type_id, discipline_id, current_year)` +- `version` - สำหรับ optimistic locking +- `last_number` - counter value (เริ่มจาก 0) + +**Important Notes:** +- ใช้ `COALESCE(recipient_organization_id, 0)` ใน Primary Key เพื่อรองรับ NULL +- Counter reset ทุกปี (เมื่อ `current_year` เปลี่ยน) +- ต้องมี seed data สำหรับ `correspondence_types`, `rfa_types`, `disciplines` ก่อน + +**Schema Details:** ดู [Implementation Guide - Section 1.1](file:///e:/np-dms/lcbp3/specs/03-implementation/document-numbering.md#11-counter-table-schema) + +### 3.11.13.2. Audit Table Requirements + +**Primary Table**: `document_number_audit` + +**Required Columns:** +- `document_id`, `generated_number`, `counter_key` (JSON) +- `template_used`, `user_id`, `ip_address` +- Performance metrics: `retry_count`, `lock_wait_ms`, `total_duration_ms` +- `fallback_used` - tracking fallback scenarios + +**Retention:** ≥ 7 ปี + +### 3.11.13.3. Error Log Requirements + +**Primary Table**: `document_number_errors` + +**Required Columns:** +- `error_type` - ENUM classification +- `error_message`, `stack_trace`, `context_data` (JSON) +- `user_id`, `ip_address`, `created_at`, `resolved_at` + +## 3.11.14. Security Considerations + +### 3.11.14.1. Authorization + +- เฉพาะ **authenticated users** เท่านั้นที่สามารถ request document number +- เฉพาะ **Project Admin** เท่านั้นที่แก้ไข template ได้ +- เฉพาะ **Super Admin** เท่านั้นที่ reset counter ได้ + +### 3.11.14.2. Rate Limiting + +**Requirements:** +- Limit ต่อ user: **10 requests/minute** (prevent abuse) +- Limit ต่อ IP: **50 requests/minute** + +**Implementation Details:** ดู [Implementation Guide - Section 5](file:///e:/np-dms/lcbp3/specs/03-implementation/document-numbering.md#5-api-controller) + +### 3.11.14.3. Audit & Compliance + +**Requirements:** +- บันทึกทุก API call ที่เกี่ยวข้องกับ document numbering +- เก็บ audit log อย่างน้อย **7 ปี** (ตาม พ.ร.บ. ข้อมูลอิเล็กทรอนิกส์) +- Audit log **ต้องไม่**สามารถแก้ไขได้ (immutable) + +--- + +## References + +- [Implementation Guide](file:///e:/np-dms/lcbp3/specs/03-implementation/document-numbering.md) +- [Operations Guide](file:///e:/np-dms/lcbp3/specs/04-operations/document-numbering-operations.md) +- [API Design](file:///e:/np-dms/lcbp3/specs/02-architecture/api-design.md) +- [Data Dictionary](file:///e:/np-dms/lcbp3/specs/04-data-dictionary/4_Data_Dictionary_V1_4_4.md) + + +``` +lock:docnum:{project_id}:{org_id}:{recip_id}:{type_id}:{sub}:{rfa}:{disc}:{year} +``` + +**Lock Configuration**: + +- **TTL**: 5 วินาที (auto-release เมื่อ timeout) +- **Acquisition Timeout**: 10 วินาที +- **Retry Delay**: 100ms (exponential backoff) +- **Drift Factor**: 0.01 (Redlock algorithm) + +**Implementation (NestJS)**: + +```typescript +// src/document-numbering/services/document-numbering-lock.service.ts +import Redlock from 'redlock'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class DocumentNumberingLockService { + private redlock: Redlock; + + async acquireLock(counterKey: CounterKey): Promise { + const lockKey = this.buildLockKey(counterKey); + return await this.redlock.acquire([lockKey], 5000); // 5s TTL + } + + private buildLockKey(key: CounterKey): string { + return `lock:docnum:${key.projectId}:${key.originatorOrgId}:` + + `${key.recipientOrgId ?? 0}:${key.correspondenceTypeId}:` + + `${key.subTypeId}:${key.rfaTypeId}:${key.disciplineId}:${key.year}`; + } +} +``` + +### 3.11.5.2. Optimistic Locking + +ใช้ **TypeORM Optimistic Lock** ร่วมกับ `@Version()` decorator: + +**Entity Definition**: + +```typescript +// src/document-numbering/entities/document-number-counter.entity.ts +import { Entity, Column, PrimaryColumn, VersionColumn } from 'typeorm'; + +@Entity('document_number_counters') +export class DocumentNumberCounter { + @PrimaryColumn({ name: 'project_id' }) + projectId: number; + + @PrimaryColumn({ name: 'originator_organization_id' }) + originatorOrganizationId: number; + + @PrimaryColumn({ name: 'recipient_organization_id', nullable: true }) + recipientOrganizationId: number | null; + + @PrimaryColumn({ name: 'correspondence_type_id' }) + correspondenceTypeId: number; + + @PrimaryColumn({ name: 'sub_type_id', default: 0 }) + subTypeId: number; + + @PrimaryColumn({ name: 'rfa_type_id', default: 0 }) + rfaTypeId: number; + + @PrimaryColumn({ name: 'discipline_id', default: 0 }) + disciplineId: number; + + @PrimaryColumn({ name: 'current_year' }) + currentYear: number; + + @VersionColumn({ name: 'version' }) + version: number; + + @Column({ name: 'last_number', default: 0 }) + lastNumber: number; +} +``` + +**Transaction Handling**: + +```typescript +// ใช้ TypeORM Transaction + Optimistic Lock +await this.connection.transaction(async (manager) => { + const counter = await manager.findOne(DocumentNumberCounter, { + where: counterKey + }); + + counter.lastNumber += 1; + await manager.save(counter); // auto-check version +}); +``` + +หาก version conflict → TypeORM throw `OptimisticLockVersionMismatchError` → retry + +### 3.11.5.3. Database Constraints + +**Unique Constraints**: + +```sql +-- บน documents table +ALTER TABLE documents +ADD CONSTRAINT uq_document_number UNIQUE (document_number); +``` + +**Foreign Key Constraints**: + +- `project_id` → `projects(id)` ON DELETE CASCADE +- `originator_organization_id` → `organizations(id)` ON DELETE CASCADE +- `recipient_organization_id` → `organizations(id)` ON DELETE CASCADE +- `correspondence_type_id` → `correspondence_types(id)` ON DELETE CASCADE + +**Check Constraints**: + +```sql +-- ตรวจสอบว่า last_number ≥ 0 +ALTER TABLE document_number_counters +ADD CONSTRAINT chk_last_number_positive CHECK (last_number >= 0); + +-- ตรวจสอบว่า current_year เป็นปี ค.ศ. ที่สมเหตุสมผล +ALTER TABLE document_number_counters +ADD CONSTRAINT chk_current_year_valid +CHECK (current_year BETWEEN 2020 AND 2100); +``` + +## 3.11.6. Retry Mechanism & Error Handling + +### 3.11.6.1. Scenario 1: Redis Unavailable + +**Fallback Strategy**: Database-only Pessimistic Locking + +**Implementation**: + +```typescript +// src/document-numbering/services/document-numbering.service.ts +@Injectable() +export class DocumentNumberingService { + async generateDocumentNumber(dto: GenerateNumberDto): Promise { + try { + // พยายามใช้ Redis lock ก่อน + return await this.generateWithRedisLock(dto); + } catch (error) { + if (error instanceof RedisConnectionError) { + // Fallback: ใช้ database lock + this.logger.warn('Redis unavailable, falling back to DB lock'); + await this.alertOpsTeam('redis_unavailable'); + return await this.generateWithDbLock(dto); + } + throw error; + } + } + + private async generateWithDbLock(dto: GenerateNumberDto): Promise { + return await this.connection.transaction(async (manager) => { + // SELECT ... FOR UPDATE = Pessimistic Lock + const counter = await manager + .createQueryBuilder(DocumentNumberCounter, 'c') + .where(counterKeyCondition) + .setLock('pessimistic_write') + .getOne(); + + counter.lastNumber += 1; + await manager.save(counter); + return this.formatNumber(counter); + }); + } +} +``` + +**Monitoring**: + +- Log warning พร้อม context (project_id, user_id, timestamp) +- Alert Ops Team ผ่าน Slack/Email +- ระบบยังใช้งานได้แต่ performance อาจลดลง 30-50% + +### 3.11.6.2. Scenario 2: Lock Acquisition Timeout + +**Retry Strategy**: Exponential Backoff with Jitter + +```typescript +// ใช้ @nestjs/common Retry Decorator หรือ custom retry logic +import { retry } from 'rxjs/operators'; + +const RETRY_CONFIG = { + maxRetries: 5, + delays: [1000, 2000, 4000, 8000, 16000], // exponential backoff + jitter: 0.1 // เพิ่ม randomness ป้องกัน thundering herd +}; + +async acquireLockWithRetry(key: CounterKey): Promise { + for (let i = 0; i < RETRY_CONFIG.maxRetries; i++) { + try { + return await this.lockService.acquireLock(key); + } catch (error) { + if (i === RETRY_CONFIG.maxRetries - 1) { + throw new ServiceUnavailableException( + 'ระบบกำลังยุ่ง กรุณาลองใหม่ภายหลัง' + ); + } + const delay = RETRY_CONFIG.delays[i]; + const jitter = delay * RETRY_CONFIG.jitter * Math.random(); + await this.sleep(delay + jitter); + } + } +} +``` + +**Response**: + +- HTTP Status: `503 Service Temporarily Unavailable` +- Response Body: + + ```json + { + "statusCode": 503, + "message": "ระบบกำลังยุ่ง กรุณาลองใหม่ภายหลัง", + "error": "Service Unavailable", + "retryAfter": 30 + } + ``` + +### 3.11.6.3. Scenario 3: Version Conflict (Optimistic Lock) + +**Retry Strategy**: Immediate Retry (2 attempts) + +```typescript +async incrementCounter(counterKey: CounterKey): Promise { + const MAX_RETRIES = 2; + + for (let attempt = 0; attempt < MAX_RETRIES; attempt++) { + try { + return await this.connection.transaction(async (manager) => { + const counter = await manager.findOne( + DocumentNumberCounter, + { where: counterKey } + ); + + counter.lastNumber += 1; + await manager.save(counter); // Version check ที่นี่ + return counter.lastNumber; + }); + } catch (error) { + if (error instanceof OptimisticLockVersionMismatchError) { + this.logger.warn(`Version conflict, retry ${attempt + 1}/${MAX_RETRIES}`); + if (attempt === MAX_RETRIES - 1) { + throw new ConflictException('เลขที่เอกสารถูกเปลี่ยน กรุณาลองใหม่'); + } + // Retry ทันที (ไม่มี delay) + continue; + } + throw error; + } + } +} +``` + +**Response**: + +- HTTP Status: `409 Conflict` +- Frontend Action: Auto-retry หรือแสดง toast notification + +### 3.11.6.4. Scenario 4: Database Connection Error + +**Retry Strategy**: Exponential Backoff (3 attempts) + +```typescript +const DB_RETRY_CONFIG = { + maxRetries: 3, + delays: [1000, 2000, 4000] +}; + +// TypeORM connection retry (กำหนดใน ormconfig) +{ + type: 'mysql', + extra: { + connectionLimit: 10, + acquireTimeout: 10000, + // Retry connection 3 ครั้ง + retryAttempts: 3, + retryDelay: 1000 + } +} +``` + +**Response**: + +- HTTP Status: `500 Internal Server Error` +- Response Body: + + ```json + { + "statusCode": 500, + "message": "เกิดข้อผิดพลาดในระบบ กรุณาติดต่อผู้ดูแลระบบ", + "error": "Internal Server Error", + "ref": "ERR-20250102-1234-ABCD" + } + ``` + +- Alerting: ส่ง PagerDuty/Slack alert ทันที (severity: CRITICAL) + +## 3.11.7. Configuration Management + +### 3.11.7.1. Admin Panel Configuration + +**Features**: + +- Project Admin สามารถกำหนด/แก้ไข template ผ่าน Web UI +- Preview document number ก่อนบันทึก +- Template validation แบบ real-time + +**Template Validation Logic**: + +```typescript +// src/document-numbering/validators/template.validator.ts +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class TemplateValidator { + private readonly ALLOWED_TOKENS = [ + 'PROJECT', 'ORIGINATOR', 'RECIPIENT', 'CORR_TYPE', + 'SUB_TYPE', 'RFA_TYPE', 'DISCIPLINE', 'SEQ', 'YEAR', 'REV' + ]; + + validate(template: string, correspondenceType: string): ValidationResult { + const tokens = this.extractTokens(template); + const errors: string[] = []; + + // ตรวจสอบ Token ที่ไม่รู้จัก + for (const token of tokens) { + if (!this.ALLOWED_TOKENS.includes(token.name)) { + errors.push(`Unknown token: {${token.name}}`); + } + } + + // กฎพิเศษสำหรับแต่ละประเภท + if (correspondenceType === 'RFA') { + if (!tokens.some(t => t.name === 'PROJECT')) { + errors.push('RFA template ต้องมี {PROJECT}'); + } + } + + if (correspondenceType === 'TRANSMITTAL') { + if (!tokens.some(t => t.name === 'SUB_TYPE')) { + errors.push('TRANSMITTAL template ต้องมี {SUB_TYPE}'); + } + } + + return { valid: errors.length === 0, errors }; + } +} +``` + +**API Endpoint**: + +```typescript +// PUT /api/v1/document-numbering/configs/:configId +@Put('configs/:configId') +@Roles('PROJECT_ADMIN') +async updateTemplate( + @Param('configId') configId: number, + @Body() dto: UpdateTemplateDto +): Promise { + // Validate template + const validation = await this.templateValidator.validate( + dto.template, + dto.correspondenceType + ); + + if (!validation.valid) { + throw new BadRequestException(validation.errors); + } + + // บันทึก template (ไม่ส่งผลต่อเอกสารที่สร้างแล้ว) + return await this.configService.update(configId, dto); +} +``` + +### 3.11.7.2. Template Versioning + +**Database Table**: `document_number_config_history` + +```sql +CREATE TABLE document_number_config_history ( + id INT AUTO_INCREMENT PRIMARY KEY, + config_id INT NOT NULL, + template_before TEXT, + template_after TEXT NOT NULL, + changed_by INT NOT NULL, + changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + change_reason TEXT, + + FOREIGN KEY (config_id) REFERENCES document_number_configs(id), + FOREIGN KEY (changed_by) REFERENCES users(id) +) ENGINE=InnoDB COMMENT='Template Change History'; +``` + +**Audit Trail Implementation**: + +```typescript +@Injectable() +export class ConfigHistoryService { + async recordChange( + configId: number, + oldTemplate: string, + newTemplate: string, + userId: number, + reason: string + ): Promise { + await this.historyRepo.save({ + configId, + templateBefore: oldTemplate, + templateAfter: newTemplate, + changedBy: userId, + changeReason: reason + }); + } + + async rollback(configId: number, historyId: number): Promise { + const history = await this.historyRepo.findOne({ where: { id: historyId }}); + await this.configService.update(configId, { + template: history.templateBefore + }); + } +} +``` + +### 3.11.7.3. Counter Reset Policy + +**Automatic Reset**: + +- **Yearly Reset**: ทุกวันที่ 1 มกราคม (00:00:00 ICT) + - ใช้ **BullMQ Cron Job**: + + ```typescript + // src/document-numbering/jobs/counter-reset.job.ts + @Processor('document-numbering') + export class CounterResetJob { + @Cron('0 0 1 1 *') // 1 Jan every year + async handleYearlyReset() { + const newYear = new Date().getFullYear(); + + // ไม่ต้อง reset counter เพราะ counter แยกตาม current_year อยู่แล้ว + // แค่เตรียม counter สำหรับปีใหม่ + this.logger.log(`Year changed to ${newYear}, counters ready`); + } + } + ``` + +**Manual Reset** (Admin only): + +```typescript +// POST /api/v1/document-numbering/configs/:configId/reset-counter +@Post('configs/:configId/reset-counter') +@Roles('SUPER_ADMIN') +@RequireApproval() // Custom decorator: ต้อง approve จาก 2 admins +async resetCounter( + @Param('configId') configId: number, + @Body() dto: ResetCounterDto +): Promise { + // Validate reason + if (!dto.reason || dto.reason.length < 20) { + throw new BadRequestException('ต้องระบุเหตุผลอย่างน้อย 20 ตัวอักษร'); + } + + // Audit log + await this.auditService.logCounterReset({ + configId, + userId: req.user.id, + reason: dto.reason, + previousValue: counter.lastNumber + }); + + // Reset + await this.counterService.reset(configId); +} + +## 3.11.8. Audit Trail + +### 3.11.8.1. การบันทึก Audit Log + +**Database Table**: `document_number_audit` + +```sql +CREATE TABLE document_number_audit ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + document_id INT NOT NULL, + generated_number VARCHAR(100) NOT NULL, + counter_key JSON NOT NULL COMMENT 'Counter key used (JSON format)', + template_used VARCHAR(200) NOT NULL, + user_id INT NOT NULL, + ip_address VARCHAR(45), + user_agent TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + -- Performance & Error Tracking + retry_count INT DEFAULT 0, + lock_wait_ms INT COMMENT 'Lock acquisition time in milliseconds', + total_duration_ms INT COMMENT 'Total generation time', + fallback_used ENUM('NONE', 'DB_LOCK', 'RETRY') DEFAULT 'NONE', + + INDEX idx_document_id (document_id), + INDEX idx_user_id (user_id), + INDEX idx_created_at (created_at), + FOREIGN KEY (document_id) REFERENCES documents(id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users(id) +) ENGINE=InnoDB COMMENT='Document Number Generation Audit Trail'; +``` + +**Audit Service Implementation**: + +```typescript +// src/document-numbering/services/audit.service.ts +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +@Injectable() +export class DocumentNumberAuditService { + async logGeneration(data: AuditLogData): Promise { + await this.auditRepo.save({ + documentId: data.documentId, + generatedNumber: data.number, + counterKey: JSON.stringify(data.counterKey), + templateUsed: data.template, + userId: data.userId, + ipAddress: data.ipAddress, + userAgent: data.userAgent, + retryCount: data.retryCount ?? 0, + lockWaitMs: data.lockWaitMs, + totalDurationMs: data.totalDurationMs, + fallbackUsed: data.fallbackUsed ?? 'NONE' + }); + } +} +``` + +**Usage in Service**: + +```typescript +@Injectable() +export class DocumentNumberingService { + async generateDocumentNumber(dto: GenerateNumberDto, req: Request) { + const startTime = Date.now(); + let lockWaitMs = 0; + let retryCount = 0; + let fallbackUsed = 'NONE'; + + try { + // ... generate logic ... + const number = await this.doGenerate(dto); + + // Audit log + await this.auditService.logGeneration({ + documentId: dto.documentId, + number, + counterKey: dto.counterKey, + template: config.template, + userId: req.user.id, + ipAddress: req.ip, + userAgent: req.headers['user-agent'], + retryCount, + lockWaitMs, + totalDurationMs: Date.now() - startTime, + fallbackUsed + }); + + return number; + } catch (error) { + // Log error separately + await this.errorLogService.log(error, dto); + throw error; + } + } +} +``` + +### 3.11.8.2. Conflict & Error Logging + +**Separate Error Log Table**: `document_number_errors` + +```sql +CREATE TABLE document_number_errors ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + error_type ENUM( + 'LOCK_TIMEOUT', + 'VERSION_CONFLICT', + 'DB_ERROR', + 'REDIS_ERROR', + 'VALIDATION_ERROR' + ) NOT NULL, + error_message TEXT, + stack_trace TEXT, + context_data JSON COMMENT 'Request context (user, project, etc.)', + user_id INT, + ip_address VARCHAR(45), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + resolved_at TIMESTAMP NULL, + + INDEX idx_error_type (error_type), + INDEX idx_created_at (created_at), + INDEX idx_user_id (user_id) +) ENGINE=InnoDB COMMENT='Document Numbering Error Log'; +``` + +**Error Logging Service**: + +```typescript +@Injectable() +export class ErrorLogService { + async log(error: Error, context: any): Promise { + const errorType = this.classifyError(error); + + await this.errorRepo.save({ + errorType, + errorMessage: error.message, + stackTrace: error.stack, + contextData: JSON.stringify(context), + userId: context.userId, + ipAddress: context.ipAddress + }); + + // Alert if critical + if (this.isCritical(errorType)) { + await this.alertService.sendAlert({ + severity: 'CRITICAL', + title: `Document Numbering Error: ${errorType}`, + details: error.message + }); + } + } + + private classifyError(error: Error): string { + if (error instanceof LockTimeoutError) return 'LOCK_TIMEOUT'; + if (error instanceof OptimisticLockVersionMismatchError) return 'VERSION_CONFLICT'; + if (error instanceof QueryFailedError) return 'DB_ERROR'; + if (error instanceof RedisConnectionError) return 'REDIS_ERROR'; + return 'UNKNOWN'; + } +} + +## 3.11.9. Performance Requirements + +### 3.11.9.1. Response Time + +**Target Response Times**: +- **95th percentile**: ≤ 2 วินาที +- **99th percentile**: ≤ 5 วินาที +- **Normal operation** (ไม่มี retry): ≤ 500ms + +**Performance Optimization Strategies**: + +```typescript +// 1. Database Connection Pooling +{ + type: 'mysql', + extra: { + connectionLimit: 20, // Pool size + queueLimit: 0, // Unlimited queue + acquireTimeout: 10000 // 10s timeout + } +} + +// 2. Redis Connection Pooling +import IORedis from 'ioredis'; + +const redis = new IORedis({ + host: process.env.REDIS_HOST, + port: parseInt(process.env.REDIS_PORT), + maxRetriesPerRequest: 3, + enableReadyCheck: true, + lazyConnect: false, + // Connection pool + poolSize: 10 +}); + +// 3. Query Optimization +// ใช้ Index-covered queries +const counter = await this.counterRepo + .createQueryBuilder('c') + .where('c.project_id = :projectId', { projectId }) + .andWhere('c.correspondence_type_id = :typeId', { typeId }) + .andWhere('c.current_year = :year', { year }) + .useIndex('idx_counter_lookup') // Force index usage + .getOne(); +``` + +**Performance Monitoring**: + +```typescript +// Prometheus metrics +import { Counter, Histogram } from 'prom-client'; + +const generationDuration = new Histogram({ + name: 'docnum_generation_duration_seconds', + help: 'Document number generation duration', + labelNames: ['project', 'type', 'status'], + buckets: [0.1, 0.5, 1, 2, 5, 10] +}); + +// Usage +const timer = generationDuration.startTimer(); +try { + const number = await this.generate(dto); + timer({ status: 'success' }); +} catch (error) { + timer({ status: 'error' }); + throw error; +} +``` + +### 3.11.9.2. Throughput + +**Capacity Requirements**: + +- **Normal load**: ≥ 50 requests/second +- **Peak load**: ≥ 100 requests/second (ช่วงเร่งงาน) +- **Burst capacity**: ≥ 200 requests/second (short duration) + +**Load Balancing Strategy**: + +```yaml +# docker-compose.yml +services: + backend: + image: lcbp3-backend:latest + deploy: + replicas: 3 # 3 instances + resources: + limits: + cpus: '1.0' + memory: 1G + reservations: + cpus: '0.5' + memory: 512M + + nginx: + image: nginx:alpine + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf + ports: + - "80:80" +``` + +```nginx +# nginx.conf - Load Balancing Configuration +upstream backend { + least_conn; # Least connections algorithm + server backend:3000 max_fails=3 fail_timeout=30s; + server backend:3001 max_fails=3 fail_timeout=30s; + server backend:3002 max_fails=3 fail_timeout=30s; +} + +server { + location /api/v1/documents/ { + proxy_pass http://backend; + proxy_next_upstream error timeout; + proxy_connect_timeout 10s; + proxy_read_timeout 30s; + } +} +``` + +**Rate Limiting**: + +```typescript +// ใช้ @nestjs/throttler +import { ThrottlerGuard } from '@nestjs/throttler'; + +@Controller('document-numbering') +@UseGuards(ThrottlerGuard) +export class DocumentNumberingController { + @Throttle(10, 60) // 10 requests per 60 seconds per user + @Post('generate') + async generate(@Body() dto: GenerateNumberDto) { + return await this.service.generate(dto); + } +} +``` + +### 3.11.9.3. Availability + +**SLA Targets**: + +- **Uptime**: ≥ 99.5% (excluding planned maintenance) +- **Maximum downtime**: ≤ 3.6 ชั่วโมง/เดือน +- **Recovery Time Objective (RTO)**: ≤ 30 นาที +- **Recovery Point Objective (RPO)**: ≤ 5 นาที + +**High Availability Setup**: + +```yaml +# High Availability Architecture +services: + # MariaDB - Master/Replica + mariadb-master: + image: mariadb:10.11 + environment: + MYSQL_REPLICATION_MODE: master + + mariadb-replica: + image: mariadb:10.11 + environment: + MYSQL_REPLICATION_MODE: slave + MYSQL_MASTER_HOST: mariadb-master + + # Redis - Sentinel Mode + redis-master: + image: redis:7-alpine + command: redis-server --appendonly yes + + redis-replica: + image: redis:7-alpine + command: redis-server --replicaof redis-master 6379 + + redis-sentinel: + image: redis:7-alpine + command: > + redis-sentinel /etc/redis/sentinel.conf + --sentinel monitor mymaster redis-master 6379 2 +``` + +**Health Checks**: + +```typescript +// src/health/health.controller.ts +import { HealthCheck, HealthCheckService, TypeOrmHealthIndicator } from '@nestjs/terminus'; + +@Controller('health') +export class HealthController { + @Get() + @HealthCheck() + check() { + return this.health.check([ + () => this.db.pingCheck('database'), + () => this.redis.pingCheck('redis'), + () => this.customHealthCheck() + ]); + } + + private async customHealthCheck() { + // ทดสอบ generate document number + const canGenerate = await this.testGeneration(); + return { documentNumbering: { status: canGenerate ? 'up' : 'down' }}; + } +} + +## 3.11.10. Monitoring & Alerting + +### 3.11.10.1. Metrics Collection + +**Prometheus Metrics Implementation**: + +```typescript +// src/document-numbering/metrics/metrics.service.ts +import { Injectable } from '@nestjs/common'; +import { Counter, Histogram, Gauge } from 'prom-client'; + +@Injectable() +export class DocumentNumberingMetrics { + // Lock acquisition metrics + private lockAcquisitionDuration = new Histogram({ + name: 'docnum_lock_acquisition_duration_ms', + help: 'Lock acquisition time in milliseconds', + labelNames: ['project', 'type'], + buckets: [10, 50, 100, 200, 500, 1000, 2000, 5000] + }); + + private lockAcquisitionFailures = new Counter({ + name: 'docnum_lock_acquisition_failures_total', + help: 'Total number of lock acquisition failures', + labelNames: ['project', 'type', 'reason'] + }); + + // Generation metrics + private generationDuration = new Histogram({ + name: 'docnum_generation_duration_ms', + help: 'Total document number generation time', + labelNames: ['project', 'type', 'status'], + buckets: [100, 200, 500, 1000, 2000, 5000] + }); + + private retryCount = new Histogram({ + name: 'docnum_retry_count', + help: 'Number of retries per generation', + labelNames: ['project', 'type'], + buckets: [0, 1, 2, 3, 5, 10] + }); + + // Connection health + private redisConnectionStatus = new Gauge({ + name: 'docnum_redis_connection_status', + help: 'Redis connection status (1=up, 0=down)' + }); + + private dbConnectionPoolUsage = new Gauge({ + name: 'docnum_db_connection_pool_usage', + help: 'Database connection pool usage percentage' + }); +} +``` + +### 3.11.10.2. Alert Rules + +**Prometheus Alert Rules** (`prometheus/alerts.yml`): + +```yaml +groups: + - name: document_numbering_alerts + interval: 30s + rules: + # CRITICAL: Redis unavailable + - alert: RedisUnavailable + expr: docnum_redis_connection_status == 0 + for: 1m + labels: + severity: critical + component: document-numbering + annotations: + summary: "Redis is unavailable for document numbering" + description: "System is falling back to DB-only locking" + + # CRITICAL: High lock failure rate + - alert: HighLockFailureRate + expr: | + rate(docnum_lock_acquisition_failures_total[5m]) > 0.1 + for: 5m + labels: + severity: critical + annotations: + summary: "Lock acquisition failure rate > 10%" + description: "Check Redis and database performance" + + # WARNING: Elevated lock failure rate + - alert: ElevatedLockFailureRate + expr: | + rate(docnum_lock_acquisition_failures_total[5m]) > 0.05 + for: 5m + labels: + severity: warning + annotations: + summary: "Lock acquisition failure rate > 5%" + + # WARNING: Slow lock acquisition + - alert: SlowLockAcquisition + expr: | + histogram_quantile(0.95, + rate(docnum_lock_acquisition_duration_ms_bucket[5m]) + ) > 1000 + for: 5m + labels: + severity: warning + annotations: + summary: "P95 lock acquisition time > 1 second" + + # WARNING: High retry count + - alert: HighRetryCount + expr: | + sum by (project) ( + rate(docnum_retry_count_sum[1h]) + ) > 100 + for: 1h + labels: + severity: warning + annotations: + summary: "Retry count > 100 per hour in project {{ $labels.project }}" + + # WARNING: Slow generation + - alert: SlowDocumentNumberGeneration + expr: | + histogram_quantile(0.95, + rate(docnum_generation_duration_ms_bucket[5m]) + ) > 2000 + for: 5m + labels: + severity: warning + annotations: + summary: "P95 generation time > 2 seconds" +``` + +**AlertManager Configuration** (`alertmanager/config.yml`): + +```yaml +route: + group_by: ['alertname', 'severity'] + group_wait: 30s + group_interval: 5m + repeat_interval: 4h + receiver: 'ops-team' + routes: + # CRITICAL alerts → PagerDuty + Slack + - match: + severity: critical + receiver: 'pagerduty-critical' + continue: true + + # WARNING alerts → Slack only + - match: + severity: warning + receiver: 'slack-warnings' + +receivers: + - name: 'pagerduty-critical' + pagerduty_configs: + - service_key: + description: '{{ .CommonAnnotations.summary }}' + + - name: 'slack-warnings' + slack_configs: + - api_url: + channel: '#lcbp3-alerts' + title: '⚠️ {{ .GroupLabels.alertname }}' + text: '{{ .CommonAnnotations.description }}' + + - name: 'ops-team' + email_configs: + - to: 'ops@example.com' +``` + +### 3.11.10.3. Grafana Dashboard + +**Dashboard Configuration** (`grafana/dashboards/document-numbering.json`): + +```json +{ + "title": "Document Numbering Performance", + "panels": [ + { + "title": "Lock Acquisition Success Rate", + "targets": [{ + "expr": "1 - (rate(docnum_lock_acquisition_failures_total[5m]) / rate(docnum_lock_acquisition_total[5m]))" + }], + "type": "graph", + "gridPos": { "x": 0, "y": 0, "w": 12, "h": 8 } + }, + { + "title": "Lock Acquisition Time (Percentiles)", + "targets": [ + { + "expr": "histogram_quantile(0.50, rate(docnum_lock_acquisition_duration_ms_bucket[5m]))", + "legendFormat": "P50" + }, + { + "expr": "histogram_quantile(0.95, rate(docnum_lock_acquisition_duration_ms_bucket[5m]))", + "legendFormat": "P95" + }, + { + "expr": "histogram_quantile(0.99, rate(docnum_lock_acquisition_duration_ms_bucket[5m]))", + "legendFormat": "P99" + } + ], + "type": "graph", + "gridPos": { "x": 12, "y": 0, "w": 12, "h": 8 } + }, + { + "title": "Generation Rate (per minute)", + "targets": [{ + "expr": "sum(rate(docnum_generation_duration_ms_count[1m])) * 60" + }], + "type": "stat", + "gridPos": { "x": 0, "y": 8, "w": 6, "h": 4 } + }, + { + "title": "Redis Connection Status", + "targets": [{ + "expr": "docnum_redis_connection_status" + }], + "type": "stat", + "gridPos": { "x": 6, "y": 8, "w": 6, "h": 4 }, + "thresholds": { + "mode": "absolute", + "steps": [ + { "value": 0, "color": "red" }, + { "value": 1, "color": "green" } + ] + } + }, + { + "title": "Error Rate by Type", + "targets": [{ + "expr": "sum by (reason) (rate(docnum_lock_acquisition_failures_total[5m]))" + }], + "type": "graph", + "gridPos": { "x": 12, "y": 8, "w": 12, "h": 8 } + } + ] +} +``` + +**Key Dashboard Panels**: + +- **Lock Acquisition Success Rate**: Real-time success % +- **Lock Wait Time Percentiles**: P50, P95, P99 latency +- **Generation Rate**: Documents/minute +- **Error Breakdown**: By error type (LOCK_TIMEOUT, VERSION_CONFLICT, etc.) +- **Redis/DB Health**: Connection status +- **Retry Distribution**: Histogram of retry counts ## 3.11.12. API Reference @@ -460,12 +1787,14 @@ ON document_number_counters ( ### 3.11.14.3. Important Notes > **💡 Counter Key Design** +> > - ใช้ `COALESCE(recipient_organization_id, 0)` ใน Primary Key เพื่อรองรับ NULL > - `version` column สำหรับ Optimistic Locking (ป้องกัน race condition) > - `last_number` เริ่มจาก 0 และเพิ่มขึ้นทีละ 1 > - Counter reset ทุกปี (เมื่อ `current_year` เปลี่ยน) > **⚠️ Migration Notes** +> > - ไม่มีข้อมูลเก่า ไม่ต้องทำ backward compatibility > - สามารถสร้าง table ใหม่ได้เลยตาม schema ข้างต้น > - ต้องมี seed data สำหรับ `correspondence_types`, `rfa_types`, `disciplines` ก่อน diff --git a/specs/01-requirements/README.md b/specs/01-requirements/README.md index 3dc38bf..e90336d 100644 --- a/specs/01-requirements/README.md +++ b/specs/01-requirements/README.md @@ -1,58 +1,173 @@ -# 📋 Requirements Specification v1.5.0 +# 📋 Requirements Specification -## Status: first-draft +**Version:** 1.5.1 +**Status:** Active +**Last Updated:** 2025-12-02 -**Date:** 2025-11-30 +--- + +## 📖 Overview + +This directory contains the functional and non-functional requirements for the LCBP3-DMS (Laem Chabang Port Phase 3 - Document Management System). The requirements are organized by functional area and feature. --- ## 📑 Table of Contents -1. [Objectives & Goals](./01-objectives.md) -2. [System Architecture & Technology](./02-architecture.md) -3. [Functional Requirements](./03-functional-requirements.md) - - [3.1 Project & Organization Management](./03.1-project-management.md) - - [3.2 Correspondence Management](./03.2-correspondence.md) - - [3.3 RFA Management](./03.3-rfa.md) - - [3.4 Contract Drawing Management](./03.4-contract-drawing.md) - - [3.5 Shop Drawing Management](./03.5-shop-drawing.md) - - [3.6 Unified Workflow](./03.6-unified-workflow.md) - - [3.7 Transmittals Management](./03.7-transmittals.md) - - [3.8 Circulation Sheet Management](./03.8-circulation-sheet.md) - - [3.9 Revisions Management](./03.9-revisions.md) - - [3.10 File Handling](./03.10-file-handling.md) - - [3.11 Document Numbering](./03.11-document-numbering.md) - - [3.12 JSON Details](./03.12-json-details.md) -4. [Access Control & RBAC](./04-access-control.md) -5. [UI/UX Requirements](./05-ui-ux.md) -6. [Non-Functional Requirements](./06-non-functional.md) -7. [Testing Requirements](./07-testing.md) +### Core Requirements + +1. [Objectives & Goals](./01-objectives.md) - Project objectives and success criteria +2. [System Architecture & Technology](./02-architecture.md) - High-level architecture requirements +3. [Functional Requirements](./03-functional-requirements.md) - Detailed feature specifications + +### Functional Areas + +#### Document Management + +- [3.1 Project & Organization Management](./03.1-project-management.md) - Projects, contracts, organizations +- [3.2 Correspondence Management](./03.2-correspondence.md) - Letters and communications +- [3.3 RFA Management](./03.3-rfa.md) - Request for Approval +- [3.4 Contract Drawing Management](./03.4-contract-drawing.md) - Contract drawings (แบบคู่สัญญา) +- [3.5 Shop Drawing Management](./03.5-shop-drawing.md) - Shop drawings (แบบก่อสร้าง) + +#### Supporting Features + +- [3.6 Unified Workflow](./03.6-unified-workflow.md) - Workflow engine and routing +- [3.7 Transmittals Management](./03.7-transmittals.md) - Document transmittals +- [3.8 Circulation Sheet Management](./03.8-circulation-sheet.md) - Document circulation +- [3.9 Revisions Management](./03.9-revisions.md) - Version control +- [3.10 File Handling](./03.10-file-handling.md) - File storage and processing + +#### **⭐ Document Numbering System** + +- [3.11 Document Numbering](./03.11-document-numbering.md) - **Requirements** + - Automatic number generation + - Template-based formatting + - Concurrent request handling + - Counter management + +**Implementation & Operations:** + +- 📘 [Implementation Guide](../03-implementation/document-numbering.md) - NestJS, TypeORM, Redis code examples +- 📗 [Operations Guide](../04-operations/document-numbering-operations.md) - Monitoring, troubleshooting, runbooks + +#### Technical Details + +- [3.12 JSON Details](./03.12-json-details.md) - JSON field specifications + +### Cross-Cutting Concerns + +4. [Access Control & RBAC](./04-access-control.md) - 4-level hierarchical RBAC +5. [UI/UX Requirements](./05-ui-ux.md) - User interface specifications +6. [Non-Functional Requirements](./06-non-functional.md) - Performance, security, scalability +7. [Testing Requirements](./07-testing.md) - Test strategy and coverage --- ## 🔄 Recent Changes -See [CHANGELOG.md](../../CHANGELOG.md) for detailed version history. +### v1.5.1 (2025-12-02) -### v1.4.5 (2025-11-30) +- ✅ **Reorganized Document Numbering documentation** + - Split into: Requirements → Implementation → Operations + - Created [document-numbering.md](../03-implementation/document-numbering.md) implementation guide + - Created [document-numbering-operations.md](../04-operations/document-numbering-operations.md) ops guide +- ✅ Updated schema to match v1.6.0 requirements +- ✅ Enhanced cross-references between documents + +### v1.5.0 (2025-12-01) - ✅ Added comprehensive security requirements - ✅ Enhanced resilience patterns - ✅ Added performance targets - ⚠️ **Breaking:** Changed document numbering from stored procedure to app-level locking ---- +### v1.4.5 (2025-11-30) -## 📊 Compliance Matrix +- ✅ Initial requirements documentation +- ✅ Functional requirements specified -| Requirement | Status | Owner | Target Release | -| ----------------------------- | ----------- | ------------ | -------------- | -| FR-001: Correspondence CRUD | ✅ Done | Backend Team | v1.0 | -| FR-002: RFA Workflow | In Progress | Backend Team | v1.1 | -| NFR-001: API Response < 200ms | Planned | DevOps | v1.2 | +See [CHANGELOG.md](../../CHANGELOG.md) for detailed version history. --- -## 📬 Feedback +## 📊 Requirements Traceability -Found issues? [Open an issue](https://github.com/your-org/lcbp3-dms/issues/new?template=spec-issue.md) +### By Feature Status + +| Feature Area | Requirements Doc | Status | Implementation | Operations | +|----------------------------|----------------------------------------|-------------|----------------|------------| +| Correspondence Management | [03.2](./03.2-correspondence.md) | ✅ Complete | Planned | N/A | +| RFA Management | [03.3](./03.3-rfa.md) | ✅ Complete | Planned | N/A | +| Workflow Engine | [03.6](./03.6-unified-workflow.md) | ✅ Complete | Planned | N/A | +| **Document Numbering** | [03.11](./03.11-document-numbering.md) | ✅ Complete | [Guide](../03-implementation/document-numbering.md) | [Guide](../04-operations/document-numbering-operations.md) | +| Access Control | [04](./04-access-control.md) | ✅ Complete | Planned | N/A | + +### By Priority + +- **P0 (Critical):** Access Control, Document Numbering +- **P1 (High):** Correspondence, RFA, Workflow Engine +- **P2 (Medium):** Transmittals, Circulation, Search +- **P3 (Low):** Reporting, Analytics + +--- + +## 🎯 Requirements Quality Checklist + +All requirements documents must meet these criteria: + +- [ ] **Clear:** Written in simple, unambiguous language +- [ ] **Testable:** Can be verified through testing +- [ ] **Traceable:** Linked to business objectives +- [ ] **Feasible:** Technically achievable within constraints +- [ ] **Complete:** All edge cases and scenarios covered +- [ ] **Consistent:** No contradictions with other requirements + +--- + +## 📖 Reading Guide + +### For Product Owners / Business Analysts + +1. Start with [Objectives & Goals](./01-objectives.md) +2. Review [Functional Requirements](./03-functional-requirements.md) +3. Check specific feature requirements (3.1-3.12) + +### For Developers + +1. Read requirements document for your feature +2. Check [Implementation Guides](../03-implementation/) for technical details +3. Review [ADRs](../05-decisions/) for architectural decisions +4. Check [Tasks](../06-tasks/) for development breakdown + +### For QA / Testers + +1. Review [Testing Requirements](./07-testing.md) +2. Use requirements as test case source +3. Verify [Non-Functional Requirements](./06-non-functional.md) + +### For Operations Team + +1. Read [Non-Functional Requirements](./06-non-functional.md) for SLAs +2. Check [Operations Guides](../04-operations/) for specific features +3. Review monitoring and alerting requirements + +--- + +## 📬 Feedback & Issues + +**Found issues or have suggestions?** + +- Requirements clarity issues → [Open Issue](https://github.com/your-org/lcbp3-dms/issues/new?template=spec-issue.md) +- Feature requests → Contact Product Owner +- Technical questions → Contact System Architect + +--- + +## 📝 Document Control + +- **Version:** 1.5.1 +- **Owner:** System Architect (Nattanin Peancharoen) +- **Last Review:** 2025-12-02 +- **Next Review:** 2026-01-01 +- **Classification:** Internal Use Only diff --git a/specs/02-architecture/README.md b/specs/02-architecture/README.md index 4bf0980..92762ea 100644 --- a/specs/02-architecture/README.md +++ b/specs/02-architecture/README.md @@ -1,4 +1,4 @@ -# 📋 Architecture Specification v1.5.0 +# 📋 Architecture Specification > **สถาปัตยกรรมระบบ LCBP3-DMS** > @@ -10,9 +10,9 @@ | Attribute | Value | | ------------------ | -------------------------------- | -| **Version** | 1.5.0 | -| **Status** | First Draft | -| **Last Updated** | 2025-11-30 | +| **Version** | 1.5.1 | +| **Status** | Active | +| **Last Updated** | 2025-12-02 | | **Owner** | Nattanin Peancharoen | | **Classification** | Internal Technical Documentation | @@ -251,9 +251,34 @@ Layer 6: File Security (Virus Scanning, Access Control) - Workflow Versioning - Polymorphic Entity Relationships -**Related:** [specs/05-decisions/001-workflow-engine.md](../05-decisions/001-workflow-engine.md) +**Related:** [ADR-001](../05-decisions/ADR-001-unified-workflow-engine.md) -### ADR-002: Two-Phase File Storage +### ADR-002: Document Numbering Strategy + +**Decision:** ใช้ Application-Level Locking แทน Database Stored Procedure + +**Rationale:** + +- ยืดหยุ่นกว่า (Template-Based Generator) +- ง่ายต่อการ Debug และ Monitoring +- รองรับ Complex Numbering Rules +- Support ทุกประเภทเอกสาร (LETTER, RFA, TRANSMITTAL, etc.) + +**Implementation:** + +- **Layer 1:** Redis Redlock (Distributed Lock) +- **Layer 2:** Optimistic Database Lock (`@VersionColumn()`) +- **Retry:** Exponential Backoff with Jitter +- **Counter Key:** Composite PK (8 columns) + +**Documentation:** +- 📋 [Requirements](../01-requirements/03.11-document-numbering.md) +- 📘 [Implementation Guide](../03-implementation/document-numbering.md) +- 📗 [Operations Guide](../04-operations/document-numbering-operations.md) + +**Related:** [ADR-002](../05-decisions/ADR-002-document-numbering-strategy.md) + +### ADR-003: Two-Phase File Storage **Decision:** แยกการอัปโหลดไฟล์เป็น 2 ขั้นตอน (Upload → Commit) @@ -269,23 +294,7 @@ Layer 6: File Security (Virus Scanning, Access Control) 2. Phase 2: Commit to `permanent/` when operation succeeds 3. Cleanup: Cron Job ลบไฟล์ค้างใน `temp/` > 24h -**Related:** [specs/05-decisions/002-file-storage.md](../05-decisions/002-file-storage.md) - -### ADR-003: Document Numbering Strategy - -**Decision:** ใช้ Application-Level Locking แทน Database Stored Procedure - -**Rationale:** - -- ยืดหยุ่นกว่า (Token-Based Generator) -- ง่ายต่อการ Debug -- รองรับ Complex Numbering Rules - -**Implementation:** - -- Layer 1: Redis Distributed Lock -- Layer 2: Optimistic Database Lock (`@VersionColumn()`) -- Retry with Exponential Backoff +**Related:** [ADR-003](../05-decisions/ADR-003-file-storage-approach.md) ### ADR-004: 4-Level RBAC @@ -303,6 +312,8 @@ Layer 6: File Security (Virus Scanning, Access Control) - Redis Cache for Performance - Permission Checking at Guard Level +**Related:** [ADR-004](../05-decisions/ADR-004-rbac-implementation.md) + --- ## 📊 Architecture Diagrams diff --git a/specs/02-architecture/api-design.md b/specs/02-architecture/api-design.md index 5e02214..acf7a97 100644 --- a/specs/02-architecture/api-design.md +++ b/specs/02-architecture/api-design.md @@ -3,10 +3,10 @@ --- **title:** 'API Design' -**version:** 1.5.0 -**status:** first-draft +**version:** 1.5.1 +**status:** active **owner:** Nattanin Peancharoen -**last_updated:** 2025-11-30 +**last_updated:** 2025-12-02 **related:** - specs/01-requirements/02-architecture.md @@ -545,7 +545,7 @@ X-API-Deprecation-Info: https://docs.np-dms.work/migration/v2 **Document Control:** -- **Version:** 1.5.0 -- **Status:** First Draft -- **Last Updated:** 2025-11-30 +- **Version:** 1.5.1 +- **Status:** Active +- **Last Updated:** 2025-12-02 - **Owner:** Nattanin Peancharoen diff --git a/specs/02-architecture/system-architecture.md b/specs/02-architecture/system-architecture.md index bce9d63..19f8727 100644 --- a/specs/02-architecture/system-architecture.md +++ b/specs/02-architecture/system-architecture.md @@ -44,7 +44,7 @@ - RAM: 32GB - Storage: /share/dms-data - **IP Address:** 159.192.126.103 -- **Domain:** np-dms.work, www.np-dms.work +- **Domain:** np-dms.work, - **Containerization:** Docker & Docker Compose via Container Station - **Development Environment:** VS Code/Cursor on Windows 11 @@ -943,9 +943,9 @@ graph LR **Document Control:** -- **Version:** 1.5.0 -- **Status:** First Draft -- **Last Updated:** 2025-11-30 +- **Version:** 1.5.1 +- **Status:** Active +- **Last Updated:** 2025-12-02 - **Owner:** Nattanin Peancharoen ``` diff --git a/specs/03-implementation/document-numbering.md b/specs/03-implementation/document-numbering.md new file mode 100644 index 0000000..e06f192 --- /dev/null +++ b/specs/03-implementation/document-numbering.md @@ -0,0 +1,627 @@ +# Document Numbering Implementation Guide + +--- +title: 'Implementation Guide: Document Numbering System' +version: 1.5.1 +status: draft +owner: Development Team +last_updated: 2025-12-02 +related: + +- specs/01-requirements/03.11-document-numbering.md +- specs/04-operations/document-numbering-operations.md + +--- + +## Overview + +เอกสารนี้อธิบาย implementation details สำหรับระบบ Document Numbering ตาม requirements ใน [03.11-document-numbering.md](file:///e:/np-dms/lcbp3/specs/01-requirements/03.11-document-numbering.md) + +## Technology Stack + +- **Backend Framework**: NestJS 10.x +- **ORM**: TypeORM 0.3.x +- **Database**: MariaDB 10.11 +- **Cache/Lock**: Redis 7.x + Redlock +- **Message Queue**: BullMQ +- **Monitoring**: Prometheus + Grafana + +## 1. Database Implementation + +### 1.1. Counter Table Schema + +```sql +CREATE TABLE document_number_counters ( + project_id INT NOT NULL, + originator_organization_id INT NOT NULL, + recipient_organization_id INT NULL, + correspondence_type_id INT NOT NULL, + sub_type_id INT DEFAULT 0, + rfa_type_id INT DEFAULT 0, + discipline_id INT DEFAULT 0, + current_year INT NOT NULL, + version INT DEFAULT 0 NOT NULL, + last_number INT DEFAULT 0, + + PRIMARY KEY ( + project_id, + originator_organization_id, + COALESCE(recipient_organization_id, 0), + correspondence_type_id, + sub_type_id, + rfa_type_id, + discipline_id, + current_year + ), + + FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE, + FOREIGN KEY (originator_organization_id) REFERENCES organizations(id) ON DELETE CASCADE, + FOREIGN KEY (recipient_organization_id) REFERENCES organizations(id) ON DELETE CASCADE, + FOREIGN KEY (correspondence_type_id) REFERENCES correspondence_types(id) ON DELETE CASCADE, + + INDEX idx_counter_lookup (project_id, correspondence_type_id, current_year), + INDEX idx_counter_org (originator_organization_id, current_year), + + CONSTRAINT chk_last_number_positive CHECK (last_number >= 0), + CONSTRAINT chk_current_year_valid CHECK (current_year BETWEEN 2020 AND 2100) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci + COMMENT='ตารางเก็บ Running Number Counters'; +``` + +### 1.2. Audit Table Schema + +```sql +CREATE TABLE document_number_audit ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + document_id INT NOT NULL, + generated_number VARCHAR(100) NOT NULL, + counter_key JSON NOT NULL COMMENT 'Counter key used (JSON format)', + template_used VARCHAR(200) NOT NULL, + user_id INT NOT NULL, + ip_address VARCHAR(45), + user_agent TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + -- Performance & Error Tracking + retry_count INT DEFAULT 0, + lock_wait_ms INT COMMENT 'Lock acquisition time in milliseconds', + total_duration_ms INT COMMENT 'Total generation time', + fallback_used ENUM('NONE', 'DB_LOCK', 'RETRY') DEFAULT 'NONE', + + INDEX idx_document_id (document_id), + INDEX idx_user_id (user_id), + INDEX idx_created_at (created_at), + FOREIGN KEY (document_id) REFERENCES documents(id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users(id) +) ENGINE=InnoDB COMMENT='Document Number Generation Audit Trail'; +``` + +### 1.3. Error Log Table + +```sql +CREATE TABLE document_number_errors ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + error_type ENUM( + 'LOCK_TIMEOUT', + 'VERSION_CONFLICT', + 'DB_ERROR', + 'REDIS_ERROR', + 'VALIDATION_ERROR' + ) NOT NULL, + error_message TEXT, + stack_trace TEXT, + context_data JSON COMMENT 'Request context (user, project, etc.)', + user_id INT, + ip_address VARCHAR(45), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + resolved_at TIMESTAMP NULL, + + INDEX idx_error_type (error_type), + INDEX idx_created_at (created_at), + INDEX idx_user_id (user_id) +) ENGINE=InnoDB COMMENT='Document Numbering Error Log'; +``` + +## 2. NestJS Implementation + +### 2.1. Module Structure + +``` +src/modules/document-numbering/ +├── document-numbering.module.ts +├── controllers/ +│ └── document-numbering.controller.ts +├── services/ +│ ├── document-numbering.service.ts +│ ├── document-numbering-lock.service.ts +│ ├── counter.service.ts +│ ├── template.service.ts +│ └── audit.service.ts +├── entities/ +│ ├── document-number-counter.entity.ts +│ ├── document-number-audit.entity.ts +│ └── document-number-error.entity.ts +├── dto/ +│ ├── generate-number.dto.ts +│ └── update-template.dto.ts +├── validators/ +│ └── template.validator.ts +├── jobs/ +│ └── counter-reset.job.ts +└── metrics/ + └── metrics.service.ts +``` + +### 2.2. TypeORM Entity + +```typescript +// File: src/modules/document-numbering/entities/document-number-counter.entity.ts +import { Entity, Column, PrimaryColumn, VersionColumn } from 'typeorm'; + +@Entity('document_number_counters') +export class DocumentNumberCounter { + @PrimaryColumn({ name: 'project_id' }) + projectId: number; + + @PrimaryColumn({ name: 'originator_organization_id' }) + originatorOrganizationId: number; + + @PrimaryColumn({ name: 'recipient_organization_id', nullable: true }) + recipientOrganizationId: number | null; + + @PrimaryColumn({ name: 'correspondence_type_id' }) + correspondenceTypeId: number; + + @PrimaryColumn({ name: 'sub_type_id', default: 0 }) + subTypeId: number; + + @PrimaryColumn({ name: 'rfa_type_id', default: 0 }) + rfaTypeId: number; + + @PrimaryColumn({ name: 'discipline_id', default: 0 }) + disciplineId: number; + + @PrimaryColumn({ name: 'current_year' }) + currentYear: number; + + @VersionColumn({ name: 'version' }) + version: number; + + @Column({ name: 'last_number', default: 0 }) + lastNumber: number; +} +``` + +### 2.3. Redis Lock Service + +```typescript +// File: src/modules/document-numbering/services/document-numbering-lock.service.ts +import { Injectable, Logger } from '@nestjs/common'; +import Redlock from 'redlock'; +import { InjectRedis } from '@nestjs-modules/ioredis'; +import { Redis } from 'ioredis'; + +interface CounterKey { + projectId: number; + originatorOrgId: number; + recipientOrgId: number | null; + correspondenceTypeId: number; + subTypeId: number; + rfaTypeId: number; + disciplineId: number; + year: number; +} + +@Injectable() +export class DocumentNumberingLockService { + private readonly logger = new Logger(DocumentNumberingLockService.name); + private redlock: Redlock; + + constructor(@InjectRedis() private readonly redis: Redis) { + this.redlock = new Redlock([redis], { + driftFactor: 0.01, + retryCount: 5, + retryDelay: 100, + retryJitter: 50, + }); + } + + async acquireLock(counterKey: CounterKey): Promise { + const lockKey = this.buildLockKey(counterKey); + const ttl = 5000; // 5 วินาที + + try { + const lock = await this.redlock.acquire([lockKey], ttl); + this.logger.debug(`Acquired lock: ${lockKey}`); + return lock; + } catch (error) { + this.logger.error(`Failed to acquire lock: ${lockKey}`, error); + throw error; + } + } + + async releaseLock(lock: Redlock.Lock): Promise { + try { + await lock.release(); + this.logger.debug('Released lock'); + } catch (error) { + this.logger.warn('Failed to release lock (may have expired)', error); + } + } + + private buildLockKey(key: CounterKey): string { + return `lock:docnum:${key.projectId}:${key.originatorOrgId}:` + + `${key.recipientOrgId ?? 0}:${key.correspondenceTypeId}:` + + `${key.subTypeId}:${key.rfaTypeId}:${key.disciplineId}:${key.year}`; + } +} +``` + +### 2.4. Counter Service + +```typescript +// File: src/modules/document-numbering/services/counter.service.ts +import { Injectable, ConflictException, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository, DataSource } from 'typeorm'; +import { DocumentNumberCounter } from '../entities/document-number-counter.entity'; +import { OptimisticLockVersionMismatchError } from 'typeorm'; + +@Injectable() +export class CounterService { + private readonly logger = new Logger(CounterService.name); + + constructor( + @InjectRepository(DocumentNumberCounter) + private counterRepo: Repository, + private dataSource: DataSource, + ) {} + + async incrementCounter(counterKey: CounterKey): Promise { + const MAX_RETRIES = 2; + + for (let attempt = 0; attempt < MAX_RETRIES; attempt++) { + try { + return await this.dataSource.transaction(async (manager) => { + // ใช้ Optimistic Locking + const counter = await manager.findOne(DocumentNumberCounter, { + where: this.buildWhereClause(counterKey), + }); + + if (!counter) { + // สร้าง counter ใหม่ + const newCounter = manager.create(DocumentNumberCounter, { + ...counterKey, + lastNumber: 1, + version: 0, + }); + await manager.save(newCounter); + return 1; + } + + counter.lastNumber += 1; + await manager.save(counter); // Auto-check version + return counter.lastNumber; + }); + } catch (error) { + if (error instanceof OptimisticLockVersionMismatchError) { + this.logger.warn( + `Version conflict, retry ${attempt + 1}/${MAX_RETRIES}`, + ); + if (attempt === MAX_RETRIES - 1) { + throw new ConflictException('เลขที่เอกสารถูกเปลี่ยน กรุณาลองใหม่'); + } + continue; + } + throw error; + } + } + } + + private buildWhereClause(key: CounterKey) { + return { + projectId: key.projectId, + originatorOrganizationId: key.originatorOrgId, + recipientOrganizationId: key.recipientOrgId, + correspondenceTypeId: key.correspondenceTypeId, + subTypeId: key.subTypeId, + rfaTypeId: key.rfaTypeId, + disciplineId: key.disciplineId, + currentYear: key.year, + }; + } +} +``` + +### 2.5. Main Service with Retry Logic + +```typescript +// File: src/modules/document-numbering/services/document-numbering.service.ts +import { Injectable, ServiceUnavailableException, Logger } from '@nestjs/common'; +import { DocumentNumberingLockService } from './document-numbering-lock.service'; +import { CounterService } from './counter.service'; +import { AuditService } from './audit.service'; +import { RedisConnectionError } from 'ioredis'; + +@Injectable() +export class DocumentNumberingService { + private readonly logger = new Logger(DocumentNumberingService.name); + + constructor( + private lockService: DocumentNumberingLockService, + private counterService: CounterService, + private auditService: AuditService, + ) {} + + async generateDocumentNumber(dto: GenerateNumberDto): Promise { + const startTime = Date.now(); + let lockWaitMs = 0; + let retryCount = 0; + let fallbackUsed = 'NONE'; + + try { + // พยายามใช้ Redis lock ก่อน + return await this.generateWithRedisLock(dto); + } catch (error) { + if (error instanceof RedisConnectionError) { + // Fallback: ใช้ database lock + this.logger.warn('Redis unavailable, falling back to DB lock'); + fallbackUsed = 'DB_LOCK'; + return await this.generateWithDbLock(dto); + } + throw error; + } finally { + // บันทึก audit log + await this.auditService.logGeneration({ + documentId: dto.documentId, + counterKey: dto.counterKey, + lockWaitMs, + totalDurationMs: Date.now() - startTime, + fallbackUsed, + retryCount, + }); + } + } + + private async generateWithRedisLock(dto: GenerateNumberDto): Promise { + const lock = await this.lockService.acquireLock(dto.counterKey); + + try { + const nextNumber = await this.counterService.incrementCounter(dto.counterKey); + return this.formatNumber(dto.template, nextNumber, dto.counterKey); + } finally { + await this.lockService.releaseLock(lock); + } + } + + private async generateWithDbLock(dto: GenerateNumberDto): Promise { + // ใช้ pessimistic lock + // Implementation details... + } + + private formatNumber(template: string, seq: number, key: CounterKey): string { + // Template formatting logic + // Example: `คคง.-สคฉ.3-0001-2568` + return template + .replace('{SEQ:4}', seq.toString().padStart(4, '0')) + .replace('{YEAR:B.E.}', (key.year + 543).toString()); + // ... more replacements + } +} +``` + +## 3. Template Validation + +```typescript +// File: src/modules/document-numbering/validators/template.validator.ts +import { Injectable } from '@nestjs/common'; + +interface ValidationResult { + valid: boolean; + errors: string[]; +} + +@Injectable() +export class TemplateValidator { + private readonly ALLOWED_TOKENS = [ + 'PROJECT', 'ORIGINATOR', 'RECIPIENT', 'CORR_TYPE', + 'SUB_TYPE', 'RFA_TYPE', 'DISCIPLINE', 'SEQ', 'YEAR', 'REV', + ]; + + validate(template: string, correspondenceType: string): ValidationResult { + const tokens = this.extractTokens(template); + const errors: string[] = []; + + // ตรวจสอบ Token ที่ไม่รู้จัก + for (const token of tokens) { + if (!this.ALLOWED_TOKENS.includes(token.name)) { + errors.push(`Unknown token: {${token.name}}`); + } + } + + // กฎพิเศษสำหรับแต่ละประเภท + if (correspondenceType === 'RFA') { + if (!tokens.some((t) => t.name === 'PROJECT')) { + errors.push('RFA template ต้องมี {PROJECT}'); + } + if (!tokens.some((t) => t.name === 'DISCIPLINE')) { + errors.push('RFA template ต้องมี {DISCIPLINE}'); + } + } + + if (correspondenceType === 'TRANSMITTAL') { + if (!tokens.some((t) => t.name === 'SUB_TYPE')) { + errors.push('TRANSMITTAL template ต้องมี {SUB_TYPE}'); + } + } + + // ทุก template ต้องมี {SEQ} + if (!tokens.some((t) => t.name.startsWith('SEQ'))) { + errors.push('Template ต้องมี {SEQ:n}'); + } + + return { valid: errors.length === 0, errors }; + } + + private extractTokens(template: string) { + const regex = /\{([^}]+)\}/g; + const tokens: Array<{ name: string; full: string }> = []; + let match; + + while ((match = regex.exec(template)) !== null) { + const tokenName = match[1].split(':')[0]; // SEQ:4 → SEQ + tokens.push({ name: tokenName, full: match[1] }); + } + + return tokens; + } +} +``` + +## 4. BullMQ Job for Counter Reset + +```typescript +// File: src/modules/document-numbering/jobs/counter-reset.job.ts +import { Processor, WorkerHost } from '@nestjs/bullmq'; +import { Injectable, Logger } from '@nestjs/common'; +import { Cron } from '@nestjs/schedule'; + +@Processor('document-numbering') +@Injectable() +export class CounterResetJob extends WorkerHost { + private readonly logger = new Logger(CounterResetJob.name); + + @Cron('0 0 1 1 *') // 1 Jan every year at 00:00 + async handleYearlyReset() { + const newYear = new Date().getFullYear(); + + // ไม่ต้อง reset counter เพราะ counter แยกตาม current_year อยู่แล้ว + // แค่เตรียม counter สำหรับปีใหม่ + this.logger.log(`Year changed to ${newYear}, counters are ready`); + + // สามารถทำ cleanup counter ปีเก่าได้ (optional) + // await this.cleanupOldCounters(newYear - 5); // เก็บ 5 ปี + } + + async process() { + // BullMQ job processing + } +} +``` + +## 5. API Controller + +```typescript +// File: src/modules/document-numbering/controllers/document-numbering.controller.ts +import { Controller, Post, Put, Body, Param, UseGuards } from '@nestjs/common'; +import { ThrottlerGuard } from '@nestjs/throttler'; +import { Throttle } from '@nestjs/throttler'; +import { DocumentNumberingService } from '../services/document-numbering.service'; +import { Roles } from 'src/auth/decorators/roles.decorator'; + +@Controller('document-numbering') +@UseGuards(ThrottlerGuard) +export class DocumentNumberingController { + constructor( + private readonly documentNumberingService: DocumentNumberingService, + ) {} + + @Post('generate') + @Throttle(10, 60) // 10 requests per 60 seconds + async generateNumber(@Body() dto: GenerateNumberDto) { + const number = await this.documentNumberingService.generateDocumentNumber(dto); + return { documentNumber: number }; + } + + @Put('configs/:configId') + @Roles('PROJECT_ADMIN') + async updateTemplate( + @Param('configId') configId: number, + @Body() dto: UpdateTemplateDto, + ) { + // Update template configuration + } + + @Post('configs/:configId/reset-counter') + @Roles('SUPER_ADMIN') + async resetCounter( + @Param('configId') configId: number, + @Body() dto: ResetCounterDto, + ) { + // Manual counter reset (requires approval) + } +} +``` + +## 6. Module Configuration + +```typescript +// File: src/modules/document-numbering/document-numbering.module.ts +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { BullModule } from '@nestjs/bullmq'; +import { ThrottlerModule } from '@nestjs/throttler'; +import { DocumentNumberCounter } from './entities/document-number-counter.entity'; +import { DocumentNumberAudit } from './entities/document-number-audit.entity'; +import { DocumentNumberError } from './entities/document-number-error.entity'; +import { DocumentNumberingService } from './services/document-numbering.service'; +import { DocumentNumberingLockService } from './services/document-numbering-lock.service'; +import { CounterService } from './services/counter.service'; +import { AuditService } from './services/audit.service'; +import { TemplateValidator } from './validators/template.validator'; +import { CounterResetJob } from './jobs/counter-reset.job'; +import { DocumentNumberingController } from './controllers/document-numbering.controller'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([ + DocumentNumberCounter, + DocumentNumberAudit, + DocumentNumberError, + ]), + BullModule.registerQueue({ + name: 'document-numbering', + }), + ThrottlerModule.forRoot({ + ttl: 60, + limit: 10, + }), + ], + controllers: [DocumentNumberingController], + providers: [ + DocumentNumberingService, + DocumentNumberingLockService, + CounterService, + AuditService, + TemplateValidator, + CounterResetJob, + ], + exports: [DocumentNumberingService], +}) +export class DocumentNumberingModule {} +``` + +## 7. Environment Configuration + +```typescript +// .env.example +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_PASSWORD= + +DB_HOST=localhost +DB_PORT=3306 +DB_USERNAME=lcbp3 +DB_PASSWORD= +DB_DATABASE=lcbp3_db +DB_POOL_SIZE=20 + +# Prometheus +PROMETHEUS_PORT=9090 +``` + +## References + +- [Requirements](file:///e:/np-dms/lcbp3/specs/01-requirements/03.11-document-numbering.md) +- [Operations Guide](file:///e:/np-dms/lcbp3/specs/04-operations/document-numbering-operations.md) +- [Backend Guidelines](file:///e:/np-dms/lcbp3/specs/03-implementation/backend-guidelines.md) diff --git a/specs/04-operations/README.md b/specs/04-operations/README.md index 5fd04a2..a2e7854 100644 --- a/specs/04-operations/README.md +++ b/specs/04-operations/README.md @@ -1,8 +1,8 @@ # Operations Documentation **Project:** LCBP3-DMS (Laem Chabang Port Phase 3 - Document Management System) -**Version:** 1.5.0 -**Last Updated:** 2025-12-01 +**Version:** 1.5.1 +**Last Updated:** 2025-12-02 --- @@ -185,6 +185,6 @@ graph TB --- -**Version:** 1.5.0 +**Version:** 1.5.1 **Status:** Active **Classification:** Internal Use Only diff --git a/specs/04-operations/backup-recovery.md b/specs/04-operations/backup-recovery.md index 58556b6..022a6e1 100644 --- a/specs/04-operations/backup-recovery.md +++ b/specs/04-operations/backup-recovery.md @@ -1,8 +1,8 @@ # Backup & Recovery Procedures **Project:** LCBP3-DMS -**Version:** 1.5.0 -**Last Updated:** 2025-12-01 +**Version:** 1.5.1 +**Last Updated:** 2025-12-02 --- @@ -369,6 +369,6 @@ WHERE created_at < DATE_SUB(NOW(), INTERVAL 1 YEAR); --- -**Version:** 1.5.0 +**Version:** 1.5.1 **Last Review:** 2025-12-01 **Next Review:** 2026-03-01 diff --git a/specs/04-operations/deployment-guide.md b/specs/04-operations/deployment-guide.md new file mode 100644 index 0000000..0705bdf --- /dev/null +++ b/specs/04-operations/deployment-guide.md @@ -0,0 +1,937 @@ +# Deployment Guide: LCBP3-DMS + +--- + +**Project:** LCBP3-DMS (Laem Chabang Port Phase 3 - Document Management System) +**Version:** 1.5.1 +**Last Updated:** 2025-12-02 +**Owner:** Operations Team +**Status:** Active + +--- + +## 📋 Overview + +This guide provides step-by-step instructions for deploying the LCBP3-DMS system on QNAP Container Station using Docker Compose with Blue-Green deployment strategy. + +### Deployment Strategy + +- **Platform:** QNAP TS-473A with Container Station +- **Orchestration:** Docker Compose +- **Deployment Method:** Blue-Green Deployment +- **Zero Downtime:** Yes +- **Rollback Capability:** Instant rollback via NGINX switch + +--- + +## 🎯 Prerequisites + +### Hardware Requirements + +| Component | Minimum Specification | +| -------------- | -------------------------- | +| CPU | 4 cores @ 2.0 GHz | +| RAM | 16 GB | +| Storage | 500 GB SSD (System + Data) | +| Network | 1 Gbps Ethernet | +| QNAP Model | TS-473A or equivalent | + +### Software Requirements + +| Software | Version | Purpose | +| ----------------- | ------- | ------------------------ | +| QNAP QTS | 5.x+ | Operating System | +| Container Station | 3.x+ | Docker Management | +| Docker | 20.10+ | Container Runtime | +| Docker Compose | 2.x+ | Multi-container Orchestr | + +### Network Requirements + +- Static IP address for QNAP server +- Domain name (e.g., `lcbp3-dms.example.com`) +- SSL certificate (Let's Encrypt or commercial) +- Firewall rules: + - Port 80 (HTTP → HTTPS redirect) + - Port 443 (HTTPS) + - Port 22 (SSH for management) + +--- + +## 🏗️ Infrastructure Setup + +### 1. Directory Structure + +Create the following directory structure on QNAP: + +```bash +# SSH into QNAP +ssh admin@qnap-ip + +# Create base directory +mkdir -p /volume1/lcbp3 + +# Create blue-green environments +mkdir -p /volume1/lcbp3/blue +mkdir -p /volume1/lcbp3/green + +# Create shared directories +mkdir -p /volume1/lcbp3/shared/uploads +mkdir -p /volume1/lcbp3/shared/logs +mkdir -p /volume1/lcbp3/shared/backups + +# Create persistent volumes +mkdir -p /volume1/lcbp3/volumes/mariadb-data +mkdir -p /volume1/lcbp3/volumes/redis-data +mkdir -p /volume1/lcbp3/volumes/elastic-data + +# Create NGINX proxy directory +mkdir -p /volume1/lcbp3/nginx-proxy + +# Set permissions +chmod -R 755 /volume1/lcbp3 +chown -R admin:administrators /volume1/lcbp3 +``` + +**Final Structure:** + +``` +/volume1/lcbp3/ +├── blue/ # Blue environment +│ ├── docker-compose.yml +│ ├── .env.production +│ └── nginx.conf +│ +├── green/ # Green environment +│ ├── docker-compose.yml +│ ├── .env.production +│ └── nginx.conf +│ +├── nginx-proxy/ # Main reverse proxy +│ ├── docker-compose.yml +│ ├── nginx.conf +│ └── ssl/ +│ ├── cert.pem +│ └── key.pem +│ +├── shared/ # Shared across blue/green +│ ├── uploads/ +│ ├── logs/ +│ └── backups/ +│ +├── volumes/ # Persistent data +│ ├── mariadb-data/ +│ ├── redis-data/ +│ └── elastic-data/ +│ +├── scripts/ # Deployment scripts +│ ├── deploy.sh +│ ├── rollback.sh +│ └── health-check.sh +│ +└── current # File containing "blue" or "green" +``` + +### 2. SSL Certificate Setup + +```bash +# Option 1: Let's Encrypt (Recommended) +# Install certbot on QNAP +opkg install certbot + +# Generate certificate +certbot certonly --standalone \ + -d lcbp3-dms.example.com \ + --email admin@example.com \ + --agree-tos + +# Copy to nginx-proxy +cp /etc/letsencrypt/live/lcbp3-dms.example.com/fullchain.pem \ + /volume1/lcbp3/nginx-proxy/ssl/cert.pem +cp /etc/letsencrypt/live/lcbp3-dms.example.com/privkey.pem \ + /volume1/lcbp3/nginx-proxy/ssl/key.pem + +# Option 2: Commercial Certificate +# Upload cert.pem and key.pem to /volume1/lcbp3/nginx-proxy/ssl/ +``` + +--- + +## 📝 Configuration Files + +### 1. Environment Variables (.env.production) + +Create `.env.production` in both `blue/` and `green/` directories: + +```bash +# File: /volume1/lcbp3/blue/.env.production +# DO NOT commit this file to Git! + +# Application +NODE_ENV=production +APP_NAME=LCBP3-DMS +APP_URL=https://lcbp3-dms.example.com + +# Database +DB_HOST=lcbp3-mariadb +DB_PORT=3306 +DB_USERNAME=lcbp3_user +DB_PASSWORD= +DB_DATABASE=lcbp3_dms +DB_POOL_SIZE=20 + +# Redis +REDIS_HOST=lcbp3-redis +REDIS_PORT=6379 +REDIS_PASSWORD= +REDIS_DB=0 + +# JWT Authentication +JWT_SECRET= +JWT_EXPIRES_IN=8h +JWT_REFRESH_EXPIRES_IN=7d + +# File Storage +UPLOAD_PATH=/app/uploads +MAX_FILE_SIZE=52428800 +ALLOWED_FILE_TYPES=.pdf,.doc,.docx,.xls,.xlsx,.dwg,.zip + +# Email (SMTP) +SMTP_HOST=smtp.gmail.com +SMTP_PORT=587 +SMTP_SECURE=false +SMTP_USERNAME= +SMTP_PASSWORD= +SMTP_FROM=noreply@example.com + +# Elasticsearch +ELASTICSEARCH_NODE=http://lcbp3-elasticsearch:9200 +ELASTICSEARCH_USERNAME=elastic +ELASTICSEARCH_PASSWORD= + +# Rate Limiting +THROTTLE_TTL=60 +THROTTLE_LIMIT=100 + +# Logging +LOG_LEVEL=info +LOG_FILE_PATH=/app/logs + +# ClamAV (Virus Scanning) +CLAMAV_HOST=lcbp3-clamav +CLAMAV_PORT=3310 +``` + +### 2. Docker Compose - Blue Environment + +```yaml +# File: /volume1/lcbp3/blue/docker-compose.yml +version: '3.8' + +services: + backend: + image: lcbp3-backend:latest + container_name: lcbp3-blue-backend + restart: unless-stopped + env_file: + - .env.production + volumes: + - /volume1/lcbp3/shared/uploads:/app/uploads + - /volume1/lcbp3/shared/logs:/app/logs + depends_on: + - mariadb + - redis + - elasticsearch + networks: + - lcbp3-network + healthcheck: + test: ['CMD', 'curl', '-f', 'http://localhost:3000/health'] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + frontend: + image: lcbp3-frontend:latest + container_name: lcbp3-blue-frontend + restart: unless-stopped + environment: + - NEXT_PUBLIC_API_URL=https://lcbp3-dms.example.com/api + depends_on: + - backend + networks: + - lcbp3-network + healthcheck: + test: ['CMD', 'curl', '-f', 'http://localhost:3000'] + interval: 30s + timeout: 10s + retries: 3 + + mariadb: + image: mariadb:10.11 + container_name: lcbp3-mariadb + restart: unless-stopped + environment: + MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} + MYSQL_DATABASE: ${DB_DATABASE} + MYSQL_USER: ${DB_USERNAME} + MYSQL_PASSWORD: ${DB_PASSWORD} + volumes: + - /volume1/lcbp3/volumes/mariadb-data:/var/lib/mysql + networks: + - lcbp3-network + command: > + --character-set-server=utf8mb4 + --collation-server=utf8mb4_unicode_ci + --max_connections=200 + --innodb_buffer_pool_size=2G + healthcheck: + test: ['CMD', 'mysqladmin', 'ping', '-h', 'localhost'] + interval: 10s + timeout: 5s + retries: 5 + + redis: + image: redis:7-alpine + container_name: lcbp3-redis + restart: unless-stopped + command: > + redis-server + --requirepass ${REDIS_PASSWORD} + --appendonly yes + --appendfsync everysec + --maxmemory 2gb + --maxmemory-policy allkeys-lru + volumes: + - /volume1/lcbp3/volumes/redis-data:/data + networks: + - lcbp3-network + healthcheck: + test: ['CMD', 'redis-cli', 'ping'] + interval: 10s + timeout: 3s + retries: 3 + + elasticsearch: + image: elasticsearch:8.11.0 + container_name: lcbp3-elasticsearch + restart: unless-stopped + environment: + - discovery.type=single-node + - xpack.security.enabled=true + - ELASTIC_PASSWORD=${ELASTICSEARCH_PASSWORD} + - "ES_JAVA_OPTS=-Xms2g -Xmx2g" + volumes: + - /volume1/lcbp3/volumes/elastic-data:/usr/share/elasticsearch/data + networks: + - lcbp3-network + healthcheck: + test: ['CMD-SHELL', 'curl -f http://localhost:9200/_cluster/health || exit 1'] + interval: 30s + timeout: 10s + retries: 5 + +networks: + lcbp3-network: + name: lcbp3-blue-network + driver: bridge +``` + +### 3. Docker Compose - NGINX Proxy + +```yaml +# File: /volume1/lcbp3/nginx-proxy/docker-compose.yml +version: '3.8' + +services: + nginx: + image: nginx:alpine + container_name: lcbp3-nginx + restart: unless-stopped + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf:ro + - ./ssl:/etc/nginx/ssl:ro + - /volume1/lcbp3/shared/logs/nginx:/var/log/nginx + networks: + - lcbp3-blue-network + - lcbp3-green-network + healthcheck: + test: ['CMD', 'nginx', '-t'] + interval: 30s + timeout: 10s + retries: 3 + +networks: + lcbp3-blue-network: + external: true + lcbp3-green-network: + external: true +``` + +### 4. NGINX Configuration + +```nginx +# File: /volume1/lcbp3/nginx-proxy/nginx.conf + +user nginx; +worker_processes auto; +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + client_max_body_size 50M; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_types text/plain text/css text/xml text/javascript + application/json application/javascript application/xml+rss; + + # Upstream backends (switch between blue/green) + upstream backend { + server lcbp3-blue-backend:3000 max_fails=3 fail_timeout=30s; + keepalive 32; + } + + upstream frontend { + server lcbp3-blue-frontend:3000 max_fails=3 fail_timeout=30s; + keepalive 32; + } + + # HTTP to HTTPS redirect + server { + listen 80; + server_name lcbp3-dms.example.com; + return 301 https://$server_name$request_uri; + } + + # HTTPS server + server { + listen 443 ssl http2; + server_name lcbp3-dms.example.com; + + # SSL configuration + ssl_certificate /etc/nginx/ssl/cert.pem; + ssl_certificate_key /etc/nginx/ssl/key.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + + # Security headers + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + + # Frontend (Next.js) + location / { + proxy_pass http://frontend; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + } + + # Backend API + location /api { + proxy_pass http://backend; + proxy_http_version 1.1; + proxy_set_header Connection ""; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # Timeouts for file uploads + proxy_connect_timeout 300s; + proxy_send_timeout 300s; + proxy_read_timeout 300s; + } + + # Health check endpoint (no logging) + location /health { + proxy_pass http://backend/health; + access_log off; + } + + # Static files caching + location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ { + proxy_pass http://frontend; + expires 1y; + add_header Cache-Control "public, immutable"; + } + } +} +``` + +--- + +## 🚀 Initial Deployment + +### Step 1: Prepare Docker Images + +```bash +# Build images (on development machine) +cd /path/to/lcbp3/backend +docker build -t lcbp3-backend:1.0.0 . +docker tag lcbp3-backend:1.0.0 lcbp3-backend:latest + +cd /path/to/lcbp3/frontend +docker build -t lcbp3-frontend:1.0.0 . +docker tag lcbp3-frontend:1.0.0 lcbp3-frontend:latest + +# Save images to tar files +docker save lcbp3-backend:latest | gzip > lcbp3-backend-latest.tar.gz +docker save lcbp3-frontend:latest | gzip > lcbp3-frontend-latest.tar.gz + +# Transfer to QNAP +scp lcbp3-backend-latest.tar.gz admin@qnap-ip:/volume1/lcbp3/ +scp lcbp3-frontend-latest.tar.gz admin@qnap-ip:/volume1/lcbp3/ + +# Load images on QNAP +ssh admin@qnap-ip +cd /volume1/lcbp3 +docker load < lcbp3-backend-latest.tar.gz +docker load < lcbp3-frontend-latest.tar.gz +``` + +### Step 2: Initialize Database + +```bash +# Start MariaDB only +cd /volume1/lcbp3/blue +docker-compose up -d mariadb + +# Wait for MariaDB to be ready +docker exec lcbp3-mariadb mysqladmin ping -h localhost + +# Run migrations +docker-compose up -d backend +docker exec lcbp3-blue-backend npm run migration:run + +# Seed initial data (if needed) +docker exec lcbp3-blue-backend npm run seed +``` + +### Step 3: Start Blue Environment + +```bash +cd /volume1/lcbp3/blue + +# Start all services +docker-compose up -d + +# Check status +docker-compose ps + +# View logs +docker-compose logs -f + +# Wait for health checks +sleep 30 + +# Test health endpoint +curl http://localhost:3000/health +``` + +### Step 4: Start NGINX Proxy + +```bash +cd /volume1/lcbp3/nginx-proxy + +# Create networks (if not exist) +docker network create lcbp3-blue-network +docker network create lcbp3-green-network + +# Start NGINX +docker-compose up -d + +# Test NGINX configuration +docker exec lcbp3-nginx nginx -t + +# Check NGINX logs +docker logs lcbp3-nginx +``` + +### Step 5: Set Current Environment + +```bash +# Mark blue as current +echo "blue" > /volume1/lcbp3/current +``` + +### Step 6: Verify Deployment + +```bash +# Test HTTPS endpoint +curl -k https://lcbp3-dms.example.com/health + +# Test API +curl -k https://lcbp3-dms.example.com/api/health + +# Check all containers +docker ps --filter "name=lcbp3" + +# Check logs for errors +docker-compose -f /volume1/lcbp3/blue/docker-compose.yml logs --tail=100 +``` + +--- + +## 🔄 Blue-Green Deployment Process + +### Deployment Script + +```bash +# File: /volume1/lcbp3/scripts/deploy.sh +#!/bin/bash + +set -e # Exit on error + +# Configuration +LCBP3_DIR="/volume1/lcbp3" +CURRENT=$(cat $LCBP3_DIR/current) +TARGET=$([[ "$CURRENT" == "blue" ]] && echo "green" || echo "blue") + +echo "=========================================" +echo "LCBP3-DMS Blue-Green Deployment" +echo "=========================================" +echo "Current environment: $CURRENT" +echo "Target environment: $TARGET" +echo "=========================================" + +# Step 1: Backup database +echo "[1/9] Creating database backup..." +BACKUP_FILE="$LCBP3_DIR/shared/backups/db-backup-$(date +%Y%m%d-%H%M%S).sql" +docker exec lcbp3-mariadb mysqldump -u root -p${DB_PASSWORD} lcbp3_dms > $BACKUP_FILE +gzip $BACKUP_FILE +echo "✓ Backup created: $BACKUP_FILE.gz" + +# Step 2: Pull latest images +echo "[2/9] Pulling latest Docker images..." +cd $LCBP3_DIR/$TARGET +docker-compose pull +echo "✓ Images pulled" + +# Step 3: Update configuration +echo "[3/9] Updating configuration..." +# Copy .env if changed +if [ -f "$LCBP3_DIR/.env.production.new" ]; then + cp $LCBP3_DIR/.env.production.new $LCBP3_DIR/$TARGET/.env.production + echo "✓ Configuration updated" +fi + +# Step 4: Start target environment +echo "[4/9] Starting $TARGET environment..." +docker-compose up -d +echo "✓ $TARGET environment started" + +# Step 5: Wait for services to be ready +echo "[5/9] Waiting for services to be healthy..." +sleep 10 + +# Check backend health +for i in {1..30}; do + if docker exec lcbp3-${TARGET}-backend curl -f http://localhost:3000/health > /dev/null 2>&1; then + echo "✓ Backend is healthy" + break + fi + if [ $i -eq 30 ]; then + echo "✗ Backend health check failed!" + docker-compose logs backend + exit 1 + fi + sleep 2 +done + +# Step 6: Run database migrations +echo "[6/9] Running database migrations..." +docker exec lcbp3-${TARGET}-backend npm run migration:run +echo "✓ Migrations completed" + +# Step 7: Switch NGINX to target environment +echo "[7/9] Switching NGINX to $TARGET..." +sed -i "s/lcbp3-${CURRENT}-backend/lcbp3-${TARGET}-backend/g" $LCBP3_DIR/nginx-proxy/nginx.conf +sed -i "s/lcbp3-${CURRENT}-frontend/lcbp3-${TARGET}-frontend/g" $LCBP3_DIR/nginx-proxy/nginx.conf +docker exec lcbp3-nginx nginx -t +docker exec lcbp3-nginx nginx -s reload +echo "✓ NGINX switched to $TARGET" + +# Step 8: Verify new environment +echo "[8/9] Verifying new environment..." +sleep 5 +if curl -f -k https://lcbp3-dms.example.com/health > /dev/null 2>&1; then + echo "✓ New environment is responding" +else + echo "✗ New environment verification failed!" + echo "Rolling back..." + ./rollback.sh + exit 1 +fi + +# Step 9: Stop old environment +echo "[9/9] Stopping $CURRENT environment..." +cd $LCBP3_DIR/$CURRENT +docker-compose down +echo "✓ $CURRENT environment stopped" + +# Update current pointer +echo "$TARGET" > $LCBP3_DIR/current + +echo "=========================================" +echo "✓ Deployment completed successfully!" +echo "Active environment: $TARGET" +echo "=========================================" + +# Send notification (optional) +# /scripts/send-notification.sh "Deployment completed: $TARGET is now active" +``` + +### Rollback Script + +```bash +# File: /volume1/lcbp3/scripts/rollback.sh +#!/bin/bash + +set -e + +LCBP3_DIR="/volume1/lcbp3" +CURRENT=$(cat $LCBP3_DIR/current) +PREVIOUS=$([[ "$CURRENT" == "blue" ]] && echo "green" || echo "blue") + +echo "=========================================" +echo "LCBP3-DMS Rollback" +echo "=========================================" +echo "Current: $CURRENT" +echo "Rolling back to: $PREVIOUS" +echo "=========================================" + +# Switch NGINX back +echo "[1/3] Switching NGINX to $PREVIOUS..." +sed -i "s/lcbp3-${CURRENT}-backend/lcbp3-${PREVIOUS}-backend/g" $LCBP3_DIR/nginx-proxy/nginx.conf +sed -i "s/lcbp3-${CURRENT}-frontend/lcbp3-${PREVIOUS}-frontend/g" $LCBP3_DIR/nginx-proxy/nginx.conf +docker exec lcbp3-nginx nginx -s reload +echo "✓ NGINX switched" + +# Start previous environment if stopped +echo "[2/3] Ensuring $PREVIOUS environment is running..." +cd $LCBP3_DIR/$PREVIOUS +docker-compose up -d +sleep 10 +echo "✓ $PREVIOUS environment is running" + +# Verify +echo "[3/3] Verifying rollback..." +if curl -f -k https://lcbp3-dms.example.com/health > /dev/null 2>&1; then + echo "✓ Rollback successful" + echo "$PREVIOUS" > $LCBP3_DIR/current +else + echo "✗ Rollback verification failed!" + exit 1 +fi + +echo "=========================================" +echo "✓ Rollback completed" +echo "Active environment: $PREVIOUS" +echo "=========================================" +``` + +### Make Scripts Executable + +```bash +chmod +x /volume1/lcbp3/scripts/deploy.sh +chmod +x /volume1/lcbp3/scripts/rollback.sh +``` + +--- + +## 📋 Deployment Checklist + +### Pre-Deployment + +- [ ] Backup current database +- [ ] Tag Docker images with version +- [ ] Update `.env.production` if needed +- [ ] Review migration scripts +- [ ] Notify stakeholders of deployment window +- [ ] Verify SSL certificate validity (> 30 days) +- [ ] Check disk space (> 20% free) +- [ ] Review recent error logs + +### During Deployment + +- [ ] Pull latest Docker images +- [ ] Start target environment (blue/green) +- [ ] Run database migrations +- [ ] Verify health checks pass +- [ ] Switch NGINX proxy +- [ ] Verify application responds correctly +- [ ] Check for errors in logs +- [ ] Monitor performance metrics + +### Post-Deployment + +- [ ] Monitor logs for 30 minutes +- [ ] Check performance metrics +- [ ] Verify all features working +- [ ] Test critical user flows +- [ ] Stop old environment +- [ ] Update deployment log +- [ ] Notify stakeholders of completion +- [ ] Archive old Docker images + +--- + +## 🔍 Troubleshooting + +### Common Issues + +#### 1. Container Won't Start + +```bash +# Check logs +docker logs lcbp3-blue-backend + +# Check resource usage +docker stats + +# Restart container +docker restart lcbp3-blue-backend +``` + +#### 2. Database Connection Failed + +```bash +# Check MariaDB is running +docker ps | grep mariadb + +# Test connection +docker exec lcbp3-mariadb mysql -u lcbp3_user -p -e "SELECT 1" + +# Check environment variables +docker exec lcbp3-blue-backend env | grep DB_ +``` + +#### 3. NGINX 502 Bad Gateway + +```bash +# Check backend is running +curl http://localhost:3000/health + +# Check NGINX configuration +docker exec lcbp3-nginx nginx -t + +# Check NGINX logs +docker logs lcbp3-nginx + +# Reload NGINX +docker exec lcbp3-nginx nginx -s reload +``` + +#### 4. Migration Failed + +```bash +# Check migration status +docker exec lcbp3-blue-backend npm run migration:show + +# Revert last migration +docker exec lcbp3-blue-backend npm run migration:revert + +# Re-run migrations +docker exec lcbp3-blue-backend npm run migration:run +``` + +--- + +## 📊 Monitoring + +### Health Checks + +```bash +# Backend health +curl https://lcbp3-dms.example.com/health + +# Database health +docker exec lcbp3-mariadb mysqladmin ping + +# Redis health +docker exec lcbp3-redis redis-cli ping + +# All containers status +docker ps --filter "name=lcbp3" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" +``` + +### Performance Monitoring + +```bash +# Container resource usage +docker stats --no-stream + +# Disk usage +df -h /volume1/lcbp3 + +# Database size +docker exec lcbp3-mariadb mysql -u root -p -e " + SELECT table_schema AS 'Database', + ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS 'Size (MB)' + FROM information_schema.tables + WHERE table_schema = 'lcbp3_dms' + GROUP BY table_schema;" +``` + +--- + +## 🔐 Security Best Practices + +1. **Change Default Passwords:** Update all passwords in `.env.production` +2. **SSL/TLS:** Always use HTTPS in production +3. **Firewall:** Only expose ports 80, 443, and 22 (SSH) +4. **Regular Updates:** Keep Docker images updated +5. **Backup Encryption:** Encrypt database backups +6. **Access Control:** Limit SSH access to specific IPs +7. **Secrets Management:** Never commit `.env` files to Git +8. **Log Monitoring:** Review logs daily for suspicious activity + +--- + +## 📚 Related Documentation + +- [Environment Setup Guide](./environment-setup.md) +- [Backup & Recovery](./backup-recovery.md) +- [Monitoring & Alerting](./monitoring-alerting.md) +- [Maintenance Procedures](./maintenance-procedures.md) +- [ADR-015: Deployment Infrastructure](../05-decisions/ADR-015-deployment-infrastructure.md) + +--- + +**Version:** 1.5.1 +**Last Updated:** 2025-12-02 +**Next Review:** 2026-06-01 diff --git a/specs/04-operations/deployment.md b/specs/04-operations/deployment.md deleted file mode 100644 index e69de29..0000000 diff --git a/specs/04-operations/disaster-recovery.md b/specs/04-operations/disaster-recovery.md deleted file mode 100644 index e69de29..0000000 diff --git a/specs/04-operations/document-numbering-operations.md b/specs/04-operations/document-numbering-operations.md new file mode 100644 index 0000000..29712c0 --- /dev/null +++ b/specs/04-operations/document-numbering-operations.md @@ -0,0 +1,684 @@ +# Document Numbering Operations Guide + +--- +title: 'Operations Guide: Document Numbering System' +version: 1.6.0 +status: draft +owner: Operations Team +last_updated: 2025-12-02 +related: + - specs/01-requirements/03.11-document-numbering.md + - specs/03-implementation/document-numbering.md + - specs/04-operations/monitoring-alerting.md +--- + +## Overview + +เอกสารนี้อธิบาย operations procedures, monitoring, และ troubleshooting สำหรับระบบ Document Numbering + +## 1. Performance Requirements + +### 1.1. Response Time Targets + +| Metric | Target | Measurement | +|--------|--------|-------------| +| 95th percentile | ≤ 2 วินาที | ตั้งแต่ request ถึง response | +| 99th percentile | ≤ 5 วินาที | ตั้งแต่ request ถึง response | +| Normal operation | ≤ 500ms | ไม่มี retry | + +### 1.2. Throughput Targets + +| Load Level | Target | Notes | +|------------|--------|-------| +| Normal load | ≥ 50 req/s | ใช้งานปกติ | +| Peak load | ≥ 100 req/s | ช่วงเร่งงาน | +| Burst capacity | ≥ 200 req/s | Short duration (< 1 min) | + +### 1.3. Availability SLA + +- **Uptime**: ≥ 99.5% (excluding planned maintenance) +- **Maximum downtime**: ≤ 3.6 ชั่วโมง/เดือน (~ 8.6 นาที/วัน) +- **Recovery Time Objective (RTO)**: ≤ 30 นาที +- **Recovery Point Objective (RPO)**: ≤ 5 นาที + +## 2. Infrastructure Setup + +### 2.1. Database Configuration + +#### MariaDB Connection Pool + +```typescript +// ormconfig.ts +{ + type: 'mysql', + host: process.env.DB_HOST, + port: parseInt(process.env.DB_PORT), + username: process.env.DB_USERNAME, + password: process.env.DB_PASSWORD, + database: process.env.DB_DATABASE, + extra: { + connectionLimit: 20, // Pool size + queueLimit: 0, // Unlimited queue + acquireTimeout: 10000, // 10s timeout + retryAttempts: 3, + retryDelay: 1000 + } +} +``` + +#### High Availability Setup + +```yaml +# docker-compose.yml +services: + mariadb-master: + image: mariadb:10.11 + environment: + MYSQL_REPLICATION_MODE: master + MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD} + volumes: + - mariadb-master-data:/var/lib/mysql + networks: + - backend + + mariadb-replica: + image: mariadb:10.11 + environment: + MYSQL_REPLICATION_MODE: slave + MYSQL_MASTER_HOST: mariadb-master + MYSQL_MASTER_ROOT_PASSWORD: ${DB_ROOT_PASSWORD} + volumes: + - mariadb-replica-data:/var/lib/mysql + networks: + - backend +``` + +### 2.2. Redis Configuration + +#### Redis Sentinel for High Availability + +```yaml +# docker-compose.yml +services: + redis-master: + image: redis:7-alpine + command: redis-server --appendonly yes + volumes: + - redis-master-data:/data + networks: + - backend + + redis-replica: + image: redis:7-alpine + command: redis-server --replicaof redis-master 6379 --appendonly yes + volumes: + - redis-replica-data:/data + networks: + - backend + + redis-sentinel: + image: redis:7-alpine + command: > + redis-sentinel /etc/redis/sentinel.conf + --sentinel monitor mymaster redis-master 6379 2 + --sentinel down-after-milliseconds mymaster 5000 + --sentinel failover-timeout mymaster 10000 + networks: + - backend +``` + +#### Redis Connection Pool + +```typescript +// redis.config.ts +import IORedis from 'ioredis'; + +export const redisConfig = { + host: process.env.REDIS_HOST || 'localhost', + port: parseInt(process.env.REDIS_PORT) || 6379, + password: process.env.REDIS_PASSWORD, + maxRetriesPerRequest: 3, + enableReadyCheck: true, + lazyConnect: false, + poolSize: 10, + retryStrategy: (times: number) => { + if (times > 3) { + return null; // Stop retry + } + return Math.min(times * 100, 3000); + }, +}; +``` + +### 2.3. Load Balancing + +#### Nginx Configuration + +```nginx +# nginx.conf +upstream backend { + least_conn; # Least connections algorithm + server backend-1:3000 max_fails=3 fail_timeout=30s weight=1; + server backend-2:3000 max_fails=3 fail_timeout=30s weight=1; + server backend-3:3000 max_fails=3 fail_timeout=30s weight=1; + + keepalive 32; +} + +server { + listen 80; + server_name api.lcbp3.local; + + location /api/v1/document-numbering/ { + proxy_pass http://backend; + proxy_http_version 1.1; + proxy_set_header Connection ""; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + proxy_next_upstream error timeout; + proxy_connect_timeout 10s; + proxy_send_timeout 30s; + proxy_read_timeout 30s; + } +} +``` + +#### Docker Compose Scaling + +```yaml +# docker-compose.yml +services: + backend: + image: lcbp3-backend:latest + deploy: + replicas: 3 + resources: + limits: + cpus: '1.0' + memory: 1G + reservations: + cpus: '0.5' + memory: 512M + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + environment: + NODE_ENV: production + DB_POOL_SIZE: 20 + networks: + - backend +``` + +## 3. Monitoring & Metrics + +### 3.1. Prometheus Metrics + +#### Key Metrics to Collect + +```typescript +// metrics.service.ts +import { Counter, Histogram, Gauge } from 'prom-client'; + +// Lock acquisition metrics +export const lockAcquisitionDuration = new Histogram({ + name: 'docnum_lock_acquisition_duration_ms', + help: 'Lock acquisition time in milliseconds', + labelNames: ['project', 'type'], + buckets: [10, 50, 100, 200, 500, 1000, 2000, 5000], +}); + +export const lockAcquisitionFailures = new Counter({ + name: 'docnum_lock_acquisition_failures_total', + help: 'Total number of lock acquisition failures', + labelNames: ['project', 'type', 'reason'], +}); + +// Generation metrics +export const generationDuration = new Histogram({ + name: 'docnum_generation_duration_ms', + help: 'Total document number generation time', + labelNames: ['project', 'type', 'status'], + buckets: [100, 200, 500, 1000, 2000, 5000], +}); + +export const retryCount = new Histogram({ + name: 'docnum_retry_count', + help: 'Number of retries per generation', + labelNames: ['project', 'type'], + buckets: [0, 1, 2, 3, 5, 10], +}); + +// Connection health +export const redisConnectionStatus = new Gauge({ + name: 'docnum_redis_connection_status', + help: 'Redis connection status (1=up, 0=down)', +}); + +export const dbConnectionPoolUsage = new Gauge({ + name: 'docnum_db_connection_pool_usage', + help: 'Database connection pool usage percentage', +}); +``` + +### 3.2. Prometheus Alert Rules + +```yaml +# prometheus/alerts.yml +groups: + - name: document_numbering_alerts + interval: 30s + rules: + # CRITICAL: Redis unavailable + - alert: RedisUnavailable + expr: docnum_redis_connection_status == 0 + for: 1m + labels: + severity: critical + component: document-numbering + annotations: + summary: "Redis is unavailable for document numbering" + description: "System is falling back to DB-only locking. Performance degraded by 30-50%." + runbook_url: "https://wiki.lcbp3/runbooks/redis-unavailable" + + # CRITICAL: High lock failure rate + - alert: HighLockFailureRate + expr: | + rate(docnum_lock_acquisition_failures_total[5m]) > 0.1 + for: 5m + labels: + severity: critical + component: document-numbering + annotations: + summary: "Lock acquisition failure rate > 10%" + description: "Check Redis and database performance immediately" + runbook_url: "https://wiki.lcbp3/runbooks/high-lock-failure" + + # WARNING: Elevated lock failure rate + - alert: ElevatedLockFailureRate + expr: | + rate(docnum_lock_acquisition_failures_total[5m]) > 0.05 + for: 5m + labels: + severity: warning + component: document-numbering + annotations: + summary: "Lock acquisition failure rate > 5%" + description: "Monitor closely. May escalate to critical soon." + + # WARNING: Slow lock acquisition + - alert: SlowLockAcquisition + expr: | + histogram_quantile(0.95, + rate(docnum_lock_acquisition_duration_ms_bucket[5m]) + ) > 1000 + for: 5m + labels: + severity: warning + component: document-numbering + annotations: + summary: "P95 lock acquisition time > 1 second" + description: "Lock acquisition is slower than expected. Check Redis latency." + + # WARNING: High retry count + - alert: HighRetryCount + expr: | + sum by (project) ( + rate(docnum_retry_count_sum[1h]) + ) > 100 + for: 1h + labels: + severity: warning + component: document-numbering + annotations: + summary: "Retry count > 100 per hour in project {{ $labels.project }}" + description: "High contention detected. Consider scaling." + + # WARNING: Slow generation + - alert: SlowDocumentNumberGeneration + expr: | + histogram_quantile(0.95, + rate(docnum_generation_duration_ms_bucket[5m]) + ) > 2000 + for: 5m + labels: + severity: warning + component: document-numbering + annotations: + summary: "P95 generation time > 2 seconds" + description: "Document number generation is slower than SLA target" +``` + +### 3.3. AlertManager Configuration + +```yaml +# alertmanager/config.yml +global: + resolve_timeout: 5m + slack_api_url: ${SLACK_WEBHOOK_URL} + +route: + group_by: ['alertname', 'severity', 'project'] + group_wait: 30s + group_interval: 5m + repeat_interval: 4h + receiver: 'ops-team' + + routes: + # CRITICAL alerts → PagerDuty + Slack + - match: + severity: critical + receiver: 'pagerduty-critical' + continue: true + + - match: + severity: critical + receiver: 'slack-critical' + continue: false + + # WARNING alerts → Slack only + - match: + severity: warning + receiver: 'slack-warnings' + +receivers: + - name: 'pagerduty-critical' + pagerduty_configs: + - service_key: ${PAGERDUTY_SERVICE_KEY} + description: '{{ .GroupLabels.alertname }}: {{ .CommonAnnotations.summary }}' + details: + firing: '{{ .Alerts.Firing | len }}' + resolved: '{{ .Alerts.Resolved | len }}' + runbook: '{{ .CommonAnnotations.runbook_url }}' + + - name: 'slack-critical' + slack_configs: + - channel: '#lcbp3-critical-alerts' + title: '🚨 CRITICAL: {{ .GroupLabels.alertname }}' + text: | + *Summary:* {{ .CommonAnnotations.summary }} + *Description:* {{ .CommonAnnotations.description }} + *Runbook:* {{ .CommonAnnotations.runbook_url }} + color: 'danger' + + - name: 'slack-warnings' + slack_configs: + - channel: '#lcbp3-alerts' + title: '⚠️ WARNING: {{ .GroupLabels.alertname }}' + text: '{{ .CommonAnnotations.description }}' + color: 'warning' + + - name: 'ops-team' + email_configs: + - to: 'ops@example.com' + subject: '[LCBP3] {{ .GroupLabels.alertname }}' +``` + +### 3.4. Grafana Dashboard + +Dashboard panels ที่สำคัญ: + +1. **Lock Acquisition Success Rate** (Gauge) + - Query: `1 - (rate(docnum_lock_acquisition_failures_total[5m]) / rate(docnum_lock_acquisition_total[5m]))` + - Alert threshold: < 95% + +2. **Lock Acquisition Time Percentiles** (Graph) + - P50: `histogram_quantile(0.50, rate(docnum_lock_acquisition_duration_ms_bucket[5m]))` + - P95: `histogram_quantile(0.95, rate(docnum_lock_acquisition_duration_ms_bucket[5m]))` + - P99: `histogram_quantile(0.99, rate(docnum_lock_acquisition_duration_ms_bucket[5m]))` + +3. **Generation Rate** (Stat) + - Query: `sum(rate(docnum_generation_duration_ms_count[1m])) * 60` + - Unit: documents/minute + +4. **Error Rate by Type** (Graph) + - Query: `sum by (reason) (rate(docnum_lock_acquisition_failures_total[5m]))` + +5. **Redis Connection Status** (Stat) + - Query: `docnum_redis_connection_status` + - Thresholds: 0 = red, 1 = green + +6. **DB Connection Pool Usage** (Gauge) + - Query: `docnum_db_connection_pool_usage` + - Alert threshold: > 80% + +## 4. Troubleshooting Runbooks + +### 4.1. Scenario: Redis Unavailable + +**Symptoms:** +- Alert: `RedisUnavailable` +- System falls back to DB-only locking +- Performance degraded 30-50% + +**Action Steps:** + +1. **Check Redis status:** + ```bash + docker exec lcbp3-redis redis-cli ping + # Expected: PONG + ``` + +2. **Check Redis logs:** + ```bash + docker logs lcbp3-redis --tail=100 + ``` + +3. **Restart Redis (if needed):** + ```bash + docker restart lcbp3-redis + ``` + +4. **Verify failover (if using Sentinel):** + ```bash + docker exec lcbp3-redis-sentinel redis-cli -p 26379 SENTINEL masters + ``` + +5. **Monitor recovery:** + - Check metric: `docnum_redis_connection_status` returns to 1 + - Check performance: P95 latency returns to normal (< 500ms) + +### 4.2. Scenario: High Lock Failure Rate + +**Symptoms:** +- Alert: `HighLockFailureRate` (> 10%) +- Users report "ระบบกำลังยุ่ง" errors + +**Action Steps:** + +1. **Check concurrent load:** + ```bash + # Check current request rate + curl http://prometheus:9090/api/v1/query?query=rate(docnum_generation_duration_ms_count[1m]) + ``` + +2. **Check database connections:** + ```sql + SHOW PROCESSLIST; + -- Look for waiting/locked queries + ``` + +3. **Check Redis memory:** + ```bash + docker exec lcbp3-redis redis-cli INFO memory + ``` + +4. **Scale up if needed:** + ```bash + # Increase backend replicas + docker-compose up -d --scale backend=5 + ``` + +5. **Check for deadlocks:** + ```sql + SHOW ENGINE INNODB STATUS; + -- Look for LATEST DETECTED DEADLOCK section + ``` + +### 4.3. Scenario: Slow Performance + +**Symptoms:** +- Alert: `SlowDocumentNumberGeneration` +- P95 > 2 seconds + +**Action Steps:** + +1. **Check database query performance:** + ```sql + SELECT * FROM document_number_counters USE INDEX (idx_counter_lookup) + WHERE project_id = 2 AND correspondence_type_id = 6 AND current_year = 2025; + + -- Check execution plan + EXPLAIN SELECT ...; + ``` + +2. **Check for missing indexes:** + ```sql + SHOW INDEX FROM document_number_counters; + ``` + +3. **Check Redis latency:** + ```bash + docker exec lcbp3-redis redis-cli --latency + ``` + +4. **Check network latency:** + ```bash + ping mariadb-master + ping redis-master + ``` + +5. **Review slow query log:** + ```bash + docker exec lcbp3-mariadb-master cat /var/log/mysql/slow.log + ``` + +### 4.4. Scenario: Version Conflicts + +**Symptoms:** +- High retry count +- Users report "เลขที่เอกสารถูกเปลี่ยน" errors + +**Action Steps:** + +1. **Check concurrent requests to same counter:** + ```sql + SELECT + project_id, + correspondence_type_id, + COUNT(*) as concurrent_requests + FROM document_number_audit + WHERE created_at > NOW() - INTERVAL 5 MINUTE + GROUP BY project_id, correspondence_type_id + HAVING COUNT(*) > 10 + ORDER BY concurrent_requests DESC; + ``` + +2. **Investigate specific counter:** + ```sql + SELECT * FROM document_number_counters + WHERE project_id = X AND correspondence_type_id = Y; + + -- Check audit trail + SELECT * FROM document_number_audit + WHERE counter_key LIKE '%project_id:X%' + ORDER BY created_at DESC + LIMIT 20; + ``` + +3. **Check for application bugs:** + - Review error logs for stack traces + - Check if retry logic is working correctly + +4. **Temporary mitigation:** + - Increase retry count in application config + - Consider manual counter adjustment (last resort) + +## 5. Maintenance Procedures + +### 5.1. Counter Reset (Manual) + +**Requires:** SUPER_ADMIN role + 2-person approval + +**Steps:** + +1. **Request approval via API:** + ```bash + POST /api/v1/document-numbering/configs/{configId}/reset-counter + { + "reason": "เหตุผลที่ชัดเจน อย่างน้อย 20 ตัวอักษร", + "approver_1": "user_id", + "approver_2": "user_id" + } + ``` + +2. **Verify in audit log:** + ```sql + SELECT * FROM document_number_config_history + WHERE config_id = X + ORDER BY changed_at DESC + LIMIT 1; + ``` + +### 5.2. Template Update + +**Best Practices:** + +1. Always test template in staging first +2. Preview generated numbers before applying +3. Document reason for change +4. Template changes do NOT affect existing documents + +**API Call:** +```bash +PUT /api/v1/document-numbering/configs/{configId} +{ + "template": "{ORIGINATOR}-{RECIPIENT}-{SEQ:4}-{YEAR:B.E.}", + "change_reason": "เหตุผลในการเปลี่ยนแปลง" +} +``` + +### 5.3. Database Maintenance + +**Weekly Tasks:** +- Check slow query log +- Optimize tables if needed: + ```sql + OPTIMIZE TABLE document_number_counters; + OPTIMIZE TABLE document_number_audit; + ``` + +**Monthly Tasks:** +- Review and archive old audit logs (> 2 years) +- Check index usage: + ```sql + SELECT * FROM sys.schema_unused_indexes + WHERE object_schema = 'lcbp3_db'; + ``` + +## 6. Backup & Recovery + +### 6.1. Backup Strategy + +**Database:** +- Full backup: Daily at 02:00 AM +- Incremental backup: Every 4 hours +- Retention: 30 days + +**Redis:** +- AOF (Append-Only File) enabled +- Snapshot every 1 hour +- Retention: 7 days + +### 6.2. Recovery Procedures + +See: [Backup & Recovery Guide](file:///e:/np-dms/lcbp3/specs/04-operations/backup-recovery.md) + +## References + +- [Requirements](file:///e:/np-dms/lcbp3/specs/01-requirements/03.11-document-numbering.md) +- [Implementation Guide](file:///e:/np-dms/lcbp3/specs/03-implementation/document-numbering.md) +- [Monitoring & Alerting](file:///e:/np-dms/lcbp3/specs/04-operations/monitoring-alerting.md) +- [Incident Response](file:///e:/np-dms/lcbp3/specs/04-operations/incident-response.md) diff --git a/specs/04-operations/environment-setup.md b/specs/04-operations/environment-setup.md index cf7e9d4..f5bf7db 100644 --- a/specs/04-operations/environment-setup.md +++ b/specs/04-operations/environment-setup.md @@ -1,8 +1,8 @@ # Environment Setup & Configuration **Project:** LCBP3-DMS -**Version:** 1.5.0 -**Last Updated:** 2025-12-01 +**Version:** 1.5.1 +**Last Updated:** 2025-12-02 --- @@ -458,6 +458,6 @@ docker exec lcbp3-backend env | grep NODE_ENV --- -**Version:** 1.5.0 +**Version:** 1.5.1 **Last Review:** 2025-12-01 **Next Review:** 2026-03-01 diff --git a/specs/04-operations/incident-response.md b/specs/04-operations/incident-response.md index 1f3f825..4546d38 100644 --- a/specs/04-operations/incident-response.md +++ b/specs/04-operations/incident-response.md @@ -1,8 +1,8 @@ # Incident Response Procedures **Project:** LCBP3-DMS -**Version:** 1.5.0 -**Last Updated:** 2025-12-01 +**Version:** 1.5.1 +**Last Updated:** 2025-12-02 --- @@ -478,6 +478,6 @@ Database connection pool was exhausted due to slow queries not releasing connect --- -**Version:** 1.5.0 +**Version:** 1.5.1 **Last Review:** 2025-12-01 **Next Review:** 2026-03-01 diff --git a/specs/04-operations/maintenance-procedures.md b/specs/04-operations/maintenance-procedures.md index 30a9381..a6fc528 100644 --- a/specs/04-operations/maintenance-procedures.md +++ b/specs/04-operations/maintenance-procedures.md @@ -1,8 +1,8 @@ # Maintenance Procedures **Project:** LCBP3-DMS -**Version:** 1.5.0 -**Last Updated:** 2025-12-01 +**Version:** 1.5.1 +**Last Updated:** 2025-12-02 --- @@ -496,6 +496,6 @@ echo "Security maintenance completed: $(date)" --- -**Version:** 1.5.0 +**Version:** 1.5.1 **Last Review:** 2025-12-01 **Next Review:** 2026-03-01 diff --git a/specs/04-operations/monitoring-alerting.md b/specs/04-operations/monitoring-alerting.md index d5f84a4..8195070 100644 --- a/specs/04-operations/monitoring-alerting.md +++ b/specs/04-operations/monitoring-alerting.md @@ -1,8 +1,8 @@ # Monitoring & Alerting **Project:** LCBP3-DMS -**Version:** 1.5.0 -**Last Updated:** 2025-12-01 +**Version:** 1.5.1 +**Last Updated:** 2025-12-02 --- @@ -438,6 +438,6 @@ ab -n 1000 -c 10 \ --- -**Version:** 1.5.0 +**Version:** 1.5.1 **Last Review:** 2025-12-01 **Next Review:** 2026-03-01 diff --git a/specs/04-operations/monitoring.md b/specs/04-operations/monitoring.md deleted file mode 100644 index e69de29..0000000 diff --git a/specs/04-operations/security-operations.md b/specs/04-operations/security-operations.md index 89a1b61..c13d106 100644 --- a/specs/04-operations/security-operations.md +++ b/specs/04-operations/security-operations.md @@ -1,8 +1,8 @@ # Security Operations **Project:** LCBP3-DMS -**Version:** 1.5.0 -**Last Updated:** 2025-12-01 +**Version:** 1.5.1 +**Last Updated:** 2025-12-02 --- @@ -439,6 +439,6 @@ echo "Account compromise response completed for User ID: $USER_ID" --- -**Version:** 1.5.0 +**Version:** 1.5.1 **Last Review:** 2025-12-01 **Next Review:** 2026-03-01 diff --git a/specs/05-decisions/ADR-002-document-numbering-strategy.md b/specs/05-decisions/ADR-002-document-numbering-strategy.md index e8eebec..d1353ac 100644 --- a/specs/05-decisions/ADR-002-document-numbering-strategy.md +++ b/specs/05-decisions/ADR-002-document-numbering-strategy.md @@ -197,6 +197,7 @@ CREATE TABLE document_number_audit ( #### 1. Correspondence (หนังสือราชการ) **Letter Type (TYPE = 03):** + ``` Template: {ORG}-{ORG}-{TYPE}-{SEQ:4}-{YEAR:B.E.} Example: คคง.-สคฉ.3-0985-2568 @@ -204,6 +205,7 @@ Counter Key: project_id + doc_type_id + sub_type_id + year ``` **Other Correspondence:** + ``` Template: {ORG}-{ORG}-{TYPE}-{SEQ:4}-{YEAR:B.E.} Example: คคง.-สคฉ.3-STR-0001-2568 @@ -213,6 +215,7 @@ Counter Key: project_id + doc_type_id + sub_type_id + year #### 2. Transmittal **To Owner (Special Format):** + ``` Template: {ORG}-{ORG}-{TYPE}-{SUB_TYPE}-{SEQ:4}-{YEAR:B.E.} Example: คคง.-สคฉ.3-03-21-0117-2568 @@ -221,6 +224,7 @@ Note: recipient_type แยก counter จาก To Contractor ``` **To Contractor/Others:** + ``` Template: {ORG}-{ORG}-{TYPE}-{SEQ:4}-{YEAR:B.E.} Example: ผรม.2-คคง.-0117-2568 @@ -228,6 +232,7 @@ Counter Key: project_id + doc_type_id + recipient_type('CONTRACTOR') + year ``` **Alternative Project-based:** + ``` Template: {PROJECT}-{ORG}-{TYPE}-{DISCIPLINE}-{SEQ:4}-{REV} Example: LCBP3-TR-STR-0001-A @@ -586,6 +591,7 @@ sequenceDiagram **Trigger:** Redis connection error, Redis down **Fallback:** + - ใช้ Database-only locking (`SELECT ... FOR UPDATE`) - Log warning และแจ้ง ops team - ระบบยังใช้งานได้แต่ performance ลดลง (slower) @@ -595,6 +601,7 @@ sequenceDiagram **Trigger:** หลาย requests แย่งชิง lock พร้อมกัน **Retry Logic:** + - Retry 5 ครั้งด้วย exponential backoff: 1s, 2s, 4s, 8s, 16s (รวม ~31 วินาที) - หลัง 5 ครั้ง: Return HTTP 503 "Service Temporarily Unavailable" - Frontend: แสดง "ระบบกำลังยุ่ง กรุณาลองใหม่ภายหลัง" @@ -604,6 +611,7 @@ sequenceDiagram **Trigger:** Optimistic lock version mismatch **Retry Logic:** + - Retry 2 ครั้ง (reload counter + retry transaction) - หลัง 2 ครั้ง: Return HTTP 409 Conflict - Frontend: แสดง "เลขที่เอกสารถูกเปลี่ยน กรุณาลองใหม่" @@ -613,6 +621,7 @@ sequenceDiagram **Trigger:** Database connection timeout, connection pool exhausted **Retry Logic:** + - Retry 3 ครั้งด้วย exponential backoff: 1s, 2s, 4s - หลัง 3 ครั้ง: Return HTTP 500 "Internal Server Error" - Frontend: แสดง "เกิดข้อผิดพลาดในระบบ กรุณาติดต่อผู้ดูแลระบบ" diff --git a/specs/05-decisions/README.md b/specs/05-decisions/README.md index 1ea0083..f7ab98f 100644 --- a/specs/05-decisions/README.md +++ b/specs/05-decisions/README.md @@ -1,6 +1,7 @@ # Architecture Decision Records (ADRs) -**Last Updated:** 2025-11-30 +**Version:** 1.5.1 +**Last Updated:** 2025-12-02 **Project:** LCBP3-DMS (Laem Chabang Port Phase 3 - Document Management System) --- @@ -81,7 +82,10 @@ Architecture Decision Records (ADRs) เป็นเอกสารที่บ ### 2. Data Integrity & Concurrency -- **ADR-002:** Document Numbering - Double-lock เพื่อป้องกัน Race Condition +- **ADR-002:** Document Numbering - Double-lock (Redis Redlock + DB Optimistic) เพื่อป้องกัน Race Condition + - 📋 [Requirements](../01-requirements/03.11-document-numbering.md) + - 📘 [Implementation Guide](../03-implementation/document-numbering.md) + - 📗 [Operations Guide](../04-operations/document-numbering-operations.md) - **ADR-003:** File Storage - Two-phase เพื่อ Transaction safety - **ADR-009:** Database Migration - TypeORM Migrations พร้อม Blue-Green Deployment @@ -352,5 +356,5 @@ graph TB --- -**Version:** 1.5.0 -**Last Review:** 2025-11-30 +**Version:** 1.5.1 +**Last Review:** 2025-12-02 diff --git a/specs/06-tasks/README.md b/specs/06-tasks/README.md index cb69e2c..8dcab8b 100644 --- a/specs/06-tasks/README.md +++ b/specs/06-tasks/README.md @@ -1,8 +1,8 @@ # Development Tasks **Project:** LCBP3-DMS (Laem Chabang Port Phase 3 - Document Management System) -**Version:** 1.5.0 -**Last Updated:** 2025-12-01 +**Version:** 1.5.1 +**Last Updated:** 2025-12-02 --- @@ -280,10 +280,17 @@ graph TB - **Type:** Core Service - **Key Deliverables:** - - Double-lock mechanism (Redis + DB) - - Template-based generator - - Concurrent-safe implementation + - Double-lock mechanism (Redis Redlock + DB Optimistic Lock) + - Template-based generator (10 token types) + - Concurrent-safe implementation (100+ concurrent requests) + - Comprehensive error handling (4 scenarios) + - Monitoring & alerting (Prometheus + Grafana) +- **Documentation:** + - 📋 [Requirements](../01-requirements/03.11-document-numbering.md) + - 📘 [Implementation Guide](../03-implementation/document-numbering.md) + - 📗 [Operations Guide](../04-operations/document-numbering-operations.md) - **Related ADR:** [ADR-002](../05-decisions/ADR-002-document-numbering-strategy.md) +- **Task Details:** [TASK-BE-004](./TASK-BE-004-document-numbering.md) ### TASK-BE-006: Workflow Engine @@ -619,5 +626,5 @@ Add these features when: --- -**Version:** 1.5.0 -**Last Updated:** 2025-11-30 +**Version:** 1.5.1 +**Last Updated:** 2025-12-02 diff --git a/specs/06-tasks/TASK-BE-004-document-numbering.md b/specs/06-tasks/TASK-BE-004-document-numbering.md index 9b7cc5a..8bb7a3e 100644 --- a/specs/06-tasks/TASK-BE-004-document-numbering.md +++ b/specs/06-tasks/TASK-BE-004-document-numbering.md @@ -10,21 +10,26 @@ ## 📋 Overview -สร้าง DocumentNumberingService ที่ใช้ Double-Lock mechanism (Redis + DB Optimistic Lock) สำหรับการสร้างเลขที่เอกสารอัตโนมัติ พร้อม comprehensive error handling, monitoring, และ audit logging +สร้าง DocumentNumberingService ที่ใช้ Double-Lock mechanism (Redis + DB Optimistic Lock) สำหรับการสร้างเลขที่เอกสารอัตโนมัติ ตาม requirements ใน [03.11-document-numbering.md](file:///e:/np-dms/lcbp3/specs/01-requirements/03.11-document-numbering.md) + +### เอกสารอ้างอิง + +- **Requirements**: [03.11-document-numbering.md](file:///e:/np-dms/lcbp3/specs/01-requirements/03.11-document-numbering.md) +- **Implementation Guide**: [document-numbering.md](file:///e:/np-dms/lcbp3/specs/03-implementation/document-numbering.md) +- **Operations Guide**: [document-numbering-operations.md](file:///e:/np-dms/lcbp3/specs/04-operations/document-numbering-operations.md) --- ## 🎯 Objectives -- ✅ Template-Based Number Generation (รองรับ 9 token types) -- ✅ Double-Lock Protection (Redis + DB Optimistic Lock) +- ✅ Template-Based Number Generation (รองรับ 10 token types) +- ✅ Double-Lock Protection (Redis Redlock + DB Optimistic Lock) - ✅ Concurrent-Safe (No duplicate numbers, tested with 100+ concurrent requests) -- ✅ Support 4 Document Types (Correspondence, RFA, Transmittal, Drawing) -- ✅ Year-Based Reset (พ.ศ. และ ค.ศ.) -- ✅ Transmittal Special Logic (To Owner vs To Contractor) +- ✅ Support All Document Types (LETTER, RFA, TRANSMITTAL, RFI, MEMO, etc.) +- ✅ Year-Based Auto Reset (ปี ค.ศ.) - ✅ 4 Error Scenarios with Fallback Strategies - ✅ Comprehensive Audit Logging -- ✅ Monitoring & Alerting +- ✅ Monitoring & Alerting (Prometheus + Grafana) - ✅ Rate Limiting & Security --- @@ -34,42 +39,44 @@ ### 1. Number Generation - ✅ Generate unique sequential numbers -- ✅ Support all 9 token types: `{PROJECT}`, `{ORG}`, `{TYPE}`, `{SUB_TYPE}`, `{DISCIPLINE}`, `{CATEGORY}`, `{SEQ:n}`, `{YEAR:B.E.}`, `{YEAR:A.D}`, `{REV}` +- ✅ Support all 10 token types: `{PROJECT}`, `{ORIGINATOR}`, `{RECIPIENT}`, `{CORR_TYPE}`, `{SUB_TYPE}`, `{RFA_TYPE}`, `{DISCIPLINE}`, `{SEQ:n}`, `{YEAR:B.E.}`, `{YEAR:A.D.}`, `{REV}` - ✅ No duplicates even with 100+ concurrent requests - ✅ Performance: <500ms (normal), <2s (p95), <5s (p99) ### 2. Lock Mechanism -- ✅ Redis distributed lock (TTL: 5 seconds) -- ✅ DB optimistic lock with version column +- ✅ Redis Redlock distributed lock (TTL: 5 seconds) +- ✅ DB optimistic lock with `version` column - ✅ Fallback to DB pessimistic lock when Redis unavailable - ✅ Retry with exponential backoff (5 retries max for lock, 2 for version conflict, 3 for DB errors) ### 3. Document Types Support -- ✅ Correspondence (Letter Type และ Other Types) -- ✅ RFA with Discipline -- ✅ Transmittal (To Owner vs To Contractor with different formats) -- ✅ Drawing with Category +- ✅ LETTER / RFI / MEMO / EMAIL / MOM / INSTRUCTION / NOTICE / OTHER + - Counter Key: `(project_id, originator_org_id, recipient_org_id, corr_type_id, 0, 0, 0, year)` +- ✅ TRANSMITTAL + - Counter Key: `(project_id, originator_org_id, recipient_org_id, corr_type_id, sub_type_id, 0, 0, year)` +- ✅ RFA + - Counter Key: `(project_id, originator_org_id, NULL, corr_type_id, 0, rfa_type_id, discipline_id, year)` ### 4. Error Handling -- ✅ Scenario 1: Redis Unavailable → Fallback to DB lock +- ✅ Scenario 1: Redis Unavailable → Fallback to DB pessimistic lock - ✅ Scenario 2: Lock Timeout → Retry 5x with exponential backoff -- ✅ Scenario 3: Version Conflict → Retry 2x -- ✅ Scenario 4: DB Connection Error → Retry 3x +- ✅ Scenario 3: Version Conflict → Retry 2x immediately +- ✅ Scenario 4: DB Connection Error → Retry 3x with exponential backoff ### 5. Audit & Monitoring -- ✅ Audit log for every generated number -- ✅ Track lock wait times, retry counts, errors -- ✅ Metrics collection for monitoring dashboard +- ✅ Audit log for every generated number (with performance metrics) +- ✅ Error logging with classification (LOCK_TIMEOUT, VERSION_CONFLICT, etc.) +- ✅ Prometheus metrics collection - ✅ Alerting on failures >5% ### 6. Security -- ✅ Rate limiting: 10 req/min per user, 50 req/min per IP -- ✅ Authorization checks +- ✅ Rate limiting: 10 req/min per user, 50 req/min per IP (using @nestjs/throttler) +- ✅ Authorization checks (JWT + Roles) - ✅ IP address logging --- @@ -136,37 +143,56 @@ export class DocumentNumberConfig { import { Entity, PrimaryColumn, Column, UpdateDateColumn, VersionColumn } from 'typeorm'; +/** + * ตาราง document_number_counters + * Composite PK: (project_id, originator_organization_id, recipient_organization_id, + * correspondence_type_id, sub_type_id, rfa_type_id, discipline_id, current_year) + * + * References: specs/01-requirements/03.11-document-numbering.md#counter-key-components + */ @Entity('document_number_counters') export class DocumentNumberCounter { - @PrimaryColumn() - project_id: number; + @PrimaryColumn({ name: 'project_id' }) + projectId: number; - @PrimaryColumn() - doc_type_id: number; + @PrimaryColumn({ name: 'originator_organization_id' }) + originatorOrganizationId: number; - @PrimaryColumn({ default: 0 }) - sub_type_id: number; // สำหรับ Correspondence types + @PrimaryColumn({ name: 'recipient_organization_id', nullable: true }) + recipientOrganizationId: number | null; // NULL for RFA - @PrimaryColumn({ default: 0 }) - discipline_id: number; // สำหรับ RFA, Drawing + @PrimaryColumn({ name: 'correspondence_type_id' }) + correspondenceTypeId: number; - @PrimaryColumn({ type: 'varchar', length: 20, nullable: true, default: null }) - recipient_type: string; // สำหรับ Transmittal: 'OWNER', 'CONTRACTOR', 'CONSULTANT', 'OTHER' + @PrimaryColumn({ name: 'sub_type_id', default: 0 }) + subTypeId: number; // for TRANSMITTAL only - @PrimaryColumn() - year: number; // ปี พ.ศ. หรือ ค.ศ. + @PrimaryColumn({ name: 'rfa_type_id', default: 0 }) + rfaTypeId: number; // for RFA only - @Column({ default: 0 }) - last_number: number; + @PrimaryColumn({ name: 'discipline_id', default: 0 }) + disciplineId: number; // for RFA only - @VersionColumn({ comment: 'Optimistic Lock version' }) + @PrimaryColumn({ name: 'current_year' }) + currentYear: number; // ปี ค.ศ. + + @Column({ name: 'last_number', default: 0 }) + lastNumber: number; + + @VersionColumn({ name: 'version', comment: 'Optimistic Lock version' }) version: number; - @UpdateDateColumn() - updated_at: Date; + @UpdateDateColumn({ name: 'updated_at' }) + updatedAt: Date; } ``` +> **⚠️ หมายเหตุ Schema:** +> +> - Primary Key ใช้ `COALESCE(recipient_organization_id, 0)` ในการสร้าง constraint (ดู migration file) +> - `sub_type_id`, `rfa_type_id`, `discipline_id` ใช้ `0` แทน NULL +> - Counter reset อัตโนมัติทุกปี (แยก counter ตาม `current_year`) + #### 1.3 Document Number Audit Entity ```typescript @@ -1179,6 +1205,7 @@ ensure: ## 📦 Deliverables ### Core Implementation + - [x] DocumentNumberingService with all 4 error scenarios - [x] DocumentNumberCounter Entity (with sub_type_id, recipient_type) - [x] DocumentNumberConfig Entity @@ -1188,12 +1215,14 @@ ensure: - [x] Retry Logic with Exponential Backoff ### API & Security + - [x] DocumentNumberingController with 4 endpoints - [x] Rate Limiting Guard (10/min per user, 50/min per IP) - [x] Authorization Guards - [x] API Documentation (Swagger) ### Testing + - [x] Unit Tests (targeting 90%+ coverage) - [x] Concurrent Tests (100+ simultaneous requests) - [x] Error Scenario Tests (all 4 scenarios) @@ -1202,6 +1231,7 @@ ensure: - [x] Load Tests (Artillery config for 50-100 req/sec) ### Monitoring & Documentation + - [x] Metrics Collection Integration - [x] Audit Logging - [x] Implementation Documentation @@ -1225,11 +1255,13 @@ ensure: ## 📌 Implementation Notes ### Performance Targets + - **Normal Operation:** <500ms (no conflicts, Redis available) - **95th Percentile:** <2 seconds (including retries) - **99th Percentile:** <5 seconds (worst case scenarios) ### Lock Configuration + - **Redis Lock TTL:** 5 seconds (auto-release) - **Lock Acquisition Timeout:** 10 seconds - **Max Retries (Lock):** 5 times with exponential backoff (1s, 2s, 4s, 8s, 16s) @@ -1237,18 +1269,22 @@ ensure: - **Max Retries (DB Error):** 3 times with exponential backoff (1s, 2s, 4s) ### Rate Limiting + - **Per User:** 10 requests/minute - **Per IP:** 50 requests/minute - **Global:** 5000 requests/minute ### Format Templates + Stored in database (`document_number_configs` table), configurable per: + - Project - Document Type - Sub Type (optional, use 0 for fallback) - Discipline (optional, use 0 for fallback) ### Counter Reset + - Automatic reset per year (based on `{YEAR:B.E.}` or `{YEAR:A.D.}` in template) - Manual reset available (Super Admin only, with audit log)