[CLAUDE] Domain+Infra: 7 ContractType-specific Details + ContractChangelog (migration 9)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m37s

User decision: Option B — bảng riêng cho mỗi loại HĐ (chuẩn nhất, schema
chuyên biệt). Plus: ContractChangelog audit log thống nhất Header /
Detail / Workflow / Comment / Attachment.

## 8 entities mới

### Details (7) — Domain/Contracts/Details/

| Bảng | Loại HĐ | Field đặc trưng |
|---|---|---|
| ThauPhuDetails | 1 (Thầu phụ) | HangMuc, KhoiLuong, DonGia, ThoiGianHoanThanh |
| GiaoKhoanDetails | 2 (Giao khoán) | MaCongViec, KhoiLuong, YeuCauKyThuat |
| NhaCungCapDetails | 3 (NCC) | MaSP, ThongSoKyThuat, SoLuong, ThoiGianGiao, XuatXu |
| DichVuDetails | 4 (Dịch vụ) | MaDichVu, ThoiGian, TuNgay/DenNgay |
| MuaBanDetails | 5 (Mua bán) | MaSP, SoLuong, DonGia, ThueVAT (%), XuatXu |
| NguyenTacNccDetails | 6 (Nguyên tắc NCC) | NhomSP, DonGiaToiThieu/ToiDa, DieuKienGiaoHang |
| NguyenTacDvDetails | 7 (Nguyên tắc DV) | LoaiDichVu, DonGiaToiThieu/ToiDa, PhamViDichVu, SLA |

Common base `ContractDetailBase`: ContractId FK + Order + ThanhTien
decimal(18,2) + GhiChu nvarchar(1000) + audit (BaseEntity).

### ContractChangelog (1) — Domain/Contracts/

Unified audit log. Khác ContractApprovals (workflow-only, dùng cho guard
logic) — Changelog là VIEW LAYER cho user đọc lịch sử thao tác:
- EntityType enum: Contract | Detail | Workflow | Comment | Attachment
- Action enum: Insert | Update | Delete | Transition
- PhaseAtChange snapshot
- UserId + UserName denormalize (log readable)
- Summary human-readable + FieldChangesJson [{Field, Old, New}]
- ContextNote (comment kèm theo)

## EF Configurations

ContractDetailsConfiguration.cs (1 file gộp 7 IEntityTypeConfiguration):
- ToTable + HasMaxLength + HasPrecision per type
- HasOne(Contract).WithMany(<TypeDetails>) cascade delete
- IX (ContractId, Order) cho load timeline

ContractChangelogConfiguration.cs:
- Cascade delete khi Contract xóa
- IX (ContractId, CreatedAt) timeline + IX (ContractId, EntityType) filter

## DbContext + IApplicationDbContext

+ 8 DbSet mới (7 Details + ContractChangelogs).

## Migration 9: AddContractDetailsAndChangelog

3-file rule (gotcha #17): .cs + .Designer.cs + ApplicationDbContextModel
Snapshot.cs đầy đủ. Applied LocalDB SolutionErp_Dev OK — 24 + 8 = 32 bảng
total.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-04-23 10:08:42 +07:00
parent d326e80082
commit 70810e1b34
17 changed files with 3327 additions and 0 deletions

View File

@ -1,4 +1,5 @@
using SolutionErp.Domain.Common;
using SolutionErp.Domain.Contracts.Details;
namespace SolutionErp.Domain.Contracts;
@ -26,4 +27,16 @@ public class Contract : AuditableEntity
public List<ContractApproval> Approvals { get; set; } = new();
public List<ContractComment> Comments { get; set; } = new();
public List<ContractAttachment> Attachments { get; set; } = new();
public List<ContractChangelog> Changelogs { get; set; } = new();
// Per-type details — chỉ 1 collection có data tương ứng với Type. Backend
// logic dispatch theo Contract.Type để load đúng bảng. KHÔNG load eager
// tất cả 7 — overhead. CQRS handler tự chọn DbSet theo Type.
public List<ThauPhuDetail> ThauPhuDetails { get; set; } = new();
public List<GiaoKhoanDetail> GiaoKhoanDetails { get; set; } = new();
public List<NhaCungCapDetail> NhaCungCapDetails { get; set; } = new();
public List<DichVuDetail> DichVuDetails { get; set; } = new();
public List<MuaBanDetail> MuaBanDetails { get; set; } = new();
public List<NguyenTacNccDetail> NguyenTacNccDetails { get; set; } = new();
public List<NguyenTacDvDetail> NguyenTacDvDetails { get; set; } = new();
}

