# 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, 19 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: 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 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ả **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 - 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