Backend Contracts domain (5 entities):
- Contract aggregate: Phase (9 enum), SlaDeadline, MaHopDong, BypassProcurementAndCCM, DraftData, SlaWarningSent
- ContractApproval: FromPhase → ToPhase, ApproverUserId (null = system auto-approve), Decision, Comment
- ContractComment: thread theo Phase current
- ContractAttachment: FileName + StoragePath + Purpose (DraftExport/ScannedSigned/SealedCopy)
- ContractCodeSequence: Prefix PK + LastSeq — atomic gen
EF configs:
- Unique MaHopDong filtered [MaHopDong] IS NOT NULL
- Indexes: Phase+IsDeleted, SupplierId, ProjectId, SlaDeadline, ContractId+ApprovedAt, ContractId+CreatedAt
- Cascade delete Approvals/Comments/Attachments khi Contract xoa
- Query filter IsDeleted
- Migration AddContractsWorkflow (DB 19 tables)
Workflow service:
- IContractWorkflowService.TransitionAsync:
- Adjacency check qua Transitions Dict<(from,to), roles[]> (12 transitions)
- Role guard: user phai co role ∈ allowed
- Admin bypass (role Admin pass moi check)
- System bypass (userId=null + Decision=AutoApprove → cho SLA job sau nay)
- Bypass CCM: BypassProcurementAndCCM=true cho phep DangInKy → DangTrinhKy skip phase 6
- Gen ma HD khi chuyen DangDongDau (idempotent — khong gen lai neu da co)
- Reset SlaDeadline = UtcNow + PhaseSla
- Insert ContractApproval row
Code generator (RG-001):
- 7 format theo ContractType: HDTP / HDGK / NCC / HDDV / MB + 2 framework (year prefix)
- BeginTransactionAsync(Serializable) + ContractCodeSequences UPSERT → atomic
- Idempotent: neu MaHopDong da co thi skip
CQRS (8 feature, ContractFeatures.cs):
- CreateContractCommand + Validator + Handler (set SlaDeadline = +7d)
- UpdateContractDraftCommand (chi khi Phase=DangSoanThao)
- TransitionContractCommand (delegate → WorkflowService)
- AddCommentCommand (phase = hien tai)
- ListContractsQuery (PagedResult + filter phase/supplier/project/search)
- GetMyInboxQuery (map Phase → actor roles, filter theo role user)
- GetContractQuery (detail + approvals + comments + attachments + resolve user names)
- DeleteContractCommand (soft, block > DangInKy)
Controller:
- ContractsController 8 endpoint: GET list/inbox/detail, POST create/transition/comment, PUT update, DELETE
Frontend fe-admin (2 page moi):
- types/contracts.ts: ContractPhase const + Label + Color maps + types
- components/PhaseBadge.tsx
- pages/contracts/ContractsListPage.tsx: filter phase + search + click → detail
- pages/contracts/ContractDetailPage.tsx: 2-col layout (info+comments | timeline), action dialog select target phase + comment
Frontend fe-user (4 page moi + 14 file shared):
- cp 14 file shared tu fe-admin (menuKeys, types/*, DataTable, PhaseBadge, Dialog, Textarea, Select, apiError, usePermission, PermissionGuard)
- AuthContext update: load menu tu /menus/me + cache
- Layout: menu fixed 3 muc + user info + roles display
- InboxPage: list HD cho role user xu ly (sort theo SLA)
- ContractCreatePage: form chon loai + template + NCC + du an + gia tri + bypass CDT
- ContractDetailPage: duplicate fe-admin pattern (convention)
- MyContractsPage: list HD cua toi
- App.tsx: 4 route moi
E2E verified:
- Setup Supplier + Project
- POST /contracts → 201 + phase=2
- POST /contracts/{id}/transitions x7 → di het 9 phase
- Final: MaHopDong = "FLOCK 01/HĐGK/SOL&PVL2026/01" dung format RG-001
- Approvals: 7 rows audit day du
Docs:
- .claude/skills/contract-workflow/SKILL.md: placeholder → full spec voi state machine, SLA table, role matrix, 7 code format, code pointers, API, E2E workflow, pitfalls
- docs/changelog/sessions/2026-04-21-1330-phase3-workflow.md: session log
- docs/STATUS.md: Phase 3 MVP done, next Phase 4
- docs/HANDOFF.md: update phase status + file tree + commit log + testing points
- docs/changelog/migration-todos.md: tick Phase 3 MVP items + add iteration 2 list
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6.3 KiB
6.3 KiB
Session 2026-04-21 13:30 — Phase 3 Workflow MVP
Dev: Claude (Opus 4.7)
Duration: ~1h15m
Base commit: 5113e4c
Làm được
Chunk G — Domain + EF + Migration
- Domain/Contracts: Contract (aggregate, 15 fields) + ContractApproval (history) + ContractComment (thread) + ContractAttachment (files với
AttachmentPurposeenum) + ContractCodeSequence (Prefix PK + LastSeq) - EF configs với:
- Unique MaHopDong filtered
WHERE [MaHopDong] IS NOT NULL - Indexes: (Phase, IsDeleted), SupplierId, ProjectId, SlaDeadline
- Cascade delete cho Approvals/Comments/Attachments khi Contract xóa
- Query filter IsDeleted
- Unique MaHopDong filtered
- DbSets +
IApplicationDbContextupdate (5 DbSet mới) - Migration
AddContractsWorkflow
Chunk H — Workflow service + CodeGenerator + CQRS + Controller
Services:
IContractWorkflowService.TransitionAsync(contract, targetPhase, userId, roles, decision, comment):- Adjacency check qua
TransitionsDictionary<(from,to), roles[]> - Role check (Admin bypass)
- System actor bypass (cho SLA job Phase 3.2)
- Bypass rule Chủ đầu tư (DangInKy → DangTrinhKy skip CCM)
- Gen mã HĐ khi DangDongDau (nếu chưa có)
- Reset SlaDeadline theo target phase SLA
- Insert ContractApproval row
- Adjacency check qua
IContractCodeGenerator.GenerateAsync():- 7 format theo ContractType (HĐTP/HĐGK/NCC/HĐDV/MB + 2 framework)
BeginTransactionAsync(IsolationLevel.Serializable)+ContractCodeSequencesUPSERT → atomic
- Register scoped trong Infrastructure DI
CQRS (ContractFeatures.cs):
CreateContractCommand+ Validator + Handler (tạo draft, SlaDeadline = UtcNow + 7d)UpdateContractDraftCommand(chỉ khi Phase = DangSoanThao)TransitionContractCommand(delegate → WorkflowService)AddCommentCommand(phase = hiện tại)ListContractsQuery(PagedResult với filter phase/supplier/project + search)GetMyInboxQuery(map Phase → roles, return HĐ phù hợp role user)GetContractQuery(detail + approvals + comments + attachments, resolve user names)DeleteContractCommand(soft, block > DangInKy)
Controller:
ContractsControllervới 8 endpoint: GET list/inbox/detail, POST create/transition/comment, PUT update, DELETE- Body DTOs inline:
TransitionContractBody,AddCommentBody
Chunk I — Frontend 2 app
fe-admin (2 page mới):
types/contracts.ts— ContractPhase/Decision const-object + Label + Color maps + typescomponents/PhaseBadge.tsx— badge màu theo phasepages/contracts/ContractsListPage.tsx— DataTable full filter + click row → detailpages/contracts/ContractDetailPage.tsx— 2-col layout: info + comments + timeline + action dialog (approve/reject với select target phase + comment)
fe-user (4 page mới + port shared):
- Copy 14 file shared từ fe-admin (menuKeys, types/*, DataTable, PhaseBadge, Dialog, Textarea, Select, apiError, usePermission, PermissionGuard)
contexts/AuthContext.tsxupdate — load menu from/menus/me+ localStorage cachecomponents/Layout.tsx— menu fixed 3 mục (Inbox / Tạo mới / HĐ của tôi) + user info + role displaypages/InboxPage.tsx— list/api/contracts/inbox(HĐ chờ role của tôi xử lý, sort theo SLA)pages/contracts/ContractCreatePage.tsx— form chọn loại HĐ + template + NCC + dự án + giá trị + bypass CĐTpages/contracts/ContractDetailPage.tsx— duplicate fe-admin pattern (convention)pages/contracts/MyContractsPage.tsx— HĐ của tôiApp.tsx— 4 route mới
E2E verified
# Setup
POST /api/suppliers { code: "PVL2026", type: 1 } → 201
POST /api/projects { code: "FLOCK 01" } → 201
# Tạo HĐ
POST /api/contracts { type: 2, supplierId, projectId, giaTri: 150000000 }
→ 201 contractId e20083d5
→ phase=2 (DangSoanThao), slaDeadline=+7d
# Inbox
GET /api/contracts/inbox → 1 contract
# Full workflow 9 phase (admin bypass)
POST /transitions {targetPhase: 3} → 204
POST /transitions {targetPhase: 4} → 204
POST /transitions {targetPhase: 5} → 204
POST /transitions {targetPhase: 6} → 204
POST /transitions {targetPhase: 7} → 204
POST /transitions {targetPhase: 8} → 204 ← GEN MÃ HĐ
POST /transitions {targetPhase: 9} → 204
# Final
GET /api/contracts/{id}
→ phase: 9 (DaPhatHanh)
→ maHopDong: "FLOCK 01/HĐGK/SOL&PVL2026/01" ✅ ĐÚNG FORMAT RG-001
→ 7 approvals với đủ from/to/decision/comment
TS check fe-admin + fe-user pass.
Bug gặp + fix
| Bug | Fix |
|---|---|
| Edit tool "File not read" sau system-reminder | Re-read file rồi Write full |
Python f-string backslash trong curl test script |
Bỏ escape trong f-string |
Docs updates
.claude/skills/contract-workflow/SKILL.md— full spec từ placeholder: state machine, role matrix, SLA, RG-001 format 7 loại, code pointers, API, workflow E2E, pitfalls- Session log này (session #4 trong changelog/sessions)
Handoff cho session tiếp theo
Phase 3 iteration 2 (optional — polish)
SlaExpiryJobBackgroundService (hosted service, 15min interval, auto-approve quá hạn vớiDecision=AutoApprove) — spec ởdocs/flows/sla-expiry-flow.md- Email notification (MailKit) khi chuyển phase
- In-app notification (SignalR + badge counter)
- Upload attachment endpoint + FE (multipart, store vào
wwwroot/uploads/contracts/{id}/) - RowVersion optimistic concurrency (2 user cùng duyệt → 409)
- Render HĐ ra .docx lúc tạo (link với TemplateId + DraftData + merge ContractClause appendix)
- E2E test permission matrix: tạo user role Drafter thật → verify chỉ thấy nút approve khi ở đúng phase
Phase 4 — Report + Polish
Xem docs/changelog/migration-todos.md section Phase 4.
Quick wins
- FE Users management (tạo user, gán role) — để test workflow với multi-user
- Filter Inbox theo phase (dropdown FE)
- Export HĐ list ra Excel
Blocker
- ⏳ Gitea remote URL (vẫn chờ)
Thông số cumulative
| Sau Phase 2 | Sau Phase 3 MVP | |
|---|---|---|
| BE LOC | ~1900 | ~2700 |
| DB tables | 14 | 19 (+Contracts, ContractApprovals, Comments, Attachments, CodeSequences) |
| API endpoints | ~23 | ~31 (+8 contract) |
| Migrations | 4 | 5 |
| FE pages | 7 (fe-admin) + 2 (fe-user) | 9 + 5 |
| Commits | 5 | 6 (sắp) |