View File

@ -0,0 +1,44 @@
using SolutionErp.Domain.Common;
namespace SolutionErp.Domain.Contracts;
// Audit log thống nhất cho mọi thay đổi liên quan HĐ — Header / Details /
// Workflow transition / Comment / Attachment. Khác `ContractApprovals` (chỉ
// log workflow transitions, dùng cho guard logic) — Changelog là VIEW LAYER
// cho user đọc lịch sử thao tác.
//
// Populate qua MediatR `AuditBehavior` interceptor — auto từ Insert/Update/
// Delete commands trên Contract + Details (xem Application/Common/Behaviors/
// AuditBehavior.cs).
public class ContractChangelog : BaseEntity
{
public Guid ContractId { get; set; }
public Contract? Contract { get; set; }
public ChangelogEntityType EntityType { get; set; } // Contract | Detail | Workflow | Comment | Attachment
public Guid? EntityId { get; set; } // PK của child entity (null nếu là Contract header)
public ChangelogAction Action { get; set; } // Insert | Update | Delete | Transition
public ContractPhase? PhaseAtChange { get; set; } // Snapshot phase tại thời điểm change
public Guid? UserId { get; set; } // Null = system (vd SLA auto-approve)
public string? UserName { get; set; } // Denormalize cho log readable
public string? Summary { get; set; } // Human-readable: "Đổi giá trị 100M → 150M"
public string? FieldChangesJson { get; set; } // JSON [{Field, OldValue, NewValue}] cho update
public string? ContextNote { get; set; } // Comment khi user kèm theo thay đổi
}
public enum ChangelogEntityType
{
Contract = 1,
Detail = 2,
Workflow = 3,
Comment = 4,
Attachment = 5,
}
public enum ChangelogAction
{
Insert = 1,
Update = 2,
Delete = 3,
Transition = 4, // Riêng cho workflow phase change
}

View File

@ -0,0 +1,18 @@
using SolutionErp.Domain.Common;
namespace SolutionErp.Domain.Contracts.Details;
// Base cho 7 ContractType Details. Mỗi loại HĐ có 1 bảng riêng (Option B
// theo user decision) — schema đặc thù từng loại nhưng share common fields:
// ContractId FK + Order + ThanhTien + GhiChu + audit (CreatedAt/By).
//
// Không dùng EF TPH/TPT inheritance (mỗi subclass là 1 bảng độc lập, không
// chung type discriminator) — mỗi subclass tự khai báo entity ToTable riêng.
public abstract class ContractDetailBase : BaseEntity
{
public Guid ContractId { get; set; }
public int Order { get; set; } // Thứ tự dòng trong HĐ (1, 2, 3...)
public decimal ThanhTien { get; set; } // Computed/manual entered
public string? GhiChu { get; set; }
public Contract? Contract { get; set; }
}

View File

@ -0,0 +1,14 @@
namespace SolutionErp.Domain.Contracts.Details;
// Chi tiết HĐ Dịch vụ (Type=4) — gói dịch vụ thuê (vd thuê thiết bị, tư vấn).
public class DichVuDetail : ContractDetailBase
{
public string MaDichVu { get; set; } = "";
public string TenDichVu { get; set; } = "";
public string? MoTa { get; set; }
public string DonViTinh { get; set; } = ""; // Giờ, ngày, tháng, gói
public decimal ThoiGian { get; set; } // Số đơn vị (vd 30 ngày)
public decimal DonGia { get; set; } // Đơn giá / đơn vị
public DateTime? TuNgay { get; set; }
public DateTime? DenNgay { get; set; }
}

View File

