Files
solution-erp/docs/architecture.md
pqhuy1987 b874743081
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m21s
[CLAUDE] Docs+Tests: chốt final session 5 — 77 test (Phase 3 mini PE WF) + 3 gotcha CI + 8 doc updates
Final close session 5 — bao gồm:

==== Tests Phase 3 mini (NEW) ====
tests/SolutionErp.Infrastructure.Tests/Application/PeWorkflowAdminTests.cs
- 6 test CreatePeWorkflowDefinitionCommandHandler:
  - First version → IsActive=true, Version=1, ActivatedAt set
  - Second version same Code → auto-increment v2 + deactivate v1 (atomic)
  - Different EvaluationType (A vs B) → independent active state
  - Persists steps ordered by Order field
  - Persists approvers per step
  - Third version → v1 + v2 deactivate, v3 active

Total tests: 71 → 77 pass / ~2s (54 Domain + 23 Infra).
Skip Phase 3 full (UpsertOpinion + Budget link validation) — cần
Identity UserManager DI helper, defer session sau.

==== 3 gotcha CI mới (#39 #40 #41) ====
- #39 act_runner github.com TCP timeout 21s → manual checkout fix (run #108/#109 fail, #110 pass)
- #40 npm junction cache `tsc not found` after Move-Item — rolled back, hypothesis nested junctions
  trong node_modules disrupt .bin/ paths. TODO debug session sau với robocopy hoặc act_runner cache.host
- #41 Gitea Actions paths-ignore behavior — workflow file change vẫn trigger (correct), commit
  MD-only skip 100% (verify 512880c → no run #113)
+ Checklist debug bug mới items 18-20 referencing 3 gotcha trên.

==== Doc updates (8 file) ====
- STATUS.md: header Phase 8 update + 3 row Recently Done CI fixes + cumulative test 71→77
- HANDOFF.md: TL;DR + CI optimize section + Phase status + gotcha count 38→41
- migration-todos.md: Phase 8 §E updated với Phase 3 mini done + CI fixes
- rules.md §7 Testing: rewrite full — stack + test pyramid + quy tắc bổ sung mỗi feature +
  workflow user end-of-task (`dotnet test` local trước push) + CI gate behavior
- architecture.md §11: update test pyramid + phased priority + CI optimization sub-section
  (3 fix manual checkout / path filter / npm cache rollback) + tốc độ deploy table
- gotchas.md: + #39 #40 #41 đầy đủ (triệu chứng + nguyên nhân + fix + reference)
- ef-core-migration SKILL: Phase 8 update note thêm CI fixes + 77 test
- CLAUDE.md root: test count 71→77 + folder structure + CI/CD pipeline 3 fix section
- memory project_solution_erp.md: session 5 summary + workflow user mới
- session log 2026-04-29-2300-chot-final-ci-tests-gotchas.md (NEW — 9 section detail)

==== Skill audit cron ====
`solution-erp-skill-audit-monthly` next fire 2026-05-01 (2 ngày sau).
Cron survives across sessions (setup commit b904a25). Khi fire sẽ:
- Cross-check 6 skill với STATUS/gotchas/migration-todos
- Auto-refresh stale + đề xuất add/archive cho human approve
- Log vào docs/changelog/skill-audit-2026-05.md
- ABORT nếu repo dirty

==== Verify ====
- dotnet test SolutionErp.slnx → 77 pass / ~2s (54 Domain + 23 Infra)
- git status clean sau commit này
- CI: commit này chứa code (test + workflow) → trigger CI test gate

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 23:43:42 +07:00

22 KiB
Raw Blame History

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.

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"

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 Debug chuyển phase, 403, mã HĐ, bypass CĐT
form-engine Render template, upload, placeholder không replace
permission-matrix 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.

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.

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.

- 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