Files
solution-erp/docs/rules.md
pqhuy1987 d2298fa614 [CLAUDE] Docs: chốt rule timing unit test (1 bảng 5-row compact)
- rules.md §7: thêm section "Khi nào viết test — timing rule"
  4 case (feature mới/bug fix/critical algorithm/spec change) + 1 skip list
- CLAUDE.md root: 1 dòng quick reference timing rule
- User confirm pattern Phase 1-8 (test-after pragmatic) là default cho solo dev
- Test-before BẮT BUỘC chỉ khi: bug fix (regression test) + critical algorithm
- Spec change KHÔNG ngừng test — update test cũ + code, commit chung

Commit MD-only → CI skip (path filter gotcha #41), 0s deploy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 00:40:24 +07:00

17 KiB

Rules — Coding Conventions SOLUTION_ERP

Nguồn duy nhất cho rules. Bất cứ commit nào vi phạm cần justify trong message.

1. Ngôn ngữ

Thành phần Ngôn ngữ
UI FE 100% tiếng Việt
Code (.cs/.ts) English (tên biến/hàm/class)
Comment code Tiếng Anh hoặc Việt — miễn rõ
Tên DB table + column English PascalCase (Contracts, SupplierId)
Business label Tiếng Việt (vd "Đang soạn thảo" cho phase 2)
Commit message English hoặc Việt — preferred English
Doc MD (docs/) Tiếng Việt (audience là người Việt)

2. Backend — .NET 10

2.1 Clean Architecture dependency rule

Api → Application ← Domain
      ↑
Infrastructure ──┘
  • Domain: pure, chỉ dùng Microsoft.Extensions.Identity.Stores (cần cho IdentityUser) — không depend ASP.NET Core framework
  • Application: depend Domain; CQRS handlers + interfaces + DTOs + validators
  • Infrastructure: depend Application + Domain; impl interfaces (EF, JWT, rendering, services)
  • Api: depend Application + Infrastructure; Controllers + middleware + composition root

2.2 CQRS + MediatR pattern

  • 1 feature = 1 file {Verb}{Entity}Command.cs / {Verb}{Entity}Query.cs
  • File chứa: Command record + Validator + Handler (cùng 1 file cho compact)
  • Dài thì tách: {Feature}/Commands/{Create}/...

Command naming:

Create{Entity}Command      // tạo mới
Update{Entity}Command      // update full
Patch{Entity}FieldCommand  // update 1 field
Delete{Entity}Command      // soft delete (default)
{Action}{Entity}Command    // action domain-specific (vd TransitionContract)

Query naming:

Get{Entity}Query            // single by Id
List{Entity}sQuery          // list với paging
{Entity}{Purpose}Query      // custom query (vd GetMyInboxQuery)

2.3 Validation

  • FluentValidation (KHÔNG dùng DataAnnotation)
  • Validator = class trong cùng file command: {Command}Validator : AbstractValidator<{Command}>
  • Pipeline: ValidationBehavior auto-chạy trước Handler — throw ValidationException → 400 ProblemDetails

2.4 Error handling

  • KHÔNG try-catch ở Controller
  • Throw domain exception: NotFoundException, ForbiddenException, UnauthorizedException, ConflictException, ValidationException
  • GlobalExceptionMiddleware map exception → HTTP status + ProblemDetails JSON
  • Logging: Serilog structured, không Console.WriteLine

2.5 Async/await

  • Tất cả I/O (DB, HTTP, file) PHẢI async
  • Truyền CancellationToken mọi async call
  • Tên method async: suffix Async (trừ ASP.NET action signature)

2.6 Entity rules

  • Mọi entity mới extend BaseEntity (Id Guid + audit) hoặc AuditableEntity (+ soft delete)
  • PK: Id type Guid, default Guid.NewGuid() trong constructor
  • FK: {Entity}Id type Guid
  • Enum: HasConversion<int>() trong EF config
  • String: luôn HasMaxLength(n) trong EF config — không để nvarchar(max) trừ khi là JSON/rich text
  • Money: decimal + HasPrecision(18, 2)
  • DateTime: luôn UTC, lấy từ IDateTime.UtcNow (không DateTime.Now)
  • Query filter soft delete: HasQueryFilter(x => !x.IsDeleted) cho AuditableEntity

2.7 DI registration

  • Singleton: stateless services (DateTimeService, FormRenderer)
  • Scoped: per-request services (JwtTokenService, ContractWorkflowService, DbContext)
  • Transient: lightweight utility (hiếm dùng)

2.8 Package version pinning

  • KHÔNG dùng * hoặc latest — luôn pin version cụ thể
  • Check compatibility với .NET 10 trước khi add. Gotchas đã biết:
    • MediatR 14 → lỗi IMediator DI, pin 12.4.1
    • Swashbuckle 10 → conflict OpenApi 2, pin 6.9.0
  • Xem gotchas.md cho full list

3. Frontend — React 19 + Vite 8 + TS 6

3.1 Named export, không default export

// ✅
export function SuppliersPage() { }
export const MyConstant = 1

// ❌
export default function SuppliersPage() { }

// Exception: App.tsx dùng default export (Vite convention)

3.2 erasableSyntaxOnly (Vite 8)

KHÔNG dùng enum — dùng const-object pattern:

// ❌
export enum SupplierType { NhaCungCap = 1 }

// ✅
export const SupplierType = { NhaCungCap: 1, NhaThauPhu: 2 } as const
export type SupplierType = typeof SupplierType[keyof typeof SupplierType]

3.3 Path alias

  • Dùng @/ cho absolute import: import { api } from '@/lib/api'
  • Config ở vite.config.ts + tsconfig.app.json (chỉ paths, không baseUrl)

3.4 Data fetching

  • TanStack Query cho mọi API call (KHÔNG useState + useEffect thủ công)
  • useQuery cho GET, useMutation cho POST/PUT/DELETE
  • invalidateQueries sau mutation thành công

3.5 API calls

  • Qua api instance (src/lib/api.ts) — đã wire axios interceptor JWT + 401 redirect
  • Base URL: /api (Vite proxy → :5443)

3.6 Permission guard

<PermissionGuard menuKey="Contracts" action="Update">
  <Button>Sửa</Button>
</PermissionGuard>

// Hook
const { can } = usePermission()
if (!can('Contracts', 'Update')) return null

Nhớ: FE guard chỉ là UX. BE policy [Authorize(Policy = "Contracts.Update")] là source of truth.

3.7 Component naming

  • PascalCase cho file: SuppliersPage.tsx, DataTable.tsx
  • kebab-case cho utility: api.ts, cn.ts
  • React component = PascalCase
  • Hook: prefix use, ví dụ usePermission

3.8 Toast + error

  • Dùng sonner (đã wire <Toaster> ở App)
  • toast.success('...'), toast.error(getErrorMessage(err))
  • Error helper: src/lib/apiError.ts extract từ ProblemDetails

3.9 Duplicate giữa 2 FE

CÓ CHỦ ĐÍCH. Mỗi app (fe-admin + fe-user) là standalone — copy shared code giữa 2 app thay vì extract ra package chung. Lý do: 2 app có UX rất khác (admin mission-critical, user workflow-heavy), evolution độc lập.

Khi share code: cp file từ app này sang app kia. Đồng bộ tay khi có breaking change.

4. Database

4.1 Convention

Item Rule
Schema 1 schema duy nhất: dbo
Table PascalCase tiếng Anh, plural: Contracts, Suppliers
Column PascalCase: FullName, CreatedAt
PK Iduniqueidentifier
FK {Entity}Iduniqueidentifier
Index IX_{Table}_{Col}
Unique UX_{Table}_{Col}

Full: xem database/database-guide.md.

4.2 Migration

  • Một commit = 1 migration (trừ khi nhiều change nhỏ trong cùng feature)
  • Naming PascalCase: AddMasterData, AddContractsWorkflow
  • Commit đủ 3 file: {Name}.cs, {Name}.Designer.cs, ApplicationDbContextModelSnapshot.cs

5. Git & commits

5.1 Branch

  • main — deploy branch
  • Feature branch (nếu nhiều người): feature/phase{N}-{feature}

5.2 Commit format

[CLAUDE] <scope>: <imperative message>

<body optional — chi tiết what + why>

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Scope: Contract · Form · Workflow · Supplier · Auth · Admin · Api · App · Domain · Infra · FE-Admin · FE-User · Docs · CICD · Scripts · Report · Dashboard

5.3 Một commit nên

  • Build pass (BE + FE)
  • Không leave WIP nửa chừng (trừ khi commit message nói rõ)
  • Test E2E với endpoint mới (curl trong session log)

5.4 Không commit

  • .env, appsettings.*.Local.json
  • node_modules/, bin/, obj/
  • wwwroot/uploads/ (user files)
  • SolutionErp_Design database (tạm cho EF CLI)

6. Docs

6.1 Khi nào viết session log

  • Session có commit thay đổi code → PHẢI tạo docs/changelog/sessions/YYYY-MM-DD-HHMM-{topic}.md
  • Session chỉ Q&A, không đụng file → không cần

6.2 Session log format

# Session YYYY-MM-DD HH:MM — <topic>

**Dev:** Claude / Copilot / <name>
**Duration:** ~Nh
**Base commit:** <sha>

## Làm được

### Chunk X — <name>
- bullet list deliverable

## E2E verified

<curl output hoặc screenshot>

## Bug gặp + fix

| Bug | Fix |

## Docs updates

## Handoff

## Thông số cumulative

6.3 Update khi thêm feature

7. Testing (Phase 8 active — 77 test pass + CI gate live)

Stack đã apply

  • xUnit 2.9.3 + FluentAssertions 7.2 (pin trước v8 commercial license)
  • Microsoft.EntityFrameworkCore.Sqlite 10 (in-memory testing — supports transactions, không như InMemory provider)
  • Custom TestApplicationDbContext override nvarchar(max) → TEXT cho SQLite compat (xem tests/.../Common/SqliteDbFixture.cs)
  • NO mock framework (tránh mock-heavy, prefer EF in-memory hoặc fake data)

Test pyramid (bottom-heavy, KHÔNG E2E)

✅ Phase 1: Domain policy            54 test  (pure functions, no DB)
✅ Phase 2: Infra code generator     17 test  (SQLite in-mem)
✅ Phase 3 mini: Application versioning  6 test  (SQLite, no UserManager)
⏸️ Phase 3 full: Application handlers w/ Identity (cần UserManager DI helper)
⏸️ Phase 4: API smoke (WebApplicationFactory)
⏸️ Phase 5: FE Vitest cho lib utility
❌ E2E (Playwright) — KHÔNG làm (brittle cho solo dev)

Khi nào viết test — timing rule

Tình huống Quy tắc
Feature mới Test-after: UAT 2-3 lần ổn → viết test (≤1 commit theo sau). Pattern Phase 1-8 đã làm.
Bug fix Test-before BẮT BUỘC: reproduce bug bằng test failing → fix → green. "1 bug = 1 regression test before merge".
Critical algorithm (code generator atomic, workflow guard, financial calc, security check) Test-before merge — edge case nhiều, race condition đắt nếu break prod.
Spec change (không phải bug) Update test cũ + code, commit chung. KHÔNG xóa, KHÔNG skip test cũ. Coverage chỉ tăng.
Skip test DTO mapping, CRUD master đơn giản, FE component snapshot, wrapper passthrough, migration backfill idempotent.

Quy tắc bổ sung mỗi feature mới

  • Domain entity / enum → 0 test (compile check đủ)
  • Domain policy method → 2-3 test (positive + negative + edge case)
  • CQRS handler có guard logic → 1 test mỗi guard branch
  • Migration mới → smoke test "DbInitializer chạy được + seed không lỗi"
  • API endpoint mới → 1 happy path smoke (cho endpoint critical)
  • Bug found in production / staging → 1 regression test BEFORE merge fix

Pattern: "1 bug found → 1 regression test added".

Workflow user end-of-task

# Sau mỗi task code (BẮT BUỘC trước commit code):
dotnet test SolutionErp.slnx        # local verify ~3s
git add -A && git commit -m "..."
git push                             # CI tự run test gate trước build/deploy

Test fail local → fix trước khi push. Test fail trên CI → no deploy.

CI gate behavior

.gitea/workflows/deploy.yml:

# Path filter: skip CI hoàn toàn cho commit MD-only
on:
  push:
    paths-ignore:
      - 'docs/**'
      - '**/*.md'
      - '.claude/skills/**'

# 2 step test gate trước build/deploy (fail → exit → no deploy)
- name: Run unit tests (Domain)
- name: Run integration tests (Infrastructure)

Pending (Phase 3-5 future)

  • Application handler tests cần Identity UserManager DI setup helper
  • API smoke tests via WebApplicationFactory + SQLite
  • FE Vitest cho lib/queryMatches, lib/cn, money formatter

Detail: docs/architecture.md §11 Testing strategy.

8. Security baseline

  • HTTPS everywhere prod
  • JWT Secret trong user-secrets / env var (không commit appsettings)
  • Password hash: PBKDF2 (Identity default)
  • CORS whitelist chỉ 2 FE origin
  • SQL injection: EF Core parameterized (không raw SQL trừ khi phải)
  • Audit log: mọi ghi phải log qua ContractApproval hoặc AuditLog (future)
  • Rate limit: /api/auth/login 5 req/min/IP (chưa implement — Phase 5)

9. Skills (.claude/skills/)

Project-level skills encode kiến thức SOLUTION_ERP-specific. Claude tự auto-invoke qua Skill tool dựa trên semantic match với description trong SKILL.md. PHẢI dùng skill khi gặp task tương ứng — không tự suy luận lại.

9.1 Skills hiện có (6)

Domain (3) — logic nghiệp vụ:

Skill Trigger khi
contract-workflow Bug chuyển phase, 403, mã HĐ sai, versioned WF, "HĐ cũ giữ cũ"
form-engine Render template docx/xlsx, FieldSpec, PDF export, upload form mới
permission-matrix Permission denied, gán role, menu không hiện, inherit Contracts/Workflows

Ops (3) — devops + security + schema:

Skill Trigger khi
dependency-audit-erp npm audit, dotnet vulnerable, scan CVE, nâng cấp package
ef-core-migration migrations add, schema change, snapshot lỗi, revert migration, DesignTimeDbContextFactory
iis-deploy-runbook Prod 500/502, cert hết hạn, restart app pool, gitea-runner fail, SignalR 401

9.2 Nguyên tắc tạo skill mới

Project-specific, KHÔNG clone generic. User-level skills global đã có sẵn (code-reviewer, sql-database-assistant, focused-fix, senior-frontend, mcp-builder, webapp-testing, review, security-review, ...). Project-level skills chỉ thêm khi:

  • Encode kiến thức SOLUTION_ERP-only (path, version pin, gotcha number, commit convention)
  • Generic skill không thể có (vd: 8 migration history cụ thể của project)
  • Có YAML when-to-use triggers rõ ràng (cả tiếng Việt + Anh)
  • Workflow Vietnamese-first (audience là dev Việt)

9.3 Format bắt buộc SKILL.md

---
name: kebab-case-name
description: 1-3 câu mô tả + stack specificity (cho semantic match đúng)
when-to-use:
  - "trigger english"
  - "trigger tiếng Việt"
---

# Skill Name

## Context
## Workflow / Commands (copy-pastable, không pseudocode)
## Pitfalls
## Code pointers (path đầy đủ src/Backend/...)
## Related (dẫn chiếu gotcha #N, docs/...)

9.4 Audit định kỳ — mỗi 4 tuần

Cadence: Mỗi đầu tháng (hoặc thứ Hai tuần đầu tháng), chạy audit skill list:

1. Đọc .claude/skills/README.md — list 6 skill hiện có
2. Cross-check với:
   - docs/STATUS.md "Recently Done" — feature mới nào chưa có skill cover?
   - docs/gotchas.md — gotcha mới (>3 dòng) nào nên gom thành skill mới?
   - migration-todos.md "In Progress" / "Next up" — task lớn sắp tới có cần skill?
3. Check repo nguồn skill 3rd party có gì mới:
   - https://github.com/alirezarezvani/claude-skills (default-branch)
   - Anthropic skills built-in (xem system reminder skill list)
4. Đánh giá staleness:
   - Mỗi skill: SKILL.md có còn match code thực tế không?
   - Trigger phrases có còn hiệu quả?
   - Code pointers có còn đúng path?
5. Hành động:
   - Skill stale → update SKILL.md + commit `[CLAUDE] Skill: refresh <name>`
   - Skill cần thêm → tạo theo §9.2 §9.3 + commit `[CLAUDE] Skill: add <name>`
   - Skill không còn cần → archive vào .claude/skills/_archived/ + commit
6. Update bảng §9.1 trong rules.md + .claude/skills/README.md + docs/CLAUDE.md count
7. Log vào docs/changelog/skill-audit-{YYYY-MM}.md (1 page max)

Trigger: Khi user nói "audit skill", "check skill list", "skill staleness", "định kỳ kiểm tra skill" → chạy workflow này.

Lịch chính thức:

  • Tháng 1, 2, 3, ... — đầu tháng audit (1 buổi sáng)
  • Hoặc khi bắt đầu Phase mới (vd Phase 6) → audit ngay trước phase
  • Hoặc khi docs/gotchas.md vượt mốc (cứ +5 gotcha thì xem có gom thành skill được không)

9.5 Anti-patterns

KHÔNG bulk-clone repo skill 3rd party — phần lớn doc-dump generic, không có when-to-use, dễ trigger sai.

KHÔNG viết skill chỉ để "có thêm" — skill = trigger semantic match, nếu description quá chung sẽ trigger nhầm trong session khác.

KHÔNG copy nội dung skill từ docs/ sang SKILL.md — skill phải ngắn gọn, action-oriented; docs/ là reference dài. Skill dẫn chiếu docs.

KHÔNG bỏ qua skill khi gặp task khớp — tự suy luận lại = lãng phí context + dễ sai.

10. Liên quan