@ -0,0 +1,13 @@
namespace SolutionErp.Domain.Contracts.Details;
// Chi tiết HĐ Giao khoán (Type=2) — gói công việc giao khoán cho tổ đội.
public class GiaoKhoanDetail : ContractDetailBase
{
public string MaCongViec { get; set; } = ""; // Mã định danh công việc
public string TenCongViec { get; set; } = "";
public string DonViTinh { get; set; } = "";
public decimal KhoiLuong { get; set; }
public decimal DonGia { get; set; }
public DateTime? ThoiGianHoanThanh { get; set; }
public string? YeuCauKyThuat { get; set; } // Spec yêu cầu
}

View File

@ -0,0 +1,15 @@
namespace SolutionErp.Domain.Contracts.Details;
// Chi tiết HĐ Mua bán (Type=5) — danh mục sản phẩm mua bán + thuế VAT.
public class MuaBanDetail : ContractDetailBase
{
public string MaSP { get; set; } = "";
public string TenSP { get; set; } = "";
public string? MoTa { get; set; }
public string DonViTinh { get; set; } = "";
public decimal SoLuong { get; set; }
public decimal DonGia { get; set; } // Chưa VAT
public decimal ThueVAT { get; set; } // % (vd 10 = 10%)
// ThanhTien (base class) = SoLuong * DonGia * (1 + ThueVAT/100)
public string? XuatXu { get; set; }
}

View File

@ -0,0 +1,13 @@
namespace SolutionErp.Domain.Contracts.Details;
// Chi tiết HĐ Nguyên tắc Dịch vụ (Type=7) — framework agreement cho dịch vụ.
public class NguyenTacDvDetail : ContractDetailBase
{
public string LoaiDichVu { get; set; } = ""; // Phân loại (vd: "Vận chuyển", "Bảo trì")
public string TenDichVu { get; set; } = "";
public string DonViTinh { get; set; } = "";
public decimal DonGiaToiThieu { get; set; }
public decimal DonGiaToiDa { get; set; }
public string? PhamViDichVu { get; set; } // Scope of service
public string? SLA { get; set; } // SLA cam kết (vd "24h response")
}

View File

@ -0,0 +1,15 @@
namespace SolutionErp.Domain.Contracts.Details;
// Chi tiết HĐ Nguyên tắc NCC (Type=6) — framework agreement, không cố định
// số lượng, chỉ liệt kê nhóm SP + price range. ThanhTien base class = 0
// (không cố định), phục vụ tham chiếu khi tạo PO sau này.
public class NguyenTacNccDetail : ContractDetailBase
{
public string NhomSP { get; set; } = ""; // Nhóm sản phẩm (vd: "Vật tư xây dựng")
public string TenSP { get; set; } = "";
public string DonViTinh { get; set; } = "";
public decimal DonGiaToiThieu { get; set; }
public decimal DonGiaToiDa { get; set; }
public string? DieuKienGiaoHang { get; set; }
public string? DieuKienThanhToan { get; set; }
}

View File

@ -0,0 +1,14 @@
namespace SolutionErp.Domain.Contracts.Details;
// Chi tiết HĐ Nhà cung cấp (Type=3) — danh mục sản phẩm mua từ NCC.
public class NhaCungCapDetail : ContractDetailBase
{
public string MaSP { get; set; } = ""; // Mã sản phẩm
public string TenSP { get; set; } = "";
public string? ThongSoKyThuat { get; set; } // Specification
public string DonViTinh { get; set; } = "";
public decimal SoLuong { get; set; }
public decimal DonGia { get; set; }
public DateTime? ThoiGianGiao { get; set; } // Lead time
public string? XuatXu { get; set; } // Origin country
}

View File

@ -0,0 +1,12 @@
namespace SolutionErp.Domain.Contracts.Details;
// Chi tiết HĐ Thầu phụ (Type=1) — line item theo hạng mục công việc thầu phụ.
public class ThauPhuDetail : ContractDetailBase
{
public string HangMuc { get; set; } = ""; // Tên hạng mục công việc
public string DonViTinh { get; set; } = ""; // m2, m3, kg, ngày công, ...
public decimal KhoiLuong { get; set; } // 4 chữ số thập phân (decimal 18,4)
public decimal DonGia { get; set; } // VND
// ThanhTien = KhoiLuong * DonGia (FE/BE compute, không trigger DB)
public DateTime? ThoiGianHoanThanh { get; set; }
}