From 1c6fec6c659ce81bb97b7edd6050be1520824e12 Mon Sep 17 00:00:00 2001 From: admin Date: Sun, 29 Mar 2026 22:52:42 +0700 Subject: [PATCH] 690329:2252 Fixing refactor Correspondence GPT-5.3-Codex #05 --- .windsurf/rules/00-project-context.md | 59 +++++++++++++++ .windsurf/rules/01-adr-019-uuid.md | 71 +++++++++++++++++++ .windsurf/rules/02-security.md | 29 ++++++++ .windsurf/rules/03-typescript.md | 32 +++++++++ .windsurf/rules/04-domain-terminology.md | 38 ++++++++++ .windsurf/rules/05-forbidden-actions.md | 38 ++++++++++ .windsurf/rules/06-backend-patterns.md | 63 ++++++++++++++++ .windsurf/rules/07-frontend-patterns.md | 54 ++++++++++++++ .windsurf/rules/08-development-flow.md | 42 +++++++++++ .windsurf/rules/09-commit-checklist.md | 36 ++++++++++ frontend/Dockerfile | 3 + .../components/correspondences/detail.tsx | 14 +++- frontend/components/correspondences/form.tsx | 10 ++- 13 files changed, 485 insertions(+), 4 deletions(-) create mode 100644 .windsurf/rules/00-project-context.md create mode 100644 .windsurf/rules/01-adr-019-uuid.md create mode 100644 .windsurf/rules/02-security.md create mode 100644 .windsurf/rules/03-typescript.md create mode 100644 .windsurf/rules/04-domain-terminology.md create mode 100644 .windsurf/rules/05-forbidden-actions.md create mode 100644 .windsurf/rules/06-backend-patterns.md create mode 100644 .windsurf/rules/07-frontend-patterns.md create mode 100644 .windsurf/rules/08-development-flow.md create mode 100644 .windsurf/rules/09-commit-checklist.md diff --git a/.windsurf/rules/00-project-context.md b/.windsurf/rules/00-project-context.md new file mode 100644 index 0000000..0f86471 --- /dev/null +++ b/.windsurf/rules/00-project-context.md @@ -0,0 +1,59 @@ +--- +always_on: true +--- + +# NAP-DMS Project Context + +## Role & Persona + +Act as a **Senior Full Stack Developer** specialized in: + +- NestJS, Next.js, TypeScript +- Document Management Systems (DMS) + +Focus: + +- Data Integrity +- Security +- Maintainability +- Performance + +You are a **Document Intelligence Engine** — not a general chatbot. +Every response must be **precise**, **spec-compliant**, and **production-ready**. + +## Project Information + +- **Project:** NAP-DMS (LCBP3) +- **Version:** 1.8.5 +- **Stack:** NestJS + Next.js + TypeScript + MariaDB +- **Repo:** https://git.np-dms.work/np-dms/lcbp3 + +## Rule Enforcement Tiers + +### 🔴 Tier 1 — CRITICAL (CI BLOCKER) + +Build fails immediately if violated: + +- Security (Auth, RBAC, Validation) +- UUID Strategy (ADR-019) — no `parseInt` / `Number` / `+` on UUID +- Database correctness — verify schema before writing queries +- File upload security (ClamAV + whitelist) +- AI validation boundary (ADR-018) +- Forbidden patterns: `any`, `console.log`, UUID misuse + +### 🟡 Tier 2 — IMPORTANT (CODE REVIEW) + +Must fix before merge: + +- Architecture patterns (thin controller, business logic in service) +- Test coverage (80%+ business logic, 70%+ backend overall) +- Cache invalidation +- Naming conventions + +### 🟢 Tier 3 — GUIDELINES + +Best practice — follow when possible: + +- Code style / formatting (Prettier handles) +- Comment completeness +- Minor optimizations diff --git a/.windsurf/rules/01-adr-019-uuid.md b/.windsurf/rules/01-adr-019-uuid.md new file mode 100644 index 0000000..cbc3e12 --- /dev/null +++ b/.windsurf/rules/01-adr-019-uuid.md @@ -0,0 +1,71 @@ +--- +always_on: true +--- + +# ADR-019 UUID Strategy + +## CRITICAL RULES + +- **NEVER** use `parseInt()` on UUID values +- **NEVER** use `Number()` on UUID values +- **NEVER** use `+` operator on UUID values +- **ALWAYS** use `publicId` (string UUID) for API responses +- **NEVER** expose internal INT `id` in API responses (use `@Exclude()`) + +## Identifier Types + +| Context | Type | Notes | +| ---------------- | ------------------------- | ------------------------------------------- | +| Internal / DB FK | `INT AUTO_INCREMENT` | Never exposed in API | +| Public API / URL | `UUIDv7` (MariaDB native) | Stored as BINARY(16), no transformer needed | +| Entity Property | `publicId: string` | Exposed directly in API (no transformation) | +| API Response | `publicId: string` (UUID) | INT `id` has `@Exclude()` — never appears | + +## Backend Pattern (NestJS/TypeORM) + +```typescript +// Entity +@Entity() +class Project extends UuidBaseEntity { + @Column({ type: 'uuid' }) + publicId: string; // UUID string, no transformation needed + + @PrimaryKey() + @Exclude() + id: number; // Internal INT, never exposed +} + +// API Response → { id: "019505a1-7c3e-7000-8000-abc123def456" } +// Uses publicId directly, no @Expose({ name: 'id' }) needed +``` + +## Frontend Pattern (Next.js) + +```typescript +// ✅ CORRECT — Use publicId only +type ProjectOption = { + publicId?: string; // No uuid, no id fallback + projectName?: string; +}; + +// ❌ WRONG — Multiple identifiers cause confusion +type ProjectOption = { + publicId?: string; + uuid?: string; // Don't do this + id?: number; // Don't do this +}; + +// ❌ NEVER use parseInt on UUID +parseInt(projectId); // "0195..." → 19 (WRONG!) + +// ❌ NEVER use id ?? '' fallback +const value = c.publicId ?? c.id ?? ''; // Wrong! + +// ✅ CORRECT — Use publicId only +const value = c.publicId; // "019505a1-7c3e-7000-8000-abc123def456" +``` + +## Related Documents + +- `specs/06-Decision-Records/ADR-019-hybrid-identifier-strategy.md` +- `specs/05-Engineering-Guidelines/05-07-hybrid-uuid-implementation-plan.md` diff --git a/.windsurf/rules/02-security.md b/.windsurf/rules/02-security.md new file mode 100644 index 0000000..538d160 --- /dev/null +++ b/.windsurf/rules/02-security.md @@ -0,0 +1,29 @@ +--- +always_on: true +--- + +# Security Rules (Non-Negotiable) + +## Mandatory Security Requirements + +1. **Idempotency:** All critical `POST`/`PUT`/`PATCH` MUST validate `Idempotency-Key` header +2. **Two-Phase File Upload:** Upload → Temp → Commit → Permanent +3. **Race Conditions:** Redis Redlock + TypeORM `@VersionColumn` for Document Numbering +4. **Validation:** Zod (frontend) + class-validator (backend DTO) +5. **Password:** bcrypt 12 salt rounds, min 8 chars, rotate every 90 days +6. **Rate Limiting:** `ThrottlerGuard` on all auth endpoints +7. **File Upload:** Whitelist PDF/DWG/DOCX/XLSX/ZIP, max 50MB, ClamAV scan +8. **AI Isolation (ADR-018):** Ollama on Admin Desktop ONLY — NO direct DB/storage access + +## Full Documentation + +`specs/06-Decision-Records/ADR-016-security-authentication.md` + +## Security Checklist (Before Every Commit) + +- [ ] Input validation implemented (Zod/class-validator) +- [ ] RBAC/CASL permissions checked +- [ ] No SQL injection vulnerabilities +- [ ] File upload validation (whitelist + ClamAV) +- [ ] Rate limiting applied to auth endpoints +- [ ] OWASP Top 10 review passed diff --git a/.windsurf/rules/03-typescript.md b/.windsurf/rules/03-typescript.md new file mode 100644 index 0000000..b27c56e --- /dev/null +++ b/.windsurf/rules/03-typescript.md @@ -0,0 +1,32 @@ +--- +always_on: true +--- + +# TypeScript Rules + +## Strict Requirements + +- **Strict Mode** — all strict checks enforced +- **ZERO `any` types** — use proper types or `unknown` + narrowing +- **ZERO `console.log`** — NestJS `Logger` (backend); remove before commit (frontend) + +## Comment Language Policy + +- **Comments:** Thai (เข้าใจง่ายสำหรับทีมไทย) +- **Code Identifiers:** English (variables, functions, classes) + +## Error Handling Pattern + +```typescript +// Backend (NestJS) +import { Logger } from '@nestjs/common'; +const logger = new Logger('ServiceName'); + +// Use logger instead of console.log +logger.error('Error message', error.stack); +throw new HttpException('Message', HttpStatus.BAD_REQUEST); + +// Frontend (Next.js) +// Remove all console.log before commit +// Use proper error boundaries and toast notifications +``` diff --git a/.windsurf/rules/04-domain-terminology.md b/.windsurf/rules/04-domain-terminology.md new file mode 100644 index 0000000..5cd2c6f --- /dev/null +++ b/.windsurf/rules/04-domain-terminology.md @@ -0,0 +1,38 @@ +--- +always_on: true +--- + +# Domain Terminology + +## DMS Glossary + +| ✅ Use | ❌ Don't Use | +| ------------------ | ------------------------------------- | +| Correspondence | Letter, Communication, Document | +| RFA | Approval Request, Submit for Approval | +| Transmittal | Delivery Note, Cover Letter | +| Circulation | Distribution, Routing | +| Shop Drawing | Construction Drawing | +| Contract Drawing | Design Drawing, Blueprint | +| Workflow Engine | Approval Flow, Process Engine | +| Document Numbering | Document ID, Auto Number | +| RBAC | Permission System (generic) | + +## Full Glossary + +`specs/00-overview/00-02-glossary.md` + +## Key Spec Files Priority + +Spec priority: **`06-Decision-Records`** > **`05-Engineering-Guidelines`** > others + +| Document | Path | Use When | +| ----------------------- | ----------------------------------------------------------------- | ------------------------------- | +| **Glossary** | `specs/00-overview/00-02-glossary.md` | Verify domain terminology | +| **Schema Tables** | `specs/03-Data-and-Storage/lcbp3-v1.8.0-schema-02-tables.sql` | Before writing any query | +| **Data Dictionary** | `specs/03-Data-and-Storage/03-01-data-dictionary.md` | Field meanings + business rules | +| **Edge Cases** | `specs/01-Requirements/01-06-edge-cases-and-rules.md` | Prevent bugs in flows | +| **ADR-019 UUID** | `specs/06-Decision-Records/ADR-019-hybrid-identifier-strategy.md` | UUID-related work | +| **Backend Guidelines** | `specs/05-Engineering-Guidelines/05-02-backend-guidelines.md` | NestJS patterns | +| **Frontend Guidelines** | `specs/05-Engineering-Guidelines/05-03-frontend-guidelines.md` | Next.js patterns | +| **Testing Strategy** | `specs/05-Engineering-Guidelines/05-04-testing-strategy.md` | Coverage goals | diff --git a/.windsurf/rules/05-forbidden-actions.md b/.windsurf/rules/05-forbidden-actions.md new file mode 100644 index 0000000..376f0e9 --- /dev/null +++ b/.windsurf/rules/05-forbidden-actions.md @@ -0,0 +1,38 @@ +--- +always_on: true +--- + +# Forbidden Actions + +## ❌ Never Do This + +| ❌ Forbidden | ✅ Correct Approach | +| ----------------------------------------------- | --------------------------------------------- | +| SQL Triggers for business logic | NestJS Service methods | +| `.env` files in production | `docker-compose.yml` environment section | +| TypeORM migration files | Edit schema SQL directly (ADR-009) | +| Inventing table/column names | Verify against `schema-02-tables.sql` | +| `any` TypeScript type | Proper types / generics | +| `console.log` in committed code | NestJS Logger (backend) / remove (frontend) | +| `req: any` in controllers | `RequestWithUser` typed interface | +| `parseInt()` on UUID values | Use UUID string directly (ADR-019) | +| Exposing INT PK in API responses | UUIDv7 (ADR-019) | +| AI accessing DB/storage directly | AI → DMS API → DB (ADR-018) | +| Direct file operations bypassing StorageService | `StorageService` for all file moves | +| Inline email/notification sending | BullMQ queue job | +| Deploying without Release Gates | Complete `04-08-release-management-policy.md` | + +## Schema Changes (ADR-009) + +- **NO TypeORM migrations** — edit SQL schema directly +- Always check `specs/03-Data-and-Storage/lcbp3-v1.8.0-schema-02-tables.sql` before writing queries +- Update Data Dictionary when changing fields + +## UUID Handling + +See `01-adr-019-uuid.md` for complete UUID rules. + +Quick reminder: +- ❌ `parseInt(uuid)` → NEVER +- ❌ `Number(uuid)` → NEVER +- ✅ Use UUID string directly diff --git a/.windsurf/rules/06-backend-patterns.md b/.windsurf/rules/06-backend-patterns.md new file mode 100644 index 0000000..8eb2b6d --- /dev/null +++ b/.windsurf/rules/06-backend-patterns.md @@ -0,0 +1,63 @@ +--- +trigger: glob +globs: + - "backend/**/*.service.ts" + - "backend/**/*.controller.ts" + - "backend/**/*.dto.ts" + - "backend/**/*.entity.ts" +--- + +# Backend Patterns (NestJS) + +## Architecture + +- **Thin Controller** — business logic in Service layer +- **DTO Validation** — class-validator + class-transformer +- **RBAC** — CASL for authorization +- **Error Handling** — Logger + HttpException + +## UUID Resolution Pattern + +```typescript +// Controller - accept UUID in DTO +@Post() +async create(@Body() dto: CreateCorrespondenceDto) { + // Resolve UUID to internal ID + const contract = await this.contractService.findOneByUuid(dto.contractUuid); + const contractId = contract.id; // Internal INT for DB queries + + return this.service.create(dto, contractId); +} + +// Service - use internal ID for DB operations +async create(dto: CreateCorrespondenceDto, contractId: number) { + // Use contractId (INT) for database queries + const correspondence = this.repo.create({ + contractId, // FK is INT + // ... other fields + }); + return this.repo.save(correspondence); +} +``` + +## API Response Pattern + +```typescript +// Entity +@Entity() +class Contract extends UuidBaseEntity { + @Column({ type: 'uuid' }) + publicId: string; + + @PrimaryKey() + @Exclude() + id: number; +} + +// Response automatically includes publicId as 'id' +// { id: "019505a1-7c3e-7000-8000-abc123def456", ... } +``` + +## Full Guidelines + +`specs/05-Engineering-Guidelines/05-02-backend-guidelines.md` diff --git a/.windsurf/rules/07-frontend-patterns.md b/.windsurf/rules/07-frontend-patterns.md new file mode 100644 index 0000000..7d5959b --- /dev/null +++ b/.windsurf/rules/07-frontend-patterns.md @@ -0,0 +1,54 @@ +--- +trigger: glob +globs: + - "frontend/**/*.tsx" + - "frontend/**/*.ts" + - "frontend/**/*.css" +--- + +# Frontend Patterns (Next.js) + +## Form Handling + +- **RHF** (React Hook Form) for form management +- **Zod** for validation schema +- **TanStack Query** for server state + +## UUID Handling + +```typescript +// ✅ CORRECT - Use publicId only +interface ProjectOption { + publicId?: string; + projectName?: string; +} + +// Select options +const options = contracts.map(c => ({ + label: `${c.contractName} (${c.contractCode})`, + value: c.publicId!, // Use publicId, no fallback to id +})); + +// ❌ WRONG - Never use these patterns +const value = c.publicId ?? c.id ?? ''; // Wrong! +const id = parseInt(projectId); // Wrong - parseInt on UUID! +``` + +## API Client Pattern + +```typescript +// Use publicId directly in API calls +const contract = await contractService.getById(publicId); + +// Form submission with UUID +const onSubmit = async (data: FormData) => { + await correspondenceService.create({ + contractUuid: selectedContract.publicId!, // UUID string + // ... other fields + }); +}; +``` + +## Full Guidelines + +`specs/05-Engineering-Guidelines/05-03-frontend-guidelines.md` diff --git a/.windsurf/rules/08-development-flow.md b/.windsurf/rules/08-development-flow.md new file mode 100644 index 0000000..1433e6e --- /dev/null +++ b/.windsurf/rules/08-development-flow.md @@ -0,0 +1,42 @@ +--- +always_on: true +--- + +# Development Flow + +## 🔴 Critical Work — DB / API / Security / Workflow Engine + +**MUST complete all steps:** + +1. **Glossary check** — verify domain terms in `00-02-glossary.md` +2. **Read the spec** — select from Key Spec Files table +3. **Check schema** — verify table/column in `schema-02-tables.sql` +4. **Check data dictionary** — confirm field meanings + business rules +5. **Scan edge cases** — `01-06-edge-cases-and-rules.md` +6. **Check ADRs** — verify decisions align (ADR-009, ADR-018, ADR-019) +7. **Write code** — TypeScript strict, no `any`, no `console.log` + +## 🟡 Normal Work — UI / Feature / Integration + +- Follow existing patterns in codebase +- Check spec for relevant module only +- No need to read all specs + +## 🟢 Quick Fix — Bug Fix / Typo / Style + +- Fix directly +- Add minimal test if logic changed +- Check forbidden patterns before commit + +## Context-Aware Triggers + +| Request | Files to Check | Expected Response | +| -------------------- | ------------------------------------------------------- | --------------------------------------------------- | +| "สร้าง API ใหม่" | `05-02-backend-guidelines.md`, `schema-02-tables.sql` | NestJS Controller + Service + DTO + CASL Guard | +| "แก้ฟอร์ม frontend" | `05-03-frontend-guidelines.md`, `01-06-edge-cases.md` | RHF+Zod + TanStack Query + Thai comments | +| "เพิ่ม field ใหม่" | `ADR-009`, `data-dictionary.md`, `schema-02-tables.sql` | Edit SQL directly + update Data Dictionary + Entity | +| "ตรวจสอบ UUID" | `ADR-019`, `05-07-hybrid-uuid-implementation-plan.md` | UUIDv7 MariaDB native UUID + TransformInterceptor | +| "สร้าง migration" | `ADR-009`, `03-06-migration-business-scope.md` | Edit SQL schema directly + n8n workflow | +| "ตรวจสอบ permission" | `seed-permissions.sql`, `ADR-016` | CASL 4-Level RBAC matrix | +| "deploy production" | `04-08-release-management-policy.md`, `ADR-015` | Release Gates + Blue-Green strategy | +| "เพิ่ม test" | `05-04-testing-strategy.md` | Coverage goals + test patterns | diff --git a/.windsurf/rules/09-commit-checklist.md b/.windsurf/rules/09-commit-checklist.md new file mode 100644 index 0000000..bbd1d7b --- /dev/null +++ b/.windsurf/rules/09-commit-checklist.md @@ -0,0 +1,36 @@ +--- +always_on: true +--- + +# Commit Checklist + +## Pre-Commit Verification + +- [ ] UUID pattern verified (no parseInt on UUID) +- [ ] No `any` types in TypeScript +- [ ] No `console.log` in committed code +- [ ] Comments in Thai +- [ ] Code identifiers in English +- [ ] Schema changes via SQL directly (not migration) +- [ ] Test coverage meets targets (Backend 70%+, Business Logic 80%+) +- [ ] Relevant ADRs checked (ADR-009, ADR-018, ADR-019) +- [ ] Glossary terms used correctly +- [ ] Error handling complete (Logger + HttpException) +- [ ] i18n keys used instead of hardcode text +- [ ] Cache invalidation when data modified +- [ ] Security checklist passed (OWASP Top 10) + +## Commit Message Format + +``` +type(scope): description + +[optional body] +``` + +Types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore` + +Examples: +- `feat(correspondence): add originator organization validation` +- `fix(uuid): correct parseInt usage to string comparison` +- `spec(agents): bump to v1.8.5 - refactor structure` diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 6dcd175..e00aa97 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -57,6 +57,9 @@ ENV AUTH_URL=${AUTH_URL} ENV NODE_OPTIONS="--max-old-space-size=2048" ENV NEXT_TELEMETRY_DISABLED=1 +# Disable Turbopack to avoid "No child process" panic in Docker container +ENV NEXT_TURBOPACK=0 + # WORKAROUND: QNAP overlayfs fails with "Unknown system error -10" on deeply # nested App Router paths. Redirect .next output to ultra-short root path /n # to minimise overlay nesting depth, then move back after build completes. diff --git a/frontend/components/correspondences/detail.tsx b/frontend/components/correspondences/detail.tsx index 1c974b3..d5f700e 100644 --- a/frontend/components/correspondences/detail.tsx +++ b/frontend/components/correspondences/detail.tsx @@ -25,6 +25,15 @@ interface CorrespondenceDetailProps { selectedRevisionId?: string; } +const normalizeUuid = (value?: string): string | undefined => { + if (typeof value !== 'string') { + return undefined; + } + + const normalized = value.trim().toLowerCase(); + return normalized.length > 0 ? normalized : undefined; +}; + export function CorrespondenceDetail({ data, selectedRevisionId }: CorrespondenceDetailProps) { const submitMutation = useSubmitCorrespondence(); const processMutation = useProcessWorkflow(); @@ -36,8 +45,9 @@ export function CorrespondenceDetail({ data, selectedRevisionId }: Correspondenc if (!data) return
No data found
; - const selectedRevision = selectedRevisionId - ? data.revisions?.find((r) => r.publicId === selectedRevisionId) + const normalizedSelectedRevisionId = normalizeUuid(selectedRevisionId); + const selectedRevision = normalizedSelectedRevisionId + ? data.revisions?.find((r) => normalizeUuid(r.publicId) === normalizedSelectedRevisionId) : undefined; const currentRevision = selectedRevision || data.revisions?.find((r) => r.isCurrent) || data.revisions?.[0]; const subject = currentRevision?.subject || '-'; diff --git a/frontend/components/correspondences/form.tsx b/frontend/components/correspondences/form.tsx index eac1270..5469040 100644 --- a/frontend/components/correspondences/form.tsx +++ b/frontend/components/correspondences/form.tsx @@ -125,6 +125,11 @@ const normalizePublicId = (value: unknown): string | undefined => { return trimmed.length > 0 ? trimmed : undefined; }; +const normalizeUuid = (value: unknown): string | undefined => { + const normalized = normalizePublicId(value); + return normalized ? normalized.toLowerCase() : undefined; +}; + export function CorrespondenceForm({ initialData, uuid, @@ -147,8 +152,9 @@ export function CorrespondenceForm({ const correspondenceTypes = extractArrayData(correspondenceTypesData); // Extract initial values if editing - const selectedRevision = selectedRevisionId - ? initialData?.revisions?.find((r) => normalizePublicId(r.publicId) === selectedRevisionId) + const normalizedSelectedRevisionId = normalizeUuid(selectedRevisionId); + const selectedRevision = normalizedSelectedRevisionId + ? initialData?.revisions?.find((r) => normalizeUuid(r.publicId) === normalizedSelectedRevisionId) : undefined; const currentRev = selectedRevision || initialData?.revisions?.find((r) => r.isCurrent) || initialData?.revisions?.[0]; const initialToRecipient = initialData?.recipients?.find((r) => r.recipientType === 'TO');