# Contract Creation Flow > **Status:** 📝 Planned (Phase 2) > **Actors:** Drafter (QS/NV.PB) > **Entry:** User mở `/contracts/new` ở fe-user ## 1. Tổng quan Drafter chọn loại HĐ → chọn template form → điền field động → preview → lưu draft. Sau đó có thể tiếp tục soạn hoặc submit lên phase `DangGopY` (xem [`contract-approval-flow.md`](contract-approval-flow.md)). ## 2. Sequence ```mermaid sequenceDiagram actor D as Drafter participant FE as fe-user
/contracts/new participant API as ContractsController participant CMD as CreateContractCommandHandler participant TMPL as FormsController
.GetTemplate participant REN as IFormRenderer participant DB participant FS as File Storage
(wwwroot/uploads) D->>FE: Click "Tạo HĐ mới" D->>FE: Step 1 — Chọn loại HĐ
(ContractType dropdown) FE->>API: GET /api/forms/templates?type={contractType} API->>DB: SELECT ContractTemplates WHERE Type = ? AND IsActive = 1 API-->>FE: Template list
[{id, formCode, name, fieldSpec}] D->>FE: Step 2 — Chọn template (vd FO-002.05 Giao khoán) FE->>TMPL: GET /api/forms/templates/{id} TMPL->>DB: SELECT template + FieldSpec JSON TMPL-->>FE: fieldSpec (array of {name, type, required, ...}) FE->>FE: Dynamic form builder render form
theo fieldSpec D->>FE: Step 3 — Điền field (NCC, dự án,
giá trị, hạng mục, nghiệm thu…) D->>FE: Click "Preview" FE->>API: POST /api/forms/render
{templateId, data} API->>REN: DocxRenderer.Render(template, data) REN->>REN: Load .docx template
replace placeholder {{field}}
with values REN->>FS: Write temp file uploads/preview/{guid}.docx REN-->>API: (bytes + temp path) API-->>FE: 200 file blob (inline hoặc Content-Disposition: inline) FE->>FE: Display PDF preview
(convert server-side via LibreOffice) D->>FE: Step 4 — Click "Lưu draft" FE->>API: POST /api/contracts
{type, supplierId, projectId,
templateId, draftData} API->>CMD: Send(CreateContractCommand) CMD->>CMD: Validate (FluentValidation)
supplier exists, project active... CMD->>DB: INSERT Contracts (Phase=DangSoanThao,
DraftData=JSON.serialize) CMD-->>API: ContractId API-->>FE: 201 Created + Location header FE->>D: Redirect /contracts/{id}
toast "Đã lưu draft" ``` ## 3. API chi tiết ### `GET /api/forms/templates?type=HopDongGiaoKhoan` Response: ```json [ { "id": "a0b1-...", "formCode": "FO-002.05", "name": "Hợp đồng Giao khoán", "contractType": 2 } ] ``` ### `GET /api/forms/templates/{id}` Response: ```json { "id": "a0b1-...", "formCode": "FO-002.05", "name": "Hợp đồng Giao khoán", "fieldSpec": { "sections": [ { "title": "Thông tin hai bên", "fields": [ { "name": "benA_tenCongTy", "label": "Tên Bên A", "type": "string", "required": true }, { "name": "benA_diaChi", "label": "Địa chỉ Bên A", "type": "text" }, { "name": "benA_maSoThue", "label": "MST Bên A", "type": "string", "pattern": "^[0-9]{10,13}$" }, { "name": "benB_supplierId", "label": "NCC (Bên B)", "type": "reference", "table": "Suppliers" } ] }, { "title": "Giá trị hợp đồng", "fields": [ { "name": "giaTri", "label": "Giá trị (VND)", "type": "decimal", "required": true, "min": 0 }, { "name": "ngayKy", "label": "Ngày ký dự kiến", "type": "date" } ] } ] } } ``` ### `POST /api/forms/render` Request: ```json { "templateId": "a0b1-...", "data": { "benA_tenCongTy": "Công ty TNHH Xây dựng Solutions", "benA_diaChi": "123 Đường ABC, TP.HCM", "benB_supplierId": "d9e2-...", "giaTri": 150000000, "ngayKy": "2026-05-10" } } ``` Response: binary stream `.docx` (or `.pdf` nếu convert). ### `POST /api/contracts` Request: ```json { "type": 2, "supplierId": "d9e2-...", "projectId": "p7a1-...", "departmentId": "dp3b-...", "templateId": "a0b1-...", "giaTri": 150000000, "draftData": { /* full field values */ } } ``` Response: ```json { "id": "c5d6-...", "phase": "DangSoanThao", "createdAt": "2026-04-21T10:00:00Z" } ``` ## 4. Validation rules (FluentValidation) ```csharp public class CreateContractCommandValidator : AbstractValidator { public CreateContractCommandValidator(IApplicationDbContext db) { RuleFor(x => x.Type).IsInEnum(); RuleFor(x => x.SupplierId).NotEmpty().MustAsync(SupplierExists); RuleFor(x => x.ProjectId).NotEmpty().MustAsync(ProjectActive); RuleFor(x => x.TemplateId).NotEmpty().MustAsync(TemplateActive); RuleFor(x => x.GiaTri).GreaterThan(0); RuleFor(x => x.DraftData).NotNull(); // Template field validation — đọc FieldSpec + validate draftData match RuleFor(x => x).CustomAsync(async (cmd, ctx, ct) => { var tpl = await db.ContractTemplates.FindAsync(cmd.TemplateId, ct); var spec = JsonSerializer.Deserialize(tpl.FieldSpec); // check required fields present in draftData }); } } ``` ## 5. Side effects | Action | Side effect | |---|---| | `POST /contracts` | INSERT `Contracts` (Phase=`DangSoanThao`) | | | AuditInterceptor: set `CreatedAt`, `CreatedBy` = Drafter.Id | | | (Phase 3) Emit `ContractCreatedEvent` domain event | | `POST /forms/render` preview | Write temp file `wwwroot/uploads/preview/{guid}.docx` (auto cleanup sau 1h) | | `POST /forms/render` với `persist=true` | Save final file vào `ContractAttachments` với `Purpose='Export'` | ## 6. UI wireframe (fe-user) ``` /contracts/new ┌────────────────────────────────────────────────────┐ │ Tạo hợp đồng mới │ ├────────────────────────────────────────────────────┤ │ Bước 1/4: Chọn loại hợp đồng │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ Trọn gói │ │ Giao │ │ Mua bán │ ... │ │ │ NC+VT │ │ khoán │ │ │ │ │ └──────────┘ └──────────┘ └──────────┘ │ ├────────────────────────────────────────────────────┤ │ Bước 2/4: Chọn template │ │ ○ FO-002.02 Trọn gói nhân công + vật tư │ │ ● FO-002.05 Giao khoán ← đã chọn │ ├────────────────────────────────────────────────────┤ │ Bước 3/4: Điền thông tin │ │ [Thông tin 2 bên] │ │ Tên Bên A: [Công ty TNHH ...] │ │ NCC (Bên B): [Autocomplete → chọn NCC] │ │ [Giá trị HĐ] │ │ Giá trị: [150,000,000 VND] │ │ ... │ ├────────────────────────────────────────────────────┤ │ Bước 4/4: Preview + Lưu │ │ [PDF preview inline] │ │ [← Quay lại] [💾 Lưu draft] [🚀 Submit góp ý] │ └────────────────────────────────────────────────────┘ ``` ## 7. Edge cases | Case | Xử lý | |---|---| | NCC chưa có trong DB | FE modal "Tạo NCC mới" inline → callback back vào form | | Template bị deactivate khi đang soạn | Warning + cho phép tiếp tục với template cũ | | Draft bị trùng (user nhấn Save 2 lần) | Idempotency key header — return existing contract | | User mất kết nối khi preview | FE retry + show spinner; nếu fail 3 lần → disable button + show cached data | | Giá trị HĐ vượt ngân sách dự án | Warning (không block) — sẽ check lại ở phase CCM Review | | Template thiếu field bắt buộc | FormValidator block Save + highlight field đỏ | ## 8. Performance - Template load 1 lần + cache TanStack Query `{queryKey: ['template', id], staleTime: 10min}` - Preview render: có thể tốn 1-3s cho .docx lớn → show spinner + allow cancel - Save draft < 500ms (chỉ INSERT 1 row) ## 9. Liên quan - [`form-render-flow.md`](form-render-flow.md) — chi tiết render engine - [`contract-approval-flow.md`](contract-approval-flow.md) — sau Save, phase tiếp theo - [`../forms-spec.md`](../forms-spec.md) — spec 8 form