Files
solution-erp/docs/architecture.md
pqhuy1987 d206e14550 [CLAUDE] Docs+Skill: chốt session 8 ending — patch count drift + skill refresh
Session 8 ending workflow theo template (kết thúc lúc ~13:00).

Patch drift count post Migration 16:
- docs/architecture.md §1: "(dbo schema, 19 bảng)" → 55 bảng
  (drift cũ session 6 chưa fix, giờ fix luôn)
- docs/database/schema-diagram.md §8 Migration history: thêm row 16
  AddTwoStageDeptApprovalAndSmartReject + Tổng 52 → 55 bảng
- .claude/skills/ef-core-migration/SKILL.md: 15→16 migration history,
  52→55 tables, description + code pointers + related links
- .claude/skills/README.md: ef-core-migration row "13 migration" → 16,
  gotchas count "32 bẫy" → 41 (drift cũ chưa fix)

Memory entry mới (cross-session pattern):
- feedback_per_chunk_commit.md: rule per-chunk commit cho big-feature
  (>500 LOC, multi-layer). Bài học S8 — 5 commit A-B-C-D-E thay vì
  1 monolithic. MEMORY.md index +1 entry (6 total).

Skip (intentional, theo §6.5 KHÔNG cố sửa):
- docs/gotchas.md: KHÔNG có gotcha mới đáng note (S8 dùng pattern
  đã proven, không có bug pitfall mới)
- docs/database/database-guide.md: KHÔNG có migration table chi tiết,
  chỉ conventions. Skip.
- contract-workflow + permission-matrix skills: KHÔNG touch (PE module
  only, contract policy chưa đổi, permission không liên quan bypass-review)

Verify:
- 77 unit test vẫn pass (54 Domain + 23 Infra)
- Build pass

Total session 8 cumulative: 6 commit (5 code A-B-C-D-E1 + 1 docs ending).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 13:02:22 +07:00

