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>
22 KiB
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.
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}")]→MenuPermissionHandlercheck
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.WorkflowDefinitionIdtheo 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:
- Phase 1 (Done) — Domain policy: 3 test file (Contract WF / PE WF / Budget) cover transition rules + Registry + FromDefinition. 54 test, < 1s.
- Phase 2 (Done) — Infra code generator: format RG-001/PE + sequence increment + year boundary (framework HĐ). 17 test, ~2s.
- Phase 3 mini (Done) — Application versioning:
CreatePeWorkflowDefinitionCommandauto-increment + deactivate cũ + EvaluationType independence. 6 test. - 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.
- Phase 4 (Pending) — API smoke: WebApplicationFactory + SQLite, 5-7 endpoint critical roundtrip.
- 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:
-
Manual checkout bypass github.com (gotcha #39):
- Replace
uses: actions/checkout@v4bằng manualgit init+git fetchtừ 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
- Replace
-
Path filter docs-only skip (gotcha #41):
paths-ignore: ['docs/**', '**/*.md', '.claude/skills/**']trongon: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
-
npm junction cache (gotcha #40) — thử ở commit
29eb5d9, failtsc not found:- Hypothesis: Move-Item của
node_moduleschứa nested junctions → .bin/ relative paths broken - Rolled back ở
a21790d, giữ freshnpm installmỗi run (49s + 33s = 82s) - TODO future session: thử
robocopy /MIRhoặc act_runner built-in cache server
- Hypothesis: Move-Item của
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— coding conventionsdatabase/database-guide.md— DB schema chi tiếtflows/— per-feature sequence diagramsworkflow-contract.md— state machine specforms-spec.md— 8 form catalog