# Data Model: RFA Approval System Refactor **Date**: 2026-05-11 **Based on**: research.md decisions, spec.md requirements --- ## Entity Relationship Diagram ``` ┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ │ ReviewTeam │────<│ ReviewTeamMember │>────│ User │ ├─────────────────┤ ├──────────────────┤ ├─────────────────┤ │ publicId (PK) │ │ teamId (FK) │ │ publicId (PK) │ │ projectId (FK) │ │ userId (FK) │ │ ... │ │ name │ │ disciplineId(FK) │ └─────────────────┘ │ isActive │ │ role │ └────────┬────────┘ └──────────────────┘ │ │ 1:N ▼ ┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ │ ReviewTask │>────│ RfaRevision │<────│ RfaStatusCode │ ├─────────────────┤ ├──────────────────┤ ├─────────────────┤ │ publicId (PK) │ │ id (PK) │ │ id (PK) │ │ teamId (FK) │ │ correspondenceId │ │ statusCode │ │ disciplineId │ │ revisionNumber │ └─────────────────┘ │ assignedToId │ └──────────────────┘ │ status │ │ dueDate │ │ responseCodeId │>────┐ │ comments │ │ └─────────────────┘ │ ▼ ┌─────────────────┐ │ ResponseCode │ ├─────────────────┤ │ id (PK) │ │ code │ │ subStatus │ │ category │ │ descriptionTh │ │ descriptionEn │ │ implications │ └────────┬────────┘ │ 1:N ▼ ┌─────────────────┐ │ResponseCodeRule │ ├─────────────────┤ │ id (PK) │ │ projectId (FK) │ │ documentTypeId │ │ responseCodeId │ │ isEnabled │ │ requiresComments│ │ triggersNotification│ │ parentRuleId │ └─────────────────┘ ┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ │ Delegation │ │ ReminderRule │ │ DistributionMatrix│ ├─────────────────┤ ├──────────────────┤ ├─────────────────┤ │ publicId (PK) │ │ publicId (PK) │ │ publicId (PK) │ │ delegatorId │ │ name │ │ name │ │ delegateeId │ │ projectId │ │ documentTypeId│ │ startDate │ │ documentTypeId │ │ responseCodeId│ │ endDate │ │ triggerDays │ │ conditions │ │ scope │ │ reminderType │ │ isActive │ │ isActive │ │ recipients │ └────────┬────────┘ └─────────────────┘ │ messageTemplate │ │ 1:N └──────────────────┘ ▼ ┌─────────────────┐ │DistributionRecipient│ ├─────────────────┤ │ id (PK) │ │ matrixId (FK) │ │ recipientType │ │ recipientId │ │ deliveryMethod │ └─────────────────┘ ``` --- ## Core Entities ### 1. ReviewTeam (ทีมตรวจสอบ) ```typescript @Entity('review_teams') class ReviewTeam { @PrimaryGeneratedColumn('increment') @Exclude() id: number; @Column({ type: 'uuid', unique: true }) publicId: string; // ADR-019: UUIDv7 string @Column() @Exclude() projectId: number; // FK to projects @Column({ length: 100 }) name: string; @Column({ length: 255, nullable: true }) description?: string; @Column('simple-array', { nullable: true }) defaultForRfaTypes?: string[]; // ['SDW', 'DDW', 'ADW'] @Column({ default: true }) isActive: boolean; @CreateDateColumn() createdAt: Date; @UpdateDateColumn() updatedAt: Date; // Relations @OneToMany(() => ReviewTeamMember, member => member.team) members: ReviewTeamMember[]; } ``` **Key Fields**: - `defaultForRfaTypes`: Auto-assign this team to specific RFA types - `isActive`: Soft delete support --- ### 2. ReviewTeamMember (สมาชิกทีม) ```typescript @Entity('review_team_members') class ReviewTeamMember { @PrimaryGeneratedColumn('increment') @Exclude() id: number; @Column({ type: 'uuid', unique: true }) publicId: string; @Column() @Exclude() teamId: number; @Column() @Exclude() userId: number; @Column() @Exclude() disciplineId: number; // FK to disciplines @Column({ length: 50, default: 'REVIEWER' }) role: 'REVIEWER' | 'LEAD' | 'MANAGER'; @Column({ default: 0 }) priorityOrder: number; // For sequential assignment fallback @CreateDateColumn() createdAt: Date; // Relations @ManyToOne(() => ReviewTeam, team => team.members) @JoinColumn({ name: 'teamId' }) team: ReviewTeam; @ManyToOne(() => User, user => user.reviewTeamMemberships) @JoinColumn({ name: 'userId' }) user: User; } ``` --- ### 3. ReviewTask (งานตรวจสอบ) ```typescript export enum ReviewTaskStatus { PENDING = 'PENDING', // รอดำเนินการ IN_PROGRESS = 'IN_PROGRESS', // กำลังตรวจสอบ COMPLETED = 'COMPLETED', // เสร็จสิ้น (มีผลลัพธ์) DELEGATED = 'DELEGATED', // ถูกมอบหมายให้ผู้อื่น EXPIRED = 'EXPIRED', // เกินกำหนด CANCELLED = 'CANCELLED', // ยกเลิก } @Entity('review_tasks') class ReviewTask { @PrimaryGeneratedColumn('increment') @Exclude() id: number; @Column({ type: 'uuid', unique: true }) publicId: string; @Column() @Exclude() rfaRevisionId: number; // FK to rfa_revisions @Column() @Exclude() teamId: number; @Column() @Exclude() disciplineId: number; @Column({ nullable: true }) @Exclude() assignedToUserId?: number; // Null = auto-assign by discipline @Column({ type: 'enum', enum: ReviewTaskStatus, default: ReviewTaskStatus.PENDING }) status: ReviewTaskStatus; @Column({ type: 'date', nullable: true }) dueDate?: Date; @Column({ nullable: true }) @Exclude() responseCodeId?: number; @Column({ type: 'text', nullable: true }) comments?: string; @Column({ type: 'json', nullable: true }) attachments?: string[]; // Array of attachment publicIds @Column({ nullable: true }) @Exclude() delegatedFromUserId?: number; // For delegation tracking @Column({ type: 'timestamp', nullable: true }) completedAt?: Date; @CreateDateColumn() createdAt: Date; @UpdateDateColumn() updatedAt: Date; // Relations @ManyToOne(() => ReviewTeam, team => team.tasks) team: ReviewTeam; @ManyToOne(() => ResponseCode) @JoinColumn({ name: 'responseCodeId' }) responseCode?: ResponseCode; } ``` --- ### 4. ResponseCode (รหัสตอบกลับมาตรฐาน) ```typescript export enum ResponseCodeCategory { ENGINEERING = 'ENGINEERING', // Shop Drawing / MS MATERIAL = 'MATERIAL', // Material / Procurement CONTRACT = 'CONTRACT', // Contract / Cost / BOQ TESTING = 'TESTING', // Testing / Handover / QA ESG = 'ESG', // Environment / Social } @Entity('response_codes') class ResponseCode { @PrimaryGeneratedColumn('increment') @Exclude() id: number; @Column({ type: 'uuid', unique: true }) publicId: string; @Column({ length: 10 }) code: string; // '1A', '1B', '1C', '1D', '1E', '1F', '1G', '2', '3', '4' @Column({ length: 10, nullable: true }) subStatus?: string; // '1A', '1B', etc. @Column({ type: 'enum', enum: ResponseCodeCategory }) category: ResponseCodeCategory; @Column({ type: 'text' }) descriptionTh: string; @Column({ type: 'text' }) descriptionEn: string; @Column({ type: 'json', nullable: true }) implications?: { affectsSchedule?: boolean; affectsCost?: boolean; requiresContractReview?: boolean; requiresEiaAmendment?: boolean; }; @Column({ type: 'simple-array', nullable: true }) notifyRoles?: string[]; // ['CONTRACT_MANAGER', 'QS_MANAGER', 'EIA_OFFICER'] @Column({ default: true }) isActive: boolean; @Column({ default: false }) isSystem: boolean; // System default, cannot delete @CreateDateColumn() createdAt: Date; // Relations @OneToMany(() => ResponseCodeRule, rule => rule.responseCode) rules: ResponseCodeRule[]; } ``` --- ### 5. ResponseCodeRule (กฎการใช้รหัส) ```typescript @Entity('response_code_rules') class ResponseCodeRule { @PrimaryGeneratedColumn('increment') @Exclude() id: number; @Column({ type: 'uuid', unique: true }) publicId: string; @Column({ nullable: true }) @Exclude() projectId?: number; // NULL = global default @Column() @Exclude() documentTypeId: number; @Column() @Exclude() responseCodeId: number; @Column({ default: true }) isEnabled: boolean; @Column({ default: false }) requiresComments: boolean; @Column({ default: false }) triggersNotification: boolean; @Column({ nullable: true }) @Exclude() parentRuleId?: number; // Inheritance tracking @CreateDateColumn() createdAt: Date; @UpdateDateColumn() updatedAt: Date; // Relations @ManyToOne(() => ResponseCode, code => code.rules) @JoinColumn({ name: 'responseCodeId' }) responseCode: ResponseCode; } ``` --- ### 6. Delegation (การมอบหมาย) ```typescript export enum DelegationScope { ALL = 'ALL', RFA_ONLY = 'RFA_ONLY', CORRESPONDENCE_ONLY = 'CORRESPONDENCE_ONLY', SPECIFIC_TYPES = 'SPECIFIC_TYPES', } @Entity('delegations') class Delegation { @PrimaryGeneratedColumn('increment') @Exclude() id: number; @Column({ type: 'uuid', unique: true }) publicId: string; @Column() @Exclude() delegatorId: number; // ผู้มอบหมาย @Column() @Exclude() delegateeId: number; // ผู้รับมอบหมาย @Column({ type: 'date' }) startDate: Date; @Column({ type: 'date', nullable: true }) endDate?: Date; @Column({ type: 'enum', enum: DelegationScope, default: DelegationScope.ALL }) scope: DelegationScope; @Column('simple-array', { nullable: true }) documentTypes?: string[]; // ['SDW', 'DDW'] when scope = SPECIFIC_TYPES @Column({ default: true }) isActive: boolean; @Column({ type: 'text', nullable: true }) reason?: string; @CreateDateColumn() createdAt: Date; @UpdateDateColumn() updatedAt: Date; // Relations @ManyToOne(() => User) @JoinColumn({ name: 'delegatorId' }) delegator: User; @ManyToOne(() => User) @JoinColumn({ name: 'delegateeId' }) delegatee: User; } ``` --- ### 7. ReminderRule (กฎการแจ้งเตือน) ```typescript export enum ReminderType { DUE_SOON = 'DUE_SOON', // X days before due ON_DUE = 'ON_DUE', // On due date OVERDUE = 'OVERDUE', // After due date (repeating) ESCALATION_L1 = 'ESCALATION_L1', // Level 1 escalation ESCALATION_L2 = 'ESCALATION_L2', // Level 2 escalation } @Entity('reminder_rules') class ReminderRule { @PrimaryGeneratedColumn('increment') @Exclude() id: number; @Column({ type: 'uuid', unique: true }) publicId: string; @Column({ length: 100 }) name: string; @Column({ nullable: true }) @Exclude() projectId?: number; // NULL = global @Column({ nullable: true }) @Exclude() documentTypeId?: number; // NULL = all types @Column({ default: 2 }) triggerDaysBeforeDue: number; // Days before due for DUE_SOON @Column({ default: 1 }) escalationDaysAfterDue: number; // Days after due for L1 escalation @Column({ type: 'enum', enum: ReminderType }) reminderType: ReminderType; @Column({ type: 'simple-array' }) recipients: ('ASSIGNEE' | 'MANAGER' | 'PROJECT_MANAGER')[]; @Column({ type: 'text' }) messageTemplateTh: string; @Column({ type: 'text' }) messageTemplateEn: string; @Column({ default: true }) isActive: boolean; @CreateDateColumn() createdAt: Date; @UpdateDateColumn() updatedAt: Date; } ``` --- ### 8. DistributionMatrix (ตารางกระจายเอกสาร) ```typescript @Entity('distribution_matrices') class DistributionMatrix { @PrimaryGeneratedColumn('increment') @Exclude() id: number; @Column({ type: 'uuid', unique: true }) publicId: string; @Column({ length: 100 }) name: string; @Column() @Exclude() documentTypeId: number; @Column({ nullable: true }) @Exclude() responseCodeId?: number; // NULL = all codes @Column({ type: 'json', nullable: true }) conditions?: { codes?: string[]; // ['1A', '1B', '2'] excludeCodes?: string[]; // ['3', '4'] projectPhase?: string; }; @Column({ default: true }) isActive: boolean; @CreateDateColumn() createdAt: Date; // Relations @OneToMany(() => DistributionRecipient, recipient => recipient.matrix) recipients: DistributionRecipient[]; } ``` --- ### 9. DistributionRecipient (ผู้รับเอกสาร) ```typescript export enum RecipientType { USER = 'USER', ORGANIZATION = 'ORGANIZATION', TEAM = 'TEAM', ROLE = 'ROLE', // e.g., 'ALL_QS', 'ALL_SITE_ENG' } export enum DeliveryMethod { EMAIL = 'EMAIL', IN_APP = 'IN_APP', BOTH = 'BOTH', } @Entity('distribution_recipients') class DistributionRecipient { @PrimaryGeneratedColumn('increment') @Exclude() id: number; @Column() @Exclude() matrixId: number; @Column({ type: 'enum', enum: RecipientType }) recipientType: RecipientType; @Column({ type: 'uuid' }) // Can be userId, orgId, teamId recipientPublicId: string; @Column({ type: 'enum', enum: DeliveryMethod, default: DeliveryMethod.BOTH }) deliveryMethod: DeliveryMethod; @Column({ nullable: true }) sequence?: number; // For ordered delivery @CreateDateColumn() createdAt: Date; } ``` --- ## Database Indexes ```sql -- Review Tasks - Core query patterns CREATE INDEX idx_review_tasks_rfa_revision ON review_tasks(rfaRevisionId); CREATE INDEX idx_review_tasks_status ON review_tasks(status); CREATE INDEX idx_review_tasks_assigned ON review_tasks(assignedToUserId, status); -- Response Code Rules - Lookup by document type CREATE INDEX idx_response_rules_lookup ON response_code_rules( projectId, documentTypeId, isEnabled ); -- Delegations - Active lookup CREATE INDEX idx_delegations_active ON delegations( delegatorId, isActive, startDate, endDate ); -- Distribution - Matrix lookup CREATE INDEX idx_distribution_lookup ON distribution_matrices( documentTypeId, responseCodeId, isActive ); ``` --- ## Validation Rules | Entity | Rule | Implementation | |--------|------|----------------| | ReviewTask | Cannot assign completed task | Check status before update | | Delegation | No circular chains | BFS/DFS validation on create | | Delegation | Max 3 levels deep | Enforce in service layer | | ResponseCodeRule | Only one enabled per doc type + code per project | Unique constraint | | ReviewTeam | At least one member with LEAD role | Validate on activation | --- ## Next Steps 1. Generate SQL schema file (follows ADR-009: no TypeORM migrations) 2. Create TypeORM entities in `backend/src/modules/` 3. Create API contracts in `contracts/`