401 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Architecture — SOLUTION_ERP
> Kiến trúc tổng thể + trách nhiệm từng layer + diagram luồng dữ liệu.
## 1. Layered overview
```
┌──────────────────────────────────────────────────────────────┐
│ CLIENT TIER │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ fe-admin :8082 │ │ fe-user :8080 │ │
│ │ React 19 + Vite │ │ React 19 + Vite │ │
│ │ Tailwind 4 │ │ Tailwind 4 │ │
│ │ TanStack Query │ │ TanStack Query │ │
│ └────────┬─────────┘ └────────┬─────────┘ │
└───────────┼──────────────────────────┼───────────────────────┘
│ Vite dev proxy /api │
│ IIS URL Rewrite prod │
▼ ▼
┌──────────────────────────────────────────────────────────────┐
│ API LAYER (:5443) │
│ SolutionErp.Api — ASP.NET Core 10 Web API │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Controllers: Auth, Menus, Roles, Permissions, │ │
│ │ Suppliers, Projects, Departments, │ │
│ │ Forms, Contracts, Reports │ │
│ │ Middleware: GlobalException, Serilog, CORS, JWT │ │
│ │ Authorization: MenuPermissionHandler (policy-based) │ │
│ │ Services: CurrentUserService, WebHostEnvLocator │ │
│ │ wwwroot/templates/ (5 .docx/.xlsx) │ │
│ └────────────────────┬───────────────────────────────────┘ │
└───────────────────────┼──────────────────────────────────────┘
│ MediatR ISender.Send(cmd)
┌──────────────────────────────────────────────────────────────┐
│ APPLICATION LAYER │
│ SolutionErp.Application │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ Auth │ │ Master │ │ Permissions │ │
│ │ (Login/Me) │ │ (CRUD 3 │ │ (Menu tree + │ │
│ │ │ │ entity) │ │ matrix) │ │
│ └──────────────┘ └──────────────┘ └──────────────────┘ │
│ ┌──────────────┐ ┌──────────────────┐ │
│ │ Forms │ │ Contracts │ ┌──────────────┐ │
│ │ (Render │ │ (Workflow 9 │ │ Reports │ │
│ │ engine) │ │ phase, Inbox) │ │ (Dashboard + │ │
│ └──────────────┘ └──────────────────┘ │ Excel exp) │ │
│ └──────────────┘ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Common: Exceptions, Behaviors (ValidationPipeline), │ │
│ │ Interfaces (IDbContext, ICurrentUser, ...), │ │
│ │ Models (PagedResult) │ │
│ └──────────────────────────────────────────────────────┘ │
└───────┬──────────────────────────────────────────┬───────────┘
│ depends on interface │ runs on
▼ ▼
┌──────────────────────┐ ┌───────────────────────────┐
│ DOMAIN LAYER │ │ INFRASTRUCTURE LAYER │
│ SolutionErp.Domain │ │ SolutionErp.Infrastructure│
│ │ │ │
│ Common/BaseEntity │ │ Persistence/ApplicationD │
│ Contracts/ │ │ bContext + Migrations │
│ Forms/ │ │ Identity/JwtTokenService │
│ Identity/ │ │ Forms/Docx+XlsxRenderer │
│ Master/ │ │ Services/ContractWorkflow│
│ │ │ + ContractCodeGenerator│
│ Enum + value object │ │ Reports/ExcelExporter │
└──────────────────────┘ │ Services/DateTimeService │
↑ └────────────┬──────────────┘
│ references │
└───────────────────────────────────────┘
┌───────────────────────┐
│ DATA TIER │
│ SQL Server 2022 │
│ (dbo schema, 55 bảng)│
└───────────────────────┘
```
## 2. Request lifecycle (1 POST/api/contracts)
```
1. Browser → POST /api/contracts { type, supplierId, ... }
2. Vite → proxy tới :5443
3. JwtBearerMiddleware → validate token, set ClaimsPrincipal
4. Routing → ContractsController.Create(cmd)
5. MediatR.Send(CreateContractCommand)
6. ValidationBehavior (pipeline) → FluentValidation run
7. CreateContractCommandHandler
├─ check Supplier/Project exists (IApplicationDbContext)
├─ new Contract entity + set DrafterUserId (ICurrentUser)
├─ set SlaDeadline (IDateTime + IContractWorkflowService.GetPhaseSla)
├─ db.Contracts.Add(...)
└─ SaveChangesAsync
├─ AuditingInterceptor sets CreatedAt/CreatedBy
└─ EF Core INSERT → SQL Server
8. Return Guid id
9. Controller → 201 Created + Location header
10. Axios interceptor (FE) → TanStack Query invalidate + UI update
```
## 3. Workflow state machine (Phase 3)
Xem full ở [`workflow-contract.md`](workflow-contract.md).
```
DangChon → DangSoanThao → DangGopY → DangDamPhan → DangInKy →
→ DangKiemTraCCM → DangTrinhKy → DangDongDau → DaPhatHanh
Alternates: → TuChoi (từ DangSoanThao)
→ DangSoanThao (revise từ bất kỳ phase duyệt)
Bypass CĐT (BypassProcurementAndCCM=true):
DangInKy → DangTrinhKy (skip CCM)
```
**Code generator trigger:** khi `targetPhase = DangDongDau` + `MaHopDong IS NULL` → gen format RG-001 với transaction SERIALIZABLE.
## 4. Permission model
```
User ──(AspNetUserRoles)── Role ──(Permissions)── MenuItem
├── CanRead
├── CanCreate
├── CanUpdate
└── CanDelete
```
**Resolution:**
- Login → JWT chứa claims (sub, email, roles)
- `/api/menus/me` → query Permissions theo roleIds, **union OR** CRUD flags, filter tree theo CanRead
- FE cache menu trong `AuthContext` + localStorage
- Mỗi API action: `[Authorize(Policy = "{MenuKey}.{Action}")]``MenuPermissionHandler` check
**Admin bypass:** role `Admin` luôn pass mọi policy (seed default full CRUD).
## 5. Data flow — "tạo HĐ và chạy hết workflow"
```mermaid
sequenceDiagram
actor D as Drafter
actor M as Manager (PD/PM)
actor C as CCM
actor B as BOD
actor H as HRA
participant FE as fe-user
participant API
participant WF as WorkflowService
participant CG as CodeGenerator
participant DB
D->>FE: POST /contracts/new
FE->>API: POST /api/contracts
API->>DB: INSERT Contracts (Phase=DangSoanThao, SLA=+7d)
API-->>FE: 201 {id}
D->>FE: Click "Submit → góp ý"
FE->>API: POST /contracts/{id}/transitions {target:3}
API->>WF: Transition(contract, 3, roles=[Drafter])
WF->>WF: Check adjacency + role
WF->>DB: INSERT ContractApproval + UPDATE Contract Phase=3
API-->>FE: 204
M->>FE: Inbox → click HĐ → góp ý + "Chuyển tiếp"
FE->>API: POST /transitions {target:4}
API->>WF: Transition → Phase 4
Note over WF: Chạy tương tự qua 5,6,7
B->>FE: Duyệt → target:8 DangDongDau
FE->>API: POST /transitions {target:8}
API->>WF: Transition
WF->>CG: GenerateAsync(contract, project, supplier)
CG->>DB: BEGIN TRAN SERIALIZABLE
CG->>DB: SELECT/UPDATE ContractCodeSequences
CG->>DB: COMMIT
CG-->>WF: "FLOCK 01/HĐGK/SOL&PVL/03"
WF->>DB: UPDATE Contract SET MaHopDong, Phase=8
API-->>FE: 204
H->>FE: Click đóng dấu → target:9
FE->>API: POST /transitions {target:9}
API->>WF: Transition (role HrAdmin)
WF->>DB: UPDATE Phase=9 (DaPhatHanh)
API-->>FE: 204
```
## 6. Deployment architecture (Phase 5 — planned)
```
┌─────────────────────────────┐
│ Internet / Corp LAN │
└──────────────┬──────────────┘
┌──────────▼──────────┐
│ IIS (Win Server) │
│ URL Rewrite / ARR │
└──────────┬──────────┘
┌─────────────┼─────────────┐
│ │ │
┌──────▼──────┐ ┌───▼────┐ ┌──────▼──────┐
│ fe-admin │ │ Api │ │ fe-user │
│ static files│ │ Kestrel│ │ static files│
│ (dist/) │ │ :5443 │ │ (dist/) │
└─────────────┘ └───┬────┘ └─────────────┘
┌────────▼────────┐
│ SQL Server │
│ (same host OR │
│ separate VM) │
└─────────────────┘
```
- IIS app pool riêng cho Api, Integrated Managed Pipeline, .NET CLR disabled (hosting .NET 10 OOP)
- Static files 2 FE deploy vào `C:\inetpub\wwwroot\solution-erp-admin\` + `...user\`
- HTTPS: Let's Encrypt qua win-acme (hoặc cert mua)
- Backup SQL: daily full + 15min log → D:\Backups
## 7. Skill library (AI agent support)
`.claude/skills/` có 3 skill chuyên biệt:
| Skill | Dùng khi |
|---|---|
| [`contract-workflow`](../.claude/skills/contract-workflow/SKILL.md) | Debug chuyển phase, 403, mã HĐ, bypass CĐT |
| [`form-engine`](../.claude/skills/form-engine/SKILL.md) | Render template, upload, placeholder không replace |
| [`permission-matrix`](../.claude/skills/permission-matrix/SKILL.md) | Access denied, menu không hiện, gán role |
Claude auto-invoke theo description matching.
## 8. Non-functional
| Aspect | Current | Phase 5 target |
|---|---|---|
| Availability | dev-only | 99.5% (IIS restart, SQL HA optional) |
| Latency | <200ms P95 local | <500ms P95 prod |
| Concurrency | unrestricted | rate limit 100 req/min/IP |
| Observability | Serilog console | + file rolling daily + Seq/ELK |
| Security | JWT + HTTPS dev | + rate limit + audit log + CSP |
## 9. PurchaseEvaluation (Phase 6 — tiền-HĐ)
Module mới song song Contract phiếu trình duyệt so sánh giá N NCC × M hạng mục, duyệt xong kế thừa làm .
```
PurchaseEvaluation (Header) ─< PurchaseEvaluationSupplier (N:M × Supplier)
─< PurchaseEvaluationDetail (hạng mục) ─< PurchaseEvaluationQuote (báo giá N×M)
─< PurchaseEvaluationApproval (workflow history)
─< PurchaseEvaluationChangelog (audit)
─< PurchaseEvaluationAttachment (file)
─> PurchaseEvaluationWorkflowDefinition (PINNED at create)
─> Contract? (nullable FK — set khi gen HĐ từ phiếu DaDuyet)
```
**Workflow** (tách riêng Phase enum khác ContractPhase):
- A `DuyetNcc` 3 step: Drafter Procurement CostControl Director DaDuyet
- B `DuyetNccPhuongAn` 5 step: + ProjectManager sau Procurement, + Director duyệt PA trước duyệt NCC
**Kế thừa HĐ** (`CreateContractFromEvaluationCommand`):
- Guard: phase = DaDuyet, SelectedSupplierId != null, ContractId = null
- User pick ContractType (1-7) gen Contract draft với SupplierId/ProjectId/GiaTri kế thừa
- Pin `Contract.WorkflowDefinitionId` theo ContractType chọn
- Link 2 chiều: `PurchaseEvaluation.ContractId = contract.Id`
Chi tiết: [`database/schema-diagram.md §11`](database/schema-diagram.md).
## 10. Budget (Phase 7 — Module Ngân sách)
Module quản ngân sách dự án: header + chi tiết hạng mục + workflow simple 3-step + audit log. Liên kết nullable cả Contract PurchaseEvaluation để đối chiếu chi phí.
```
Budget (Header) ─< BudgetDetail (flat row hạng mục)
─< BudgetApproval (workflow history)
─< BudgetChangelog (audit log)
─> Project (FK Restrict)
─> Department? (FK Restrict)
─> User Drafter (FK Restrict)
Contract.BudgetId? ──────────► Budget (link đối chiếu chi phí HĐ)
PurchaseEvaluation.BudgetId? ─► Budget (link đối chiếu chi phí tiền-HĐ)
```
**Phase enum** (`BudgetPhase` 5-state):
```
DangSoanThao(1) ──Trình──► ChoCCM(2) ──Duyệt──► ChoCEO(3) ──Duyệt──► DaDuyet(4)
▲ │ │
└──Reject(99)───────────┴─────────────────────┘
```
**Workflow simple hardcoded** (`BudgetPolicy.Default`):
- Drafter / DeptManager: DangSoanThao ChoCCM (Trình) hoặc TuChoi (Hủy)
- CostControl (CCM): ChoCCM ChoCEO (Duyệt) hoặc DangSoanThao (Trả về)
- Director / AuthorizedSigner: ChoCEO DaDuyet (Duyệt) hoặc DangSoanThao (Trả về)
**Mã ngân sách** `NS-{YYYYMM}-{Random:4d}` hiện Random.Shared, sẽ chuyển atomic SERIALIZABLE khi format chốt chính thức (mirror Contract/PE pattern).
**Auto-recompute** `TongNganSach`:
- Sau Add/Update/Delete BudgetDetail handler tự sum `Sum(d.ThanhTien)` lại Header.
- Tránh state drift, đơn giản hơn trigger DB.
**Integration roadmap**:
- PE form select Budget (filter `Phase=DaDuyet && NamNganSach=current && ProjectId match`)
- Contract form tương tự
- Tab Hạng mục PE/HD compute "So với ngân sách" (match GroupCode + ItemCode)
Chi tiết: [`database/schema-diagram.md §12`](database/schema-diagram.md).
## 11. Testing strategy (Phase 8 — Session 5)
Test pyramid bottom-heavy, **không E2E** (brittle, maintenance cao cho solo dev).
```
┌──────────────────────────────────┐ ❌ Skip
│ E2E / UI (Playwright) │ Brittle, slow, tốn time maintain
├──────────────────────────────────┤ ⚠️ TODO Phase 4
│ API smoke (WebApplicationFactory)│ 1-2 happy path roundtrip per critical endpoint
├──────────────────────────────────┤ 🟡 Mini done — 6 test (29/04)
│ Application Handler (CQRS) │ PE Workflow Definition versioning. Phase 3 full
│ │ (Opinion + Budget link) cần UserManager DI helper
├──────────────────────────────────┤ ✅ Done Phase 2 — 17 test
│ Infrastructure (SQLite in-mem) │ Code generator format + sequence + year boundary
├──────────────────────────────────┤ ✅ Done Phase 1 — 54 test
│ Domain Policy (pure functions) │ State machine + role transition + Registry mapping
└──────────────────────────────────┘
Total: 77 test pass / ~3s
```
**Stack:**
| Layer | Framework | Note |
|---|---|---|
| .NET test | xUnit 2.9.3 | Industry standard |
| Assertion | FluentAssertions 7.2 | Pin trước v8 commercial license |
| EF testing | Microsoft.EntityFrameworkCore.Sqlite 10 | Phase 2+ supports transactions |
| Test fixture | Custom `SqliteDbFixture` + `TestApplicationDbContext` | Override `nvarchar(max) → TEXT` cho SQLite compat |
**CI gate:** `.gitea/workflows/deploy.yml` chạy `dotnet test` cho từng test project TRƯỚC build/publish/deploy. Test fail `exit $LASTEXITCODE` no deploy.
```yaml
- name: Run unit tests (Domain)
run: dotnet test tests/SolutionErp.Domain.Tests/...
- name: Run integration tests (Infrastructure)
run: dotnet test tests/SolutionErp.Infrastructure.Tests/...
```
**Phase priority — anti-overkill:**
1. **Phase 1 (Done)** Domain policy: 3 test file (Contract WF / PE WF / Budget) cover transition rules + Registry + FromDefinition. **54 test**, < 1s.
2. **Phase 2 (Done)** Infra code generator: format RG-001/PE + sequence increment + year boundary (framework ). **17 test**, ~2s.
3. **Phase 3 mini (Done)** Application versioning: `CreatePeWorkflowDefinitionCommand` auto-increment + deactivate + EvaluationType independence. **6 test**.
4. **Phase 3 full (Pending)** Application handlers cần Identity (UpsertOpinion, Budget link validation in CreatePE/CreateContract). Cần build UserManager DI helper trong test fixture.
5. **Phase 4 (Pending)** API smoke: WebApplicationFactory + SQLite, 5-7 endpoint critical roundtrip.
6. **Phase 5 (Pending)** FE Vitest cho lib utility (queryMatches, fmtMoney) chỉ pure functions, KHÔNG component snapshot.
**Quy tắc bổ sung mỗi feature mới:**
- Domain policy method 2-3 test (positive + negative + edge)
- CQRS handler guard 1 test mỗi guard branch
- Bug found in production 1 regression test added before merge
Detail run: `dotnet test SolutionErp.slnx` chạy cả **77 test ~3s**.
### CI/CD optimization (29/04)
3 fix lớn cho Gitea Actions sau khi gặp incident #108/#109:
1. **Manual checkout bypass github.com (gotcha #39)**:
- Replace `uses: actions/checkout@v4` bằng manual `git init` + `git fetch` từ Gitea internal
- do: act_runner mỗi run đều fetch action source từ github.com TCP timeout 21s liên tục toàn job fail trước test gate
- Token `${{ github.token }}` Gitea tự cấp per-job, fetch by ref + depth=30
2. **Path filter docs-only skip (gotcha #41)**:
- `paths-ignore: ['docs/**', '**/*.md', '.claude/skills/**']` trong `on:push`
- Commit chỉ touch docs/MD/skill **Gitea NO trigger workflow** (saving 100% time, ~196s/commit)
- Workflow file `.gitea/workflows/**` KHÔNG ignore vẫn trigger khi sửa CI config
3. **npm junction cache (gotcha #40)** thử commit `29eb5d9`, fail `tsc not found`:
- Hypothesis: Move-Item của `node_modules` chứa nested junctions .bin/ relative paths broken
- Rolled back `a21790d`, giữ fresh `npm install` mỗi run (49s + 33s = 82s)
- TODO future session: thử `robocopy /MIR` hoặc act_runner built-in cache server
**Tốc độ deploy hiện tại** (run #110/#112):
| Trigger | CI behavior | Total time |
|---|---|---:|
| Code commit | Build + test gate + deploy | ~3 phút |
| Docs-only commit | **Skip CI** (path filter) | **0s** |
| Test fail | Skip build/deploy | ~30-40s |
## 12. Liên quan
- [`rules.md`](rules.md) coding conventions
- [`database/database-guide.md`](database/database-guide.md) DB schema chi tiết
- [`flows/`](flows/) per-feature sequence diagrams
- [`workflow-contract.md`](workflow-contract.md) state machine spec
- [`forms-spec.md`](forms-spec.md) 8 form catalog