Files
solution-erp/docs/changelog/sessions/2026-04-21-1330-phase3-workflow.md
pqhuy1987 7e957a7654 [CLAUDE] Phase3: Workflow MVP — 9-phase state machine + code gen + FE Inbox/Detail
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>
2026-04-21 12:26:09 +07:00

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 AttachmentPurpose enum) + 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
  • DbSets + IApplicationDbContext update (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 Transitions Dictionary<(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
  • IContractCodeGenerator.GenerateAsync():
    • 7 format theo ContractType (HĐTP/HĐGK/NCC/HĐDV/MB + 2 framework)
    • BeginTransactionAsync(IsolationLevel.Serializable) + ContractCodeSequences UPSERT → 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:

  • ContractsController vớ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 + types
  • components/PhaseBadge.tsx — badge màu theo phase
  • pages/contracts/ContractsListPage.tsx — DataTable full filter + click row → detail
  • pages/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.tsx update — load menu from /menus/me + localStorage cache
  • components/Layout.tsx — menu fixed 3 mục (Inbox / Tạo mới / HĐ của tôi) + user info + role display
  • pages/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ĐT
  • pages/contracts/ContractDetailPage.tsx — duplicate fe-admin pattern (convention)
  • pages/contracts/MyContractsPage.tsx — HĐ của tôi
  • App.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)

  • SlaExpiryJob BackgroundService (hosted service, 15min interval, auto-approve quá hạn với Decision=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)