Combined audit (skill staleness + doc drift) theo §6.4 + §9.4. Cron 2026-05-01 trễ 4 ngày (cron empty), chạy manual 2026-05-04 sau Session 9 close Chunk E-bis. Drift patched (Phase 1 — count cross-check): - docs/CLAUDE.md:70 52 bảng → 55 bảng (+§14 DepartmentApprovals Mig 16) - docs/rules.md:368 Phase 8 active — 77 test → Phase 9 — 83 test - docs/architecture.md:329, 365 77 test → 83 test (2 chỗ) - .claude/skills/ef-core-migration/SKILL.md:52 77 test → 83 test + ghi 6 PE 2-stage S9 - .claude/skills/dependency-audit-erp/SKILL.md:153 26+ bẫy → 41 bẫy Skill content patch (Phase 2 — staleness): - contract-workflow: thêm "Phase 9 cross-ref (Mig 16)" block + section "Phase 9 done" (2-stage dept approval + smart reject + lock edit + CanBypassReview) → KHÔNG tạo skill 2-stage riêng (§9.5 anti-pattern "viết skill chỉ để có thêm") KEEP per §6.5 (không cắt narrative): - docs/rules.md:328 "52 bảng" example minh họa rule §6.5 - docs/changelog/migration-todos.md:152, 196 historical session record - 9 session log mention 77/52 — snapshot lịch sử Total patches: 6 file ~+30 / ~12 lines, KHÔNG rewrite, đáp ứng 3 câu validation §6.5. Audit log: docs/changelog/skill-audit-2026-05.md (1 page max per cadence). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
401 lines
22 KiB
Markdown
401 lines
22 KiB
Markdown
# 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 HĐ.
|
||
|
||
```
|
||
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 vì 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 lý 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 và 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: 83 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 HĐ). **17 test**, ~2s.
|
||
3. **Phase 3 mini (Done)** — Application versioning: `CreatePeWorkflowDefinitionCommand` auto-increment + deactivate cũ + 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 có 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ả **83 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
|
||
- Lý 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
|