260322:1648 Correct Coresspondence / Doing RFA / Correct CI
This commit is contained in:
@@ -222,20 +222,9 @@ CREATE TABLE workflow_histories (
|
||||
```typescript
|
||||
// workflow-engine.module.ts
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
WorkflowDefinition,
|
||||
WorkflowInstance,
|
||||
WorkflowHistory,
|
||||
]),
|
||||
UserModule,
|
||||
],
|
||||
imports: [TypeOrmModule.forFeature([WorkflowDefinition, WorkflowInstance, WorkflowHistory]), UserModule],
|
||||
controllers: [WorkflowEngineController],
|
||||
providers: [
|
||||
WorkflowEngineService,
|
||||
WorkflowDslService,
|
||||
WorkflowEventService,
|
||||
],
|
||||
providers: [WorkflowEngineService, WorkflowDslService, WorkflowEventService],
|
||||
exports: [WorkflowEngineService],
|
||||
})
|
||||
export class WorkflowEngineModule {}
|
||||
@@ -275,12 +264,7 @@ export class WorkflowEngineService {
|
||||
payload: Record<string, unknown> = {}
|
||||
) {
|
||||
// Evaluation via WorkflowDslService
|
||||
const evaluation = this.dslService.evaluate(
|
||||
compiled,
|
||||
instance.currentState,
|
||||
action,
|
||||
context
|
||||
);
|
||||
const evaluation = this.dslService.evaluate(compiled, instance.currentState, action, context);
|
||||
|
||||
// Update state to target State
|
||||
instance.currentState = evaluation.nextState;
|
||||
|
||||
@@ -196,22 +196,23 @@ CREATE TABLE document_number_audit (
|
||||
|
||||
| Token | Description | Example Value | Database Source |
|
||||
| -------------- | ------------------------- | ------------------------------ | --------------------------------------------------------------------- |
|
||||
| `{PROJECT}` | รหัสโครงการ | `LCBP3`, `LCBP3-C2` | `projects.project_code` |
|
||||
| `{ORIGINATOR}` | รหัสองค์กรผู้ส่ง | `คคง.`, `ผรม.1` | `organizations.organization_code` via `correspondences.originator_id` |
|
||||
| `{RECIPIENT}` | รหัสองค์กรผู้รับหลัก (TO) | `สคฉ.3`, `กทท.` | `organizations.organization_code` via `correspondence_recipients` |
|
||||
| `{CORR_TYPE}` | รหัสประเภทเอกสาร | `RFA`, `TRANSMITTAL`, `LETTER` | `correspondence_types.type_code` |
|
||||
| `{SUB_TYPE}` | หมายเลขประเภทย่อย | `11`, `12`, `21` | `correspondence_sub_types.sub_type_number` |
|
||||
| `{RFA_TYPE}` | รหัสประเภท RFA | `SDW`, `RPT`, `MAT` | `rfa_types.type_code` |
|
||||
| `{DISCIPLINE}` | รหัสสาขาวิชา | `STR`, `TER`, `GEO` | `disciplines.discipline_code` |
|
||||
| `{PROJECT}` | รหัสโครงการ | `LCBP3`, `LCBP3-C2` | `projects.project_code` |
|
||||
| `{ORIGINATOR}` | รหัสองค์กรผู้ส่ง | `คคง.`, `ผรม.1` | `organizations.organization_code` via `correspondences.originator_id` |
|
||||
| `{RECIPIENT}` | รหัสองค์กรผู้รับหลัก (TO) | `สคฉ.3`, `กทท.` | `organizations.organization_code` via `correspondence_recipients` |
|
||||
| `{CORR_TYPE}` | รหัสประเภทเอกสาร | `RFA`, `TRANSMITTAL`, `LETTER` | `correspondence_types.type_code` |
|
||||
| `{SUB_TYPE}` | หมายเลขประเภทย่อย | `11`, `12`, `21` | `correspondence_sub_types.sub_type_number` |
|
||||
| `{RFA_TYPE}` | รหัสประเภท RFA | `SDW`, `RPT`, `MAT` | `rfa_types.type_code` |
|
||||
| `{DISCIPLINE}` | รหัสสาขาวิชา | `STR`, `TER`, `GEO` | `disciplines.discipline_code` |
|
||||
| `{SEQ:n}` | Running number (n digits) | `0001`, `0029`, `0985` | `document_number_counters.last_number + 1` |
|
||||
| `{YEAR:B.E.}` | ปี พ.ศ. | `2568` | `current_year + 543` |
|
||||
| `{YEAR:A.D.}` | ปี ค.ศ. | `2025` | `current_year` |
|
||||
| `{YEAR:B.E.}` | ปี พ.ศ. | `2568` | `current_year + 543` |
|
||||
| `{YEAR:A.D.}` | ปี ค.ศ. | `2025` | `current_year` |
|
||||
| `{REV}` | Revision Code | `A`, `B`, `AA` | `correspondence_revisions.revision_label` |
|
||||
|
||||
> [!WARNING]
|
||||
> **Deprecated Token Names (DO NOT USE)**
|
||||
>
|
||||
> The following tokens were used in earlier drafts but are now **deprecated**:
|
||||
>
|
||||
> - ~~`{ORG}`~~ → Use `{ORIGINATOR}` or `{RECIPIENT}` (explicit roles)
|
||||
> - ~~`{TYPE}`~~ → Use `{CORR_TYPE}`, `{SUB_TYPE}`, or `{RFA_TYPE}` (context-specific)
|
||||
> - ~~`{CATEGORY}`~~ → Not used in current system
|
||||
@@ -221,6 +222,7 @@ CREATE TABLE document_number_audit (
|
||||
### Format Resolution Strategy (Fallback Logic)
|
||||
|
||||
The system resolves the numbering format using the following priority:
|
||||
|
||||
1. **Specific Format:** Search for a record matching both `project_id` and `correspondence_type_id`.
|
||||
2. **Default Format:** If not found, search for a record with matching `project_id` where `correspondence_type_id` is `NULL`.
|
||||
3. **System Fallback:** If neither exists, use the hardcoded system default: `{ORG}-{RECIPIENT}-{SEQ:4}-{YEAR:BE}`.
|
||||
@@ -267,6 +269,7 @@ Counter Key: (project_id, originator_org_id, recipient_org_id, corr_type_id, sub
|
||||
```
|
||||
|
||||
**Token Breakdown:**
|
||||
|
||||
- `คคง.` = `{ORIGINATOR}` - ผู้ส่ง
|
||||
- `สคฉ.3` = `{RECIPIENT}` - ผู้รับหลัก (TO)
|
||||
- `21` = `{SUB_TYPE}` - หมายเลขประเภทย่อย (11=MAT, 12=SHP, 13=DWG, 21=...)
|
||||
@@ -284,6 +287,7 @@ Counter Key: (project_id, originator_org_id, NULL, corr_type_id, 0, rfa_type_id,
|
||||
```
|
||||
|
||||
**Token Breakdown:**
|
||||
|
||||
- `LCBP3-C2` = `{PROJECT}` - รหัสโครงการ
|
||||
- `RFA` = `{CORR_TYPE}` - ประเภทเอกสาร (**แสดง**ในtemplate สำหรับ RFA เท่านั้น)
|
||||
- `TER` = `{DISCIPLINE}` - รหัสสาขา (TER=Terminal, STR=Structure, GEO=Geotechnical)
|
||||
@@ -340,8 +344,8 @@ export class DocumentNumberingService {
|
||||
|
||||
async generateNextNumber(context: NumberingContext): Promise<string> {
|
||||
const year = context.year || new Date().getFullYear() + 543; // พ.ศ.
|
||||
const subTypeId = context.subTypeId || 0; // Fallback for NULL
|
||||
const disciplineId = context.disciplineId || 0; // Fallback for NULL
|
||||
const subTypeId = context.subTypeId || 0; // Fallback for NULL
|
||||
const disciplineId = context.disciplineId || 0; // Fallback for NULL
|
||||
|
||||
// Build Redis lock key
|
||||
const lockKey = this.buildLockKey(
|
||||
@@ -355,14 +359,8 @@ export class DocumentNumberingService {
|
||||
|
||||
// Retry with exponential backoff (Scenario 2 & 3)
|
||||
return this.retryWithBackoff(
|
||||
async () => await this.generateNumberWithLock(
|
||||
lockKey,
|
||||
context,
|
||||
year,
|
||||
subTypeId,
|
||||
disciplineId
|
||||
),
|
||||
5, // Max 5 retries
|
||||
async () => await this.generateNumberWithLock(lockKey, context, year, subTypeId, disciplineId),
|
||||
5, // Max 5 retries
|
||||
1000 // Initial delay 1s
|
||||
);
|
||||
}
|
||||
@@ -444,12 +442,7 @@ export class DocumentNumberingService {
|
||||
}
|
||||
|
||||
// Step 4: Generate formatted number
|
||||
const config = await this.getConfig(
|
||||
context.projectId,
|
||||
context.docTypeId,
|
||||
subTypeId,
|
||||
disciplineId
|
||||
);
|
||||
const config = await this.getConfig(context.projectId, context.docTypeId, subTypeId, disciplineId);
|
||||
|
||||
const formattedNumber = await this.formatNumber(config.template, {
|
||||
...context,
|
||||
@@ -471,7 +464,6 @@ export class DocumentNumberingService {
|
||||
|
||||
this.logger.log(`Generated: ${formattedNumber} (wait: ${lockWaitMs}ms)`);
|
||||
return formattedNumber;
|
||||
|
||||
} finally {
|
||||
// Step 6: Release Redis lock
|
||||
if (lock) {
|
||||
@@ -506,35 +498,29 @@ export class DocumentNumberingService {
|
||||
}
|
||||
|
||||
private buildLockKey(...parts: Array<number | string | null | undefined>): string {
|
||||
return `doc_num:${parts.filter(p => p !== null && p !== undefined).join(':')}`;
|
||||
return `doc_num:${parts.filter((p) => p !== null && p !== undefined).join(':')}`;
|
||||
}
|
||||
|
||||
// Scenario 2: Lock Acquisition Timeout - Exponential Backoff
|
||||
private async retryWithBackoff<T>(
|
||||
fn: () => Promise<T>,
|
||||
maxRetries: number,
|
||||
initialDelay: number
|
||||
): Promise<T> {
|
||||
private async retryWithBackoff<T>(fn: () => Promise<T>, maxRetries: number, initialDelay: number): Promise<T> {
|
||||
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (error) {
|
||||
const isRetryable =
|
||||
error instanceof ConflictException ||
|
||||
error.code === 'ECONNREFUSED' || // Scenario 4
|
||||
error.code === 'ETIMEDOUT'; // Scenario 4
|
||||
error.code === 'ECONNREFUSED' || // Scenario 4
|
||||
error.code === 'ETIMEDOUT'; // Scenario 4
|
||||
|
||||
if (!isRetryable || attempt === maxRetries) {
|
||||
if (attempt === maxRetries) {
|
||||
throw new ServiceUnavailableException(
|
||||
'ระบบกำลังยุ่ง กรุณาลองใหม่ภายหลัง'
|
||||
);
|
||||
throw new ServiceUnavailableException('ระบบกำลังยุ่ง กรุณาลองใหม่ภายหลัง');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
const delay = initialDelay * Math.pow(2, attempt);
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
this.logger.warn(`Retry ${attempt + 1}/${maxRetries} after ${delay}ms`);
|
||||
}
|
||||
}
|
||||
@@ -721,8 +707,8 @@ sequenceDiagram
|
||||
|
||||
### Alert Conditions
|
||||
|
||||
| Severity | Condition | Action |
|
||||
| ---------- | ---------------------------- | ------------------ |
|
||||
| Severity | Condition | Action |
|
||||
| ----------- | ---------------------------- | ------------------ |
|
||||
| 🔴 Critical | Redis unavailable >1 minute | Page ops team |
|
||||
| 🔴 Critical | Lock failures >10% in 5 min | Page ops team |
|
||||
| 🟡 Warning | Lock failures >5% in 5 min | Alert ops team |
|
||||
@@ -823,7 +809,7 @@ describe('DocumentNumberingService - Concurrent Generation', () => {
|
||||
expect(unique.size).toBe(100);
|
||||
|
||||
// Check format
|
||||
results.forEach(num => {
|
||||
results.forEach((num) => {
|
||||
expect(num).toMatch(/^LCBP3-C2-RFI-STR-\d{4}-A$/);
|
||||
});
|
||||
});
|
||||
@@ -874,9 +860,7 @@ describe('DocumentNumberingService - Concurrent Generation', () => {
|
||||
it('should throw 503 after max lock acquisition retries', async () => {
|
||||
jest.spyOn(redlock, 'acquire').mockRejectedValue(new Error('Lock timeout'));
|
||||
|
||||
await expect(service.generateNextNumber(context))
|
||||
.rejects
|
||||
.toThrow(ServiceUnavailableException);
|
||||
await expect(service.generateNextNumber(context)).rejects.toThrow(ServiceUnavailableException);
|
||||
});
|
||||
});
|
||||
```
|
||||
@@ -977,4 +961,4 @@ ensure:
|
||||
| 1.0 | 2025-11-30 | Initial decision |
|
||||
| 2.0 | 2025-12-02 | Updated with comprehensive error scenarios, monitoring, security, and all token types |
|
||||
| 3.0 | 2025-12-17 | Aligned with Requirements v1.6.2: updated counter schema, token definitions, Number State Machine |
|
||||
| 4.0 | 2026-03-21 | Added discipline_id to formats, implemented automated Upsert logic for template management |
|
||||
| 4.0 | 2026-03-21 | Added discipline_id to formats, implemented automated Upsert logic for template management |
|
||||
|
||||
@@ -89,31 +89,31 @@ LCBP3-DMS ต้องเลือก Technology Stack สำหรับพั
|
||||
|
||||
#### Backend Stack
|
||||
|
||||
| Component | Technology | Rationale |
|
||||
| :----------------- | :-------------- | :------------------------------------------------------------------------- |
|
||||
| **Runtime** | Node.js 20 LTS | Stable, modern features, long-term support |
|
||||
| **Framework** | NestJS 11 | Modular, TypeScript-first, Express v5 support |
|
||||
| **HTTP Engine** | Express 5 | Path param changes, improved error handling |
|
||||
| **Language** | TypeScript 5.x | Type safety, better DX |
|
||||
| **ORM** | TypeORM | TypeScript support, migrations, repositories |
|
||||
| **Database** | MariaDB 11.8 | JSON support, virtual columns, QNAP compatible |
|
||||
| **Validation** | class-validator | Decorator-based, integrates with NestJS |
|
||||
| **Authentication** | Passport + JWT | Standard, well-supported |
|
||||
| Component | Technology | Rationale |
|
||||
| :----------------- | :-------------- | :-------------------------------------------------------------------------- |
|
||||
| **Runtime** | Node.js 20 LTS | Stable, modern features, long-term support |
|
||||
| **Framework** | NestJS 11 | Modular, TypeScript-first, Express v5 support |
|
||||
| **HTTP Engine** | Express 5 | Path param changes, improved error handling |
|
||||
| **Language** | TypeScript 5.x | Type safety, better DX |
|
||||
| **ORM** | TypeORM | TypeScript support, migrations, repositories |
|
||||
| **Database** | MariaDB 11.8 | JSON support, virtual columns, QNAP compatible |
|
||||
| **Validation** | class-validator | Decorator-based, integrates with NestJS |
|
||||
| **Authentication** | Passport + JWT | Standard, well-supported |
|
||||
| **Authorization** | CASL **6.7.5+** | Flexible RBAC implementation ⚠️ Patched CVE-2026-1774 (Prototype Pollution) |
|
||||
| **Documentation** | Swagger/OpenAPI | Auto-generated from decorators |
|
||||
| **Testing** | Jest | Built-in with NestJS |
|
||||
| **Documentation** | Swagger/OpenAPI | Auto-generated from decorators |
|
||||
| **Testing** | Jest | Built-in with NestJS |
|
||||
|
||||
#### Frontend Stack
|
||||
|
||||
| Component | Technology | Rationale |
|
||||
| :-------------------- | :------------------ | :------------------------------------- |
|
||||
| **Framework** | Next.js 16.2.0 | App Router, SSR/SSG, React integration |
|
||||
| **UI Library** | React 19.2.4 | Industry standard, large ecosystem |
|
||||
| **Language** | TypeScript 5.x | Consistency with backend |
|
||||
| **Styling** | Tailwind CSS 4.2.2 | Utility-first, fast development |
|
||||
| **Component Library** | shadcn/ui | Accessible, customizable, TypeScript |
|
||||
| **State Management** | TanStack Query | Server state management |
|
||||
| **Form Handling** | React Hook Form 7.71.2 | Performance, ต้ validation with Zod |
|
||||
| Component | Technology | Rationale |
|
||||
| :-------------------- | :------------------------ | :------------------------------------- |
|
||||
| **Framework** | Next.js 16.2.0 | App Router, SSR/SSG, React integration |
|
||||
| **UI Library** | React 19.2.4 | Industry standard, large ecosystem |
|
||||
| **Language** | TypeScript 5.x | Consistency with backend |
|
||||
| **Styling** | Tailwind CSS 4.2.2 | Utility-first, fast development |
|
||||
| **Component Library** | shadcn/ui | Accessible, customizable, TypeScript |
|
||||
| **State Management** | TanStack Query | Server state management |
|
||||
| **Form Handling** | React Hook Form 7.71.2 | Performance, ต้ validation with Zod |
|
||||
| **Testing** | Vitest 4.1.0 + Playwright | Fast unit tests, reliable E2E |
|
||||
|
||||
#### Infrastructure
|
||||
|
||||
@@ -125,12 +125,7 @@ try {
|
||||
// Key: user:{user_id}:permissions
|
||||
// Value: JSON array of CASL rules
|
||||
// TTL: 30 minutes
|
||||
await redis.set(
|
||||
`user:${userId}:permissions`,
|
||||
JSON.stringify(abilityRules),
|
||||
'EX',
|
||||
1800
|
||||
);
|
||||
await redis.set(`user:${userId}:permissions`, JSON.stringify(abilityRules), 'EX', 1800);
|
||||
```
|
||||
|
||||
**Invalidation Strategy:**
|
||||
|
||||
@@ -253,10 +253,28 @@ export class EmailProcessor {
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; } .container { max-width: 600px;
|
||||
margin: 0 auto; padding: 20px; } .header { background: #007bff; color:
|
||||
white; padding: 20px; } .content { padding: 20px; } .button { background:
|
||||
#007bff; color: white; padding: 10px 20px; text-decoration: none; }
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.header {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
padding: 20px;
|
||||
}
|
||||
.content {
|
||||
padding: 20px;
|
||||
}
|
||||
.button {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -159,16 +159,9 @@ git commit -m "feat: add discipline_id to correspondences"
|
||||
|
||||
```typescript
|
||||
// File: backend/src/migrations/1234567890-AddDisciplineIdToCorrespondences.ts
|
||||
import {
|
||||
MigrationInterface,
|
||||
QueryRunner,
|
||||
TableColumn,
|
||||
TableForeignKey,
|
||||
} from 'typeorm';
|
||||
import { MigrationInterface, QueryRunner, TableColumn, TableForeignKey } from 'typeorm';
|
||||
|
||||
export class AddDisciplineIdToCorrespondences1234567890
|
||||
implements MigrationInterface
|
||||
{
|
||||
export class AddDisciplineIdToCorrespondences1234567890 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
// Add column
|
||||
await queryRunner.addColumn(
|
||||
@@ -192,21 +185,15 @@ export class AddDisciplineIdToCorrespondences1234567890
|
||||
);
|
||||
|
||||
// Add index
|
||||
await queryRunner.query(
|
||||
'CREATE INDEX idx_correspondences_discipline_id ON correspondences(discipline_id)'
|
||||
);
|
||||
await queryRunner.query('CREATE INDEX idx_correspondences_discipline_id ON correspondences(discipline_id)');
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
// Reverse order: index → FK → column
|
||||
await queryRunner.query(
|
||||
'DROP INDEX idx_correspondences_discipline_id ON correspondences'
|
||||
);
|
||||
await queryRunner.query('DROP INDEX idx_correspondences_discipline_id ON correspondences');
|
||||
|
||||
const table = await queryRunner.getTable('correspondences');
|
||||
const foreignKey = table.foreignKeys.find(
|
||||
(fk) => fk.columnNames.indexOf('discipline_id') !== -1
|
||||
);
|
||||
const foreignKey = table.foreignKeys.find((fk) => fk.columnNames.indexOf('discipline_id') !== -1);
|
||||
await queryRunner.dropForeignKey('correspondences', foreignKey);
|
||||
|
||||
await queryRunner.dropColumn('correspondences', 'discipline_id');
|
||||
@@ -303,9 +290,7 @@ describe('Migrations', () => {
|
||||
|
||||
// Verify tables exist
|
||||
const tables = await dataSource.query('SHOW TABLES');
|
||||
expect(tables).toContainEqual(
|
||||
expect.objectContaining({ Tables_in_lcbp3: 'correspondences' })
|
||||
);
|
||||
expect(tables).toContainEqual(expect.objectContaining({ Tables_in_lcbp3: 'correspondences' }));
|
||||
});
|
||||
|
||||
it('should rollback all migrations successfully', async () => {
|
||||
|
||||
@@ -131,9 +131,7 @@ export const logger = winston.createLogger({
|
||||
format: winston.format.combine(
|
||||
winston.format.colorize(),
|
||||
winston.format.printf(({ timestamp, level, message, ...meta }) => {
|
||||
return `${timestamp} [${level}]: ${message} ${
|
||||
Object.keys(meta).length ? JSON.stringify(meta) : ''
|
||||
}`;
|
||||
return `${timestamp} [${level}]: ${message} ${Object.keys(meta).length ? JSON.stringify(meta) : ''}`;
|
||||
})
|
||||
),
|
||||
}),
|
||||
@@ -240,10 +238,7 @@ export class RequestLoggerMiddleware implements NestMiddleware {
|
||||
// File: backend/src/config/database.config.ts
|
||||
export default {
|
||||
// ...
|
||||
logging:
|
||||
process.env.NODE_ENV === 'development'
|
||||
? 'all'
|
||||
: ['error', 'warn', 'schema'],
|
||||
logging: process.env.NODE_ENV === 'development' ? 'all' : ['error', 'warn', 'schema'],
|
||||
logger: 'advanced-console',
|
||||
maxQueryExecutionTime: 1000, // Warn if query > 1s
|
||||
};
|
||||
@@ -299,12 +294,7 @@ logger.verbose('Cache hit', { key, ttl });
|
||||
|
||||
```typescript
|
||||
// File: backend/src/common/interceptors/performance.interceptor.ts
|
||||
import {
|
||||
Injectable,
|
||||
NestInterceptor,
|
||||
ExecutionContext,
|
||||
CallHandler,
|
||||
} from '@nestjs/common';
|
||||
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
|
||||
import { Observable } from 'rxjs';
|
||||
import { tap } from 'rxjs/operators';
|
||||
import { logger } from 'src/config/logger.config';
|
||||
|
||||
@@ -119,10 +119,7 @@ export const correspondenceSchema = z.object({
|
||||
.min(5, 'Subject must be at least 5 characters')
|
||||
.max(255, 'Subject must not exceed 255 characters'),
|
||||
|
||||
description: z
|
||||
.string()
|
||||
.min(10, 'Description must be at least 10 characters')
|
||||
.optional(),
|
||||
description: z.string().min(10, 'Description must be at least 10 characters').optional(),
|
||||
|
||||
document_type_id: z.number({
|
||||
required_error: 'Document type is required',
|
||||
@@ -402,16 +399,10 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Validation failed', issues: error.errors },
|
||||
{ status: 400 }
|
||||
);
|
||||
return NextResponse.json({ error: 'Validation failed', issues: error.errors }, { status: 400 });
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -219,10 +219,7 @@ export const useNotificationStore = create<NotificationState>((set) => ({
|
||||
|
||||
addNotification: (notification) =>
|
||||
set((state) => ({
|
||||
notifications: [
|
||||
...state.notifications,
|
||||
{ ...notification, id: Math.random().toString() },
|
||||
],
|
||||
notifications: [...state.notifications, { ...notification, id: Math.random().toString() }],
|
||||
})),
|
||||
|
||||
removeNotification: (id) =>
|
||||
@@ -312,8 +309,7 @@ export const useUIStore = create<UIState>()(
|
||||
sidebarCollapsed: false,
|
||||
theme: 'light',
|
||||
|
||||
toggleSidebar: () =>
|
||||
set((state) => ({ sidebarCollapsed: !state.sidebarCollapsed })),
|
||||
toggleSidebar: () => set((state) => ({ sidebarCollapsed: !state.sidebarCollapsed })),
|
||||
|
||||
setTheme: (theme) => set({ theme }),
|
||||
}),
|
||||
|
||||
@@ -39,7 +39,7 @@ LCBP3-DMS จัดการเอกสารสำคัญของโปร
|
||||
|
||||
**Chosen:** **JWT (JSON Web Tokens) with Bearer Token Strategy (Stored in LocalStorage via Zustand)**
|
||||
|
||||
*Note: Initial plan was HTTP-only cookies, but shifted to Bearer tokens to ease cross-domain Next.js to NestJS communication.*
|
||||
_Note: Initial plan was HTTP-only cookies, but shifted to Bearer tokens to ease cross-domain Next.js to NestJS communication._
|
||||
|
||||
```typescript
|
||||
// File: src/auth/auth.service.ts
|
||||
@@ -85,10 +85,7 @@ export class AuthService {
|
||||
if (!user) return null;
|
||||
|
||||
// Use bcrypt for password comparison
|
||||
const isValid = await bcrypt.compare(
|
||||
credentials.password,
|
||||
user.password_hash
|
||||
);
|
||||
const isValid = await bcrypt.compare(credentials.password, user.password_hash);
|
||||
|
||||
return isValid ? user : null;
|
||||
}
|
||||
@@ -99,7 +96,7 @@ export class AuthService {
|
||||
|
||||
**Strategy:** **bcrypt with salt rounds = 10 (Current implementation defaults to 10 via `genSalt()`)**
|
||||
|
||||
*Note: Code currently uses `bcrypt.genSalt()` without arguments, defaulting to 10 rounds. If 12 is strictly required, codebase needs updating.*
|
||||
_Note: Code currently uses `bcrypt.genSalt()` without arguments, defaulting to 10 rounds. If 12 is strictly required, codebase needs updating._
|
||||
|
||||
```typescript
|
||||
import * as bcrypt from 'bcrypt';
|
||||
@@ -112,10 +109,7 @@ async function hashPassword(password: string): Promise<string> {
|
||||
}
|
||||
|
||||
// Verify password
|
||||
async function verifyPassword(
|
||||
password: string,
|
||||
hash: string
|
||||
): Promise<boolean> {
|
||||
async function verifyPassword(password: string, hash: string): Promise<boolean> {
|
||||
return bcrypt.compare(password, hash);
|
||||
}
|
||||
```
|
||||
@@ -177,11 +171,7 @@ function encrypt(text: string): { encrypted: string; iv: string; tag: string } {
|
||||
}
|
||||
|
||||
function decrypt(encrypted: string, iv: string, tag: string): string {
|
||||
const decipher = crypto.createDecipheriv(
|
||||
algorithm,
|
||||
key,
|
||||
Buffer.from(iv, 'hex')
|
||||
);
|
||||
const decipher = crypto.createDecipheriv(algorithm, key, Buffer.from(iv, 'hex'));
|
||||
|
||||
decipher.setAuthTag(Buffer.from(tag, 'hex'));
|
||||
|
||||
|
||||
@@ -5,11 +5,13 @@
|
||||
**Version:** 1.8.0
|
||||
**Decision Makers:** Development Team, DevOps Engineer
|
||||
**Related Documents:**
|
||||
|
||||
- [Legacy Data Migration Plan](../03-Data-and-Storage/03-04-legacy-data-migration.md)
|
||||
- [n8n Migration Setup Guide](../03-Data-and-Storage/03-05-n8n-migration-setup-guide.md)
|
||||
- [Software Architecture](../02-Architecture/02-02-software-architecture.md)
|
||||
- [Data Dictionary](../03-Data-and-Storage/03-01-data-dictionary.md)
|
||||
> **Note:** ADR-017 is clarified and hardened by ADR-018 regarding AI physical isolation. Category Enum system-driven, Idempotency Contract, Duplicate Handling Clarification, Storage Enforcement, Audit Log Enhancement, Review Queue Integration, Revision Drift Protection, Execution Time, Encoding Normalization, Security Hardening, Orchestrator on QNAP, AI Physical Isolation (Desktop Desk-5439).
|
||||
> **Note:** ADR-017 is clarified and hardened by ADR-018 regarding AI physical isolation. Category Enum system-driven, Idempotency Contract, Duplicate Handling Clarification, Storage Enforcement, Audit Log Enhancement, Review Queue Integration, Revision Drift Protection, Execution Time, Encoding Normalization, Security Hardening, Orchestrator on QNAP, AI Physical Isolation (Desktop Desk-5439).
|
||||
|
||||
---
|
||||
|
||||
## Context and Problem Statement
|
||||
@@ -19,6 +21,7 @@
|
||||
ความท้าทายหลักคือ **Data Integrity และความถูกต้องของ Metadata** เนื่องจากข้อมูลเก่ามีโอกาสเกิด Human Error เราจึงต้องการ AI ช่วย Validate ก่อนนำเข้า
|
||||
|
||||
การส่งข้อมูลขึ้น Cloud AI Provider มีปัญหา 2 ประการ:
|
||||
|
||||
1. **Data Privacy:** เอกสารก่อสร้างท่าเรือเป็นความลับ ห้ามออกนอกเครือข่าย
|
||||
2. **Cost:** ~$0.01–0.03 ต่อ Record = อาจสูงถึง $600 สำหรับ 20,000 records
|
||||
|
||||
@@ -44,6 +47,7 @@
|
||||
**Pros:** ไม่ต้องจัดหา Hardware เพิ่ม, AI ฉลาดสูง
|
||||
|
||||
**Cons:**
|
||||
|
||||
- ❌ ผิดนโยบาย Data Privacy
|
||||
- ❌ ค่าใช้จ่ายสูง (~$600)
|
||||
- ❌ Code สกปรก ปะปนกับ Source Code หลัก
|
||||
@@ -53,12 +57,14 @@
|
||||
**Pros:** เร็ว ไม่มีค่าใช้จ่าย
|
||||
|
||||
**Cons:**
|
||||
|
||||
- ❌ ความแม่นยำต่ำ ตรวจได้แค่ Format
|
||||
- ❌ ต้องใช้ Manual Review จำนวนมาก
|
||||
|
||||
### Option 3: Local AI Model (Ollama) + n8n ⭐ (Selected)
|
||||
|
||||
**Pros:**
|
||||
|
||||
- ✅ Privacy Guaranteed
|
||||
- ✅ Zero Cost
|
||||
- ✅ Clean Architecture
|
||||
@@ -67,6 +73,7 @@
|
||||
- ✅ Structured Output ด้วย JSON Schema
|
||||
|
||||
**Cons:**
|
||||
|
||||
- ❌ ต้องเปิด Desktop ทิ้งไว้ดูแล GPU Temperature
|
||||
- ❌ Model เล็กอาจแม่นน้อยกว่า Cloud AI → ต้องมี Human Review Queue
|
||||
|
||||
@@ -82,19 +89,19 @@
|
||||
|
||||
## Implementation Summary
|
||||
|
||||
| Component | รายละเอียด |
|
||||
| ---------------------- | ------------------------------------------------------------------------------- |
|
||||
| Migration Orchestrator | n8n (Docker บน QNAP NAS) |
|
||||
| AI Model Primary | Ollama `llama3.2:3b` (Validation, Summarization, Tagging) |
|
||||
| AI Model Fallback | Ollama `mistral:7b-instruct-q4_K_M` |
|
||||
| Hardware | QNAP NAS (Orchestrator) + Desktop Desk-5439 (AI Processing, RTX 2060 SUPER 8GB) |
|
||||
| DB Lookup (n8n) | n8n ทำการ Query `project_id`, `organization_id` และดึง `Tags` จาก DB ให้ AI |
|
||||
| Component | รายละเอียด |
|
||||
| ---------------------- | ------------------------------------------------------------------------------------------------------------ |
|
||||
| Migration Orchestrator | n8n (Docker บน QNAP NAS) |
|
||||
| AI Model Primary | Ollama `llama3.2:3b` (Validation, Summarization, Tagging) |
|
||||
| AI Model Fallback | Ollama `mistral:7b-instruct-q4_K_M` |
|
||||
| Hardware | QNAP NAS (Orchestrator) + Desktop Desk-5439 (AI Processing, RTX 2060 SUPER 8GB) |
|
||||
| DB Lookup (n8n) | n8n ทำการ Query `project_id`, `organization_id` และดึง `Tags` จาก DB ให้ AI |
|
||||
| Data Ingestion | 1. Staging ลง `migration_review_queue` -> 2. กดยืนยันผ่าน Frontend Management UI -> 3. Final Commit ผ่าน API |
|
||||
| Concurrency (n8n) | Sequential — Batch Size 50-100 ป้องกัน DB Connection Overload |
|
||||
| Checkpoint | MariaDB `migration_progress` และการใช้ `ON DUPLICATE KEY UPDATE` ใน Staging |
|
||||
| Fallback | Auto-switch Model เมื่อ Error ≥ Threshold |
|
||||
| Storage | Two-Phase Storage: 1. `POST /api/storage/upload` (Temp) -> 2. Commit ภายหลัง |
|
||||
| Expected Runtime | ~16.6 ชั่วโมง (~3–4 คืน) สำหรับ 20,000 records |
|
||||
| Concurrency (n8n) | Sequential — Batch Size 50-100 ป้องกัน DB Connection Overload |
|
||||
| Checkpoint | MariaDB `migration_progress` และการใช้ `ON DUPLICATE KEY UPDATE` ใน Staging |
|
||||
| Fallback | Auto-switch Model เมื่อ Error ≥ Threshold |
|
||||
| Storage | Two-Phase Storage: 1. `POST /api/storage/upload` (Temp) -> 2. Commit ภายหลัง |
|
||||
| Expected Runtime | ~16.6 ชั่วโมง (~3–4 คืน) สำหรับ 20,000 records |
|
||||
|
||||
---
|
||||
|
||||
@@ -115,14 +122,14 @@
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | คำอธิบาย |
|
||||
| -------------------- | ------------------------- | --------------------------- |
|
||||
| `is_valid` | boolean | เอกสารผ่านการตรวจสอบหรือไม่ (เปรียบเทียบ subject vs pdf) |
|
||||
| `confidence` | float (0.0–1.0) | ความมั่นใจของ AI |
|
||||
| `suggested_category` | string (enum จาก Backend) | หมวดหมู่ที่ AI แนะนำ |
|
||||
| `detected_issues` | string[] | รายการปัญหา (array ว่างถ้าไม่มี) |
|
||||
| `suggested_title` | string \| null | Title ที่แก้ไขแล้ว หรือ null |
|
||||
| `summary` | string | สรุปเนื้อหา 4-5 ประโยค สำหรับใส่ใน `body` |
|
||||
| Field | Type | คำอธิบาย |
|
||||
| -------------------- | ------------------------- | ---------------------------------------------------------------- |
|
||||
| `is_valid` | boolean | เอกสารผ่านการตรวจสอบหรือไม่ (เปรียบเทียบ subject vs pdf) |
|
||||
| `confidence` | float (0.0–1.0) | ความมั่นใจของ AI |
|
||||
| `suggested_category` | string (enum จาก Backend) | หมวดหมู่ที่ AI แนะนำ |
|
||||
| `detected_issues` | string[] | รายการปัญหา (array ว่างถ้าไม่มี) |
|
||||
| `suggested_title` | string \| null | Title ที่แก้ไขแล้ว หรือ null |
|
||||
| `summary` | string | สรุปเนื้อหา 4-5 ประโยค สำหรับใส่ใน `body` |
|
||||
| `suggested_tags` | array of objects | รายการ Tags ที่จับคู่ได้ หรือ แนะนำให้สร้างใหม่ (`is_new: true`) |
|
||||
|
||||
> ⚠️ **Patch:** `suggested_category` ต้องตรงกับ System Enum จาก `GET /api/meta/categories` เท่านั้น — ห้าม hardcode Category List ใน Prompt
|
||||
@@ -133,13 +140,13 @@
|
||||
|
||||
**ข้อมูลทุกชุดจาก n8n จะต้องถูกส่งเข้าตาราง `migration_review_queue` เสมอ** โดยจัดสถานะเบื้องต้นตาม Confidence:
|
||||
|
||||
| ระดับ Confidence | สถานะใน Review Queue |
|
||||
| ------------------------------- | --------------------------------------- |
|
||||
| `>= 0.85` และ `is_valid = true` | `PENDING` (พร้อมให้ Admin เลือก Batch Import) |
|
||||
| `0.60–0.84` | `PENDING` (ไฮไลต์แจ้งให้ Admin ตรวจสอบข้อมูลก่อน) |
|
||||
| `< 0.60` หรือ `is_valid = false` | `REJECTED` (รอให้ Admin แก้ไขข้อมูล Manual) |
|
||||
| AI Parse Error | ส่งไป Error Log + Trigger Fallback Logic |
|
||||
| Revision Drift | `PENDING` พร้อมระบุ reason: "Revision drift" |
|
||||
| ระดับ Confidence | สถานะใน Review Queue |
|
||||
| -------------------------------- | ------------------------------------------------- |
|
||||
| `>= 0.85` และ `is_valid = true` | `PENDING` (พร้อมให้ Admin เลือก Batch Import) |
|
||||
| `0.60–0.84` | `PENDING` (ไฮไลต์แจ้งให้ Admin ตรวจสอบข้อมูลก่อน) |
|
||||
| `< 0.60` หรือ `is_valid = false` | `REJECTED` (รอให้ Admin แก้ไขข้อมูล Manual) |
|
||||
| AI Parse Error | ส่งไป Error Log + Trigger Fallback Logic |
|
||||
| Revision Drift | `PENDING` พร้อมระบุ reason: "Revision drift" |
|
||||
|
||||
> ⚠️ **Tag Review:** ข้อมูลใดที่มี `is_new: true` ใน `suggested_tags` จะถูกบังคับให้ Admin ตรวจสอบบน Frontend UI ก่อน เพื่อป้องกัน AI สร้าง Tags ขยะซ้ำซ้อน
|
||||
|
||||
@@ -148,11 +155,13 @@
|
||||
## Idempotency Contract
|
||||
|
||||
**HTTP Header ที่ต้องส่งทุก Request:**
|
||||
|
||||
```
|
||||
Idempotency-Key: <document_number>:<batch_id>
|
||||
```
|
||||
|
||||
**Backend Logic:**
|
||||
|
||||
```
|
||||
IF idempotency_key EXISTS in import_transactions → RETURN HTTP 200 (no action)
|
||||
ELSE → Process normally → INSERT import_transactions → RETURN HTTP 201
|
||||
@@ -167,6 +176,7 @@ ELSE → Process normally → INSERT import_transactions → RETURN HTTP 201
|
||||
Bypass Duplicate **Validation Error**
|
||||
|
||||
Hard Rules:
|
||||
|
||||
- ❌ Migration Token ไม่สามารถ Overwrite Revision ที่มีอยู่
|
||||
- ❌ Migration Token ไม่สามารถ Delete Revision ก่อนหน้า
|
||||
- ✅ Migration Token trigger Revision increment logic ตามปกติเท่านั้น
|
||||
@@ -176,6 +186,7 @@ Hard Rules:
|
||||
## Storage Governance (Two-Phase Storage)
|
||||
|
||||
**ข้อห้าม:**
|
||||
|
||||
```
|
||||
❌ mv /data/dms/staging_ai/TCC-COR-0001.pdf /final/path/...
|
||||
```
|
||||
@@ -183,6 +194,7 @@ Hard Rules:
|
||||
**ข้อบังคับ (Two-Phase Strategy):**
|
||||
|
||||
**Phase 1: Temp Upload (โดย n8n)**
|
||||
|
||||
```
|
||||
✅ POST /api/storage/upload
|
||||
(Upload ไฟล์ PDF ได้ผลลัพธ์เป็น attachment_id เช่น 1024)
|
||||
@@ -190,12 +202,14 @@ Hard Rules:
|
||||
```
|
||||
|
||||
**Phase 2: Final Commit (โดย Frontend UI -> Backend API)**
|
||||
|
||||
```
|
||||
✅ POST /api/migration/commit_batch
|
||||
body: { queue_ids: [1, 2, 3] }
|
||||
```
|
||||
|
||||
Backend จะทำหน้าที่:
|
||||
|
||||
1. อ่านข้อมูลจาก `migration_review_queue` ซึ่งมี `temp_attachment_id` อยู่
|
||||
2. นำ `temp_attachment_id` ไปเชื่อมกับเอกสาร (Link to `correspondence_attachments`)
|
||||
3. เปลี่ยนสถานะอัพเดต `is_temporary = FALSE`
|
||||
@@ -206,8 +220,8 @@ Backend จะทำหน้าที่:
|
||||
## Review Queue Contract & Frontend UI
|
||||
|
||||
- `migration_review_queue` เป็น **Staging Table หลัก** (ไม่ auto-ingest ข้ามขั้นตอนนี้)
|
||||
- ห้ามสร้าง Correspondence record จนกว่า Admin จะสั่ง Execute การ Import จากหน้าจอ
|
||||
- **Approval Flow:**
|
||||
- ห้ามสร้าง Correspondence record จนกว่า Admin จะสั่ง Execute การ Import จากหน้าจอ
|
||||
- **Approval Flow:**
|
||||
1. N8N Insert เข้า `migration_review_queue` (พร้อม `temp_attachment_id`)
|
||||
2. Admin Review บน Frontend UI (ให้ความสำคัญกับการเช็ค `is_new: true` Tags)
|
||||
3. Admin เลือก Rows แล้วกด **"Execute Import"**
|
||||
@@ -218,6 +232,7 @@ Backend จะทำหน้าที่:
|
||||
## Revision Drift Protection
|
||||
|
||||
ถ้า Excel มี revision column:
|
||||
|
||||
```
|
||||
IF excel_revision != current_db_revision + 1
|
||||
→ ROUTE ไป Review Queue พร้อม reason: "Revision drift"
|
||||
@@ -227,20 +242,21 @@ IF excel_revision != current_db_revision + 1
|
||||
|
||||
## Execution Time Estimate
|
||||
|
||||
| Parameter | ค่า |
|
||||
| -------------------- | ---------------------------- |
|
||||
| Delay ระหว่าง Request | 2 วินาที |
|
||||
| Inference Time (avg) | ~1 วินาที |
|
||||
| เวลาต่อ Record | ~3 วินาที |
|
||||
| จำนวน Record | 20,000 |
|
||||
| เวลารวม | ~60,000 วินาที (~16.6 ชั่วโมง) |
|
||||
| **จำนวนคืนที่ต้องใช้** | **~3–4 คืน** (รัน 22:00–06:00) |
|
||||
| Parameter | ค่า |
|
||||
| ---------------------- | ------------------------------ |
|
||||
| Delay ระหว่าง Request | 2 วินาที |
|
||||
| Inference Time (avg) | ~1 วินาที |
|
||||
| เวลาต่อ Record | ~3 วินาที |
|
||||
| จำนวน Record | 20,000 |
|
||||
| เวลารวม | ~60,000 วินาที (~16.6 ชั่วโมง) |
|
||||
| **จำนวนคืนที่ต้องใช้** | **~3–4 คืน** (รัน 22:00–06:00) |
|
||||
|
||||
---
|
||||
|
||||
## Encoding Normalization
|
||||
|
||||
ก่อน Ingestion ทุกครั้ง:
|
||||
|
||||
- Excel data → Convert เป็น **UTF-8**
|
||||
- Filename → Normalize เป็น **NFC UTF-8** ป้องกันปัญหาภาษาไทยเพี้ยนข้าม OS
|
||||
|
||||
@@ -274,7 +290,7 @@ IF excel_revision != current_db_revision + 1
|
||||
|
||||
### 🟢 A. Infrastructure Validation
|
||||
|
||||
| Check | Expected | ✅ |
|
||||
| Check | Expected | ✅ |
|
||||
| ---------------------------- | ------------- | --- |
|
||||
| Ollama `/api/tags` reachable | HTTP 200 | |
|
||||
| Backend `/health` OK | HTTP 200 | |
|
||||
@@ -286,7 +302,7 @@ IF excel_revision != current_db_revision + 1
|
||||
|
||||
### 🟢 B. Security Validation
|
||||
|
||||
| Check | Expected | ✅ |
|
||||
| Check | Expected | ✅ |
|
||||
| -------------------------------------- | -------- | --- |
|
||||
| Migration Token expiry ≤ 7 days | Verified | |
|
||||
| Token IP Whitelist = NAS IP only | Verified | |
|
||||
@@ -298,7 +314,7 @@ IF excel_revision != current_db_revision + 1
|
||||
|
||||
### 🟢 C. Data Integrity Validation
|
||||
|
||||
| Check | Expected | ✅ |
|
||||
| Check | Expected | ✅ |
|
||||
| ---------------------------------------------- | -------------- | --- |
|
||||
| Enum fetched from `/api/meta/categories` | Not hardcoded | |
|
||||
| `Idempotency-Key` header enforced | Verified | |
|
||||
@@ -309,7 +325,7 @@ IF excel_revision != current_db_revision + 1
|
||||
|
||||
### 🟢 D. Workflow Validation (Dry Run 20 Records)
|
||||
|
||||
| Check | Expected | ✅ |
|
||||
| Check | Expected | ✅ |
|
||||
| ---------------------------------------- | ------------ | --- |
|
||||
| JSON parse success rate | > 95% | |
|
||||
| Confidence distribution reasonable | Mean 0.7–0.9 | |
|
||||
@@ -321,7 +337,7 @@ IF excel_revision != current_db_revision + 1
|
||||
|
||||
### 🟢 E. Performance Validation
|
||||
|
||||
| Check | Expected | ✅ |
|
||||
| Check | Expected | ✅ |
|
||||
| ------------------------------- | -------- | --- |
|
||||
| 10 records processed < 1 minute | Verified | |
|
||||
| GPU temp < 80°C | Verified | |
|
||||
@@ -330,7 +346,7 @@ IF excel_revision != current_db_revision + 1
|
||||
|
||||
### 🟢 F. Rollback Test (Mandatory)
|
||||
|
||||
| Check | Expected | ✅ |
|
||||
| Check | Expected | ✅ |
|
||||
| ------------------------------------ | ----------------- | --- |
|
||||
| Disable token works | is_active = false | |
|
||||
| Delete `SYSTEM_IMPORT` records works | COUNT = 0 | |
|
||||
@@ -343,12 +359,14 @@ IF excel_revision != current_db_revision + 1
|
||||
## GO / NO-GO Criteria
|
||||
|
||||
**GO ถ้า:**
|
||||
|
||||
- A, B, C ทุก Check = PASS
|
||||
- Dry run error rate < 10%
|
||||
- JSON parse failure < 5%
|
||||
- Revision conflict < 3%
|
||||
|
||||
**NO-GO ถ้า:**
|
||||
|
||||
- Enum mismatch (Category hardcoded)
|
||||
- Idempotency ไม่ได้ implement
|
||||
- Storage bypass (move file โดยตรง)
|
||||
@@ -358,8 +376,8 @@ IF excel_revision != current_db_revision + 1
|
||||
|
||||
## Final Architectural Assessment
|
||||
|
||||
| Area | Status |
|
||||
| ------------------ | ------------------------------------------------ |
|
||||
| Area | Status |
|
||||
| ------------------ | ------------------------------------------------- |
|
||||
| ADR Compliance | ✅ Fully aligned |
|
||||
| Security | ✅ Hardened (IP Whitelist, Rate Limit, Docker) |
|
||||
| Data Integrity | ✅ Controlled (Idempotency, Revision Drift, Enum) |
|
||||
@@ -368,4 +386,4 @@ IF excel_revision != current_db_revision + 1
|
||||
|
||||
---
|
||||
|
||||
*สำหรับขั้นตอนปฏิบัติงานแบบละเอียด ดูที่ `03-04-legacy-data-migration.md` และ `03-05-n8n-migration-setup-guide.md`*
|
||||
_สำหรับขั้นตอนปฏิบัติงานแบบละเอียด ดูที่ `03-04-legacy-data-migration.md` และ `03-05-n8n-migration-setup-guide.md`_
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
**Version:** 1.8.1
|
||||
**Decision Makers:** Development Team, Database Architect
|
||||
**Related Documents:**
|
||||
|
||||
- [Data Dictionary](../03-Data-and-Storage/03-01-data-dictionary.md)
|
||||
- [Database Schema](../03-Data-and-Storage/lcbp3-v1.8.0-schema-02-tables.sql)
|
||||
- [ADR-005: Technology Stack](ADR-005-technology-stack.md)
|
||||
@@ -41,9 +42,11 @@
|
||||
### Option 1: Replace INT with UUID as Primary Key
|
||||
|
||||
**Pros:**
|
||||
|
||||
- ✅ Opaque identifier ทุกที่
|
||||
|
||||
**Cons:**
|
||||
|
||||
- ❌ FK ทั้งหมดต้องเปลี่ยนเป็น BINARY(16) — Migration ซับซ้อนมาก
|
||||
- ❌ JOIN Performance แย่ลง (16 bytes vs 4 bytes)
|
||||
- ❌ InnoDB Clustered Index ไม่เรียงลำดับตาม INSERT Time (UUIDv4)
|
||||
@@ -53,9 +56,11 @@
|
||||
### Option 2: UUID as String Column (CHAR(36))
|
||||
|
||||
**Pros:**
|
||||
|
||||
- ✅ Human-readable
|
||||
|
||||
**Cons:**
|
||||
|
||||
- ❌ ใช้พื้นที่ 36 bytes ต่อ row (vs 16 bytes สำหรับ BINARY)
|
||||
- ❌ Index ใหญ่ ช้ากว่า BINARY(16) อย่างมีนัยสำคัญ
|
||||
- ❌ Collation issues กับ case-sensitivity
|
||||
@@ -63,6 +68,7 @@
|
||||
### Option 3: Hybrid INT + UUID (MariaDB Native) ⭐ (Selected)
|
||||
|
||||
**Pros:**
|
||||
|
||||
- ✅ INT PK ยังเป็น Internal ID → Performance ไม่เปลี่ยน
|
||||
- ✅ UUID เป็น External ID → ปลอดภัย + Space-efficient (BINARY(16) ภายใน)
|
||||
- ✅ ไม่ต้อง Migrate FK Relationships
|
||||
@@ -71,6 +77,7 @@
|
||||
- ✅ ไม่กระทบ Migration Tables (Temporary)
|
||||
|
||||
**Cons:**
|
||||
|
||||
- ❌ ต้องเพิ่ม Column ใหม่ + UNIQUE INDEX ทุก Public-Facing Table
|
||||
- ❌ Application Layer ต้อง Generate UUIDv7 ตอน INSERT
|
||||
- ❌ API Layer ต้อง Resolve UUID → INT สำหรับ Internal Queries
|
||||
@@ -89,14 +96,14 @@
|
||||
|
||||
### 1. UUID Format
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| **Type** | MariaDB Native `UUID` (available since 10.7) |
|
||||
| **Storage** | `BINARY(16)` internally (automatic) |
|
||||
| **DB Default** | `UUID()` — generates UUID v1 (time-based, fallback for seed data) |
|
||||
| **App Generation** | NestJS `@BeforeInsert()` generates UUIDv7 (RFC 9562) for time-ordering |
|
||||
| **Display** | Auto-converts to string format (8-4-4-4-12) — no conversion function needed |
|
||||
| **Index** | `UNIQUE INDEX` on `uuid` column |
|
||||
| Property | Value |
|
||||
| ------------------ | --------------------------------------------------------------------------- |
|
||||
| **Type** | MariaDB Native `UUID` (available since 10.7) |
|
||||
| **Storage** | `BINARY(16)` internally (automatic) |
|
||||
| **DB Default** | `UUID()` — generates UUID v1 (time-based, fallback for seed data) |
|
||||
| **App Generation** | NestJS `@BeforeInsert()` generates UUIDv7 (RFC 9562) for time-ordering |
|
||||
| **Display** | Auto-converts to string format (8-4-4-4-12) — no conversion function needed |
|
||||
| **Index** | `UNIQUE INDEX` on `uuid` column |
|
||||
|
||||
### 2. Column Specification
|
||||
|
||||
@@ -116,38 +123,38 @@ UNIQUE INDEX idx_{table}_uuid (uuid)
|
||||
|
||||
#### Tier 1 — Core Entity Tables (Own UUID Column)
|
||||
|
||||
| # | Table Name | Current PK | UUID Column | Notes |
|
||||
|---|-----------|-----------|-------------|-------|
|
||||
| 1 | `users` | `user_id INT AI` | `uuid UUID` | User profiles |
|
||||
| 2 | `organizations` | `id INT AI` | `uuid UUID` | Organization data |
|
||||
| 3 | `projects` | `id INT AI` | `uuid UUID` | Project data |
|
||||
| 4 | `contracts` | `id INT AI` | `uuid UUID` | Contract data |
|
||||
| 5 | `correspondences` | `id INT AI` | `uuid UUID` | Main document entity |
|
||||
| 6 | `correspondence_revisions` | `id INT AI` | `uuid UUID` | Document versions |
|
||||
| 7 | `circulations` | `id INT AI` | `uuid UUID` | Internal circulations |
|
||||
| 8 | `shop_drawings` | `id INT AI` | `uuid UUID` | Shop drawing master |
|
||||
| 9 | `shop_drawing_revisions` | `id INT AI` | `uuid UUID` | Shop drawing versions |
|
||||
| 10 | `contract_drawings` | `id INT AI` | `uuid UUID` | Contract drawing master |
|
||||
| 11 | `asbuilt_drawings` | `id INT AI` | `uuid UUID` | As-built drawing master |
|
||||
| 12 | `asbuilt_drawing_revisions` | `id INT AI` | `uuid UUID` | As-built drawing versions |
|
||||
| 13 | `attachments` | `id INT AI` | `uuid UUID` | File attachments |
|
||||
| 14 | `notifications` | `id INT AI` (partitioned) | `uuid UUID` | User notifications |
|
||||
| # | Table Name | Current PK | UUID Column | Notes |
|
||||
| --- | --------------------------- | ------------------------- | ----------- | ------------------------- |
|
||||
| 1 | `users` | `user_id INT AI` | `uuid UUID` | User profiles |
|
||||
| 2 | `organizations` | `id INT AI` | `uuid UUID` | Organization data |
|
||||
| 3 | `projects` | `id INT AI` | `uuid UUID` | Project data |
|
||||
| 4 | `contracts` | `id INT AI` | `uuid UUID` | Contract data |
|
||||
| 5 | `correspondences` | `id INT AI` | `uuid UUID` | Main document entity |
|
||||
| 6 | `correspondence_revisions` | `id INT AI` | `uuid UUID` | Document versions |
|
||||
| 7 | `circulations` | `id INT AI` | `uuid UUID` | Internal circulations |
|
||||
| 8 | `shop_drawings` | `id INT AI` | `uuid UUID` | Shop drawing master |
|
||||
| 9 | `shop_drawing_revisions` | `id INT AI` | `uuid UUID` | Shop drawing versions |
|
||||
| 10 | `contract_drawings` | `id INT AI` | `uuid UUID` | Contract drawing master |
|
||||
| 11 | `asbuilt_drawings` | `id INT AI` | `uuid UUID` | As-built drawing master |
|
||||
| 12 | `asbuilt_drawing_revisions` | `id INT AI` | `uuid UUID` | As-built drawing versions |
|
||||
| 13 | `attachments` | `id INT AI` | `uuid UUID` | File attachments |
|
||||
| 14 | `notifications` | `id INT AI` (partitioned) | `uuid UUID` | User notifications |
|
||||
|
||||
#### Tier 2 — Shared-PK Tables (Inherit UUID from Parent)
|
||||
|
||||
| # | Table Name | Shared PK Source | UUID Resolution |
|
||||
|---|-----------|-----------------|-----------------|
|
||||
| 1 | `rfas` | `correspondences.id` | Use `correspondences.uuid` |
|
||||
| 2 | `rfa_revisions` | `correspondence_revisions.id` | Use `correspondence_revisions.uuid` |
|
||||
| 3 | `transmittals` | `correspondences.id` | Use `correspondences.uuid` |
|
||||
| # | Table Name | Shared PK Source | UUID Resolution |
|
||||
| --- | --------------- | ----------------------------- | ----------------------------------- |
|
||||
| 1 | `rfas` | `correspondences.id` | Use `correspondences.uuid` |
|
||||
| 2 | `rfa_revisions` | `correspondence_revisions.id` | Use `correspondence_revisions.uuid` |
|
||||
| 3 | `transmittals` | `correspondences.id` | Use `correspondences.uuid` |
|
||||
|
||||
#### Already Using UUID — No Changes Needed
|
||||
|
||||
| Table Name | Current PK |
|
||||
|-----------|-----------|
|
||||
| Table Name | Current PK |
|
||||
| ---------------------- | --------------- |
|
||||
| `workflow_definitions` | `CHAR(36) UUID` |
|
||||
| `workflow_instances` | `CHAR(36) UUID` |
|
||||
| `workflow_histories` | `CHAR(36) UUID` |
|
||||
| `workflow_instances` | `CHAR(36) UUID` |
|
||||
| `workflow_histories` | `CHAR(36) UUID` |
|
||||
|
||||
#### Excluded Tables (Internal/Master/Junction)
|
||||
|
||||
@@ -372,14 +379,14 @@ ALTER TABLE notifications DROP INDEX idx_notifications_uuid, DROP COLUMN uuid;
|
||||
|
||||
## Storage Impact Analysis
|
||||
|
||||
| Item | Size |
|
||||
|------|------|
|
||||
| UUID (BINARY(16) internal) per row | 16 bytes |
|
||||
| UNIQUE INDEX per row | ~16 bytes (key) + ~6 bytes (pointer) ≈ 22 bytes |
|
||||
| **Total per row** | **~38 bytes** |
|
||||
| Estimated rows (all 14 tables combined) | ~100,000 (Year 1) |
|
||||
| **Total additional storage** | **~3.8 MB** |
|
||||
| Impact on QNAP NAS | **Negligible** |
|
||||
| Item | Size |
|
||||
| --------------------------------------- | ----------------------------------------------- |
|
||||
| UUID (BINARY(16) internal) per row | 16 bytes |
|
||||
| UNIQUE INDEX per row | ~16 bytes (key) + ~6 bytes (pointer) ≈ 22 bytes |
|
||||
| **Total per row** | **~38 bytes** |
|
||||
| Estimated rows (all 14 tables combined) | ~100,000 (Year 1) |
|
||||
| **Total additional storage** | **~3.8 MB** |
|
||||
| Impact on QNAP NAS | **Negligible** |
|
||||
|
||||
---
|
||||
|
||||
@@ -387,12 +394,12 @@ ALTER TABLE notifications DROP INDEX idx_notifications_uuid, DROP COLUMN uuid;
|
||||
|
||||
### UUIDv7 vs UUIDv4 for B-tree Index
|
||||
|
||||
| Property | UUIDv4 | UUIDv7 |
|
||||
|----------|--------|--------|
|
||||
| Ordering | Random | Time-ordered |
|
||||
| B-tree insert | Random page splits | Sequential append |
|
||||
| Index fragmentation | High | Low |
|
||||
| Cache efficiency | Poor | Good |
|
||||
| Property | UUIDv4 | UUIDv7 |
|
||||
| ------------------- | ------------------ | ----------------- |
|
||||
| Ordering | Random | Time-ordered |
|
||||
| B-tree insert | Random page splits | Sequential append |
|
||||
| Index fragmentation | High | Low |
|
||||
| Cache efficiency | Poor | Good |
|
||||
|
||||
**UUIDv7 ถูกเลือกเพราะ Time-ordering** ทำให้ INSERT ไม่ทำให้เกิด Random Page Split บน InnoDB B-tree ซึ่งสำคัญมากสำหรับ QNAP NAS ที่มี I/O จำกัด
|
||||
|
||||
@@ -416,46 +423,50 @@ WHERE c.uuid = '019505a1-7c3e-7000-8000-abc123def456';
|
||||
|
||||
## Security Benefits
|
||||
|
||||
| Threat | Before (INT) | After (Hybrid) |
|
||||
|--------|-------------|----------------|
|
||||
| ID Enumeration | ❌ Vulnerable (`/api/users/1,2,3...`) | ✅ Opaque UUID |
|
||||
| Record Count Leak | ❌ `id=500` reveals ~500 records | ✅ UUID reveals nothing |
|
||||
| BOLA Attack Surface | ❌ Predictable IDs | ✅ 2^122 possible values |
|
||||
| Cross-System Collision | ❌ Possible | ✅ Globally unique |
|
||||
| Threat | Before (INT) | After (Hybrid) |
|
||||
| ---------------------- | ------------------------------------- | ------------------------ |
|
||||
| ID Enumeration | ❌ Vulnerable (`/api/users/1,2,3...`) | ✅ Opaque UUID |
|
||||
| Record Count Leak | ❌ `id=500` reveals ~500 records | ✅ UUID reveals nothing |
|
||||
| BOLA Attack Surface | ❌ Predictable IDs | ✅ 2^122 possible values |
|
||||
| Cross-System Collision | ❌ Possible | ✅ Globally unique |
|
||||
|
||||
---
|
||||
|
||||
## Compatibility with Existing ADRs
|
||||
|
||||
| ADR | Impact | Notes |
|
||||
|-----|--------|-------|
|
||||
| ADR-002 (Doc Numbering) | ✅ None | Document numbers (VARCHAR) are business identifiers, unaffected |
|
||||
| ADR-005 (Tech Stack) | ✅ Compatible | uuid npm package + MariaDB native UUID type |
|
||||
| ADR-006 (Redis Caching) | ⚠️ Minor | Cache keys should use UUID instead of INT for public-facing data |
|
||||
| ADR-009 (DB Migration) | ✅ Compatible | ADD COLUMN is a safe, non-destructive migration |
|
||||
| ADR-016 (Security) | ✅ Enhanced | Strengthens OWASP BOLA defense |
|
||||
| ADR-017 (Ollama Migration) | ✅ None | Migration tables are temporary, excluded from UUID scope |
|
||||
| ADR | Impact | Notes |
|
||||
| -------------------------- | ------------- | ---------------------------------------------------------------- |
|
||||
| ADR-002 (Doc Numbering) | ✅ None | Document numbers (VARCHAR) are business identifiers, unaffected |
|
||||
| ADR-005 (Tech Stack) | ✅ Compatible | uuid npm package + MariaDB native UUID type |
|
||||
| ADR-006 (Redis Caching) | ⚠️ Minor | Cache keys should use UUID instead of INT for public-facing data |
|
||||
| ADR-009 (DB Migration) | ✅ Compatible | ADD COLUMN is a safe, non-destructive migration |
|
||||
| ADR-016 (Security) | ✅ Enhanced | Strengthens OWASP BOLA defense |
|
||||
| ADR-017 (Ollama Migration) | ✅ None | Migration tables are temporary, excluded from UUID scope |
|
||||
|
||||
---
|
||||
|
||||
## Transition Strategy
|
||||
|
||||
### Phase 1: Database (Schema Change)
|
||||
|
||||
- เพิ่ม `uuid UUID` column (MariaDB native type) กับ UNIQUE INDEX ใน 14 ตาราง
|
||||
- Existing rows ได้รับ UUID อัตโนมัติจาก DB DEFAULT
|
||||
|
||||
### Phase 2: Backend (Dual-Mode)
|
||||
|
||||
- เพิ่ม `uuid` field ใน TypeORM Entities
|
||||
- สร้าง `BaseUuidEntity` class
|
||||
- API รับได้ทั้ง INT และ UUID ผ่าน `FindByIdOrUuid` pattern
|
||||
- API Response รวม UUID เป็น `id` field
|
||||
|
||||
### Phase 3: Frontend (Gradual Migration)
|
||||
|
||||
- Frontend เปลี่ยนจากใช้ `id` (INT) เป็น `id` (UUID) ใน API response
|
||||
- URL parameters เปลี่ยนเป็น UUID
|
||||
- ไม่ต้อง Big-Bang migration — ค่อยๆ เปลี่ยนทีละ Module
|
||||
|
||||
### Phase 4: Cleanup
|
||||
|
||||
- ลบ INT ID จาก API Response (DTO)
|
||||
- ลบ INT-based route handlers
|
||||
- Update API Documentation
|
||||
@@ -464,15 +475,15 @@ WHERE c.uuid = '019505a1-7c3e-7000-8000-abc123def456';
|
||||
|
||||
## Final Assessment
|
||||
|
||||
| Area | Status |
|
||||
|------|--------|
|
||||
| Security | ✅ Eliminates ID enumeration |
|
||||
| Performance | ✅ No impact on internal JOINs |
|
||||
| Migration Risk | ✅ Low — ADD COLUMN only |
|
||||
| Storage Impact | ✅ Negligible (~3.8 MB) |
|
||||
| Backward Compatibility | ✅ Dual-mode transition |
|
||||
| ADR Compliance | ✅ Compatible with all existing ADRs |
|
||||
| Area | Status |
|
||||
| ---------------------- | ------------------------------------ |
|
||||
| Security | ✅ Eliminates ID enumeration |
|
||||
| Performance | ✅ No impact on internal JOINs |
|
||||
| Migration Risk | ✅ Low — ADD COLUMN only |
|
||||
| Storage Impact | ✅ Negligible (~3.8 MB) |
|
||||
| Backward Compatibility | ✅ Dual-mode transition |
|
||||
| ADR Compliance | ✅ Compatible with all existing ADRs |
|
||||
|
||||
---
|
||||
|
||||
*สำหรับรายละเอียดการ Implement ดูที่ Implementation Plan ใน ADR-019-implementation-plan.md*
|
||||
_สำหรับรายละเอียดการ Implement ดูที่ Implementation Plan ใน ADR-019-implementation-plan.md_
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
สรุป Patch 1.8.1
|
||||
---
|
||||
## สรุป Patch 1.8.1
|
||||
|
||||
# 📘 1) Formal Spec — Version 1.8.1
|
||||
|
||||
@@ -14,9 +13,9 @@
|
||||
|
||||
Spec 1.8.1 แก้ความไม่สอดคล้องระหว่าง:
|
||||
|
||||
* 03-04-legacy-data-migration.md
|
||||
* 03-05-n8n-migration-setup-guide.md
|
||||
* ADR-017-ollama-data-migration.md
|
||||
- 03-04-legacy-data-migration.md
|
||||
- 03-05-n8n-migration-setup-guide.md
|
||||
- ADR-017-ollama-data-migration.md
|
||||
|
||||
และกำหนด Production Boundary ที่ชัดเจน
|
||||
|
||||
@@ -26,33 +25,33 @@ Spec 1.8.1 แก้ความไม่สอดคล้องระหว่
|
||||
|
||||
### Infrastructure Layout
|
||||
|
||||
| Component | Host | Responsibility |
|
||||
| ------------------ | ------------- | -------------- |
|
||||
| DMS Frontend | QNAP | Production UI |
|
||||
| DMS Backend | QNAP | Core API |
|
||||
| MariaDB | QNAP | Authoritative DB |
|
||||
| Redis | QNAP | Cache / BullMQ |
|
||||
| Elasticsearch | QNAP | Full-text Search |
|
||||
| Nginx Proxy Manager| QNAP | Public ingress / SSL |
|
||||
| n8n + n8n-db | QNAP | Automation engine |
|
||||
| Tika | QNAP | OCR / PDF extraction |
|
||||
| Gitea | QNAP | Git + CI/CD |
|
||||
| RocketChat | QNAP | Team communication |
|
||||
| Grafana | ASUSTOR | Metrics dashboard |
|
||||
| Prometheus | ASUSTOR | Metrics collection |
|
||||
| Loki | ASUSTOR | Log aggregation |
|
||||
| Promtail | ASUSTOR | Log shipper |
|
||||
| uptime-kuma | ASUSTOR | Service availability |
|
||||
| Gitea Runner | ASUSTOR | CI/CD build agent |
|
||||
| Docker Registry | ASUSTOR | Image storage |
|
||||
| Cloudflared | ASUSTOR | Tunnel / remote access |
|
||||
| Ollama | Admin Desktop | AI processing only (i9-9900K, RTX 2060 SUPER 8GB) |
|
||||
| Component | Host | Responsibility |
|
||||
| ------------------- | ------------- | ------------------------------------------------- |
|
||||
| DMS Frontend | QNAP | Production UI |
|
||||
| DMS Backend | QNAP | Core API |
|
||||
| MariaDB | QNAP | Authoritative DB |
|
||||
| Redis | QNAP | Cache / BullMQ |
|
||||
| Elasticsearch | QNAP | Full-text Search |
|
||||
| Nginx Proxy Manager | QNAP | Public ingress / SSL |
|
||||
| n8n + n8n-db | QNAP | Automation engine |
|
||||
| Tika | QNAP | OCR / PDF extraction |
|
||||
| Gitea | QNAP | Git + CI/CD |
|
||||
| RocketChat | QNAP | Team communication |
|
||||
| Grafana | ASUSTOR | Metrics dashboard |
|
||||
| Prometheus | ASUSTOR | Metrics collection |
|
||||
| Loki | ASUSTOR | Log aggregation |
|
||||
| Promtail | ASUSTOR | Log shipper |
|
||||
| uptime-kuma | ASUSTOR | Service availability |
|
||||
| Gitea Runner | ASUSTOR | CI/CD build agent |
|
||||
| Docker Registry | ASUSTOR | Image storage |
|
||||
| Cloudflared | ASUSTOR | Tunnel / remote access |
|
||||
| Ollama | Admin Desktop | AI processing only (i9-9900K, RTX 2060 SUPER 8GB) |
|
||||
|
||||
**Constraints:**
|
||||
|
||||
* Ollama MUST NOT run on QNAP (production server)
|
||||
* AI containers MUST NOT access production DB directly
|
||||
* n8n calls Ollama via internal VLAN HTTP only
|
||||
- Ollama MUST NOT run on QNAP (production server)
|
||||
- AI containers MUST NOT access production DB directly
|
||||
- n8n calls Ollama via internal VLAN HTTP only
|
||||
|
||||
---
|
||||
|
||||
@@ -88,10 +87,10 @@ Migration MUST fail if required fields invalid.
|
||||
|
||||
Automation must:
|
||||
|
||||
* Check existence by rfa_number
|
||||
* Validate file hash
|
||||
* UPDATE instead of INSERT if exists
|
||||
* Prevent duplicate revision chain
|
||||
- Check existence by rfa_number
|
||||
- Validate file hash
|
||||
- UPDATE instead of INSERT if exists
|
||||
- Prevent duplicate revision chain
|
||||
|
||||
---
|
||||
|
||||
@@ -171,10 +170,10 @@ No DB commit until validation approved.
|
||||
|
||||
AI-based migration using Ollama introduces:
|
||||
|
||||
* DB corruption risk
|
||||
* Hallucinated metadata
|
||||
* Unauthorized modification
|
||||
* Privilege escalation risk
|
||||
- DB corruption risk
|
||||
- Hallucinated metadata
|
||||
- Unauthorized modification
|
||||
- Privilege escalation risk
|
||||
|
||||
Production DMS must remain authoritative.
|
||||
|
||||
@@ -186,11 +185,11 @@ Production DMS must remain authoritative.
|
||||
|
||||
Ollama must:
|
||||
|
||||
* Run on **Admin Desktop only** (NOT on QNAP)
|
||||
* Have NO DB credentials
|
||||
* Have NO write access to uploads
|
||||
* Access only `/staging_ai`
|
||||
* Output JSON only
|
||||
- Run on **Admin Desktop only** (NOT on QNAP)
|
||||
- Have NO DB credentials
|
||||
- Have NO write access to uploads
|
||||
- Access only `/staging_ai`
|
||||
- Output JSON only
|
||||
|
||||
---
|
||||
|
||||
@@ -212,9 +211,9 @@ AI never writes directly.
|
||||
|
||||
All writes must go through:
|
||||
|
||||
* Authenticated DMS API
|
||||
* RBAC enforced
|
||||
* Audit log recorded
|
||||
- Authenticated DMS API
|
||||
- RBAC enforced
|
||||
- Audit log recorded
|
||||
|
||||
---
|
||||
|
||||
@@ -222,10 +221,10 @@ All writes must go through:
|
||||
|
||||
AI output must:
|
||||
|
||||
* Match schema
|
||||
* Pass validation script
|
||||
* Fail on missing required fields
|
||||
* Reject unknown users
|
||||
- Match schema
|
||||
- Pass validation script
|
||||
- Fail on missing required fields
|
||||
- Reject unknown users
|
||||
|
||||
---
|
||||
|
||||
@@ -244,14 +243,14 @@ AI output must:
|
||||
|
||||
Pros:
|
||||
|
||||
* Production safe
|
||||
* Predictable migration
|
||||
* Audit trail preserved
|
||||
- Production safe
|
||||
- Predictable migration
|
||||
- Audit trail preserved
|
||||
|
||||
Cons:
|
||||
|
||||
* Slightly slower pipeline
|
||||
* Requires validation layer
|
||||
- Slightly slower pipeline
|
||||
- Requires validation layer
|
||||
|
||||
---
|
||||
|
||||
@@ -288,7 +287,7 @@ Cons:
|
||||
|
||||
Batch size recommendation:
|
||||
|
||||
* 20–50 RFAs per batch
|
||||
- 20–50 RFAs per batch
|
||||
|
||||
Process:
|
||||
|
||||
@@ -330,11 +329,11 @@ When all batches pass:
|
||||
|
||||
Monitor:
|
||||
|
||||
* DB errors
|
||||
* Duplicate insert
|
||||
* Missing files
|
||||
* AI extraction errors
|
||||
* API error rate
|
||||
- DB errors
|
||||
- Duplicate insert
|
||||
- Missing files
|
||||
- AI extraction errors
|
||||
- API error rate
|
||||
|
||||
If anomaly >5% → trigger rollback plan.
|
||||
|
||||
@@ -368,11 +367,11 @@ Target RTO: < 2 hours
|
||||
|
||||
System may go live only if:
|
||||
|
||||
* All dry-run tests pass
|
||||
* 100% required fields valid
|
||||
* 0 duplicate RFA
|
||||
* Sample QA pass >95%
|
||||
* Backup verified
|
||||
- All dry-run tests pass
|
||||
- 100% required fields valid
|
||||
- 0 duplicate RFA
|
||||
- Sample QA pass >95%
|
||||
- Backup verified
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -28,42 +28,42 @@ Architecture Decision Records (ADRs) เป็นเอกสารที่บ
|
||||
|
||||
### Core Architecture Decisions
|
||||
|
||||
| ADR | Title | Status | Date | Summary |
|
||||
| --------------------------------------------------- | --------------------------- | ---------- | ---------- | ------------------------------------------------------------------------- |
|
||||
| ADR | Title | Status | Date | Summary |
|
||||
| --------------------------------------------------- | --------------------------- | ----------- | ---------- | ---------------------------------------------------------------------------- |
|
||||
| [ADR-001](./ADR-001-unified-workflow-engine.md) | Unified Workflow Engine | ✅ Accepted | 2026-02-24 | ใช้ DSL-based Workflow Engine สำหรับ Correspondences, RFAs, และ Circulations |
|
||||
| [ADR-002](./ADR-002-document-numbering-strategy.md) | Document Numbering Strategy | ✅ Accepted | 2026-02-24 | Double-lock mechanism (Redis + DB Optimistic Lock) สำหรับเลขที่เอกสาร |
|
||||
| [ADR-002](./ADR-002-document-numbering-strategy.md) | Document Numbering Strategy | ✅ Accepted | 2026-02-24 | Double-lock mechanism (Redis + DB Optimistic Lock) สำหรับเลขที่เอกสาร |
|
||||
|
||||
### Security & Access Control
|
||||
|
||||
| ADR | Title | Status | Date | Summary |
|
||||
| ----------------------------------------------- | ---------------------------------- | ---------- | ---------- | -------------------------------------------- |
|
||||
| ADR | Title | Status | Date | Summary |
|
||||
| ----------------------------------------------- | ---------------------------------- | ----------- | ---------- | -------------------------------------------- |
|
||||
| [ADR-016](./ADR-016-security-authentication.md) | Security & Authentication Strategy | ✅ Accepted | 2026-02-24 | JWT + bcrypt + OWASP Security Best Practices |
|
||||
|
||||
### Technology & Infrastructure
|
||||
|
||||
| ADR | Title | Status | Date | Summary |
|
||||
| --------------------------------------------------- | ------------------------------------ | -------------------- | ---------- | ------------------------------------------------------------ |
|
||||
| ADR | Title | Status | Date | Summary |
|
||||
| --------------------------------------------------- | ------------------------------------ | --------------------- | ---------- | --------------------------------------------------------------- |
|
||||
| [ADR-005](./ADR-005-technology-stack.md) | Technology Stack Selection | ✅ Accepted | 2026-02-24 | Full Stack TypeScript: NestJS 11 + Next.js 16 + MariaDB + Redis |
|
||||
| [ADR-006](./ADR-006-redis-caching-strategy.md) | Redis Usage & Caching Strategy | ✅ Accepted | 2026-02-24 | Redis สำหรับ Distributed Lock, Cache, Queue, และ Rate Limiting |
|
||||
| [ADR-009](./ADR-009-database-migration-strategy.md) | Database Migration & Deployment | ✅ Accepted (Pending) | 2026-02-24 | TypeORM Migrations พร้อม Blue-Green Deployment |
|
||||
| [ADR-015](./ADR-015-deployment-infrastructure.md) | Deployment & Infrastructure Strategy | ✅ Accepted | 2026-02-24 | Docker Compose with Blue-Green Deployment on QNAP |
|
||||
| [ADR-006](./ADR-006-redis-caching-strategy.md) | Redis Usage & Caching Strategy | ✅ Accepted | 2026-02-24 | Redis สำหรับ Distributed Lock, Cache, Queue, และ Rate Limiting |
|
||||
| [ADR-009](./ADR-009-database-migration-strategy.md) | Database Migration & Deployment | ✅ Accepted (Pending) | 2026-02-24 | TypeORM Migrations พร้อม Blue-Green Deployment |
|
||||
| [ADR-015](./ADR-015-deployment-infrastructure.md) | Deployment & Infrastructure Strategy | ✅ Accepted | 2026-02-24 | Docker Compose with Blue-Green Deployment on QNAP |
|
||||
|
||||
### API & Integration
|
||||
|
||||
| ADR | Title | Status | Date | Summary |
|
||||
| --------------------------------------------------- | ----------------------------- | --------------------------- | ---------- | --------------------------------------------------------------------------- |
|
||||
| ADR | Title | Status | Date | Summary |
|
||||
| --------------------------------------------------- | ----------------------------- | ---------------------------- | ---------- | ----------------------------------------------------------------------------- |
|
||||
| [ADR-008](./ADR-008-email-notification-strategy.md) | Email & Notification Strategy | ✅ Accepted (Pending Review) | 2026-02-24 | BullMQ + Redis Queue สำหรับ Multi-channel Notifications (Email, LINE, In-app) |
|
||||
|
||||
### Observability
|
||||
|
||||
| ADR | Title | Status | Date | Summary |
|
||||
| --------------------------------------------------- | ----------------------------- | -------------------- | ---------- | ------------------------------------------------------------ |
|
||||
| ADR | Title | Status | Date | Summary |
|
||||
| --------------------------------------------------- | ----------------------------- | --------------------- | ---------- | ------------------------------------------------------------- |
|
||||
| [ADR-010](./ADR-010-logging-monitoring-strategy.md) | Logging & Monitoring Strategy | ✅ Accepted (Pending) | 2026-02-24 | Winston Structured Logging พร้อม Future ELK Stack Integration |
|
||||
|
||||
### Frontend Architecture
|
||||
|
||||
| ADR | Title | Status | Date | Summary |
|
||||
| ------------------------------------------------ | -------------------------------- | ---------- | ---------- | ----------------------------------------------------- |
|
||||
| ADR | Title | Status | Date | Summary |
|
||||
| ------------------------------------------------ | -------------------------------- | ----------- | ---------- | ----------------------------------------------------- |
|
||||
| [ADR-011](./ADR-011-nextjs-app-router.md) | Next.js App Router & Routing | ✅ Accepted | 2025-12-01 | App Router with Server Components and Nested Layouts |
|
||||
| [ADR-012](./ADR-012-ui-component-library.md) | UI Component Library (Shadcn/UI) | ✅ Accepted | 2026-02-24 | Shadcn/UI + Tailwind CSS for Full Component Ownership |
|
||||
| [ADR-013](./ADR-013-form-handling-validation.md) | Form Handling & Validation | ✅ Accepted | 2026-02-24 | React Hook Form + Zod for Type-Safe Forms |
|
||||
@@ -71,9 +71,9 @@ Architecture Decision Records (ADRs) เป็นเอกสารที่บ
|
||||
|
||||
### Data & Identity
|
||||
|
||||
| ADR | Title | Status | Date | Summary |
|
||||
| ------------------------------------------------------------ | ---------------------------- | ---------- | ---------- | -------------------------------------------------------- |
|
||||
| [ADR-019](./ADR-019-hybrid-identifier-strategy.md) | Hybrid Identifier Strategy | ✅ Accepted | 2026-03-11 | INT PK (internal) + UUIDv7 (public API) on 14 tables |
|
||||
| ADR | Title | Status | Date | Summary |
|
||||
| -------------------------------------------------- | -------------------------- | ----------- | ---------- | ---------------------------------------------------- |
|
||||
| [ADR-019](./ADR-019-hybrid-identifier-strategy.md) | Hybrid Identifier Strategy | ✅ Accepted | 2026-03-11 | INT PK (internal) + UUIDv7 (public API) on 14 tables |
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user