260322:1648 Correct Coresspondence / Doing RFA / Correct CI
CI Pipeline / build (push) Failing after 12m41s
Build and Deploy / deploy (push) Failing after 2m44s

This commit is contained in:
admin
2026-03-22 16:48:12 +07:00
parent e5deedb42e
commit 11984bfa29
683 changed files with 105251 additions and 29068 deletions
@@ -48,14 +48,14 @@
### **2.4 ข้อตกลงในการตั้งชื่อ (Naming Conventions)**
| Entity (สิ่งที่ตั้งชื่อ) | Convention (รูปแบบ) | Example (ตัวอย่าง) |
| :-------------------- | :----------------- | :--------------------------------- |
| Classes | PascalCase | UserService |
| Property | snake_case | user_id |
| Variables & Functions | camelCase | getUserInfo |
| Files & Folders | kebab-case | user-service.ts |
| Environment Variables | UPPERCASE | DATABASE_URL |
| Booleans | Verb + Noun | isActive, canDelete, hasPermission |
| Entity (สิ่งที่ตั้งชื่อ) | Convention (รูปแบบ) | Example (ตัวอย่าง) |
| :----------------------- | :------------------ | :--------------------------------- |
| Classes | PascalCase | UserService |
| Property | snake_case | user_id |
| Variables & Functions | camelCase | getUserInfo |
| Files & Folders | kebab-case | user-service.ts |
| Environment Variables | UPPERCASE | DATABASE_URL |
| Booleans | Verb + Noun | isActive, canDelete, hasPermission |
ใช้คำเต็ม — ไม่ใช้อักษรย่อ — ยกเว้นคำมาตรฐาน (เช่น API, URL, req, res, err, ctx)
@@ -165,13 +165,13 @@ const testScenarios = {
### **3.1.1 NestJS 11 Patterns (Updated 2026-03-16)**
| Pattern | คำอธิบาย |
| :--- | :--- |
| Pattern | คำอธิบาย |
| :-------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **`import type` สำหรับ decorated signatures** | เมื่อ `isolatedModules` + `emitDecoratorMetadata` เปิดอยู่ ต้องใช้ `import type` สำหรับ interface ที่ใช้ใน decorated parameter (เช่น `@Req() req: RequestWithUser`) |
| **Shared `RequestWithUser` interface** | ใช้ `src/common/interfaces/request-with-user.interface.ts` แทนการประกาศ local interface ในแต่ละ controller — ห้ามใช้ `req: any` |
| **`@nestjs/mapped-types` ถูกลบออก** | DTO utility types (`PartialType`, `OmitType`, `IntersectionType`) ต้อง import จาก `@nestjs/swagger` เท่านั้น |
| **Express v5** | `@nestjs/platform-express` v11 ใช้ Express 5 — path parameter syntax เปลี่ยน (`:id``:id` ยังใช้ได้ แต่ wildcard `*``*name`) |
| **Swagger version** | Swagger doc version ต้องตรงกับ project version ปัจจุบัน (`1.8.1`) |
| **Shared `RequestWithUser` interface** | ใช้ `src/common/interfaces/request-with-user.interface.ts` แทนการประกาศ local interface ในแต่ละ controller — ห้ามใช้ `req: any` |
| **`@nestjs/mapped-types` ถูกลบออก** | DTO utility types (`PartialType`, `OmitType`, `IntersectionType`) ต้อง import จาก `@nestjs/swagger` เท่านั้น |
| **Express v5** | `@nestjs/platform-express` v11 ใช้ Express 5 — path parameter syntax เปลี่ยน (`:id``:id` ยังใช้ได้ แต่ wildcard `*``*name`) |
| **Swagger version** | Swagger doc version ต้องตรงกับ project version ปัจจุบัน (`1.8.1`) |
```typescript
// ✅ ถูกต้อง — NestJS 11 pattern
@@ -428,34 +428,34 @@ Unified Workflow Engine (Core Architecture)
### **3.13 เเทคโนโลยีที่ใช้ (Technology Stack)**
| ส่วน | Library/Tool | หมายเหตุ |
| ----------------------- | ---------------------------------------------------- | -------------------------------------- |
| **Framework** | `@nestjs/core`, `@nestjs/common` | Core Framework |
| **Language** | `TypeScript` | ใช้ TypeScript ทั้งระบบ |
| **Database** | `MariaDB 11.8` | ฐานข้อมูลหลัก |
| **ORM** | `@nestjs/typeorm`, `typeorm` | 🗃️จัดการการเชื่อมต่อและ Query ฐานข้อมูล |
| **Validation** | `class-validator`, `class-transformer` | 📦ตรวจสอบและแปลงข้อมูลใน DTO |
| **Auth** | `@nestjs/jwt`, `@nestjs/passport`, `passport-jwt` | 🔐การยืนยันตัวตนด้วย JWT |
| **Authorization** | `casl` | 🔐จัดการสิทธิ์แบบ RBAC |
| **File Upload** | `multer` | 📁จัดการการอัปโหลดไฟล์ |
| **Search** | `@nestjs/elasticsearch` | 🔍สำหรับการค้นหาขั้นสูง |
| **Notification** | `nodemailer` | 📬ส่งอีเมลแจ้งเตือน |
| ส่วน | Library/Tool | หมายเหตุ |
| ----------------------- | ---------------------------------------------------- | -------------------------------------------- |
| **Framework** | `@nestjs/core`, `@nestjs/common` | Core Framework |
| **Language** | `TypeScript` | ใช้ TypeScript ทั้งระบบ |
| **Database** | `MariaDB 11.8` | ฐานข้อมูลหลัก |
| **ORM** | `@nestjs/typeorm`, `typeorm` | 🗃️จัดการการเชื่อมต่อและ Query ฐานข้อมูล |
| **Validation** | `class-validator`, `class-transformer` | 📦ตรวจสอบและแปลงข้อมูลใน DTO |
| **Auth** | `@nestjs/jwt`, `@nestjs/passport`, `passport-jwt` | 🔐การยืนยันตัวตนด้วย JWT |
| **Authorization** | `casl` | 🔐จัดการสิทธิ์แบบ RBAC |
| **File Upload** | `multer` | 📁จัดการการอัปโหลดไฟล์ |
| **Search** | `@nestjs/elasticsearch` | 🔍สำหรับการค้นหาขั้นสูง |
| **Notification** | `nodemailer` | 📬ส่งอีเมลแจ้งเตือน |
| **Scheduling** | `@nestjs/schedule` | 📬สำหรับ Cron Jobs (เช่น แจ้งเตือน Deadline) |
| **Logging** | `winston` | 📊บันทึก Log ที่มีประสิทธิภาพ |
| **Testing** | `@nestjs/testing`, `jest`, `supertest` | 🧪ทดสอบ Unit, Integration และ E2E |
| **Documentation** | `@nestjs/swagger` | 🌐สร้าง API Documentation อัตโนมัติ |
| **Security** | `helmet`, `rate-limiter-flexible` | 🛡️เพิ่มความปลอดภัยให้ API |
| **Resilience** | `@nestjs/circuit-breaker` | 🔄 Circuit breaker pattern |
| **Caching** | `@nestjs/cache-manager`, `cache-manager-redis-store` | 💾 Distributed caching |
| **Security** | `helmet`, `csurf`, `rate-limiter-flexible` | 🛡️ Security enhancements |
| **Validation** | `class-validator`, `class-transformer` | ✅ Input validation |
| **Monitoring** | `@nestjs/monitoring`, `winston` | 📊 Application monitoring |
| **File Processing** | `clamscan` | 🦠 Virus scanning |
| **Cryptography** | `bcrypt`, `crypto` | 🔐 Password hashing และ checksums |
| **JSON Validation** | `ajv`, `ajv-formats` | 🎯 JSON schema validation |
| **JSON Processing** | `jsonpath`, `json-schema-ref-parser` | 🔧 JSON manipulation |
| **Data Transformation** | `class-transformer` | 🔄 Object transformation |
| **Compression** | `compression` | 📦 JSON compression |
| **Logging** | `winston` | 📊บันทึก Log ที่มีประสิทธิภาพ |
| **Testing** | `@nestjs/testing`, `jest`, `supertest` | 🧪ทดสอบ Unit, Integration และ E2E |
| **Documentation** | `@nestjs/swagger` | 🌐สร้าง API Documentation อัตโนมัติ |
| **Security** | `helmet`, `rate-limiter-flexible` | 🛡️เพิ่มความปลอดภัยให้ API |
| **Resilience** | `@nestjs/circuit-breaker` | 🔄 Circuit breaker pattern |
| **Caching** | `@nestjs/cache-manager`, `cache-manager-redis-store` | 💾 Distributed caching |
| **Security** | `helmet`, `csurf`, `rate-limiter-flexible` | 🛡️ Security enhancements |
| **Validation** | `class-validator`, `class-transformer` | ✅ Input validation |
| **Monitoring** | `@nestjs/monitoring`, `winston` | 📊 Application monitoring |
| **File Processing** | `clamscan` | 🦠 Virus scanning |
| **Cryptography** | `bcrypt`, `crypto` | 🔐 Password hashing และ checksums |
| **JSON Validation** | `ajv`, `ajv-formats` | 🎯 JSON schema validation |
| **JSON Processing** | `jsonpath`, `json-schema-ref-parser` | 🔧 JSON manipulation |
| **Data Transformation** | `class-transformer` | 🔄 Object transformation |
| **Compression** | `compression` | 📦 JSON compression |
### **3.14 Security Testing:**
@@ -511,13 +511,11 @@ Backend (NestJS) ควรเป็น **Stateless** (ไม่เก็บส
1. User สร้างเอกสาร → เลือก routing template
2. System สร้าง routing instances ตาม template
3. สำหรับแต่ละ routing step:
- กำหนด due date (จาก expected_days)
- ส่ง notification ไปยังองค์กรผู้รับ
- อัพเดทสถานะเป็น SENT
4. เมื่อองค์กรผู้รับดำเนินการ:
- อัพเดทสถานะเป็น ACTIONED/FORWARDED/REPLIED
- บันทึก processed_by และ processed_at
- ส่ง notification ไปยังขั้นตอนต่อไป (ถ้ามี)
@@ -731,27 +729,23 @@ export function QueryProvider({ children }: { children: React.ReactNode }) {
สำหรับ Next.js App Router เราจะใช้ State Management แบบ Simplified โดยแบ่งเป็น 3 ระดับหลัก:
- 4.10.1. **Server State (สถานะข้อมูลจากเซิร์ฟเวอร์)**
- **เครื่องมือ:** **TanStack Query (React Query)**
- **ใช้เมื่อ:** จัดการข้อมูลที่ดึงมาจาก NestJS API ทั้งหมด
- **ครอบคลุม:** รายการ correspondences, rfas, drawings, users, permissions
- **ประโยชน์:** จัดการ Caching, Re-fetching, Background Sync อัตโนมัติ
- 4.10.2. **Form State (สถานะของฟอร์ม):**
- **เครื่องมือ:** **React Hook Form** + **Zod** (สำหรับ validation)
- **ใช้เมื่อ:** จัดการฟอร์มที่ซับซ้อนทั้งหมด
- **ครอบคลุม:** ฟอร์มสร้าง/แก้ไข RFA, Correspondence, Circulation
- **รวมฟีเจอร์:** Auto-save drafts ลง LocalStorage
- 4.10.3. **UI State (สถานะ UI ชั่วคราว):**
- **เครื่องมือ:** **useState**, **useReducer** (ใน Component) หรือ **Zustand** (สำหรับ Global Client State เช่น Preferences, Auth)
- **ใช้เมื่อ:** จัดการสถานะเฉพาะ Component หรือข้อมูลที่แชร์ทั้งแอปโดยไม่พึ่งพาเซิร์ฟเวอร์
- **ครอบคลุม:** Modal เปิด/ปิด, Dropdown state, Loading states, Themes, Sidebar
- **ยกเลิกการใช้:**
- ❌ Context API สำหรับ Server State (ใช้ React Query แทน)
- **ตัวอย่าง Implementation:**
@@ -868,15 +862,15 @@ updateRFA(@Param('id') id: string) {
## 🔗 **7. แนวทางการบูรณาการ Full Stack (Full Stack Integration Guidelines)**
| Aspect (แง่มุม) | Backend (NestJS) | Frontend (NextJS) | UI Layer (Tailwind/Shadcn) |
| :----------------------- | :------------------------- | :---------------------------- | :------------------------------------- |
| API | REST / GraphQL Controllers | API hooks ผ่าน fetch/axios/SWR | Components ที่รับข้อมูล |
| Validation (การตรวจสอบ) | class-validator DTOs | zod / react-hook-form | สถานะของฟอร์ม/input ใน Shadcn |
| Auth (การยืนยันตัวตน) | Guards, JWT | NextAuth / cookies | สถานะ UI ของ Auth (loading, signed in) |
| Errors (ข้อผิดพลาด) | Global filters | Toasts / modals | Alerts / ข้อความ feedback |
| Testing (การทดสอบ) | Jest (unit/e2e) | Vitest / Playwright | Visual regression |
| Styles (สไตล์) | Scoped modules (ถ้าจำเป็น) | Tailwind / Shadcn | Tailwind utilities |
| Accessibility (การเข้าถึง) | Guards + filters | ARIA attributes | Semantic HTML |
| Aspect (แง่มุม) | Backend (NestJS) | Frontend (NextJS) | UI Layer (Tailwind/Shadcn) |
| :------------------------- | :------------------------- | :----------------------------- | :------------------------------------- |
| API | REST / GraphQL Controllers | API hooks ผ่าน fetch/axios/SWR | Components ที่รับข้อมูล |
| Validation (การตรวจสอบ) | class-validator DTOs | zod / react-hook-form | สถานะของฟอร์ม/input ใน Shadcn |
| Auth (การยืนยันตัวตน) | Guards, JWT | NextAuth / cookies | สถานะ UI ของ Auth (loading, signed in) |
| Errors (ข้อผิดพลาด) | Global filters | Toasts / modals | Alerts / ข้อความ feedback |
| Testing (การทดสอบ) | Jest (unit/e2e) | Vitest / Playwright | Visual regression |
| Styles (สไตล์) | Scoped modules (ถ้าจำเป็น) | Tailwind / Shadcn | Tailwind utilities |
| Accessibility (การเข้าถึง) | Guards + filters | ARIA attributes | Semantic HTML |
## 🗂️ **8. ข้อตกลงเฉพาะสำหรับ DMS (LCBP3-DMS)**
@@ -886,17 +880,17 @@ updateRFA(@Param('id') id: string) {
บันทึกการดำเนินการ CRUD และการจับคู่ทั้งหมดลงในตาราง audit_logs
| Field (ฟิลด์) | Type (จาก SQL) | Description (คำอธิบาย) |
| :----------- | :------------- | :----------------------------------------------- |
| audit_id | BIGINT | Primary Key |
| user_id | INT | ผู้ใช้ที่ดำเนินการ (FK -> users) |
| action | VARCHAR(100) | rfa.create, correspondence.update, login.success |
| entity_type | VARCHAR(50) | ชื่อตาราง/โมดูล เช่น 'rfa', 'correspondence' |
| entity_id | VARCHAR(50) | Primary ID ของระเบียนที่ได้รับผลกระทบ |
| details_json | JSON | ข้อมูลบริบท (เช่น ฟิลด์ที่มีการเปลี่ยนแปลง) |
| ip_address | VARCHAR(45) | IP address ของผู้ดำเนินการ |
| user_agent | VARCHAR(255) | User Agent ของผู้ดำเนินการ |
| created_at | TIMESTAMP | Timestamp (UTC) |
| Field (ฟิลด์) | Type (จาก SQL) | Description (คำอธิบาย) |
| :------------ | :------------- | :----------------------------------------------- |
| audit_id | BIGINT | Primary Key |
| user_id | INT | ผู้ใช้ที่ดำเนินการ (FK -> users) |
| action | VARCHAR(100) | rfa.create, correspondence.update, login.success |
| entity_type | VARCHAR(50) | ชื่อตาราง/โมดูล เช่น 'rfa', 'correspondence' |
| entity_id | VARCHAR(50) | Primary ID ของระเบียนที่ได้รับผลกระทบ |
| details_json | JSON | ข้อมูลบริบท (เช่น ฟิลด์ที่มีการเปลี่ยนแปลง) |
| ip_address | VARCHAR(45) | IP address ของผู้ดำเนินการ |
| user_agent | VARCHAR(255) | User Agent ของผู้ดำเนินการ |
| created_at | TIMESTAMP | Timestamp (UTC) |
### 📂**8.2 การจัดการไฟล์ (File Handling)**
@@ -361,10 +361,7 @@ describe('DocumentNumberingService', () => {
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
DocumentNumberingService,
{ provide: RedisLock, useValue: mockRedisLock },
],
providers: [DocumentNumberingService, { provide: RedisLock, useValue: mockRedisLock }],
}).compile();
service = module.get(DocumentNumberingService);
@@ -421,10 +418,7 @@ describe('Correspondence API (e2e)', () => {
// src/modules/monitoring/logger/winston.config.ts
export const winstonConfig = {
level: process.env.NODE_ENV === 'production' ? 'warn' : 'debug',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
@@ -463,14 +457,14 @@ async approve(@Param('id') id: string, @CurrentUser() user: User) {
Backend codebase has **zero** `any` types remaining. Key techniques used:
| Pattern | Solution |
|---------|----------|
| JWT `expiresIn` branded type | `import type { StringValue } from 'ms'`; cast `as StringValue` |
| CASL `detectSubjectType` callback | Type param as `object`, internal cast via `Record<string, unknown>` |
| CASL `ability.can()` params | Export `Actions`/`Subjects` types from `ability.factory.ts`, cast explicitly |
| TypeORM nullable column clearing | Use `undefined` instead of `null as any` for optional (`?:`) properties |
| Test mock objects | Use `as unknown as InterfaceType` or `as Partial<Entity> as Entity` |
| TypeScript legacy decorators | `target: any` is unavoidable — whitelisted per TS spec limitation |
| Pattern | Solution |
| --------------------------------- | ---------------------------------------------------------------------------- |
| JWT `expiresIn` branded type | `import type { StringValue } from 'ms'`; cast `as StringValue` |
| CASL `detectSubjectType` callback | Type param as `object`, internal cast via `Record<string, unknown>` |
| CASL `ability.can()` params | Export `Actions`/`Subjects` types from `ability.factory.ts`, cast explicitly |
| TypeORM nullable column clearing | Use `undefined` instead of `null as any` for optional (`?:`) properties |
| Test mock objects | Use `as unknown as InterfaceType` or `as Partial<Entity> as Entity` |
| TypeScript legacy decorators | `target: any` is unavoidable — whitelisted per TS spec limitation |
> **Exceptions:** Only `target: any` in legacy TS decorators (`circuit-breaker.decorator.ts`, `retry.decorator.ts`) remains — this is a TypeScript language constraint, not a code quality issue.
@@ -487,7 +481,7 @@ Backend codebase has **zero** `any` types remaining. Key techniques used:
## 🔄 Update History
| Version | Date | Changes |
| ------- | ---------- | ---------------------------------- |
| 1.5.0 | 2025-12-01 | Initial backend guidelines created |
| Version | Date | Changes |
| ------- | ---------- | ------------------------------------------------------------------- |
| 1.5.0 | 2025-12-01 | Initial backend guidelines created |
| 1.8.1 | 2026-03-20 | Added `any` type elimination techniques, enforced 0 remaining `any` |
@@ -144,9 +144,7 @@ export function ResponsiveTable({ data }: { data: Correspondence[] }) {
<Card key={item.id}>
<CardContent className="pt-6">
<div className="space-y-2">
<div className="text-sm text-muted-foreground">
เลขที่เอกสาร
</div>
<div className="text-sm text-muted-foreground">เลขที่เอกสาร</div>
<div className="font-medium">{item.doc_number}</div>
<div className="text-sm text-muted-foreground">เรื่อง</div>
<div>{item.title}</div>
@@ -367,11 +365,7 @@ export default apiClient;
```typescript
// lib/services/correspondence.service.ts
import apiClient from '@/lib/api/client';
import type {
Correspondence,
CreateCorrespondenceDto,
SearchCorrespondenceDto,
} from '@/types/dto/correspondence';
import type { Correspondence, CreateCorrespondenceDto, SearchCorrespondenceDto } from '@/types/dto/correspondence';
export const correspondenceService = {
async getAll(params: SearchCorrespondenceDto): Promise<Correspondence[]> {
@@ -389,10 +383,7 @@ export const correspondenceService = {
return data;
},
async update(
id: string,
dto: Partial<CreateCorrespondenceDto>
): Promise<Correspondence> {
async update(id: string, dto: Partial<CreateCorrespondenceDto>): Promise<Correspondence> {
const { data } = await apiClient.put(`/correspondences/${id}`, dto);
return data;
},
@@ -441,24 +432,20 @@ export function DynamicForm({ schemaName, onSubmit }: DynamicFormProps) {
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
{Object.entries(schema.schema_definition.properties).map(
([key, prop]: [string, Record<string, unknown>]) => (
<FormField
key={key}
control={form.control}
name={key}
render={({ field }) => (
<FormItem>
<FormLabel>{prop.title || key}</FormLabel>
<FormControl>
{renderFieldByType(prop.type, field)}
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)
)}
{Object.entries(schema.schema_definition.properties).map(([key, prop]: [string, Record<string, unknown>]) => (
<FormField
key={key}
control={form.control}
name={key}
render={({ field }) => (
<FormItem>
<FormLabel>{prop.title || key}</FormLabel>
<FormControl>{renderFieldByType(prop.type, field)}</FormControl>
<FormMessage />
</FormItem>
)}
/>
))}
<Button type="submit">บันทึก</Button>
</form>
</Form>
@@ -525,24 +512,17 @@ export function FileUploadZone({
className={`
border-2 border-dashed rounded-lg p-8 text-center cursor-pointer
transition-colors
${
isDragActive
? 'border-primary bg-primary/5'
: 'border-muted-foreground/25'
}
${isDragActive ? 'border-primary bg-primary/5' : 'border-muted-foreground/25'}
hover:border-primary hover:bg-primary/5
`}
>
<input {...getInputProps()} />
<Upload className="mx-auto h-12 w-12 text-muted-foreground" />
<p className="mt-2 text-sm text-muted-foreground">
{isDragActive
? 'วางไฟล์ที่นี่...'
: 'ลากไฟล์มาวางที่นี่ หรือคลิกเพื่อเลือกไฟล์'}
{isDragActive ? 'วางไฟล์ที่นี่...' : 'ลากไฟล์มาวางที่นี่ หรือคลิกเพื่อเลือกไฟล์'}
</p>
<p className="mt-1 text-xs text-muted-foreground">
รองรับ: {acceptedTypes.join(', ')} (สูงสุด {maxFiles} ไฟล์,{' '}
{maxSize / 1024 / 1024}MB/ไฟล์)
รองรับ: {acceptedTypes.join(', ')} (สูงสุด {maxFiles} ไฟล์, {maxSize / 1024 / 1024}MB/ไฟล์)
</p>
</div>
);
@@ -583,9 +563,7 @@ describe('CorrespondenceForm', () => {
fireEvent.click(submitButton);
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith(
expect.objectContaining({ title: 'Test Correspondence' })
);
expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining({ title: 'Test Correspondence' }));
});
});
});
@@ -52,11 +52,11 @@ related:
### Test Distribution
| Level | Coverage | Speed | Purpose |
| ----------- | -------- | ---------- | ---------------------------- |
| Unit | 60% | Fast (ms) | ทดสอบตรรกะแต่ละ Function |
| Level | Coverage | Speed | Purpose |
| ----------- | -------- | ---------- | ------------------------------- |
| Unit | 60% | Fast (ms) | ทดสอบตรรกะแต่ละ Function |
| Integration | 30% | Medium (s) | ทดสอบการทำงานร่วมกันของ Modules |
| E2E | 10% | Slow (m) | ทดสอบ User Journey ทั้งหมด |
| E2E | 10% | Slow (m) | ทดสอบ User Journey ทั้งหมด |
---
@@ -94,9 +94,7 @@ describe('CorrespondenceService', () => {
}).compile();
service = module.get<CorrespondenceService>(CorrespondenceService);
repository = module.get<Repository<Correspondence>>(
getRepositoryToken(Correspondence)
);
repository = module.get<Repository<Correspondence>>(getRepositoryToken(Correspondence));
});
describe('findAll', () => {
@@ -106,9 +104,7 @@ describe('CorrespondenceService', () => {
{ id: '2', title: 'Test 2' },
];
jest
.spyOn(repository, 'find')
.mockResolvedValue(mockCorrespondences as any);
jest.spyOn(repository, 'find').mockResolvedValue(mockCorrespondences as any);
const result = await service.findAll();
expect(result).toEqual(mockCorrespondences);
@@ -570,9 +566,7 @@ describe('CorrespondenceForm', () => {
vi.advanceTimersByTime(30000);
await waitFor(() => {
expect(saveDraft).toHaveBeenCalledWith(
expect.objectContaining({ title: 'Draft Test' })
);
expect(saveDraft).toHaveBeenCalledWith(expect.objectContaining({ title: 'Draft Test' }));
});
vi.useRealTimers();
@@ -620,9 +614,7 @@ describe('useCorrespondences', () => {
});
it('should handle error state', async () => {
vi.mocked(correspondenceService.getAll).mockRejectedValue(
new Error('API Error')
);
vi.mocked(correspondenceService.getAll).mockRejectedValue(new Error('API Error'));
const { result } = renderHook(() => useCorrespondences('project-1'), {
wrapper,
@@ -792,9 +784,7 @@ describe('Security - SQL Injection', () => {
.expect(200);
// Should not execute malicious SQL
const tableExists = await repository.query(
`SHOW TABLES LIKE 'correspondences'`
);
const tableExists = await repository.query(`SHOW TABLES LIKE 'correspondences'`);
expect(tableExists).toHaveLength(1);
});
});
@@ -908,26 +898,22 @@ describe('Security - Authentication', () => {
### Critical Paths (Must have 100% coverage)
1. **Authentication & Authorization**
- Login/Logout flow
- Token refresh
- RBAC permission checks
2. **Document Numbering**
- Concurrent number generation
- Format validation
- Counter increment logic
3. **File Upload**
- Two-phase upload
- Virus scanning
- File type validation
- Orphan cleanup
4. **Workflow Engine**
- State transitions
- Permission checks at each step
- Notification triggers
@@ -1118,10 +1104,7 @@ export class CorrespondenceFactory {
};
}
static createMany(
count: number,
overrides?: Partial<Correspondence>
): Correspondence[] {
static createMany(count: number, overrides?: Partial<Correspondence>): Correspondence[] {
return Array(count)
.fill(null)
.map(() => this.create(overrides));
@@ -1206,18 +1189,15 @@ describe('[ClassName/FeatureName]', () => {
### Key Metrics
1. **Coverage Percentage**
- Track via CodeCov/Coveralls
- Enforce minimum thresholds in CI
2. **Test Execution Time**
- Unit tests: < 5 seconds
- Integration tests: < 30 seconds
- E2E tests: < 5 minutes
3. **Flaky Test Rate**
- Target: < 1% flaky tests
- Track and fix flaky tests immediately
@@ -110,8 +110,8 @@ git pull --rebase
git log
```
---
## 🧩 SECTION 3 ทำงานกับ Branch
### ดู branch ทั้งหมด
@@ -231,3 +231,5 @@ git clone ssh://git@git.np-dms.work:2222/np-dms/lcbp3.git
## 📌 END
```
```
@@ -24,22 +24,22 @@ This document outlines the step-by-step implementation plan to integrate UUIDv7
### Affected Tables (14)
| # | Table | PK Column | UUID Index |
|---|-------|-----------|------------|
| 1 | organizations | id | idx_organizations_uuid |
| 2 | projects | id | idx_projects_uuid |
| 3 | contracts | id | idx_contracts_uuid |
| 4 | users | user_id | idx_users_uuid |
| 5 | correspondences | id | idx_correspondences_uuid |
| 6 | correspondence_revisions | id | idx_correspondence_revisions_uuid |
| 7 | circulations | id | idx_circulations_uuid |
| 8 | shop_drawings | id | idx_shop_drawings_uuid |
| 9 | shop_drawing_revisions | id | idx_shop_drawing_revisions_uuid |
| 10 | contract_drawings | id | idx_contract_drawings_uuid |
| 11 | asbuilt_drawings | id | idx_asbuilt_drawings_uuid |
| 12 | asbuilt_drawing_revisions | id | idx_asbuilt_drawing_revisions_uuid |
| 13 | attachments | id | idx_attachments_uuid |
| 14 | notifications | id | idx_notifications_uuid |
| # | Table | PK Column | UUID Index |
| --- | ------------------------- | --------- | ---------------------------------- |
| 1 | organizations | id | idx_organizations_uuid |
| 2 | projects | id | idx_projects_uuid |
| 3 | contracts | id | idx_contracts_uuid |
| 4 | users | user_id | idx_users_uuid |
| 5 | correspondences | id | idx_correspondences_uuid |
| 6 | correspondence_revisions | id | idx_correspondence_revisions_uuid |
| 7 | circulations | id | idx_circulations_uuid |
| 8 | shop_drawings | id | idx_shop_drawings_uuid |
| 9 | shop_drawing_revisions | id | idx_shop_drawing_revisions_uuid |
| 10 | contract_drawings | id | idx_contract_drawings_uuid |
| 11 | asbuilt_drawings | id | idx_asbuilt_drawings_uuid |
| 12 | asbuilt_drawing_revisions | id | idx_asbuilt_drawing_revisions_uuid |
| 13 | attachments | id | idx_attachments_uuid |
| 14 | notifications | id | idx_notifications_uuid |
### Excluded Tables (Shared-PK / Junction — inherit UUID from parent)
@@ -116,22 +116,22 @@ export class Correspondence extends UuidBaseEntity {
### Entities to Update
| Entity File | Table |
|-------------|-------|
| `organization.entity.ts` | organizations |
| `project.entity.ts` | projects |
| `contract.entity.ts` | contracts |
| `user.entity.ts` | users |
| `correspondence.entity.ts` | correspondences |
| `correspondence-revision.entity.ts` | correspondence_revisions |
| `circulation.entity.ts` | circulations |
| `shop-drawing.entity.ts` | shop_drawings |
| `shop-drawing-revision.entity.ts` | shop_drawing_revisions |
| `contract-drawing.entity.ts` | contract_drawings |
| `asbuilt-drawing.entity.ts` | asbuilt_drawings |
| Entity File | Table |
| ------------------------------------ | ------------------------- |
| `organization.entity.ts` | organizations |
| `project.entity.ts` | projects |
| `contract.entity.ts` | contracts |
| `user.entity.ts` | users |
| `correspondence.entity.ts` | correspondences |
| `correspondence-revision.entity.ts` | correspondence_revisions |
| `circulation.entity.ts` | circulations |
| `shop-drawing.entity.ts` | shop_drawings |
| `shop-drawing-revision.entity.ts` | shop_drawing_revisions |
| `contract-drawing.entity.ts` | contract_drawings |
| `asbuilt-drawing.entity.ts` | asbuilt_drawings |
| `asbuilt-drawing-revision.entity.ts` | asbuilt_drawing_revisions |
| `attachment.entity.ts` | attachments |
| `notification.entity.ts` | notifications |
| `attachment.entity.ts` | attachments |
| `notification.entity.ts` | notifications |
---
@@ -186,7 +186,7 @@ async findByUuid(uuid: string): Promise<CorrespondenceDto> {
```typescript
// Response DTO exposes uuid, hides id
export class CorrespondenceResponseDto {
uuid: string; // ✅ Public identifier
uuid: string; // ✅ Public identifier
correspondenceNumber: string;
// id: number; // ❌ Never expose INT id
}
@@ -249,19 +249,20 @@ async findByUuidOrId(identifier: string): Promise<Entity> {
#### Remaining Issues
| File | Field | Entity | Issue |
|------|-------|--------|-------|
| `correspondences/form.tsx:212` | `projectId` | Project | `parseInt(p.id)` where `p.id` = UUID string (garbled number) |
| `correspondences/form.tsx:326` | `fromOrganizationId` | Organization | `parseInt(String(org.id))` where `org.id` = undefined (NaN) |
| `correspondences/form.tsx:349` | `toOrganizationId` | Organization | Same as above |
| `admin/users/page.tsx:47` | `primaryOrganizationId` (filter) | Organization | `parseInt(selectedOrgId)` where value = UUID string |
| `admin/user-dialog.tsx:226` | `primaryOrganizationId` | Organization | `parseInt(val)` where `org.id` = undefined → `"0"` fallback |
| `numbering/template-tester.tsx:71-74` | `originatorOrganizationId`, `recipientOrganizationId` | Organization | `parseInt` on org UUID |
| `rfas/page.tsx:17` | `projectId` (URL param) | Project | `parseInt(searchParams.get('projectId'))` — UUID if from URL |
| File | Field | Entity | Issue |
| ------------------------------------- | ----------------------------------------------------- | ------------ | ------------------------------------------------------------ |
| `correspondences/form.tsx:212` | `projectId` | Project | `parseInt(p.id)` where `p.id` = UUID string (garbled number) |
| `correspondences/form.tsx:326` | `fromOrganizationId` | Organization | `parseInt(String(org.id))` where `org.id` = undefined (NaN) |
| `correspondences/form.tsx:349` | `toOrganizationId` | Organization | Same as above |
| `admin/users/page.tsx:47` | `primaryOrganizationId` (filter) | Organization | `parseInt(selectedOrgId)` where value = UUID string |
| `admin/user-dialog.tsx:226` | `primaryOrganizationId` | Organization | `parseInt(val)` where `org.id` = undefined → `"0"` fallback |
| `numbering/template-tester.tsx:71-74` | `originatorOrganizationId`, `recipientOrganizationId` | Organization | `parseInt` on org UUID |
| `rfas/page.tsx:17` | `projectId` (URL param) | Project | `parseInt(searchParams.get('projectId'))` — UUID if from URL |
#### Fix Strategy (same pattern as Drawing Search fix)
For each affected backend DTO:
1. Add `projectUuid?: string` / `organizationUuid?: string` field
2. Controller resolves UUID → INT id via respective service's `findOneByUuid()`
3. Frontend sends UUID string directly (remove `parseInt`)
@@ -296,19 +297,19 @@ For each affected backend DTO:
## Implementation Order (Priority)
| Order | Task | Effort | Status |
|-------|------|--------|--------|
| 1 | UuidBaseEntity (no transformer needed — MariaDB native UUID) | S | ✅ Done |
| 2 | Install `uuid` package | XS | ✅ Done |
| 3 | Update 14 entity files with uuid column | M | ✅ Done |
| 4 | Create ParseUuidPipe | S | ✅ Done |
| 5 | Update controllers to use UUID params | L | ✅ Done |
| 6 | Update services with findByUuid methods | L | ✅ Done |
| 7 | Update DTOs to expose uuid, hide id | M | ✅ Done |
| 8 | Update frontend API calls & routes | L | ✅ Done |
| 9 | Drawing search: projectUuid migration | S | ✅ Done (2026-03-18) |
| 10 | FK reference UUID migration (Correspondence, User, Numbering) | M | ❌ Pending (see Phase 5.4) |
| 11 | Write unit + integration tests | M | ❌ Pending |
| Order | Task | Effort | Status |
| ----- | ------------------------------------------------------------- | ------ | -------------------------- |
| 1 | UuidBaseEntity (no transformer needed — MariaDB native UUID) | S | ✅ Done |
| 2 | Install `uuid` package | XS | ✅ Done |
| 3 | Update 14 entity files with uuid column | M | ✅ Done |
| 4 | Create ParseUuidPipe | S | ✅ Done |
| 5 | Update controllers to use UUID params | L | ✅ Done |
| 6 | Update services with findByUuid methods | L | ✅ Done |
| 7 | Update DTOs to expose uuid, hide id | M | ✅ Done |
| 8 | Update frontend API calls & routes | L | ✅ Done |
| 9 | Drawing search: projectUuid migration | S | ✅ Done (2026-03-18) |
| 10 | FK reference UUID migration (Correspondence, User, Numbering) | M | ❌ Pending (see Phase 5.4) |
| 11 | Write unit + integration tests | M | ❌ Pending |
**Estimated Remaining Effort:** ~2-3 days for FK migration + ~2 days for tests
+15 -7
View File
@@ -50,7 +50,9 @@
## 📖 คู่มือการพัฒนา (Implementation Guides)
### 1. [FullStack JS Guidelines](./05-01-fullstack-js-guidelines.md)
**แนวทางการพัฒนาภาพรวมทั้งระบบ (v1.8.1 — includes NestJS 11 Patterns)**
- โครงสร้างโปรเจกต์ (Monorepo-like focus)
- Naming Conventions & Code Style
- Secrets & Environment Management
@@ -58,7 +60,9 @@
- Double-Lock Mechanism for Numbering
### 2. [Backend Guidelines](./05-02-backend-guidelines.md)
**แนวทางการพัฒนา NestJS 11 Backend**
- Modular Architecture Detail
- DTO Validation & Transformer
- TypeORM Best Practices & Optimistic Locking
@@ -66,7 +70,9 @@
- BullMQ for Background Jobs
### 3. [Frontend Guidelines](./05-03-frontend-guidelines.md)
**แนวทางการพัฒนา Next.js 16 Frontend**
- App Router Patterns
- Shadcn/UI & Tailwind Styling
- TanStack Query for Data Fetching
@@ -74,7 +80,9 @@
- API Client Interceptors (Auth & Idempotency)
### 4. [Document Numbering System](../01-Requirements/business-rules/01-02-02-doc-numbering-rules.md)
**รายละเอียดการนำระบบออกเลขที่เอกสารไปใช้งาน**
- Table Schema: Templates, Counters, Audit
- Double-Lock Strategy (Redis Redlock + Database VersionColumn)
- Reservation Flow (Phase 1: Reserve, Phase 2: Confirm)
@@ -95,13 +103,13 @@
## 🛠️ Technology Stack Recap
| Layer | Primary Technology | Secondary/Supporting |
| ------------ | ------------------ | -------------------- |
| **Backend** | NestJS 11 (Express v5) | TypeORM, BullMQ |
| **Frontend** | Next.js 16.2.0 (React 19.2.4) | Shadcn/UI, Tailwind 4.2.2 |
| **Database** | MariaDB 11.8 | Redis 7 (Cache/Lock) |
| **Search** | Elasticsearch | - |
| **Testing** | Jest, Vitest | Playwright |
| Layer | Primary Technology | Secondary/Supporting |
| ------------ | ----------------------------- | ------------------------- |
| **Backend** | NestJS 11 (Express v5) | TypeORM, BullMQ |
| **Frontend** | Next.js 16.2.0 (React 19.2.4) | Shadcn/UI, Tailwind 4.2.2 |
| **Database** | MariaDB 11.8 | Redis 7 (Cache/Lock) |
| **Search** | Elasticsearch | - |
| **Testing** | Jest, Vitest | Playwright |
---