Development Rules
MANDATORY — Mọi contributor (human & AI) PHẢI tuân thủ. Vi phạm = block PR, không merge.
0. Engineering Mindset (CRITICAL)
0.1 Senior developer stance — mặc định
Mọi contributor (đặc biệt AI agents) PHẢI tự đặt mình vào vai senior developer khi đánh giá yêu cầu và lên kế hoạch:
- Đánh giá trước khi gật đầu — nếu requirement có lỗ hổng, rủi ro, hoặc mâu thuẫn với docs/architecture, PHẢI flag ngay, không implement blind.
- Hỏi tại sao, không hỏi làm thế nào — tìm root cause trước khi đề xuất giải pháp.
- Lock decisions trước khi code — mọi quyết định kiến trúc, data model, security boundary PHẢI được confirm explicit trước Phase 3 (PLAN) kết thúc. Không "sau này tính".
- Challenge khi recommendation sai context — ví dụ: planner đề xuất
@Intervalkhi Redis đã là hard dependency → phải nói "không đúng, dùng BullMQ". Người tốt không nói "yes" với mọi thứ. - Tính đến scale + failure mode — không chỉ happy path. Hỏi: "Nếu crash giữa bước X và Y thì sao? Horizontally scale được không? Race condition ở đâu?"
0.2 Payment-sensitive discipline — ZERO "sau này update"
Payment và tất cả code chạm vào money/credentials/refund/audit-log thuộc high-stakes domain. Áp dụng nguyên tắc sau, không ngoại lệ:
- KHÔNG TODO, KHÔNG
// fix later, KHÔNG placeholder trong code merged vàofeat/payment-*hoặc main. - KHÔNG "MVP shortcut" kiểu: bỏ observability, bỏ retry, bỏ idempotency check, hardcode value "tạm", skip tenant scope "for now". Mọi shortcut = regression waiting to happen.
- KHÔNG defer compliance/security sang phase sau — PCI-DSS boundary, AES-GCM crypto, HMAC verify, tenant isolation, audit log PHẢI đúng ngay từ commit đầu tiên.
- KHÔNG silent error — mọi failure path phải log, có error code stable, có retry hoặc dead-letter queue, không swallow exception.
- KHÔNG "sau này thêm test" — payment code vào repo KHÔNG có test = revert commit.
- Observability = required part of feature — counter/metric/log cho mọi state transition + failure path, không phải "observability epic riêng".
- Financial invariants PHẢI có unit test explicit —
refunded ≤ captured ≤ authorized, currency consistency, amount non-negative, idempotency key uniqueness.
Lý do: Payment bug = tiền thật, khách thật, legal liability thật. "Sau này fix" đồng nghĩa tiền đã mất/double-charged rồi. Cost of fixing in production ≫ cost of doing it right the first time.
Đọc ../architecture/payment-architecture.md section 9 (Security & Compliance) và ../flows/payment-flow.md (Error Codes + Flow 15 retry) trước khi code bất cứ thứ gì trong core/payment/.
0.3 Checklist trước khi bắt đầu Phase 3 (PLAN)
- Đã đọc hết docs liên quan (architecture + flow + api-design)?
- Đã list explicit mọi decision cần lock? (nếu >0 decision còn ambiguous → không bắt đầu code)
- Đã identify failure modes + mitigation cho từng cái?
- Có part nào đang nghĩ "để sau" không? Nếu có → move vào scope HOẶC document tại sao defer OK (phải có lý do kỹ thuật, không phải "làm cho nhanh")
- Task có chạm payment/auth/credentials/money? Nếu có → áp dụng §0.2 NO SHORTCUTS, re-read §9 payment-architecture.md
1. Git Discipline
KHÔNG được
- KHÔNG commit rác: file thừa, console.log, commented-out code, TODO không có issue
- KHÔNG commit trực tiếp lên
main— luôn qua PR - KHÔNG force push lên shared branches
- KHÔNG commit
.env, secrets, hoặc credentials - KHÔNG commit generated files:
dist/,node_modules/,generated/,openapi.json - KHÔNG amend commits đã push
Commit rules
- Mỗi commit PHẢI atomic — 1 commit = 1 thay đổi logic
- Commit message PHẢI follow conventional commits:
feat:,fix:,refactor:,test:,chore:,docs: - Message tiếng Anh, ngắn gọn, mô tả "what & why" không phải "how"
- Pre-commit hook sẽ tự chạy lint + format — nếu fail thì FIX, không skip
Branch naming
feat/US-{number}-{short-description}
fix/{short-description}
refactor/{short-description}
test/{short-description}
chore/{short-description}
2. Testing (CRITICAL)
Rule: Mọi API endpoint PHẢI có test TRƯỚC khi merge
| Loại | Scope | Coverage target |
|---|---|---|
| Unit test | Service methods, utils, guards | 80%+ |
| Integration test | Controller + Service + DB (real Prisma) | Mọi endpoint |
| E2E test | Critical user flows | Happy path + error cases |
Test file convention
src/core/booking/booking.service.ts → booking.service.spec.ts
src/core/booking/booking.controller.ts → booking.controller.spec.ts
test/booking.e2e-spec.ts → E2E tests
Mỗi endpoint PHẢI test
- Happy path: input hợp lệ → response đúng format (envelope)
- Validation: input sai → 400 + error details
- Not found: ID không tồn tại → 404
- Conflict: business rule violation → 409/422
- Auth: unauthorized → 401, forbidden → 403
- Tenant isolation: không thấy data tenant khác
- Pagination: page/limit params, meta response
Test TRƯỚC, code SAU (TDD)
1. Viết test → RED (fail)
2. Viết implementation → GREEN (pass)
3. Refactor → CLEAN
4. Verify coverage ≥ 80%
KHÔNG được
- KHÔNG skip tests khi thêm/sửa endpoint
- KHÔNG mock database trong integration tests — dùng real test DB
- KHÔNG viết test chỉ test happy path — phải cover error cases
- KHÔNG commit code với test fail
3. Code Quality
Zero tolerance
| Issue | Action |
|---|---|
console.log trong production code |
REMOVE — dùng NestJS Logger |
| Commented-out code | REMOVE — git có history |
| Unused imports | REMOVE — ESLint sẽ catch |
| Unused variables | REMOVE — prefix _ nếu intentional (callback params) |
| Dead functions/classes | REMOVE — không giữ "just in case" |
| Magic numbers | EXTRACT thành named constant |
| Hardcoded strings (URLs, config) | MOVE vào env hoặc config |
any type |
AVOID — chỉ dùng khi Prisma type mismatch, phải kèm eslint-disable comment giải thích lý do |
| Duplicate code | EXTRACT thành shared util nếu ≥ 3 lần |
File size
- Max 400 lines cho service/controller files
- Max 200 lines cho DTO/util files
- Max 800 lines absolute — nếu vượt thì PHẢI split
Function size
- Max 50 lines per function
- Nếu dài hơn → extract thành private methods
- Tên function phải self-documenting — KHÔNG cần comment giải thích "what"
Naming
| Element | Convention | Example |
|---|---|---|
| File | kebab-case | booking.service.ts |
| Class | PascalCase | BookingService |
| Method/function | camelCase | findAllByTenant |
| Variable | camelCase | tenantId |
| Constant | UPPER_SNAKE_CASE | MAX_PAGE_LIMIT |
| Enum value | UPPER_SNAKE_CASE | BookingStatus.CONFIRMED |
| DB column | snake_case (via @map) | tenant_id |
| API URL | kebab-case | /service-categories |
4. ESLint & Prettier (Auto-enforced)
ESLint rules (strict)
- No unused vars — error, không phải warning
- No explicit any — warn (allow khi có eslint-disable comment)
- No floating promises — error, phải await hoặc void
- No console — error trong src/, allow trong test/
- Consistent return — mọi branch phải return hoặc không
- Prefer const — không dùng
letnếu không reassign - No var — chỉ dùng const/let
Prettier rules
- Single quotes
- Trailing commas (all)
- Print width: 100
- Tab width: 2
- Semicolons: always
Auto-fix
- Pre-commit hook (Husky + lint-staged): auto format + lint trước mỗi commit
- Nếu lint fail → commit bị block → FIX rồi commit lại
- KHÔNG dùng
--no-verifyđể bypass hooks
5. Dependency Management
Rules
- Yarn only — không npm/npx
- KHÔNG add dependency mà không có lý do rõ ràng
- Prefer NestJS built-in modules trước khi thêm 3rd party
- Pin major versions trong package.json (
^cho minor, không*) - KHÔNG commit
yarn.lockchanges không liên quan đến task
Banned patterns
moment.js→ dùngdate-fnshoặc native Datelodash(full) → dùng native JS hoặclodash/specific-functionaxios→ dùng NestJS HttpModule (built-in)
6. Frontend API Consumption
Response Envelope
Mọi API response đều wrapped trong envelope {success, data, error, meta}.
api.get<T>() trả ApiResponse<T> — client PHẢI unwrap bằng .data:
// Paginated list — API trả {success, data: Resource[], meta: {total, page, limit}}
const { data } = useQuery({
queryKey: ['staff'],
queryFn: () => api.get<Resource[]>('/resources?limit=100'),
});
const staff = data?.data ?? []; // Resource[]
const meta = data?.meta; // {total, page, limit}
// Single item
const { data } = useQuery({
queryKey: ['tenant', id],
queryFn: () => api.get<Tenant>(`/tenants/${id}`),
select: (res) => res.data ?? null,
});
KHÔNG BAO GIỜ
- KHÔNG
res.data?.data?.data— chỉ cần 1 level.datasauApiResponse - KHÔNG tự define response wrapper type (vd
{ data: T[]; meta: unknown }) —ApiResponse<T>đã xử lý - KHÔNG dùng type khác cho cùng endpoint ở các component khác nhau
Pattern chuẩn cho hooks
// List hook
export function useStaffList() {
return useQuery({
queryKey: ['staff'],
queryFn: () => api.get<Resource[]>('/resources?limit=100'),
select: (res) => res.data ?? [],
});
}
// Mutation hook
export function useCreateStaff(opts: { successMessage: string; onSuccess?: () => void }) {
const qc = useQueryClient();
return useFormMutation({
mutationFn: (data: Record<string, unknown>) => api.post<Resource>('/resources', data),
successMessage: opts.successMessage,
onSuccess: () => {
void qc.invalidateQueries({ queryKey: ['staff'] });
opts.onSuccess?.();
},
});
}
7. API Implementation Checklist
Trước khi tạo PR cho bất kỳ endpoint nào:
Code
- Follow
docs/architecture/api-design.md— response envelope, error codes, etc. - DTO với class-validator + Swagger decorators
- Tenant-scoped (filter by tenantId)
- Auth guard + Role guard
- Service layer xử lý business logic (controller chỉ delegate)
Tests
- Unit tests cho service methods
- Integration tests cho controller endpoints
- Happy path + error cases + edge cases
- Tenant isolation test
- Coverage ≥ 80%
Quality
-
yarn lintpasses (zero errors) -
yarn buildpasses -
yarn testpasses - No
console.log - No commented-out code
- No unused imports/variables
- No magic numbers
- File size within limits
7. PR Rules
PR size
- Max 400 lines changed (excluding generated files, tests)
- Nếu lớn hơn → split thành multiple PRs
- Exception: initial scaffold, migration files
PR description PHẢI có
- Summary (what & why)
- Link to user story (US-X.X)
- Test plan
- Screenshots (nếu UI changes)
Review checklist
- Code follows development rules
- Tests adequate
- No security issues
- No performance concerns
- API design compliant
Merge policy
- Require ≥ 1 approval
- All checks pass (lint, build, test)
- No merge with unresolved comments
- Squash merge to keep history clean
7a. Docs maintenance (MANDATORY)
Docs are part of the feature, not an after-thought. Every shipped change MUST update the relevant docs in the same commit/PR.
Always update on every ship
docs/progress/changelog.md— chronological entry (| YYYY-MM-DD | <feature> · <what shipped> · <test counts>). Test totals in the table at the top must matchyarn jestoutput.docs/progress/features.md— flip the[ ]to[x]for the feature you shipped.- The flow doc closest to the change (e.g.
docs/flows/status-matrix/05-payment-driven.mdfor payment events). Update checklists, real-world tables, code refs. docs/progress/gaps-and-plan.md— flip the gap from[ ]to[x]and add an**Implementation**block.
Add when behaviour changes shape
- Mermaid diagram in the flow doc when you add a new state, branch,
or listener. Renderer is loaded by the docs site (
scripts/template.html) and GitHub renders mermaid natively. ASCII art is only kept as fallback. - Troubleshooting row in
docs/operations/troubleshooting.mdany time you ship a fix to a class of bug that wasn't already covered. Symptom → flow doc → code path → common causes.
Tag every defer with DEFER:<id>
When you ship with a known gap (anything you'd describe as "deferred", "follow-up", or "not in this iteration"), tag it.
- Add a comment block at the deferral site in the doc:
<!-- DEFER:<id> origin:YYYY-MM-DD trigger:<unblock-condition> [owner:<name>] --> - Add a code-side comment at the related callsite (if any):
// DEFER: <id> — <one-line why>linking back to the doc. - Run
yarn --cwd docs defer:writeto regeneratedocs/progress/DEFERRED.md. - The full convention lives in
defer-convention.md. Read it once.
Pre-commit gate
Before the commit goes in, run:
yarn --cwd docs defer:check # fails if DEFERRED.md is stale
yarn --cwd docs build # fails if any *.md is malformed
The pre-commit hook should run both. If you skip the hook, CI will run the same check on the PR — there's no path that ships without consistent docs.
When to write a new troubleshooting row
- Any fix that took >30 min to root-cause.
- Any 422/403 response code the user/customer hit "in the wild".
- Any race / outbox / projection bug you traced via logs.
The threshold is intentionally low: even a one-line row saves the next person from re-running the investigation.
8. Date & Time — Timezone Rules (CRITICAL)
Tách thành doc riêng vì rule chi tiết: xem
timezone-rules.md.Tóm tắt nguyên tắc:
- Mọi thời gian user nhìn thấy = giờ SALON (Europe/Oslo)
- Mọi thời gian lưu DB = UTC
- Browser timezone KHÔNG BAO GIỜ được dùng
- Display snapshot trên
booking.metadata.displaySnapshotbảo vệ historic bookings khỏi tenant đổi timezone về sau
Vi phạm timezone rules = data corruption. Đọc full rules tại timezone-rules.md trước khi viết bất kỳ code động chạm thời gian.