rules/development-rules.md

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 @Interval khi 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ào feat/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 explicitrefunded ≤ 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 let nế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.lock changes không liên quan đến task

Banned patterns

  • moment.js → dùng date-fns hoặc native Date
  • lodash (full) → dùng native JS hoặc lodash/specific-function
  • axios → 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 .data sau ApiResponse
  • 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 lint passes (zero errors)
  • yarn build passes
  • yarn test passes
  • 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

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.md any 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.

  1. Add a comment block at the deferral site in the doc: <!-- DEFER:<id> origin:YYYY-MM-DD trigger:<unblock-condition> [owner:<name>] -->
  2. Add a code-side comment at the related callsite (if any): // DEFER: <id> — <one-line why> linking back to the doc.
  3. Run yarn --cwd docs defer:write to regenerate docs/progress/DEFERRED.md.
  4. 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.displaySnapshot bả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.