diff --git a/fe-admin/src/components/pe/PeDetailTabs.tsx b/fe-admin/src/components/pe/PeDetailTabs.tsx index 931025f..5534add 100644 --- a/fe-admin/src/components/pe/PeDetailTabs.tsx +++ b/fe-admin/src/components/pe/PeDetailTabs.tsx @@ -1347,8 +1347,13 @@ function PeBudgetSummaryTable({ ev, readOnly }: { ev: PeDetailBundle; readOnly: return (
-
- Tổng hợp ngân sách trình ký +
+ Tổng hợp ngân sách trình ký + {bs.budgetFrozen && ( + + 🔒 Ngân sách chốt tại thời điểm duyệt + + )}
{/* [S62] Cảnh báo MỀM "vượt ngân sách" (anh Kiệt FDC) — KHÔNG chặn lưu, chỉ báo. diff --git a/fe-admin/src/types/purchaseEvaluation.ts b/fe-admin/src/types/purchaseEvaluation.ts index df11085..ab4a2ec 100644 --- a/fe-admin/src/types/purchaseEvaluation.ts +++ b/fe-admin/src/types/purchaseEvaluation.ts @@ -328,6 +328,7 @@ export type PeBudgetSummary = { previousSelectedTotal: number // SUM quote ThanhTien NCC trúng (IsWinner) WHERE Phase=DaDuyet previousSelectedCount: number currentProposalTotal: number // SUM ThanhTien quotes NCC trúng (IsWinner) phiếu NÀY (0 khi chưa chọn) + budgetFrozen: boolean // [S133] true = phiếu DaDuyet, số NS đã chốt snapshot } // Mirror BE PeDepartmentKind enum diff --git a/fe-user/src/components/pe/PeDetailTabs.tsx b/fe-user/src/components/pe/PeDetailTabs.tsx index 931025f..5534add 100644 --- a/fe-user/src/components/pe/PeDetailTabs.tsx +++ b/fe-user/src/components/pe/PeDetailTabs.tsx @@ -1347,8 +1347,13 @@ function PeBudgetSummaryTable({ ev, readOnly }: { ev: PeDetailBundle; readOnly: return (
-
- Tổng hợp ngân sách trình ký +
+ Tổng hợp ngân sách trình ký + {bs.budgetFrozen && ( + + 🔒 Ngân sách chốt tại thời điểm duyệt + + )}
{/* [S62] Cảnh báo MỀM "vượt ngân sách" (anh Kiệt FDC) — KHÔNG chặn lưu, chỉ báo. diff --git a/fe-user/src/types/purchaseEvaluation.ts b/fe-user/src/types/purchaseEvaluation.ts index df46f91..2bf7736 100644 --- a/fe-user/src/types/purchaseEvaluation.ts +++ b/fe-user/src/types/purchaseEvaluation.ts @@ -330,6 +330,7 @@ export type PeBudgetSummary = { previousSelectedTotal: number // SUM quote ThanhTien NCC trúng (IsWinner) WHERE Phase=DaDuyet previousSelectedCount: number currentProposalTotal: number // SUM ThanhTien quotes NCC trúng (IsWinner) phiếu NÀY (0 khi chưa chọn) + budgetFrozen: boolean // [S133] true = phiếu DaDuyet, số NS đã chốt snapshot } // Mirror BE PeDepartmentKind enum diff --git a/src/Backend/SolutionErp.Application/PurchaseEvaluations/Dtos/PurchaseEvaluationDtos.cs b/src/Backend/SolutionErp.Application/PurchaseEvaluations/Dtos/PurchaseEvaluationDtos.cs index 54e64ea..7272075 100644 --- a/src/Backend/SolutionErp.Application/PurchaseEvaluations/Dtos/PurchaseEvaluationDtos.cs +++ b/src/Backend/SolutionErp.Application/PurchaseEvaluations/Dtos/PurchaseEvaluationDtos.cs @@ -346,4 +346,8 @@ public record PeBudgetSummaryDto( decimal CurrentProposalTotal, // [S76] PRO column split — ban hành + hiệu chỉnh riêng cho PRO (mirror CCM Initial/Adjustment). decimal? ProInitialAmount, - decimal? ProAdjustmentAmount); + decimal? ProAdjustmentAmount, + // [S133] true = phiếu DaDuyet serve từ snapshot ngân sách (bất biến, đọc cột ApprovedBudget* + // thay record PeWorkItemBudgets live); false = phiếu chưa duyệt đọc LIVE. FE badge "đã chốt" + + // ẩn nút sửa. Append CUỐI record (default false) → construction site cũ không gãy. + bool BudgetFrozen = false); diff --git a/src/Backend/SolutionErp.Application/PurchaseEvaluations/PeBudgetAccumulator.cs b/src/Backend/SolutionErp.Application/PurchaseEvaluations/PeBudgetAccumulator.cs new file mode 100644 index 0000000..b78d807 --- /dev/null +++ b/src/Backend/SolutionErp.Application/PurchaseEvaluations/PeBudgetAccumulator.cs @@ -0,0 +1,72 @@ +using Microsoft.EntityFrameworkCore; +using SolutionErp.Application.Common.Interfaces; +using SolutionErp.Domain.PurchaseEvaluations; + +namespace SolutionErp.Application.PurchaseEvaluations; + +// [S133 pe-budget-freeze] Lũy kế ngân sách "phiếu TRƯỚC cùng cặp (ProjectId, WorkItemId)" +// + "giá trị kỳ này" (own quotes IsSelected). Extract BEHAVIOR-PRESERVING từ +// GetPurchaseEvaluationQuery :917-951 để DÙNG CHUNG cho: +// (1) live display (GetPurchaseEvaluationQuery nhánh chưa-duyệt — giữ nguyên số), +// (2) snapshot khi finalize (ApplyBudgetSnapshotOnFinalizeAsync — chốt 4 số lũy kế). +// +// Semantics GIỮ NGUYÊN (single source of truth = query handler cũ): +// peers = PurchaseEvaluations cùng (ProjectId, WorkItemId), Id != this, CreatedAt < this +// (HasQueryFilter !IsDeleted tự loại phiếu xoá mềm). +// - PrevSubmitted* : peers Phase ∈ (ChoDuyet, DaDuyet) → Count + SUM(BudgetPeriodAmount ?? 0). +// - PrevSelected* : peers Phase = DaDuyet AND có ≥1 Supplier IsWinner (existence); +// total = SUM ThanhTien quote IsSelected của các peer đó. +// - CurrentProposalTotal : SUM ThanhTien quote IsSelected của CHÍNH phiếu này (own supplier rows). +public readonly record struct PeBudgetAccumulation( + decimal PrevSubmittedTotal, + int PrevSubmittedCount, + decimal PrevSelectedTotal, + int PrevSelectedCount, + decimal CurrentProposalTotal); + +public static class PeBudgetAccumulator +{ + public static async Task ComputeAsync( + IApplicationDbContext db, + Guid projectId, + Guid workItemId, + Guid peId, + DateTime peCreatedAt, + IReadOnlyList currentSupplierRowIds, + CancellationToken ct) + { + var peers = db.PurchaseEvaluations.AsNoTracking() + .Where(p => p.ProjectId == projectId && p.WorkItemId == workItemId + && p.Id != peId && p.CreatedAt < peCreatedAt); + + var submitted = await peers + .Where(p => p.Phase == PurchaseEvaluationPhase.ChoDuyet + || p.Phase == PurchaseEvaluationPhase.DaDuyet) + .Select(p => p.BudgetPeriodAmount) + .ToListAsync(ct); + var prevSubmittedCount = submitted.Count; + var prevSubmittedTotal = submitted.Sum(v => v ?? 0m); + + var selectedPeers = peers.Where(p => p.Phase == PurchaseEvaluationPhase.DaDuyet + && p.Suppliers.Any(s => s.IsWinner)); + var prevSelectedCount = await selectedPeers.CountAsync(ct); + var prevSelectedTotal = await ( + from p in selectedPeers + join s in db.PurchaseEvaluationSuppliers.AsNoTracking() + on p.Id equals s.PurchaseEvaluationId + join q in db.PurchaseEvaluationQuotes.AsNoTracking() + on s.Id equals q.PurchaseEvaluationSupplierId + where q.IsSelected + select (decimal?)q.ThanhTien).SumAsync(ct) ?? 0m; + + var currentProposalTotal = currentSupplierRowIds.Count == 0 ? 0m + : await db.PurchaseEvaluationQuotes.AsNoTracking() + .Where(q => currentSupplierRowIds.Contains(q.PurchaseEvaluationSupplierId) && q.IsSelected) + .SumAsync(q => (decimal?)q.ThanhTien, ct) ?? 0m; + + return new PeBudgetAccumulation( + prevSubmittedTotal, prevSubmittedCount, + prevSelectedTotal, prevSelectedCount, + currentProposalTotal); + } +} diff --git a/src/Backend/SolutionErp.Application/PurchaseEvaluations/PeWorkItemBudgetFeatures.cs b/src/Backend/SolutionErp.Application/PurchaseEvaluations/PeWorkItemBudgetFeatures.cs index 85524ae..01641da 100644 --- a/src/Backend/SolutionErp.Application/PurchaseEvaluations/PeWorkItemBudgetFeatures.cs +++ b/src/Backend/SolutionErp.Application/PurchaseEvaluations/PeWorkItemBudgetFeatures.cs @@ -230,6 +230,12 @@ public class SetPeCcmBudgetPeriodCommandHandler( var pe = await db.PurchaseEvaluations.FirstOrDefaultAsync(x => x.Id == request.Id, ct) ?? throw new NotFoundException("PurchaseEvaluation", request.Id); + // [S133] Phiếu KẾT THÚC (DaDuyet / TuChoi) = ngân sách BẤT BIẾN — chặn CẢ Admin (ngay sau + // load + NotFound, TRƯỚC role-gate). Số NS đã chốt snapshot tại thời điểm duyệt. + if (pe.Phase is PurchaseEvaluationPhase.DaDuyet or PurchaseEvaluationPhase.TuChoi) + throw new ConflictException( + "Phiếu đã kết thúc — ngân sách đã chốt tại thời điểm duyệt, không sửa được nữa."); + // Fail-closed TRƯỚC mọi side-effect. if (!currentUser.Roles.Contains(AppRoles.Admin) && !currentUser.Roles.Contains(AppRoles.CostControl)) diff --git a/src/Backend/SolutionErp.Application/PurchaseEvaluations/PurchaseEvaluationFeatures.cs b/src/Backend/SolutionErp.Application/PurchaseEvaluations/PurchaseEvaluationFeatures.cs index 16d3943..3830f5f 100644 --- a/src/Backend/SolutionErp.Application/PurchaseEvaluations/PurchaseEvaluationFeatures.cs +++ b/src/Backend/SolutionErp.Application/PurchaseEvaluations/PurchaseEvaluationFeatures.cs @@ -360,6 +360,12 @@ public class AdjustPurchaseEvaluationBudgetCommandHandler( var entity = await db.PurchaseEvaluations.FirstOrDefaultAsync(x => x.Id == request.Id, ct) ?? throw new NotFoundException("PurchaseEvaluation", request.Id); + // [S133] Phiếu KẾT THÚC (DaDuyet / TuChoi) = ngân sách BẤT BIẾN — chặn CẢ Admin (đặt TRƯỚC + // toàn khối phân-quyền, áp mọi role). Số NS đã chốt snapshot tại thời điểm duyệt. + if (entity.Phase is PurchaseEvaluationPhase.DaDuyet or PurchaseEvaluationPhase.TuChoi) + throw new ConflictException( + "Phiếu đã kết thúc — ngân sách đã chốt tại thời điểm duyệt, không sửa được nữa."); + var isAdmin = currentUser.Roles.Contains(AppRoles.Admin); var isDrafter = currentUser.UserId is Guid uid && entity.DrafterUserId == uid; var actorTag = string.Empty; @@ -908,68 +914,75 @@ public class GetPurchaseEvaluationQueryHandler( var canEditPro = isAdmin || currentUser.Roles.Contains(AppRoles.Procurement); var canEditCcm = isAdmin || currentUser.Roles.Contains(AppRoles.CostControl); + // pairRec load NGUYÊN cho MỌI nhánh — nhánh frozen chỉ dùng .Id (FE link không gãy), + // KHÔNG dùng số của nó (số đọc từ cột snapshot ApprovedBudget*). var pairRec = await db.PeWorkItemBudgets.AsNoTracking() .FirstOrDefaultAsync(b => b.ProjectId == e.ProjectId && b.WorkItemId == wiKey, ct); - // Lũy kế phiếu TRƯỚC cùng cặp. Row 1 = đã trình (ChoDuyet + DaDuyet); - // Row 2 = đã chọn thầu (DaDuyet + có đơn vị thắng). TraLai/DangSoanThao - // KHÔNG tính (quay về soạn = chưa trình). - var peers = db.PurchaseEvaluations.AsNoTracking() - .Where(p => p.ProjectId == e.ProjectId && p.WorkItemId == wiKey - && p.Id != e.Id && p.CreatedAt < e.CreatedAt); - - var submitted = await peers - .Where(p => p.Phase == PurchaseEvaluationPhase.ChoDuyet - || p.Phase == PurchaseEvaluationPhase.DaDuyet) - .Select(p => p.BudgetPeriodAmount) - .ToListAsync(ct); - var prevSubmittedCount = submitted.Count; - var prevSubmittedTotal = submitted.Sum(v => v ?? 0m); - - // "đã chọn thầu" = phiếu DaDuyet có ÍT NHẤT 1 đơn vị IsWinner (existence — IsWinner - // là DERIVED == Any quote IsSelected, giữ đúng ngữ nghĩa "có chọn thầu"; A3(ii)). - var selectedPeers = peers.Where(p => p.Phase == PurchaseEvaluationPhase.DaDuyet - && p.Suppliers.Any(s => s.IsWinner)); - var prevSelectedCount = await selectedPeers.CountAsync(ct); - // [multi-NCC A3(i)[5]] Tổng "đã chọn thầu" = SUM ThanhTien quote ĐƯỢC CHỌN (Quote.IsSelected - // = nguồn-sự-thật per Detail×Supplier) của các phiếu DaDuyet trước — thay whole-supplier IsWinner. - var prevSelectedTotal = await ( - from p in selectedPeers - join s in db.PurchaseEvaluationSuppliers.AsNoTracking() - on p.Id equals s.PurchaseEvaluationId - join q in db.PurchaseEvaluationQuotes.AsNoTracking() - on s.Id equals q.PurchaseEvaluationSupplierId - where q.IsSelected - select (decimal?)q.ThanhTien).SumAsync(ct) ?? 0m; - - // Row 4 "Giá trị kỳ này" = SUM báo giá ĐƯỢC CHỌN (Quote.IsSelected) phiếu này - // [multi-NCC A3(i)[5]] (khớp winnerQuoteTotal :1189 — nguồn-sự-thật per Detail×Supplier). var curSupplierRowIds = e.Suppliers.Select(s => s.Id).ToList(); - var currentProposalTotal = curSupplierRowIds.Count == 0 ? 0m - : await db.PurchaseEvaluationQuotes.AsNoTracking() - .Where(q => curSupplierRowIds.Contains(q.PurchaseEvaluationSupplierId) && q.IsSelected) - .SumAsync(q => (decimal?)q.ThanhTien, ct) ?? 0m; - // [S76] Full mỗi cột = Initial + Adjustment (cột đó). Authoritative full cho - // Block B công thức = CCM nếu CCM đã nhập, else PRO (FullIsEstimate=true → FE - // badge "ngân sách PRO"). PRO full = ProInitial + ProAdjust (migrate từ - // ProEstimate cũ qua Mig 56). Cả 2 trống → full 0, không badge. - var hasCcm = pairRec?.InitialAmount is not null || pairRec?.AdjustmentAmount is not null; - var hasPro = pairRec?.ProInitialAmount is not null || pairRec?.ProAdjustmentAmount is not null; - var proFull = (pairRec?.ProInitialAmount ?? 0m) + (pairRec?.ProAdjustmentAmount ?? 0m); - var fullAmount = hasCcm - ? (pairRec!.InitialAmount ?? 0m) + (pairRec.AdjustmentAmount ?? 0m) - : proFull; + // [S133] Display-gate: phiếu DaDuyet ĐÃ chốt snapshot → serve BẤT BIẾN từ 11 cột + // ApprovedBudget* (KHÔNG chạy theo record PeWorkItemBudgets live — record dùng chung + // vẫn sửa được cho phiếu chưa duyệt). Fallback: DaDuyet mà SnapshotAt null → rơi xuống + // nhánh live như cũ — defense-in-depth; mọi đường tạo DaDuyet đã set SnapshotAt + // (4 site finalize + admin-override site-5 + seeder demo + backfill Mig 67 — review S133). + var frozen = e.Phase == PurchaseEvaluationPhase.DaDuyet && e.ApprovedBudgetSnapshotAt != null; + if (frozen) + { + // Row 4 "Giá trị kỳ này" — SPEC S133: VẪN tính LIVE từ own quotes (KHÔNG snapshot). + var currentProposalTotal = curSupplierRowIds.Count == 0 ? 0m + : await db.PurchaseEvaluationQuotes.AsNoTracking() + .Where(q => curSupplierRowIds.Contains(q.PurchaseEvaluationSupplierId) && q.IsSelected) + .SumAsync(q => (decimal?)q.ThanhTien, ct) ?? 0m; - peBudgetSummary = new PeBudgetSummaryDto( - pairRec?.Id, pairRec?.ProEstimateAmount, pairRec?.ProNote, - pairRec?.InitialAmount, pairRec?.AdjustmentAmount, pairRec?.CcmNote, - fullAmount, !hasCcm && hasPro, - canEditPro, canEditCcm, - prevSubmittedTotal, prevSubmittedCount, - prevSelectedTotal, prevSelectedCount, - currentProposalTotal, - pairRec?.ProInitialAmount, pairRec?.ProAdjustmentAmount); + // Full mỗi cột từ SNAPSHOT (mirror công thức :957-962). Ccm* = cột CCM đã chốt. + var sHasCcm = e.ApprovedBudgetCcmInitialAmount is not null || e.ApprovedBudgetCcmAdjustmentAmount is not null; + var sHasPro = e.ApprovedBudgetProInitialAmount is not null || e.ApprovedBudgetProAdjustmentAmount is not null; + var sProFull = (e.ApprovedBudgetProInitialAmount ?? 0m) + (e.ApprovedBudgetProAdjustmentAmount ?? 0m); + var sFullAmount = sHasCcm + ? (e.ApprovedBudgetCcmInitialAmount ?? 0m) + (e.ApprovedBudgetCcmAdjustmentAmount ?? 0m) + : sProFull; + + peBudgetSummary = new PeBudgetSummaryDto( + pairRec?.Id, null, e.ApprovedBudgetProNote, + e.ApprovedBudgetCcmInitialAmount, e.ApprovedBudgetCcmAdjustmentAmount, e.ApprovedBudgetCcmNote, + sFullAmount, !sHasCcm && sHasPro, + false, false, // phiếu đã chốt → KHÔNG cho sửa PRO/CCM + e.ApprovedBudgetPrevSubmittedTotal ?? 0m, e.ApprovedBudgetPrevSubmittedCount ?? 0, + e.ApprovedBudgetPrevSelectedTotal ?? 0m, e.ApprovedBudgetPrevSelectedCount ?? 0, + currentProposalTotal, + e.ApprovedBudgetProInitialAmount, e.ApprovedBudgetProAdjustmentAmount, + BudgetFrozen: true); + } + else + { + // Lũy kế phiếu TRƯỚC cùng cặp + "giá trị kỳ này" (behavior-preserving qua accumulator — + // extract nguyên semantics :917-951). Row 1 = đã trình (ChoDuyet + DaDuyet); Row 2 = + // đã chọn thầu (DaDuyet + có đơn vị IsWinner). TraLai/DangSoanThao KHÔNG tính. + var acc = await PeBudgetAccumulator.ComputeAsync( + db, e.ProjectId, wiKey, e.Id, e.CreatedAt, curSupplierRowIds, ct); + + // [S76] Full mỗi cột = Initial + Adjustment (cột đó). Authoritative full cho Block B = + // CCM nếu đã nhập, else PRO (FullIsEstimate=true → FE badge "ngân sách PRO"). PRO full = + // ProInitial + ProAdjust (migrate từ ProEstimate cũ qua Mig 56). Cả 2 trống → full 0. + var hasCcm = pairRec?.InitialAmount is not null || pairRec?.AdjustmentAmount is not null; + var hasPro = pairRec?.ProInitialAmount is not null || pairRec?.ProAdjustmentAmount is not null; + var proFull = (pairRec?.ProInitialAmount ?? 0m) + (pairRec?.ProAdjustmentAmount ?? 0m); + var fullAmount = hasCcm + ? (pairRec!.InitialAmount ?? 0m) + (pairRec.AdjustmentAmount ?? 0m) + : proFull; + + peBudgetSummary = new PeBudgetSummaryDto( + pairRec?.Id, pairRec?.ProEstimateAmount, pairRec?.ProNote, + pairRec?.InitialAmount, pairRec?.AdjustmentAmount, pairRec?.CcmNote, + fullAmount, !hasCcm && hasPro, + canEditPro, canEditCcm, + acc.PrevSubmittedTotal, acc.PrevSubmittedCount, + acc.PrevSelectedTotal, acc.PrevSelectedCount, + acc.CurrentProposalTotal, + pairRec?.ProInitialAmount, pairRec?.ProAdjustmentAmount, + BudgetFrozen: false); + } } // Load supplier names for PE suppliers + approver names diff --git a/src/Backend/SolutionErp.Domain/PurchaseEvaluations/PurchaseEvaluation.cs b/src/Backend/SolutionErp.Domain/PurchaseEvaluations/PurchaseEvaluation.cs index 6760ab7..7cc57dd 100644 --- a/src/Backend/SolutionErp.Domain/PurchaseEvaluations/PurchaseEvaluation.cs +++ b/src/Backend/SolutionErp.Domain/PurchaseEvaluations/PurchaseEvaluation.cs @@ -111,6 +111,26 @@ public class PurchaseEvaluation : AuditableEntity // cho phiếu DaDuyet để phiếu-đã-lên-CEO KHÔNG còn hiện sai "không qua CEO" (bug anh Kiệt S97). public bool EndedByLevelFinalize { get; set; } + // [S133 2026-07-17 — anh Kiệt FDC] BẤT BIẾN ngân sách phiếu ĐÃ DUYỆT: snapshot bộ số + // NS-gói-thầu (matrix PRO/CCM từ PeWorkItemBudgets + 4 số lũy kế phiếu-trước) vào 11 cột + // này TẠI thời điểm chuyển DaDuyet (ApplyBudgetSnapshotOnFinalizeAsync gọi ở MỌI nhánh + // finalize). Phiếu DaDuyet serve từ snapshot (display-gate `frozen` — GetPurchaseEvaluationQuery), + // phiếu chưa duyệt đọc LIVE record PeWorkItemBudgets. PeWorkItemBudgets + cơ chế nhập PRO/CCM + // GIỮ NGUYÊN (record dùng chung mọi phiếu cùng cặp — sửa được tự do khi phiếu chưa duyệt). + // SnapshotAt != null (trên phiếu DaDuyet) = cờ "đã chốt". Backfill Mig 67 cho DaDuyet cũ = live. + // NAMING: Ccm* = cột CCM (PeWorkItemBudget.InitialAmount/AdjustmentAmount); Pro* = cột PRO. + public decimal? ApprovedBudgetProInitialAmount { get; set; } + public decimal? ApprovedBudgetProAdjustmentAmount { get; set; } + public string? ApprovedBudgetProNote { get; set; } + public decimal? ApprovedBudgetCcmInitialAmount { get; set; } + public decimal? ApprovedBudgetCcmAdjustmentAmount { get; set; } + public string? ApprovedBudgetCcmNote { get; set; } + public decimal? ApprovedBudgetPrevSubmittedTotal { get; set; } + public int? ApprovedBudgetPrevSubmittedCount { get; set; } + public decimal? ApprovedBudgetPrevSelectedTotal { get; set; } + public int? ApprovedBudgetPrevSelectedCount { get; set; } + public DateTime? ApprovedBudgetSnapshotAt { get; set; } + public List Suppliers { get; set; } = new(); public List Details { get; set; } = new(); public List Quotes { get; set; } = new(); diff --git a/src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/PurchaseEvaluationConfiguration.cs b/src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/PurchaseEvaluationConfiguration.cs index 4317f5c..2a3ffb3 100644 --- a/src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/PurchaseEvaluationConfiguration.cs +++ b/src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/PurchaseEvaluationConfiguration.cs @@ -42,6 +42,16 @@ public class PurchaseEvaluationConfiguration : IEntityTypeConfiguration x.CcmSuggestedPriceNote).HasMaxLength(1000); // [S97 2026-07-01] Runtime finalize-fact — bit NOT NULL default false (mirror IsUrgent* bool). b.Property(x => x.EndedByLevelFinalize).HasDefaultValue(false); + // [S133 Mig 67] Snapshot ngân sách phiếu DaDuyet — 6 decimal (18,2) + 2 note (1000) + // mirror precision cột ApprovedPrice*/PeWorkItemBudget. Tất cả nullable (backfill Mig 67). + b.Property(x => x.ApprovedBudgetProInitialAmount).HasPrecision(18, 2); + b.Property(x => x.ApprovedBudgetProAdjustmentAmount).HasPrecision(18, 2); + b.Property(x => x.ApprovedBudgetCcmInitialAmount).HasPrecision(18, 2); + b.Property(x => x.ApprovedBudgetCcmAdjustmentAmount).HasPrecision(18, 2); + b.Property(x => x.ApprovedBudgetPrevSubmittedTotal).HasPrecision(18, 2); + b.Property(x => x.ApprovedBudgetPrevSelectedTotal).HasPrecision(18, 2); + b.Property(x => x.ApprovedBudgetProNote).HasMaxLength(1000); + b.Property(x => x.ApprovedBudgetCcmNote).HasMaxLength(1000); b.HasIndex(x => x.MaPhieu).IsUnique().HasFilter("[MaPhieu] IS NOT NULL"); b.HasIndex(x => new { x.Phase, x.IsDeleted }); diff --git a/src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs b/src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs index 53e23e8..bfbe0fb 100644 --- a/src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs +++ b/src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs @@ -1322,6 +1322,24 @@ public static class DbInitializer } pe.Phase = current; + // [S133] Demo phiếu DaDuyet cũng phải có snapshot NS: fresh-DB migrate chạy TRƯỚC seed + // → backfill Mig 67 no-op trên bảng rỗng; thiếu block này demo DaDuyet hiển thị live-drift + // (reviewer S133 catch — mirror helper ApplyBudgetSnapshotOnFinalizeAsync, null-safe). + if (current == PurchaseEvaluationPhase.DaDuyet) + { + var pairDemo = pe.WorkItemId is Guid wiDemo + ? await db.PeWorkItemBudgets + .FirstOrDefaultAsync(b => b.ProjectId == pe.ProjectId && b.WorkItemId == wiDemo) + : null; + pe.ApprovedBudgetProInitialAmount = pairDemo?.ProInitialAmount; + pe.ApprovedBudgetProAdjustmentAmount = pairDemo?.ProAdjustmentAmount; + pe.ApprovedBudgetProNote = pairDemo?.ProNote; + pe.ApprovedBudgetCcmInitialAmount = pairDemo?.InitialAmount; + pe.ApprovedBudgetCcmAdjustmentAmount = pairDemo?.AdjustmentAmount; + pe.ApprovedBudgetCcmNote = pairDemo?.CcmNote; + pe.ApprovedBudgetSnapshotAt = nowUtc; + } + // Set winner nếu DaDuyet if (current == PurchaseEvaluationPhase.DaDuyet && winnerSupplierCode is not null diff --git a/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260717032812_AddPeApprovedBudgetSnapshot.Designer.cs b/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260717032812_AddPeApprovedBudgetSnapshot.Designer.cs new file mode 100644 index 0000000..16c90fa --- /dev/null +++ b/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260717032812_AddPeApprovedBudgetSnapshot.Designer.cs @@ -0,0 +1,6405 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SolutionErp.Infrastructure.Persistence; + +#nullable disable + +namespace SolutionErp.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260717032812_AddPeApprovedBudgetSnapshot")] + partial class AddPeApprovedBudgetSnapshot + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("RoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("UserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("UserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("UserTokens", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ActivatedAt") + .HasColumnType("datetime2"); + + b.Property("ApplicableType") + .HasColumnType("int"); + + b.Property("CeoApprovalThreshold") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsUserSelectable") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Version") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ApplicableType", "IsActive"); + + b.HasIndex("Code", "Version") + .IsUnique(); + + b.ToTable("ApprovalWorkflows", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflowLevel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AllowApproverEditBudget") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("AllowApproverEditDetails") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("AllowApproverFinalize") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("AllowApproverSkipToFinal") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("AllowReturnOneLevel") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("AllowReturnOneStep") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("AllowReturnToAssignee") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("AllowReturnToDrafter") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("ApprovalWorkflowStepId") + .HasColumnType("uniqueidentifier"); + + b.Property("ApproverUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ApproverUserId"); + + b.HasIndex("ApprovalWorkflowStepId", "Order"); + + b.ToTable("ApprovalWorkflowLevels", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflowStep", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovalWorkflowId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DepartmentId") + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("ApprovalWorkflowId", "Order"); + + b.ToTable("ApprovalWorkflowSteps", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Contract", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovalWorkflowId") + .HasColumnType("uniqueidentifier"); + + b.Property("BudgetManualAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("BudgetManualName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("BypassProcurementAndCCM") + .HasColumnType("bit"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("CurrentApprovalLevelOrder") + .HasColumnType("int"); + + b.Property("CurrentWorkflowStepIndex") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DepartmentId") + .HasColumnType("uniqueidentifier"); + + b.Property("DraftData") + .HasColumnType("nvarchar(max)"); + + b.Property("DrafterUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("GiaTri") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("MaHopDong") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("NoiDung") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("Phase") + .HasColumnType("int"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("RejectedAtStepIndex") + .HasColumnType("int"); + + b.Property("RejectedFromPhase") + .HasColumnType("int"); + + b.Property("SlaDeadline") + .HasColumnType("datetime2"); + + b.Property("SlaWarningSent") + .HasColumnType("bit"); + + b.Property("SupplierId") + .HasColumnType("uniqueidentifier"); + + b.Property("TemplateId") + .HasColumnType("uniqueidentifier"); + + b.Property("TenHopDong") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("WorkflowDefinitionId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ApprovalWorkflowId"); + + b.HasIndex("MaHopDong") + .IsUnique() + .HasFilter("[MaHopDong] IS NOT NULL"); + + b.HasIndex("ProjectId"); + + b.HasIndex("SlaDeadline"); + + b.HasIndex("SupplierId"); + + b.HasIndex("Phase", "IsDeleted"); + + b.ToTable("Contracts", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractApproval", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovedAt") + .HasColumnType("datetime2"); + + b.Property("ApproverUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("Comment") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Decision") + .HasColumnType("int"); + + b.Property("FromPhase") + .HasColumnType("int"); + + b.Property("ToPhase") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ContractId", "ApprovedAt"); + + b.ToTable("ContractApprovals", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Purpose") + .HasColumnType("int"); + + b.Property("StoragePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ContractId"); + + b.ToTable("ContractAttachments", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractChangelog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Action") + .HasColumnType("int"); + + b.Property("ContextNote") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("EntityId") + .HasColumnType("uniqueidentifier"); + + b.Property("EntityType") + .HasColumnType("int"); + + b.Property("FieldChangesJson") + .HasColumnType("nvarchar(max)"); + + b.Property("PhaseAtChange") + .HasColumnType("int"); + + b.Property("Summary") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("UserName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("ContractId", "CreatedAt"); + + b.HasIndex("ContractId", "EntityType"); + + b.ToTable("ContractChangelogs", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractCodeSequence", b => + { + b.Property("Prefix") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("LastSeq") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Prefix"); + + b.ToTable("ContractCodeSequences", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractComment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Phase") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ContractId", "CreatedAt"); + + b.ToTable("ContractComments", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractDepartmentApproval", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovedAt") + .HasColumnType("datetime2"); + + b.Property("ApproverRoleSnapshot") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ApproverUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("Comment") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DepartmentId") + .HasColumnType("uniqueidentifier"); + + b.Property("IsBypassed") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("PhaseAtApproval") + .HasColumnType("int"); + + b.Property("Stage") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ApproverUserId"); + + b.HasIndex("ContractId"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("ContractId", "PhaseAtApproval", "DepartmentId", "Stage") + .IsUnique() + .HasDatabaseName("UX_ContractDeptApprovals_Contract_Phase_Dept_Stage"); + + b.ToTable("ContractDepartmentApprovals", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractLevelOpinion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovalWorkflowLevelId") + .HasColumnType("uniqueidentifier"); + + b.Property("Comment") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("SignedAt") + .HasColumnType("datetime2"); + + b.Property("SignedByFullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("SignedByUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ApprovalWorkflowLevelId"); + + b.HasIndex("ContractId", "ApprovalWorkflowLevelId") + .IsUnique(); + + b.ToTable("ContractLevelOpinions", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.DichVuDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DenNgay") + .HasColumnType("datetime2"); + + b.Property("DonGia") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DonViTinh") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("GhiChu") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("MaDichVu") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("MoTa") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("TenDichVu") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ThanhTien") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ThoiGian") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("TuNgay") + .HasColumnType("datetime2"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ContractId", "Order"); + + b.ToTable("DichVuDetails", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.GiaoKhoanDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DonGia") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DonViTinh") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("GhiChu") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("KhoiLuong") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("MaCongViec") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("TenCongViec") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ThanhTien") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ThoiGianHoanThanh") + .HasColumnType("datetime2"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("YeuCauKyThuat") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.HasKey("Id"); + + b.HasIndex("ContractId", "Order"); + + b.ToTable("GiaoKhoanDetails", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.MuaBanDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DonGia") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DonViTinh") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("GhiChu") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("MaSP") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("MoTa") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("SoLuong") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("TenSP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ThanhTien") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ThueVAT") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("XuatXu") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.HasKey("Id"); + + b.HasIndex("ContractId", "Order"); + + b.ToTable("MuaBanDetails", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.NguyenTacDvDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DonGiaToiDa") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DonGiaToiThieu") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DonViTinh") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("GhiChu") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("LoaiDichVu") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("PhamViDichVu") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("SLA") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("TenDichVu") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ThanhTien") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ContractId", "Order"); + + b.ToTable("NguyenTacDvDetails", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.NguyenTacNccDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DieuKienGiaoHang") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("DieuKienThanhToan") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("DonGiaToiDa") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DonGiaToiThieu") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DonViTinh") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("GhiChu") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("NhomSP") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("TenSP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ThanhTien") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ContractId", "Order"); + + b.ToTable("NguyenTacNccDetails", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.NhaCungCapDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DonGia") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DonViTinh") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("GhiChu") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("MaSP") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("SoLuong") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("TenSP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ThanhTien") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ThoiGianGiao") + .HasColumnType("datetime2"); + + b.Property("ThongSoKyThuat") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("XuatXu") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.HasKey("Id"); + + b.HasIndex("ContractId", "Order"); + + b.ToTable("NhaCungCapDetails", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.ThauPhuDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DonGia") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DonViTinh") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("GhiChu") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("HangMuc") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("KhoiLuong") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("ThanhTien") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ThoiGianHoanThanh") + .HasColumnType("datetime2"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ContractId", "Order"); + + b.ToTable("ThauPhuDetails", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.WorkflowDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ActivatedAt") + .HasColumnType("datetime2"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ContractType") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Version") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Code", "Version") + .IsUnique(); + + b.HasIndex("ContractType", "IsActive"); + + b.ToTable("WorkflowDefinitions", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.WorkflowStep", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DepartmentId") + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("Phase") + .HasColumnType("int"); + + b.Property("PositionLevel") + .HasColumnType("int"); + + b.Property("SlaDays") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("WorkflowDefinitionId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("WorkflowDefinitionId", "Order"); + + b.ToTable("WorkflowSteps", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.WorkflowStepApprover", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AssignmentValue") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("WorkflowStepId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("WorkflowStepId"); + + b.ToTable("WorkflowStepApprovers", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.WorkflowTypeAssignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContractType") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("PolicyName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ContractType") + .IsUnique(); + + b.ToTable("WorkflowTypeAssignments", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Forms.ContractClause", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Content") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Version") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("ContractClauses", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Forms.ContractTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContractType") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("FieldSpec") + .HasColumnType("nvarchar(max)"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("FormCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Format") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("StoragePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ContractType"); + + b.HasIndex("FormCode") + .IsUnique(); + + b.ToTable("ContractTemplates", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.Driver", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LicenseClass") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("LicenseNumber") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("PhoneNumber") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[IsDeleted] = 0"); + + b.ToTable("Drivers", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.EmployeeCodeSequence", b => + { + b.Property("Prefix") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("LastSeq") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Prefix"); + + b.ToTable("EmployeeCodeSequences", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.EmployeeDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DocumentType") + .HasColumnType("int"); + + b.Property("EmployeeProfileId") + .HasColumnType("uniqueidentifier"); + + b.Property("ExpiryDate") + .HasColumnType("date"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("FilePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IssueDate") + .HasColumnType("date"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("DocumentType"); + + b.HasIndex("EmployeeProfileId"); + + b.ToTable("EmployeeDocuments", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.EmployeeEducation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CertificateIssueDate") + .HasColumnType("date"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DegreeLevel") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("EducationMode") + .HasColumnType("int"); + + b.Property("EmployeeProfileId") + .HasColumnType("uniqueidentifier"); + + b.Property("FromDate") + .HasColumnType("date"); + + b.Property("GradeLevel") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Major") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("SchoolName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ToDate") + .HasColumnType("date"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeProfileId"); + + b.ToTable("EmployeeEducations", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.EmployeeFamilyRelation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("BirthYear") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("CurrentAddress") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("EmployeeProfileId") + .HasColumnType("uniqueidentifier"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Occupation") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Phone") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("Relationship") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeProfileId"); + + b.ToTable("EmployeeFamilyRelations", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.EmployeeProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AcademicTitle") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("AnnualLeaveDays") + .HasColumnType("decimal(5,2)"); + + b.Property("BankAccount") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("BankBranch") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("BankName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("BaseSalary") + .HasColumnType("decimal(18,2)"); + + b.Property("BirthPlace") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("BloodType") + .HasMaxLength(5) + .HasColumnType("nvarchar(5)"); + + b.Property("CommunistPartyJoinDate") + .HasColumnType("date"); + + b.Property("CompensatoryLeaveDays") + .HasColumnType("decimal(5,2)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DateOfBirth") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("EmergencyContactAddress") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("EmergencyContactName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("EmergencyContactPhone") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("EmployeeCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("EmployeeStatus") + .HasColumnType("int"); + + b.Property("EmployeeType") + .HasColumnType("int"); + + b.Property("Ethnicity") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Gender") + .HasColumnType("int"); + + b.Property("HeightCm") + .HasColumnType("int"); + + b.Property("HireDate") + .HasColumnType("date"); + + b.Property("Hometown") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("IdCardIssueDate") + .HasColumnType("date"); + + b.Property("IdCardIssuePlace") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("IdCardNumber") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("InternalPhone") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("IsCommunistParty") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsTradeUnion") + .HasColumnType("bit"); + + b.Property("IsYouthUnion") + .HasColumnType("bit"); + + b.Property("MaritalStatus") + .HasColumnType("int"); + + b.Property("MedicalRegistrationPlace") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Nationality") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Notes") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("PassportNumber") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("PermanentAddressText") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("PermanentDistrictId") + .HasColumnType("uniqueidentifier"); + + b.Property("PermanentProvinceId") + .HasColumnType("uniqueidentifier"); + + b.Property("PermanentWardId") + .HasColumnType("uniqueidentifier"); + + b.Property("PersonalEmail") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Phone") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("PhotoUrl") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Qualification") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Religion") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("RemainingLeaveDays") + .HasColumnType("decimal(5,2)"); + + b.Property("ResignDate") + .HasColumnType("date"); + + b.Property("SeniorityLeaveDays") + .HasColumnType("decimal(5,2)"); + + b.Property("SocialInsuranceNumber") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("SocialInsuranceStartDate") + .HasColumnType("date"); + + b.Property("StreetAddressPermanent") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("StreetAddressTemporary") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("TaxCode") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("TemporaryAddressText") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("TemporaryDistrictId") + .HasColumnType("uniqueidentifier"); + + b.Property("TemporaryProvinceId") + .HasColumnType("uniqueidentifier"); + + b.Property("TemporaryWardId") + .HasColumnType("uniqueidentifier"); + + b.Property("TimekeepingCode") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("TotalSalary") + .HasColumnType("decimal(18,2)"); + + b.Property("TradeUnionJoinDate") + .HasColumnType("date"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("WeightKg") + .HasColumnType("int"); + + b.Property("WorkLocation") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("YouthUnionJoinDate") + .HasColumnType("date"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeCode") + .IsUnique(); + + b.HasIndex("IsDeleted"); + + b.HasIndex("Phone"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("EmployeeProfiles", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.EmployeeSkill", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("EmployeeProfileId") + .HasColumnType("uniqueidentifier"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("LanguageId") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("Level") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeProfileId"); + + b.HasIndex("Kind"); + + b.ToTable("EmployeeSkills", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.EmployeeWorkHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CompanyAddress") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("CompanyName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("EmployeeProfileId") + .HasColumnType("uniqueidentifier"); + + b.Property("FromDate") + .HasColumnType("date"); + + b.Property("Industry") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("JobDescription") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("JobTitle") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ResignReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ToDate") + .HasColumnType("date"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeProfileId"); + + b.ToTable("EmployeeWorkHistories", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.Holiday", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsPaid") + .HasColumnType("bit"); + + b.Property("IsRecurring") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Year") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Year", "Date") + .IsUnique() + .HasFilter("[IsDeleted] = 0"); + + b.ToTable("Holidays", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.LeaveBalance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AdjustmentDays") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("EntitledDays") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LeaveTypeId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("UsedDays") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("Year") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("LeaveTypeId"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "LeaveTypeId", "Year") + .IsUnique(); + + b.ToTable("LeaveBalances", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.LeaveType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DaysPerYear") + .HasColumnType("decimal(5,2)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsPaid") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("RequiresAttachment") + .HasColumnType("bit"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[IsDeleted] = 0"); + + b.ToTable("LeaveTypes", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.OtPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("MaxHoursPerDay") + .HasColumnType("int"); + + b.Property("MaxHoursPerMonth") + .HasColumnType("int"); + + b.Property("MaxHoursPerYear") + .HasColumnType("int"); + + b.Property("MultiplierHoliday") + .HasColumnType("decimal(4,2)"); + + b.Property("MultiplierWeekday") + .HasColumnType("decimal(4,2)"); + + b.Property("MultiplierWeekend") + .HasColumnType("decimal(4,2)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[IsDeleted] = 0"); + + b.ToTable("OtPolicies", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.ShiftPattern", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("BreakMinutes") + .HasColumnType("int"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("EndTime") + .HasColumnType("time"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("StartTime") + .HasColumnType("time"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("WorkDays") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[IsDeleted] = 0"); + + b.ToTable("ShiftPatterns", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.Vehicle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LicensePlate") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("SeatCount") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[IsDeleted] = 0"); + + b.ToTable("Vehicles", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Identity.MenuItem", b => + { + b.Property("Key") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("DisplayLabel") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Icon") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("IsVisible") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("Label") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("ParentKey") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.HasKey("Key"); + + b.HasIndex("ParentKey"); + + b.ToTable("MenuItems", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Identity.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CanCreate") + .HasColumnType("bit"); + + b.Property("CanDelete") + .HasColumnType("bit"); + + b.Property("CanRead") + .HasColumnType("bit"); + + b.Property("CanUpdate") + .HasColumnType("bit"); + + b.Property("MenuKey") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("MenuKey"); + + b.HasIndex("RoleId", "MenuKey") + .IsUnique(); + + b.ToTable("Permissions", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Identity.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("ShortName") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("Roles", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Identity.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("CanBypassReview") + .HasColumnType("bit"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DepartmentId") + .HasColumnType("uniqueidentifier"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("Position") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("PositionLevel") + .HasColumnType("int"); + + b.Property("RefreshToken") + .HasMaxLength(512) + .HasColumnType("nvarchar(512)"); + + b.Property("RefreshTokenExpiresAt") + .HasColumnType("datetime2"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("Users", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Master.Catalogs.MaterialItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Category") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DefaultUnit") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OriginCountry") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Specification") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("Category"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[IsDeleted] = 0"); + + b.ToTable("MaterialItems", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Master.Catalogs.ServiceItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Category") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DefaultUnit") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("Category"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[IsDeleted] = 0"); + + b.ToTable("ServiceItems", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Master.Catalogs.UnitOfMeasure", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[IsDeleted] = 0"); + + b.ToTable("UnitsOfMeasure", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Master.Catalogs.WorkItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Category") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DefaultUnit") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("Category"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[IsDeleted] = 0"); + + b.ToTable("WorkItems", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Master.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("ManagerUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Note") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("ParentId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[IsDeleted] = 0"); + + b.HasIndex("ParentId"); + + b.ToTable("Departments", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Master.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("BudgetTotal") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("EndDate") + .HasColumnType("datetime2"); + + b.Property("Investor") + .HasMaxLength(250) + .HasColumnType("nvarchar(250)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Location") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ManagerUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Note") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Package") + .HasMaxLength(300) + .HasColumnType("nvarchar(300)"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Year") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[IsDeleted] = 0"); + + b.ToTable("Projects", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Master.Supplier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Address") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("AuthorizationNote") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("BankAccount") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ContactPerson") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ContactPhone") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ContactTitle") + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Email") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Fax") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsPublic") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("LegalRepTitle") + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("LegalRepresentative") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("LinkGpkd") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("LinkGuq") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("LinkHsnl") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("MailRecipient") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("MailingAddress") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Note") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("OfficeAddress") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("OwnerPmh") + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("PackageCategory") + .HasMaxLength(300) + .HasColumnType("nvarchar(300)"); + + b.Property("Phone") + .HasMaxLength(30) + .HasColumnType("nvarchar(30)"); + + b.Property("ReferralSource") + .HasMaxLength(300) + .HasColumnType("nvarchar(300)"); + + b.Property("SecondaryBankAccount") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("SourceUpdatedAt") + .HasColumnType("datetime2"); + + b.Property("SourceUpdatedBy") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TaxCode") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[IsDeleted] = 0 AND [Code] <> ''"); + + b.HasIndex("Type"); + + b.ToTable("Suppliers", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Notifications.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Href") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ReadAt") + .HasColumnType("datetime2"); + + b.Property("RefId") + .HasColumnType("uniqueidentifier"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("nvarchar(300)"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("UserId", "ReadAt"); + + b.ToTable("Notifications", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.Attendance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AttendanceDate") + .HasColumnType("datetime2"); + + b.Property("CheckInAccuracy") + .HasColumnType("decimal(8,2)"); + + b.Property("CheckInAt") + .HasColumnType("datetime2"); + + b.Property("CheckInLatitude") + .HasColumnType("decimal(10,7)"); + + b.Property("CheckInLongitude") + .HasColumnType("decimal(10,7)"); + + b.Property("CheckOutAccuracy") + .HasColumnType("decimal(8,2)"); + + b.Property("CheckOutAt") + .HasColumnType("datetime2"); + + b.Property("CheckOutLatitude") + .HasColumnType("decimal(10,7)"); + + b.Property("CheckOutLongitude") + .HasColumnType("decimal(10,7)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("IpAddressIn") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("IpAddressOut") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("OtHours") + .HasColumnType("decimal(5,2)"); + + b.Property("SourceIn") + .HasColumnType("int"); + + b.Property("SourceOut") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("UserFullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("WorkHours") + .HasColumnType("decimal(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "AttendanceDate") + .IsUnique(); + + b.ToTable("Attendances", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.ItTicket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AssignedToFullName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("AssignedToUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(5000) + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("MaTicket") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Priority") + .HasColumnType("int"); + + b.Property("RequesterFullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("RequesterUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("Resolution") + .HasMaxLength(5000) + .HasColumnType("nvarchar(max)"); + + b.Property("ResolvedAt") + .HasColumnType("datetime2"); + + b.Property("SlaBreached") + .HasColumnType("bit"); + + b.Property("SlaDueAt") + .HasColumnType("datetime2"); + + b.Property("SlaWarnedSent") + .HasColumnType("bit"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("nvarchar(300)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("AssignedToUserId"); + + b.HasIndex("Category"); + + b.HasIndex("MaTicket") + .IsUnique() + .HasFilter("[MaTicket] IS NOT NULL"); + + b.HasIndex("RequesterUserId"); + + b.HasIndex("Status"); + + b.ToTable("ItTickets", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.LeaveRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovalWorkflowId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("CurrentApprovalLevelOrder") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("EndDate") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LeaveTypeId") + .HasColumnType("uniqueidentifier"); + + b.Property("MaDonTu") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("NumDays") + .HasColumnType("decimal(5,2)"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("RejectedFromStatus") + .HasColumnType("int"); + + b.Property("RequesterFullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("RequesterUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("MaDonTu") + .IsUnique() + .HasFilter("[MaDonTu] IS NOT NULL"); + + b.HasIndex("RequesterUserId"); + + b.HasIndex("Status"); + + b.ToTable("LeaveRequests", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.LeaveRequestLevelOpinion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovalWorkflowLevelId") + .HasColumnType("uniqueidentifier"); + + b.Property("Comment") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LeaveRequestId") + .HasColumnType("uniqueidentifier"); + + b.Property("SignedAt") + .HasColumnType("datetime2"); + + b.Property("SignedByFullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("SignedByUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ApprovalWorkflowLevelId"); + + b.HasIndex("LeaveRequestId", "ApprovalWorkflowLevelId") + .IsUnique(); + + b.ToTable("LeaveRequestLevelOpinions", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.MeetingBooking", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("BookedByFullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("BookedByUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("EndAt") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("RoomId") + .HasColumnType("uniqueidentifier"); + + b.Property("StartAt") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("BookedByUserId"); + + b.HasIndex("RoomId", "StartAt"); + + b.ToTable("MeetingBookings", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.MeetingBookingAttendee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("BookingId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Notes") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("BookingId", "UserId") + .IsUnique(); + + b.ToTable("MeetingBookingAttendees", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.MeetingRoom", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Capacity") + .HasColumnType("int"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Equipment") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Location") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("MeetingRooms", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.OtRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovalWorkflowId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("CurrentApprovalLevelOrder") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("EndTime") + .HasColumnType("time"); + + b.Property("Hours") + .HasColumnType("decimal(5,2)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("MaDonTu") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("OtDate") + .HasColumnType("datetime2"); + + b.Property("OtPolicyId") + .HasColumnType("uniqueidentifier"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("RejectedFromStatus") + .HasColumnType("int"); + + b.Property("RequesterFullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("RequesterUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("StartTime") + .HasColumnType("time"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("MaDonTu") + .IsUnique() + .HasFilter("[MaDonTu] IS NOT NULL"); + + b.HasIndex("RequesterUserId"); + + b.HasIndex("Status"); + + b.ToTable("OtRequests", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.OtRequestLevelOpinion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovalWorkflowLevelId") + .HasColumnType("uniqueidentifier"); + + b.Property("Comment") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("OtRequestId") + .HasColumnType("uniqueidentifier"); + + b.Property("SignedAt") + .HasColumnType("datetime2"); + + b.Property("SignedByFullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("SignedByUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ApprovalWorkflowLevelId"); + + b.HasIndex("OtRequestId", "ApprovalWorkflowLevelId") + .IsUnique(); + + b.ToTable("OtRequestLevelOpinions", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.Proposal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AmountEstimate") + .HasColumnType("decimal(18,2)"); + + b.Property("ApprovalWorkflowId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("CurrentApprovalLevelOrder") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DepartmentId") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(5000) + .HasColumnType("nvarchar(max)"); + + b.Property("DrafterUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("MaDeXuat") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("RejectedFromStatus") + .HasColumnType("int"); + + b.Property("SlaDeadline") + .HasColumnType("datetime2"); + + b.Property("SlaWarningSent") + .HasColumnType("bit"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("nvarchar(300)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ApprovalWorkflowId"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("DrafterUserId"); + + b.HasIndex("MaDeXuat") + .IsUnique() + .HasFilter("[MaDeXuat] IS NOT NULL"); + + b.HasIndex("Status"); + + b.ToTable("Proposals", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.ProposalAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("FilePath") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("MimeType") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ProposalId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("UploadedByFullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UploadedByUserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ProposalId"); + + b.ToTable("ProposalAttachments", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.ProposalCodeSequence", b => + { + b.Property("Prefix") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("LastSeq") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Prefix"); + + b.ToTable("ProposalCodeSequences", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.ProposalLevelOpinion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovalWorkflowLevelId") + .HasColumnType("uniqueidentifier"); + + b.Property("Comment") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("ProposalId") + .HasColumnType("uniqueidentifier"); + + b.Property("SignedAt") + .HasColumnType("datetime2"); + + b.Property("SignedByFullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("SignedByUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ApprovalWorkflowLevelId"); + + b.HasIndex("ProposalId", "ApprovalWorkflowLevelId") + .IsUnique(); + + b.ToTable("ProposalLevelOpinions", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.TravelRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovalWorkflowId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("CurrentApprovalLevelOrder") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Destination") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("nvarchar(300)"); + + b.Property("EndDate") + .HasColumnType("datetime2"); + + b.Property("EstimatedCost") + .HasColumnType("decimal(18,2)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("MaDonTu") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("NumDays") + .HasColumnType("int"); + + b.Property("Purpose") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("RejectedFromStatus") + .HasColumnType("int"); + + b.Property("RequesterFullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("RequesterUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("MaDonTu") + .IsUnique() + .HasFilter("[MaDonTu] IS NOT NULL"); + + b.HasIndex("RequesterUserId"); + + b.HasIndex("Status"); + + b.ToTable("TravelRequests", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.TravelRequestLevelOpinion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovalWorkflowLevelId") + .HasColumnType("uniqueidentifier"); + + b.Property("Comment") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("SignedAt") + .HasColumnType("datetime2"); + + b.Property("SignedByFullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("SignedByUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("TravelRequestId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ApprovalWorkflowLevelId"); + + b.HasIndex("TravelRequestId", "ApprovalWorkflowLevelId") + .IsUnique(); + + b.ToTable("TravelRequestLevelOpinions", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.VehicleBooking", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovalWorkflowId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("CurrentApprovalLevelOrder") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Destination") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("nvarchar(300)"); + + b.Property("DriverName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("EndAt") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("MaDonTu") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Purpose") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("RejectedFromStatus") + .HasColumnType("int"); + + b.Property("RequesterFullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("RequesterUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("StartAt") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("VehicleLicense") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("VehicleName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("MaDonTu") + .IsUnique() + .HasFilter("[MaDonTu] IS NOT NULL"); + + b.HasIndex("RequesterUserId"); + + b.HasIndex("Status"); + + b.HasIndex("VehicleLicense", "StartAt"); + + b.ToTable("VehicleBookings", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.VehicleBookingLevelOpinion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovalWorkflowLevelId") + .HasColumnType("uniqueidentifier"); + + b.Property("Comment") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("SignedAt") + .HasColumnType("datetime2"); + + b.Property("SignedByFullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("SignedByUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("VehicleBookingId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ApprovalWorkflowLevelId"); + + b.HasIndex("VehicleBookingId", "ApprovalWorkflowLevelId") + .IsUnique(); + + b.ToTable("VehicleBookingLevelOpinions", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.WorkflowAppCodeSequence", b => + { + b.Property("Prefix") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("LastSeq") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Prefix"); + + b.ToTable("WorkflowAppCodeSequences", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PeWorkItemBudget", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AdjustmentAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CcmNote") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("InitialAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("ProAdjustmentAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ProEstimateAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ProInitialAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ProNote") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("WorkItemId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("WorkItemId"); + + b.HasIndex("ProjectId", "WorkItemId") + .IsUnique() + .HasFilter("[IsDeleted] = 0"); + + b.ToTable("PeWorkItemBudgets", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovalWorkflowId") + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovedBudgetCcmAdjustmentAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ApprovedBudgetCcmInitialAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ApprovedBudgetCcmNote") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("ApprovedBudgetPrevSelectedCount") + .HasColumnType("int"); + + b.Property("ApprovedBudgetPrevSelectedTotal") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ApprovedBudgetPrevSubmittedCount") + .HasColumnType("int"); + + b.Property("ApprovedBudgetPrevSubmittedTotal") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ApprovedBudgetProAdjustmentAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ApprovedBudgetProInitialAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ApprovedBudgetProNote") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("ApprovedBudgetSnapshotAt") + .HasColumnType("datetime2"); + + b.Property("ApprovedPriceAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ApprovedPriceMaxAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ApprovedPriceSource") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("BudgetPeriodAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CcmBudgetPeriodAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("CcmSuggestedPrice") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CcmSuggestedPriceNote") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("CurrentApprovalLevelOrder") + .HasColumnType("int"); + + b.Property("CurrentWorkflowStepIndex") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DepartmentId") + .HasColumnType("uniqueidentifier"); + + b.Property("DiaDiem") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("DrafterUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("EndedByLevelFinalize") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("ExpectedRemainingAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("HoSoLink") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsUrgentByCcm") + .HasColumnType("bit"); + + b.Property("IsUrgentByPro") + .HasColumnType("bit"); + + b.Property("MaPhieu") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("MoTa") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("PaymentTerms") + .HasColumnType("nvarchar(max)"); + + b.Property("Phase") + .HasColumnType("int"); + + b.Property("ProSuggestedMaxPrice") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ProSuggestedMinPrice") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ProSuggestedPriceNote") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("RejectedAtStepIndex") + .HasColumnType("int"); + + b.Property("RejectedFromPhase") + .HasColumnType("int"); + + b.Property("SelectedSupplierId") + .HasColumnType("uniqueidentifier"); + + b.Property("SlaDeadline") + .HasColumnType("datetime2"); + + b.Property("SlaWarningSent") + .HasColumnType("bit"); + + b.Property("TenGoiThau") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("WorkItemId") + .HasColumnType("uniqueidentifier"); + + b.Property("WorkflowDefinitionId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ApprovalWorkflowId"); + + b.HasIndex("ContractId"); + + b.HasIndex("MaPhieu") + .IsUnique() + .HasFilter("[MaPhieu] IS NOT NULL"); + + b.HasIndex("ProjectId"); + + b.HasIndex("SlaDeadline"); + + b.HasIndex("WorkItemId"); + + b.HasIndex("WorkflowDefinitionId"); + + b.HasIndex("Phase", "IsDeleted"); + + b.ToTable("PurchaseEvaluations", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationApproval", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovedAt") + .HasColumnType("datetime2"); + + b.Property("ApproverUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("Comment") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Decision") + .HasColumnType("int"); + + b.Property("FromPhase") + .HasColumnType("int"); + + b.Property("PurchaseEvaluationId") + .HasColumnType("uniqueidentifier"); + + b.Property("ToPhase") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("PurchaseEvaluationId", "ApprovedAt"); + + b.ToTable("PurchaseEvaluationApprovals", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("PurchaseEvaluationId") + .HasColumnType("uniqueidentifier"); + + b.Property("PurchaseEvaluationSupplierId") + .HasColumnType("uniqueidentifier"); + + b.Property("Purpose") + .HasColumnType("int"); + + b.Property("StoragePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("PurchaseEvaluationId"); + + b.HasIndex("PurchaseEvaluationSupplierId"); + + b.ToTable("PurchaseEvaluationAttachments", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationChangelog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Action") + .HasColumnType("int"); + + b.Property("ContextNote") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("EntityId") + .HasColumnType("uniqueidentifier"); + + b.Property("EntityType") + .HasColumnType("int"); + + b.Property("FieldChangesJson") + .HasColumnType("nvarchar(max)"); + + b.Property("PhaseAtChange") + .HasColumnType("int"); + + b.Property("PurchaseEvaluationId") + .HasColumnType("uniqueidentifier"); + + b.Property("Summary") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("UserName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("PurchaseEvaluationId", "CreatedAt"); + + b.HasIndex("PurchaseEvaluationId", "EntityType"); + + b.ToTable("PurchaseEvaluationChangelogs", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationCodeSequence", b => + { + b.Property("Prefix") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("LastSeq") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Prefix"); + + b.ToTable("PurchaseEvaluationCodeSequences", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationDepartmentApproval", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovedAt") + .HasColumnType("datetime2"); + + b.Property("ApproverRoleSnapshot") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ApproverUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("Comment") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DepartmentId") + .HasColumnType("uniqueidentifier"); + + b.Property("IsBypassed") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("PhaseAtApproval") + .HasColumnType("int"); + + b.Property("PurchaseEvaluationId") + .HasColumnType("uniqueidentifier"); + + b.Property("Stage") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ApproverUserId"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("PurchaseEvaluationId"); + + b.HasIndex("PurchaseEvaluationId", "PhaseAtApproval", "DepartmentId", "Stage") + .IsUnique() + .HasDatabaseName("UX_PEDeptApprovals_PE_Phase_Dept_Stage"); + + b.ToTable("PurchaseEvaluationDepartmentApprovals", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationDepartmentOpinion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("Opinion") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("PurchaseEvaluationId") + .HasColumnType("uniqueidentifier"); + + b.Property("SignedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("UserName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("PurchaseEvaluationId", "Kind") + .IsUnique(); + + b.ToTable("PurchaseEvaluationDepartmentOpinions", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DonGiaNganSach") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DonViTinh") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("GhiChu") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("GroupCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("GroupName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ItemCode") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("KhoiLuongNganSach") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("KhoiLuongThiCong") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("NoiDung") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("PurchaseEvaluationId") + .HasColumnType("uniqueidentifier"); + + b.Property("ThanhTienNganSach") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("PurchaseEvaluationId", "Order"); + + b.ToTable("PurchaseEvaluationDetails", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationLevelOpinion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovalWorkflowLevelId") + .HasColumnType("uniqueidentifier"); + + b.Property("Comment") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("PurchaseEvaluationId") + .HasColumnType("uniqueidentifier"); + + b.Property("SignedAt") + .HasColumnType("datetime2"); + + b.Property("SignedByFullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("SignedByUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ApprovalWorkflowLevelId"); + + b.HasIndex("PurchaseEvaluationId", "ApprovalWorkflowLevelId") + .IsUnique(); + + b.ToTable("PurchaseEvaluationLevelOpinions", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationQuote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("BgVat") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ChuaVat") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("IsSelected") + .HasColumnType("bit"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("PurchaseEvaluationDetailId") + .HasColumnType("uniqueidentifier"); + + b.Property("PurchaseEvaluationId") + .HasColumnType("uniqueidentifier"); + + b.Property("PurchaseEvaluationSupplierId") + .HasColumnType("uniqueidentifier"); + + b.Property("ThanhTien") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("PurchaseEvaluationId"); + + b.HasIndex("PurchaseEvaluationSupplierId"); + + b.HasIndex("PurchaseEvaluationDetailId", "PurchaseEvaluationSupplierId") + .IsUnique(); + + b.ToTable("PurchaseEvaluationQuotes", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationSupplier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContactEmail") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ContactName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ContactPhone") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DisplayName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("IsWinner") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("PaymentTermText") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("PurchaseEvaluationId") + .HasColumnType("uniqueidentifier"); + + b.Property("SupplierId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("SupplierId"); + + b.HasIndex("PurchaseEvaluationId", "SupplierId") + .IsUnique(); + + b.ToTable("PurchaseEvaluationSuppliers", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationWorkflowDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ActivatedAt") + .HasColumnType("datetime2"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("EvaluationType") + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Version") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Code", "Version") + .IsUnique(); + + b.HasIndex("EvaluationType", "IsActive"); + + b.ToTable("PurchaseEvaluationWorkflowDefinitions", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationWorkflowStep", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DepartmentId") + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("Phase") + .HasColumnType("int"); + + b.Property("PositionLevel") + .HasColumnType("int"); + + b.Property("PurchaseEvaluationWorkflowDefinitionId") + .HasColumnType("uniqueidentifier"); + + b.Property("SlaDays") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("PurchaseEvaluationWorkflowDefinitionId", "Order"); + + b.ToTable("PurchaseEvaluationWorkflowSteps", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationWorkflowStepApprover", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AssignmentValue") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("PurchaseEvaluationWorkflowStepId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("PurchaseEvaluationWorkflowStepId"); + + b.ToTable("PurchaseEvaluationWorkflowStepApprovers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("SolutionErp.Domain.Identity.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("SolutionErp.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("SolutionErp.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("SolutionErp.Domain.Identity.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SolutionErp.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("SolutionErp.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflowLevel", b => + { + b.HasOne("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflowStep", "Step") + .WithMany("Levels") + .HasForeignKey("ApprovalWorkflowStepId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SolutionErp.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("ApproverUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Step"); + }); + + modelBuilder.Entity("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflowStep", b => + { + b.HasOne("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflow", "ApprovalWorkflow") + .WithMany("Steps") + .HasForeignKey("ApprovalWorkflowId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SolutionErp.Domain.Master.Department", null) + .WithMany() + .HasForeignKey("DepartmentId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("ApprovalWorkflow"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Contract", b => + { + b.HasOne("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflow", null) + .WithMany() + .HasForeignKey("ApprovalWorkflowId") + .OnDelete(DeleteBehavior.Restrict); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractApproval", b => + { + b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract") + .WithMany("Approvals") + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contract"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractAttachment", b => + { + b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract") + .WithMany("Attachments") + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contract"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractChangelog", b => + { + b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract") + .WithMany("Changelogs") + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contract"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractComment", b => + { + b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract") + .WithMany("Comments") + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contract"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractDepartmentApproval", b => + { + b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract") + .WithMany("DepartmentApprovals") + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contract"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractLevelOpinion", b => + { + b.HasOne("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflowLevel", "Level") + .WithMany() + .HasForeignKey("ApprovalWorkflowLevelId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract") + .WithMany("LevelOpinions") + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contract"); + + b.Navigation("Level"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.DichVuDetail", b => + { + b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract") + .WithMany("DichVuDetails") + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contract"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.GiaoKhoanDetail", b => + { + b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract") + .WithMany("GiaoKhoanDetails") + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contract"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.MuaBanDetail", b => + { + b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract") + .WithMany("MuaBanDetails") + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contract"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.NguyenTacDvDetail", b => + { + b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract") + .WithMany("NguyenTacDvDetails") + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contract"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.NguyenTacNccDetail", b => + { + b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract") + .WithMany("NguyenTacNccDetails") + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contract"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.NhaCungCapDetail", b => + { + b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract") + .WithMany("NhaCungCapDetails") + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contract"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.ThauPhuDetail", b => + { + b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract") + .WithMany("ThauPhuDetails") + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contract"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.WorkflowStep", b => + { + b.HasOne("SolutionErp.Domain.Master.Department", null) + .WithMany() + .HasForeignKey("DepartmentId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SolutionErp.Domain.Contracts.WorkflowDefinition", "WorkflowDefinition") + .WithMany("Steps") + .HasForeignKey("WorkflowDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("WorkflowDefinition"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.WorkflowStepApprover", b => + { + b.HasOne("SolutionErp.Domain.Contracts.WorkflowStep", "Step") + .WithMany("Approvers") + .HasForeignKey("WorkflowStepId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Step"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.EmployeeDocument", b => + { + b.HasOne("SolutionErp.Domain.Hrm.EmployeeProfile", "EmployeeProfile") + .WithMany("Documents") + .HasForeignKey("EmployeeProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EmployeeProfile"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.EmployeeEducation", b => + { + b.HasOne("SolutionErp.Domain.Hrm.EmployeeProfile", "EmployeeProfile") + .WithMany("Educations") + .HasForeignKey("EmployeeProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EmployeeProfile"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.EmployeeFamilyRelation", b => + { + b.HasOne("SolutionErp.Domain.Hrm.EmployeeProfile", "EmployeeProfile") + .WithMany("FamilyRelations") + .HasForeignKey("EmployeeProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EmployeeProfile"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.EmployeeProfile", b => + { + b.HasOne("SolutionErp.Domain.Identity.User", "User") + .WithOne() + .HasForeignKey("SolutionErp.Domain.Hrm.EmployeeProfile", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.EmployeeSkill", b => + { + b.HasOne("SolutionErp.Domain.Hrm.EmployeeProfile", "EmployeeProfile") + .WithMany("Skills") + .HasForeignKey("EmployeeProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EmployeeProfile"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.EmployeeWorkHistory", b => + { + b.HasOne("SolutionErp.Domain.Hrm.EmployeeProfile", "EmployeeProfile") + .WithMany("WorkHistories") + .HasForeignKey("EmployeeProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EmployeeProfile"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.LeaveBalance", b => + { + b.HasOne("SolutionErp.Domain.Hrm.LeaveType", "LeaveType") + .WithMany() + .HasForeignKey("LeaveTypeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("LeaveType"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Identity.MenuItem", b => + { + b.HasOne("SolutionErp.Domain.Identity.MenuItem", "Parent") + .WithMany("Children") + .HasForeignKey("ParentKey") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Identity.Permission", b => + { + b.HasOne("SolutionErp.Domain.Identity.MenuItem", "Menu") + .WithMany("Permissions") + .HasForeignKey("MenuKey") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SolutionErp.Domain.Identity.Role", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Menu"); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Identity.User", b => + { + b.HasOne("SolutionErp.Domain.Master.Department", null) + .WithMany() + .HasForeignKey("DepartmentId") + .OnDelete(DeleteBehavior.Restrict); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.LeaveRequestLevelOpinion", b => + { + b.HasOne("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflowLevel", "Level") + .WithMany() + .HasForeignKey("ApprovalWorkflowLevelId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SolutionErp.Domain.Office.LeaveRequest", "LeaveRequest") + .WithMany("LevelOpinions") + .HasForeignKey("LeaveRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("LeaveRequest"); + + b.Navigation("Level"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.MeetingBooking", b => + { + b.HasOne("SolutionErp.Domain.Office.MeetingRoom", "Room") + .WithMany() + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.MeetingBookingAttendee", b => + { + b.HasOne("SolutionErp.Domain.Office.MeetingBooking", "Booking") + .WithMany("Attendees") + .HasForeignKey("BookingId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Booking"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.OtRequestLevelOpinion", b => + { + b.HasOne("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflowLevel", "Level") + .WithMany() + .HasForeignKey("ApprovalWorkflowLevelId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SolutionErp.Domain.Office.OtRequest", "OtRequest") + .WithMany("LevelOpinions") + .HasForeignKey("OtRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Level"); + + b.Navigation("OtRequest"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.ProposalAttachment", b => + { + b.HasOne("SolutionErp.Domain.Office.Proposal", "Proposal") + .WithMany("Attachments") + .HasForeignKey("ProposalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Proposal"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.ProposalLevelOpinion", b => + { + b.HasOne("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflowLevel", "Level") + .WithMany() + .HasForeignKey("ApprovalWorkflowLevelId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SolutionErp.Domain.Office.Proposal", "Proposal") + .WithMany("LevelOpinions") + .HasForeignKey("ProposalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Level"); + + b.Navigation("Proposal"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.TravelRequestLevelOpinion", b => + { + b.HasOne("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflowLevel", "Level") + .WithMany() + .HasForeignKey("ApprovalWorkflowLevelId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SolutionErp.Domain.Office.TravelRequest", "TravelRequest") + .WithMany("LevelOpinions") + .HasForeignKey("TravelRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Level"); + + b.Navigation("TravelRequest"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.VehicleBookingLevelOpinion", b => + { + b.HasOne("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflowLevel", "Level") + .WithMany() + .HasForeignKey("ApprovalWorkflowLevelId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SolutionErp.Domain.Office.VehicleBooking", "VehicleBooking") + .WithMany("LevelOpinions") + .HasForeignKey("VehicleBookingId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Level"); + + b.Navigation("VehicleBooking"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", b => + { + b.HasOne("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflow", null) + .WithMany() + .HasForeignKey("ApprovalWorkflowId") + .OnDelete(DeleteBehavior.Restrict); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationApproval", b => + { + b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", "PurchaseEvaluation") + .WithMany("Approvals") + .HasForeignKey("PurchaseEvaluationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PurchaseEvaluation"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationAttachment", b => + { + b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", "PurchaseEvaluation") + .WithMany("Attachments") + .HasForeignKey("PurchaseEvaluationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PurchaseEvaluation"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationChangelog", b => + { + b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", "PurchaseEvaluation") + .WithMany("Changelogs") + .HasForeignKey("PurchaseEvaluationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PurchaseEvaluation"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationDepartmentApproval", b => + { + b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", "PurchaseEvaluation") + .WithMany("DepartmentApprovals") + .HasForeignKey("PurchaseEvaluationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PurchaseEvaluation"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationDepartmentOpinion", b => + { + b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", "PurchaseEvaluation") + .WithMany("DepartmentOpinions") + .HasForeignKey("PurchaseEvaluationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PurchaseEvaluation"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationDetail", b => + { + b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", "PurchaseEvaluation") + .WithMany("Details") + .HasForeignKey("PurchaseEvaluationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PurchaseEvaluation"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationLevelOpinion", b => + { + b.HasOne("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflowLevel", "Level") + .WithMany() + .HasForeignKey("ApprovalWorkflowLevelId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", "PurchaseEvaluation") + .WithMany("LevelOpinions") + .HasForeignKey("PurchaseEvaluationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Level"); + + b.Navigation("PurchaseEvaluation"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationQuote", b => + { + b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationDetail", "Detail") + .WithMany("Quotes") + .HasForeignKey("PurchaseEvaluationDetailId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", null) + .WithMany("Quotes") + .HasForeignKey("PurchaseEvaluationId"); + + b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationSupplier", "Supplier") + .WithMany() + .HasForeignKey("PurchaseEvaluationSupplierId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Detail"); + + b.Navigation("Supplier"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationSupplier", b => + { + b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", "PurchaseEvaluation") + .WithMany("Suppliers") + .HasForeignKey("PurchaseEvaluationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PurchaseEvaluation"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationWorkflowStep", b => + { + b.HasOne("SolutionErp.Domain.Master.Department", null) + .WithMany() + .HasForeignKey("DepartmentId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationWorkflowDefinition", "Definition") + .WithMany("Steps") + .HasForeignKey("PurchaseEvaluationWorkflowDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Definition"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationWorkflowStepApprover", b => + { + b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationWorkflowStep", "Step") + .WithMany("Approvers") + .HasForeignKey("PurchaseEvaluationWorkflowStepId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Step"); + }); + + modelBuilder.Entity("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflow", b => + { + b.Navigation("Steps"); + }); + + modelBuilder.Entity("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflowStep", b => + { + b.Navigation("Levels"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Contract", b => + { + b.Navigation("Approvals"); + + b.Navigation("Attachments"); + + b.Navigation("Changelogs"); + + b.Navigation("Comments"); + + b.Navigation("DepartmentApprovals"); + + b.Navigation("DichVuDetails"); + + b.Navigation("GiaoKhoanDetails"); + + b.Navigation("LevelOpinions"); + + b.Navigation("MuaBanDetails"); + + b.Navigation("NguyenTacDvDetails"); + + b.Navigation("NguyenTacNccDetails"); + + b.Navigation("NhaCungCapDetails"); + + b.Navigation("ThauPhuDetails"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.WorkflowDefinition", b => + { + b.Navigation("Steps"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.WorkflowStep", b => + { + b.Navigation("Approvers"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.EmployeeProfile", b => + { + b.Navigation("Documents"); + + b.Navigation("Educations"); + + b.Navigation("FamilyRelations"); + + b.Navigation("Skills"); + + b.Navigation("WorkHistories"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Identity.MenuItem", b => + { + b.Navigation("Children"); + + b.Navigation("Permissions"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.LeaveRequest", b => + { + b.Navigation("LevelOpinions"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.MeetingBooking", b => + { + b.Navigation("Attendees"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.OtRequest", b => + { + b.Navigation("LevelOpinions"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.Proposal", b => + { + b.Navigation("Attachments"); + + b.Navigation("LevelOpinions"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.TravelRequest", b => + { + b.Navigation("LevelOpinions"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.VehicleBooking", b => + { + b.Navigation("LevelOpinions"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", b => + { + b.Navigation("Approvals"); + + b.Navigation("Attachments"); + + b.Navigation("Changelogs"); + + b.Navigation("DepartmentApprovals"); + + b.Navigation("DepartmentOpinions"); + + b.Navigation("Details"); + + b.Navigation("LevelOpinions"); + + b.Navigation("Quotes"); + + b.Navigation("Suppliers"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationDetail", b => + { + b.Navigation("Quotes"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationWorkflowDefinition", b => + { + b.Navigation("Steps"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationWorkflowStep", b => + { + b.Navigation("Approvers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260717032812_AddPeApprovedBudgetSnapshot.cs b/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260717032812_AddPeApprovedBudgetSnapshot.cs new file mode 100644 index 0000000..d8a360a --- /dev/null +++ b/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260717032812_AddPeApprovedBudgetSnapshot.cs @@ -0,0 +1,201 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SolutionErp.Infrastructure.Persistence.Migrations +{ + /// + public partial class AddPeApprovedBudgetSnapshot : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ApprovedBudgetCcmAdjustmentAmount", + table: "PurchaseEvaluations", + type: "decimal(18,2)", + precision: 18, + scale: 2, + nullable: true); + + migrationBuilder.AddColumn( + name: "ApprovedBudgetCcmInitialAmount", + table: "PurchaseEvaluations", + type: "decimal(18,2)", + precision: 18, + scale: 2, + nullable: true); + + migrationBuilder.AddColumn( + name: "ApprovedBudgetCcmNote", + table: "PurchaseEvaluations", + type: "nvarchar(1000)", + maxLength: 1000, + nullable: true); + + migrationBuilder.AddColumn( + name: "ApprovedBudgetPrevSelectedCount", + table: "PurchaseEvaluations", + type: "int", + nullable: true); + + migrationBuilder.AddColumn( + name: "ApprovedBudgetPrevSelectedTotal", + table: "PurchaseEvaluations", + type: "decimal(18,2)", + precision: 18, + scale: 2, + nullable: true); + + migrationBuilder.AddColumn( + name: "ApprovedBudgetPrevSubmittedCount", + table: "PurchaseEvaluations", + type: "int", + nullable: true); + + migrationBuilder.AddColumn( + name: "ApprovedBudgetPrevSubmittedTotal", + table: "PurchaseEvaluations", + type: "decimal(18,2)", + precision: 18, + scale: 2, + nullable: true); + + migrationBuilder.AddColumn( + name: "ApprovedBudgetProAdjustmentAmount", + table: "PurchaseEvaluations", + type: "decimal(18,2)", + precision: 18, + scale: 2, + nullable: true); + + migrationBuilder.AddColumn( + name: "ApprovedBudgetProInitialAmount", + table: "PurchaseEvaluations", + type: "decimal(18,2)", + precision: 18, + scale: 2, + nullable: true); + + migrationBuilder.AddColumn( + name: "ApprovedBudgetProNote", + table: "PurchaseEvaluations", + type: "nvarchar(1000)", + maxLength: 1000, + nullable: true); + + migrationBuilder.AddColumn( + name: "ApprovedBudgetSnapshotAt", + table: "PurchaseEvaluations", + type: "datetime2", + nullable: true); + + // [S133 2026-07-17 — anh Kiệt FDC] Backfill snapshot ngân sách cho phiếu ĐÃ DUYỆT + // hiện hữu = giá trị LIVE hôm nay (số tại-thời-điểm-duyệt KHÔNG khôi phục được — + // Changelogs có log budget-edit nhưng gắn phiếu-đang-mở-lúc-sửa, attribution gap). + // Idempotent: WHERE ApprovedBudgetSnapshotAt IS NULL → re-run no-op. SQL-Server-only + // (tests SQLite EnsureCreated KHÔNG replay migration — mirror Mig 61/65). + // Predicate lũy kế = BẢN DỊCH SQL của PeBudgetAccumulator.ComputeAsync (3 bản phải khớp: + // SQL này ↔ accumulator ↔ GetPurchaseEvaluationQuery cũ): peers cùng (ProjectId, WorkItemId), + // Id <> pe, CreatedAt < pe, IsDeleted = 0; submitted = Phase IN (10 ChoDuyet, 7 DaDuyet); + // selected = Phase 7 + EXISTS supplier IsWinner. Suppliers/Quotes = BaseEntity KHÔNG + // soft-delete filter → không điều kiện IsDeleted trên s/q. Phiếu WorkItemId NULL → + // 6 cột pair NULL (LEFT JOIN không match) + 4 lũy kế NULL (CASE — mirror helper runtime), + // SnapshotAt VẪN set (marker "đã chốt"). + migrationBuilder.Sql(@" +UPDATE pe SET + ApprovedBudgetProInitialAmount = b.ProInitialAmount, + ApprovedBudgetProAdjustmentAmount = b.ProAdjustmentAmount, + ApprovedBudgetProNote = b.ProNote, + ApprovedBudgetCcmInitialAmount = b.InitialAmount, + ApprovedBudgetCcmAdjustmentAmount = b.AdjustmentAmount, + ApprovedBudgetCcmNote = b.CcmNote, + ApprovedBudgetPrevSubmittedTotal = CASE WHEN pe.WorkItemId IS NULL THEN NULL ELSE ISNULL(sub.SubmittedTotal, 0) END, + ApprovedBudgetPrevSubmittedCount = CASE WHEN pe.WorkItemId IS NULL THEN NULL ELSE ISNULL(sub.SubmittedCount, 0) END, + ApprovedBudgetPrevSelectedTotal = CASE WHEN pe.WorkItemId IS NULL THEN NULL ELSE ISNULL(selt.SelectedTotal, 0) END, + ApprovedBudgetPrevSelectedCount = CASE WHEN pe.WorkItemId IS NULL THEN NULL ELSE ISNULL(sel.SelectedCount, 0) END, + ApprovedBudgetSnapshotAt = SYSUTCDATETIME() +FROM PurchaseEvaluations pe +LEFT JOIN PeWorkItemBudgets b + ON b.ProjectId = pe.ProjectId AND b.WorkItemId = pe.WorkItemId AND b.IsDeleted = 0 +OUTER APPLY ( + SELECT COUNT(*) AS SubmittedCount, + SUM(ISNULL(p.BudgetPeriodAmount, 0)) AS SubmittedTotal + FROM PurchaseEvaluations p + WHERE p.ProjectId = pe.ProjectId AND p.WorkItemId = pe.WorkItemId + AND p.Id <> pe.Id AND p.CreatedAt < pe.CreatedAt AND p.IsDeleted = 0 + AND p.Phase IN (10, 7) +) sub +OUTER APPLY ( + SELECT COUNT(*) AS SelectedCount + FROM PurchaseEvaluations p + WHERE p.ProjectId = pe.ProjectId AND p.WorkItemId = pe.WorkItemId + AND p.Id <> pe.Id AND p.CreatedAt < pe.CreatedAt AND p.IsDeleted = 0 + AND p.Phase = 7 + AND EXISTS (SELECT 1 FROM PurchaseEvaluationSuppliers s + WHERE s.PurchaseEvaluationId = p.Id AND s.IsWinner = 1) +) sel +OUTER APPLY ( + SELECT SUM(q.ThanhTien) AS SelectedTotal + FROM PurchaseEvaluations p + INNER JOIN PurchaseEvaluationSuppliers s ON s.PurchaseEvaluationId = p.Id + INNER JOIN PurchaseEvaluationQuotes q ON q.PurchaseEvaluationSupplierId = s.Id + WHERE p.ProjectId = pe.ProjectId AND p.WorkItemId = pe.WorkItemId + AND p.Id <> pe.Id AND p.CreatedAt < pe.CreatedAt AND p.IsDeleted = 0 + AND p.Phase = 7 AND q.IsSelected = 1 + AND EXISTS (SELECT 1 FROM PurchaseEvaluationSuppliers s2 + WHERE s2.PurchaseEvaluationId = p.Id AND s2.IsWinner = 1) +) selt +WHERE pe.Phase = 7 AND pe.ApprovedBudgetSnapshotAt IS NULL AND pe.IsDeleted = 0;"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ApprovedBudgetCcmAdjustmentAmount", + table: "PurchaseEvaluations"); + + migrationBuilder.DropColumn( + name: "ApprovedBudgetCcmInitialAmount", + table: "PurchaseEvaluations"); + + migrationBuilder.DropColumn( + name: "ApprovedBudgetCcmNote", + table: "PurchaseEvaluations"); + + migrationBuilder.DropColumn( + name: "ApprovedBudgetPrevSelectedCount", + table: "PurchaseEvaluations"); + + migrationBuilder.DropColumn( + name: "ApprovedBudgetPrevSelectedTotal", + table: "PurchaseEvaluations"); + + migrationBuilder.DropColumn( + name: "ApprovedBudgetPrevSubmittedCount", + table: "PurchaseEvaluations"); + + migrationBuilder.DropColumn( + name: "ApprovedBudgetPrevSubmittedTotal", + table: "PurchaseEvaluations"); + + migrationBuilder.DropColumn( + name: "ApprovedBudgetProAdjustmentAmount", + table: "PurchaseEvaluations"); + + migrationBuilder.DropColumn( + name: "ApprovedBudgetProInitialAmount", + table: "PurchaseEvaluations"); + + migrationBuilder.DropColumn( + name: "ApprovedBudgetProNote", + table: "PurchaseEvaluations"); + + migrationBuilder.DropColumn( + name: "ApprovedBudgetSnapshotAt", + table: "PurchaseEvaluations"); + } + } +} diff --git a/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs index b70d09c..0b603cd 100644 --- a/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs @@ -4682,6 +4682,47 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations b.Property("ApprovalWorkflowId") .HasColumnType("uniqueidentifier"); + b.Property("ApprovedBudgetCcmAdjustmentAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ApprovedBudgetCcmInitialAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ApprovedBudgetCcmNote") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("ApprovedBudgetPrevSelectedCount") + .HasColumnType("int"); + + b.Property("ApprovedBudgetPrevSelectedTotal") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ApprovedBudgetPrevSubmittedCount") + .HasColumnType("int"); + + b.Property("ApprovedBudgetPrevSubmittedTotal") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ApprovedBudgetProAdjustmentAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ApprovedBudgetProInitialAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ApprovedBudgetProNote") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("ApprovedBudgetSnapshotAt") + .HasColumnType("datetime2"); + b.Property("ApprovedPriceAmount") .HasPrecision(18, 2) .HasColumnType("decimal(18,2)"); diff --git a/src/Backend/SolutionErp.Infrastructure/Services/PurchaseEvaluationWorkflowService.cs b/src/Backend/SolutionErp.Infrastructure/Services/PurchaseEvaluationWorkflowService.cs index d2d5dbd..4d4b4fa 100644 --- a/src/Backend/SolutionErp.Infrastructure/Services/PurchaseEvaluationWorkflowService.cs +++ b/src/Backend/SolutionErp.Infrastructure/Services/PurchaseEvaluationWorkflowService.cs @@ -305,6 +305,10 @@ public class PurchaseEvaluationWorkflowService( // Admin manual override (vd test cứng phase) if (isAdmin) { + // [S133 site 5 — reviewer catch] Override thẳng → DaDuyet cũng PHẢI chốt snapshot NS + // (RULE helper; assignment qua BIẾN targetPhase nên literal-grep 4-site không thấy — #81). + if (targetPhase == PurchaseEvaluationPhase.DaDuyet) + await ApplyBudgetSnapshotOnFinalizeAsync(evaluation, ct); evaluation.Phase = targetPhase; evaluation.SlaDeadline = targetPhase == PurchaseEvaluationPhase.ChoDuyet ? dateTime.UtcNow.AddDays(7) : null; @@ -858,6 +862,7 @@ public class PurchaseEvaluationWorkflowService( // → EndsBeforeCeo display đọc cờ này thay vì suy từ config (phiếu opt-out lên CEO hiện đúng). evaluation.EndedByLevelFinalize = true; ApplyApprovedPriceOnFinalize(evaluation, isSystem, approvedPriceAmount, approvedPriceSource, approvedPriceMaxAmount); + await ApplyBudgetSnapshotOnFinalizeAsync(evaluation, ct); // [S133] chốt NS trước DaDuyet evaluation.Phase = PurchaseEvaluationPhase.DaDuyet; evaluation.CurrentWorkflowStepIndex = null; evaluation.CurrentApprovalLevelOrder = null; @@ -896,6 +901,7 @@ public class PurchaseEvaluationWorkflowService( $"Giá gói {winnerQuoteTotal:N0}đ ≥ ngưỡng CEO {ceoThreshold:N0}đ — phải trình CEO duyệt, không được duyệt done miễn CEO."); ApplyApprovedPriceOnFinalize(evaluation, isSystem, approvedPriceAmount, approvedPriceSource, approvedPriceMaxAmount); + await ApplyBudgetSnapshotOnFinalizeAsync(evaluation, ct); // [S133] chốt NS trước DaDuyet evaluation.Phase = PurchaseEvaluationPhase.DaDuyet; evaluation.CurrentWorkflowStepIndex = null; evaluation.CurrentApprovalLevelOrder = null; @@ -928,6 +934,7 @@ public class PurchaseEvaluationWorkflowService( // All Steps done — terminal DaDuyet. [Mig 54 ①] người duyệt cấp cuối (CEO/NV // cuối) chọn giá chốt khi duyệt — bind trước khi sang DaDuyet. ApplyApprovedPriceOnFinalize(evaluation, isSystem, approvedPriceAmount, approvedPriceSource, approvedPriceMaxAmount); + await ApplyBudgetSnapshotOnFinalizeAsync(evaluation, ct); // [S133] chốt NS trước DaDuyet evaluation.Phase = PurchaseEvaluationPhase.DaDuyet; evaluation.CurrentWorkflowStepIndex = null; evaluation.CurrentApprovalLevelOrder = null; @@ -983,6 +990,56 @@ public class PurchaseEvaluationWorkflowService( evaluation.ApprovedPriceMaxAmount = source == ProMinMaxSource ? maxAmount : null; } + // [S133 2026-07-17 — anh Kiệt FDC] Chụp (snapshot) bộ số ngân sách gói thầu vào 11 cột + // ApprovedBudget* trên phiếu TẠI thời điểm chuyển DaDuyet → phiếu ĐÃ DUYỆT bất biến số NS + // (display-gate `frozen` ở GetPurchaseEvaluationQuery); record PeWorkItemBudgets dùng chung + // vẫn sửa tự do cho phiếu CHƯA duyệt. LUÔN overwrite + LUÔN set SnapshotAt (= cờ "đã chốt"). + // RULE: MỌI nhánh set Phase=DaDuyet PHẢI gọi helper này ngay TRƯỚC assignment (reviewer grep-check 4 site). + private async Task ApplyBudgetSnapshotOnFinalizeAsync(PurchaseEvaluation evaluation, CancellationToken ct) + { + if (evaluation.WorkItemId is Guid wiKey) + { + // 6 cột pair (matrix PRO/CCM) — pair thiếu → null. NAMING: Ccm* = cột CCM + // (PeWorkItemBudget.InitialAmount/AdjustmentAmount); Pro* = cột PRO. + var pair = await db.PeWorkItemBudgets.AsNoTracking() + .FirstOrDefaultAsync(b => b.ProjectId == evaluation.ProjectId && b.WorkItemId == wiKey, ct); + evaluation.ApprovedBudgetProInitialAmount = pair?.ProInitialAmount; + evaluation.ApprovedBudgetProAdjustmentAmount = pair?.ProAdjustmentAmount; + evaluation.ApprovedBudgetProNote = pair?.ProNote; + evaluation.ApprovedBudgetCcmInitialAmount = pair?.InitialAmount; + evaluation.ApprovedBudgetCcmAdjustmentAmount = pair?.AdjustmentAmount; + evaluation.ApprovedBudgetCcmNote = pair?.CcmNote; + + // 4 cột lũy kế qua accumulator (BEHAVIOR y GetPurchaseEvaluationQuery live). + var supplierRowIds = await db.PurchaseEvaluationSuppliers.AsNoTracking() + .Where(s => s.PurchaseEvaluationId == evaluation.Id) + .Select(s => s.Id) + .ToListAsync(ct); + var acc = await SolutionErp.Application.PurchaseEvaluations.PeBudgetAccumulator.ComputeAsync( + db, evaluation.ProjectId, wiKey, evaluation.Id, evaluation.CreatedAt, supplierRowIds, ct); + evaluation.ApprovedBudgetPrevSubmittedTotal = acc.PrevSubmittedTotal; + evaluation.ApprovedBudgetPrevSubmittedCount = acc.PrevSubmittedCount; + evaluation.ApprovedBudgetPrevSelectedTotal = acc.PrevSelectedTotal; + evaluation.ApprovedBudgetPrevSelectedCount = acc.PrevSelectedCount; + } + else + { + // WorkItemId null → không có cặp gói thầu → 6 cột pair + 4 lũy kế = null. + evaluation.ApprovedBudgetProInitialAmount = null; + evaluation.ApprovedBudgetProAdjustmentAmount = null; + evaluation.ApprovedBudgetProNote = null; + evaluation.ApprovedBudgetCcmInitialAmount = null; + evaluation.ApprovedBudgetCcmAdjustmentAmount = null; + evaluation.ApprovedBudgetCcmNote = null; + evaluation.ApprovedBudgetPrevSubmittedTotal = null; + evaluation.ApprovedBudgetPrevSubmittedCount = null; + evaluation.ApprovedBudgetPrevSelectedTotal = null; + evaluation.ApprovedBudgetPrevSelectedCount = null; + } + // LUÔN set = cờ "đã chốt" cho phiếu DaDuyet (UTC — gotcha #72). + evaluation.ApprovedBudgetSnapshotAt = dateTime.UtcNow; + } + // ===== V1 legacy (Mig 21) — iterate PurchaseEvaluationWorkflowSteps ===== private async Task ApproveV1LegacyAsync( PurchaseEvaluation evaluation, @@ -1050,6 +1107,9 @@ public class PurchaseEvaluationWorkflowService( var nextIdx = currentIdx + 1; if (nextIdx >= steps.Count) { + // [S133] V1 legacy: CHỈ chốt budget-snapshot (KHÔNG ApplyApprovedPriceOnFinalize — V1 + // không có luồng giá-chốt). Ngay TRƯỚC assignment Phase=DaDuyet. + await ApplyBudgetSnapshotOnFinalizeAsync(evaluation, ct); evaluation.Phase = PurchaseEvaluationPhase.DaDuyet; evaluation.CurrentWorkflowStepIndex = null; evaluation.SlaDeadline = null; diff --git a/tests/SolutionErp.Infrastructure.Tests/Services/PeBudgetFreezeTests.cs b/tests/SolutionErp.Infrastructure.Tests/Services/PeBudgetFreezeTests.cs new file mode 100644 index 0000000..225d516 --- /dev/null +++ b/tests/SolutionErp.Infrastructure.Tests/Services/PeBudgetFreezeTests.cs @@ -0,0 +1,906 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using SolutionErp.Application.Common.Exceptions; // ConflictException (T5/T6 guards) +using SolutionErp.Application.PurchaseEvaluations; +using SolutionErp.Domain.ApprovalWorkflowsV2; +using SolutionErp.Domain.Contracts; // ApprovalDecision enum (shared HĐ/PE) +using SolutionErp.Domain.Identity; +using SolutionErp.Domain.Master; +using SolutionErp.Domain.Master.Catalogs; +using SolutionErp.Domain.PurchaseEvaluations; +using SolutionErp.Infrastructure.Services; +using SolutionErp.Infrastructure.Tests.Common; + +namespace SolutionErp.Infrastructure.Tests.Services; + +// ===== S133 (2026-07-17) T0 REPRODUCE — bug "phiếu DaDuyet đổi số ngân sách theo bảng LIVE" ===== +// UAT anh Kiệt: "các phiếu đã duyệt thì KHÔNG cho thay đổi gì hết". Hiện tại mọi phiếu cùng cặp +// (ProjectId × WorkItemId) đọc CHUNG 1 record PeWorkItemBudgets LIVE: +// - GetPurchaseEvaluationQueryHandler (PurchaseEvaluationFeatures.cs:911-912) load pairRec LIVE, +// KHÔNG phân biệt Phase → :964-972 build PeBudgetSummaryDto từ pairRec. +// ⇒ ai đó nhập lại ngân sách cho gói thầu (record dùng chung) thì phiếu ĐÃ DUYỆT cũng đổi số. +// +// TEST-BEFORE (bug fix = reproduce → fix, docs/rules.md §7). Test này PHẢI **RED** trên code hiện +// tại (đọc live → 999tr/888tr) và sẽ GREEN sau khi fix snapshot ngân sách tại thời điểm chuyển +// DaDuyet. KHÔNG touch production (src/Backend, fe-*), KHÔNG sửa test cũ — chỉ WRITE test này. +// +// ⚠️ Tiền đề T0 CHUẨN (MEMORY-SLICE): đưa phiếu tới DaDuyet QUA service finalize path (mirror +// PeApproverFinalizeTests test #5 AtLastSlot: workflow V2 1 Bước 1 Cấp AllowApproverFinalize=true, +// approver duyệt + giá chốt → TỰ DaDuyet). TUYỆT ĐỐI KHÔNG seed thẳng Phase=DaDuyet — vì sau fix, +// snapshot chỉ được set qua transition; seed thẳng sẽ làm T0 mất nghĩa (không có bản chụp để giữ). +public class PeBudgetFreezeTests +{ + // Giá chốt hợp lệ khi finalize (ApplyApprovedPriceOnFinalize đòi source ∈ ValidApprovedPriceSources). + private const decimal ValidApprovedPrice = 400_000_000m; + private const string ValidApprovedSource = "Ncc"; + + // Bộ số ngân sách TẠI thời điểm duyệt (snapshot phải giữ) vs SAU khi duyệt (record live bị đổi). + private const decimal ProAtApproval = 100_000_000m; // PRO ban hành lúc duyệt (kỳ vọng giữ) + private const decimal CcmAtApproval = 200_000_000m; // CCM ban hành lúc duyệt (kỳ vọng giữ) + private const decimal ProAfterApproval = 999_000_000m; // PRO đổi SAU khi duyệt (record live) + private const decimal CcmAfterApproval = 888_000_000m; // CCM đổi SAU khi duyệt (record live) + + private static (PurchaseEvaluationWorkflowService svc, IdentityFixture fix, + TestApplicationDbContext db) CreateService() + { + var fix = new IdentityFixture(); + var db = fix.Services.GetRequiredService(); + var um = fix.Services.GetRequiredService>(); + var clock = new FixedDateTime(new DateTime(2026, 7, 17, 0, 0, 0, DateTimeKind.Utc)); + var notify = new NoOpNotificationService(); + var svc = new PurchaseEvaluationWorkflowService(db, clock, notify, um); + return (svc, fix, db); + } + + // Workflow V2 1 Bước 1 Cấp, ApproverUserId = approver, AllowApproverFinalize=true → duyệt tới + // cấp này (slot cuối) = TỰ DaDuyet qua finalize-branch (PurchaseEvaluationWorkflowService.cs:855). + private static async Task SeedFinalizeWorkflowAsync( + TestApplicationDbContext db, Guid approverUserId, string code = "QT-BF-V2") + { + var wf = new ApprovalWorkflow + { + Code = code, // unique per call — UNIQUE(Code,Version) khi 1 test seed nhiều workflow + Version = 1, + ApplicableType = ApprovalWorkflowApplicableType.DuyetNcc, + Name = "QT test budget-freeze", + IsActive = true, + IsUserSelectable = true, + }; + var step = new ApprovalWorkflowStep + { + ApprovalWorkflowId = wf.Id, + Order = 1, + Name = "Bước 1", + }; + step.Levels.Add(new ApprovalWorkflowLevel + { + ApprovalWorkflowStepId = step.Id, + Order = 1, + Name = "Cấp 1", + ApproverUserId = approverUserId, + AllowApproverFinalize = true, // slot cuối = KẾT THÚC → duyệt là DaDuyet + }); + wf.Steps.Add(step); + db.ApprovalWorkflows.Add(wf); + await db.SaveChangesAsync(CancellationToken.None); + return wf; + } + + // ===================================================================== + // T0 REPRODUCE — phiếu DaDuyet PHẢI giữ ngân sách tại thời điểm duyệt (snapshot), + // KHÔNG chạy theo record PeWorkItemBudgets live bị đổi sau khi duyệt. + // RED hiện tại: budgetSummary đọc live → 999tr / 888tr. + // GREEN sau fix: snapshot → 100tr / 200tr. + // ===================================================================== + [Fact] + public async Task DaDuyet_BudgetSummary_KeepsSnapshotValue_DoesNotFollowLivePairUpdate() + { + var (svc, fix, db) = CreateService(); + using (fix) + { + // (a) Cặp (Dự án × Hạng mục) + record ngân sách gói thầu PRO=100tr / CCM=200tr. + var project = new Project { Id = Guid.NewGuid(), Code = "PRJ-BF", Name = "Dự án budget-freeze" }; + var wi = new WorkItem { Id = Guid.NewGuid(), Code = "WI-BF", Name = "Hạng mục budget-freeze", IsActive = true }; + db.Projects.Add(project); + db.WorkItems.Add(wi); + db.PeWorkItemBudgets.Add(new PeWorkItemBudget + { + Id = Guid.NewGuid(), + ProjectId = project.Id, + WorkItemId = wi.Id, + ProInitialAmount = ProAtApproval, // 100tr (PRO ban hành lúc duyệt) + InitialAmount = CcmAtApproval, // 200tr (CCM ban hành lúc duyệt) + }); + await db.SaveChangesAsync(CancellationToken.None); + + // approver = người duyệt cấp finalize; drafter = "user thường" xem phiếu sau khi duyệt. + var approver = (await fix.CreateUserAsync("appr@bf.test", "Approver", null, Array.Empty())).Id; + var drafter = (await fix.CreateUserAsync("drafter@bf.test", "Drafter", null, Array.Empty())).Id; + + var wf = await SeedFinalizeWorkflowAsync(db, approver); + + // (b) Phiếu tại slot duyệt cuối (ChoDuyet) gắn ĐÚNG cặp + workflow finalize. + var pe = new PurchaseEvaluation + { + Id = Guid.NewGuid(), + Type = PurchaseEvaluationType.DuyetNcc, + Phase = PurchaseEvaluationPhase.ChoDuyet, + MaPhieu = "PE-BF-001", + TenGoiThau = "Gói thầu budget-freeze", + ProjectId = project.Id, + WorkItemId = wi.Id, + DrafterUserId = drafter, + ApprovalWorkflowId = wf.Id, + CurrentWorkflowStepIndex = 0, + CurrentApprovalLevelOrder = 1, + SlaDeadline = new DateTime(2026, 7, 24, 0, 0, 0, DateTimeKind.Utc), + }; + db.PurchaseEvaluations.Add(pe); + await db.SaveChangesAsync(CancellationToken.None); + + // (b') Đưa tới DaDuyet QUA service finalize path (KHÔNG seed thẳng Phase=DaDuyet). + await svc.TransitionAsync( + evaluation: pe, + targetPhase: PurchaseEvaluationPhase.ChoDuyet, // approve-in-place + actorUserId: approver, + actorRoles: Array.Empty(), + decision: ApprovalDecision.Approve, + comment: null, + approvedPriceAmount: ValidApprovedPrice, + approvedPriceSource: ValidApprovedSource, + ct: CancellationToken.None); + + pe.Phase.Should().Be(PurchaseEvaluationPhase.DaDuyet, + "tiền đề T0: phiếu PHẢI ở DaDuyet qua finalize path (mirror PeApproverFinalizeTests), không seed thẳng"); + + // (c) SAU khi duyệt, đổi record ngân sách LIVE: PRO 100tr→999tr, CCM 200tr→888tr (Admin). + // UpdatePeBudgetPro/Ccm KHÔNG có phase-guard → chạy được ở DaDuyet (đây chính là bug: + // pair dùng chung bị đổi → phiếu đã duyệt cũng đổi số). + var admin = new TestCurrentUser(Guid.NewGuid(), "Admin", null, AppRoles.Admin); + await new UpdatePeBudgetProCommandHandler(db, admin) + .Handle(new UpdatePeBudgetProCommand(pe.Id, ProAfterApproval, null, "đổi sau khi duyệt"), + CancellationToken.None); + await new UpdatePeBudgetCcmCommandHandler(db, admin) + .Handle(new UpdatePeBudgetCcmCommand(pe.Id, CcmAfterApproval, null, "đổi sau khi duyệt"), + CancellationToken.None); + + // Sanity: record LIVE thật sự đã đổi (chứng minh update ăn — assert (d) không vacuous-pass). + var liveRec = await db.PeWorkItemBudgets.AsNoTracking() + .SingleAsync(b => b.ProjectId == project.Id && b.WorkItemId == wi.Id); + liveRec.ProInitialAmount.Should().Be(ProAfterApproval, "record LIVE đã đổi PRO → 999tr"); + liveRec.InitialAmount.Should().Be(CcmAfterApproval, "record LIVE đã đổi CCM → 888tr"); + + // (d) "user thường" (drafter, isDrafter=true qua authz) xem phiếu DaDuyet → budgetSummary + // PHẢI GIỮ số tại thời điểm duyệt (snapshot), KHÔNG chạy theo record live mới đổi. + var querier = new TestCurrentUser(drafter); // no roles = user thường + var handler = new GetPurchaseEvaluationQueryHandler( + db, fix.Services.GetRequiredService>(), querier); + var bundle = await handler.Handle(new GetPurchaseEvaluationQuery(pe.Id), CancellationToken.None); + + bundle.BudgetSummary.Should().NotBeNull("phiếu có WorkItemId → budgetSummary được build"); + var s = bundle.BudgetSummary!; + + // ⭐ RED trên code hiện tại (đọc live → 999tr / 888tr). GREEN sau fix snapshot (100tr / 200tr). + s.ProInitialAmount.Should().Be(ProAtApproval, + "phiếu DaDuyet phải GIỮ ngân sách PRO tại thời điểm duyệt (100tr), KHÔNG chạy theo record live (999tr)"); + s.InitialAmount.Should().Be(CcmAtApproval, + "phiếu DaDuyet phải GIỮ ngân sách CCM tại thời điểm duyệt (200tr), KHÔNG chạy theo record live (888tr)"); + } + } + + // ===================================================================== + // ===== INV-C (S133) T1-T9 — phủ 4 nhánh snapshot + 2 guard + edge ===== + // BE ĐÃ LAND (11 cột ApprovedBudget* + helper ApplyBudgetSnapshotOnFinalizeAsync 4 site + // + display-gate frozen/live + 2 terminal-guard). T1-T9 GREEN ngay (KHÔNG RED phase — + // T0 là reproduce duy nhất). Test theo CODE (S34 single-source-of-truth). KHÔNG sửa T0. + // ===================================================================== + + // Bộ số ngân sách gói thầu TẠI thời điểm duyệt (snapshot phải giữ nguyên bộ này). + private const decimal PairProInitial = 100_000_000m; // pair.ProInitialAmount (PRO) + private const decimal PairProAdjust = -10_000_000m; // pair.ProAdjustmentAmount (PRO, ÂM hợp lệ) + private const string PairProNote = "PRO ban hành Q3 — dự trù đơn giá"; + private const decimal PairCcmInitial = 200_000_000m; // pair.InitialAmount (CCM) + private const decimal PairCcmAdjust = -20_000_000m; // pair.AdjustmentAmount (CCM, ÂM hợp lệ) + private const string PairCcmNote = "CCM nguồn dự toán đã duyệt"; + + // Clock của CreateService (mirror T0) — SnapshotAt kỳ vọng = giá trị này. + private static readonly DateTime ClockNow = new(2026, 7, 17, 0, 0, 0, DateTimeKind.Utc); + + // ---- shared seed helpers (INV-C) ---- + + private static async Task<(Project project, WorkItem wi)> SeedProjectAndWorkItemAsync( + TestApplicationDbContext db, string suffix) + { + var project = new Project { Id = Guid.NewGuid(), Code = "PRJ-BF-" + suffix, Name = "Dự án " + suffix }; + var wi = new WorkItem { Id = Guid.NewGuid(), Code = "WI-BF-" + suffix, Name = "Hạng mục " + suffix, IsActive = true }; + db.Projects.Add(project); + db.WorkItems.Add(wi); + await db.SaveChangesAsync(CancellationToken.None); + return (project, wi); + } + + // 1 record PeWorkItemBudget per cặp (ProjectId × WorkItemId). NAMING: Initial/Adjustment = CCM. + private static async Task SeedPairAsync( + TestApplicationDbContext db, Guid projectId, Guid wiId, + decimal? proInit, decimal? proAdj, string? proNote, + decimal? ccmInit, decimal? ccmAdj, string? ccmNote) + { + db.PeWorkItemBudgets.Add(new PeWorkItemBudget + { + Id = Guid.NewGuid(), + ProjectId = projectId, + WorkItemId = wiId, + ProInitialAmount = proInit, + ProAdjustmentAmount = proAdj, + ProNote = proNote, + InitialAmount = ccmInit, + AdjustmentAmount = ccmAdj, + CcmNote = ccmNote, + }); + await db.SaveChangesAsync(CancellationToken.None); + } + + // PE đứng tại slot duyệt cuối V2 (ChoDuyet) gắn cặp + workflow đã pin. + private static PurchaseEvaluation BuildPeAtFinalizeSlot( + Guid approvalWorkflowId, Guid projectId, Guid? workItemId, Guid drafterUserId, string code) + => new() + { + Id = Guid.NewGuid(), + Type = PurchaseEvaluationType.DuyetNcc, + Phase = PurchaseEvaluationPhase.ChoDuyet, + MaPhieu = code, + TenGoiThau = "Gói thầu " + code, + ProjectId = projectId, + WorkItemId = workItemId, + DrafterUserId = drafterUserId, + ApprovalWorkflowId = approvalWorkflowId, + CurrentWorkflowStepIndex = 0, + CurrentApprovalLevelOrder = 1, + SlaDeadline = new DateTime(2026, 7, 24, 0, 0, 0, DateTimeKind.Utc), + }; + + // Approve-in-place + giá chốt hợp lệ (mọi nhánh finalize human cần giá chốt). + private static Task ApproveWithPriceAsync( + PurchaseEvaluationWorkflowService svc, PurchaseEvaluation pe, Guid approver, string[] roles) => + svc.TransitionAsync( + evaluation: pe, + targetPhase: PurchaseEvaluationPhase.ChoDuyet, + actorUserId: approver, + actorRoles: roles, + decision: ApprovalDecision.Approve, + comment: null, + approvedPriceAmount: ValidApprovedPrice, + approvedPriceSource: ValidApprovedSource, + ct: CancellationToken.None); + + // Seed 1 đơn vị IsWinner + 1 detail + 1 quote IsSelected cho phiếu (own quotes → winnerQuoteTotal). + private static async Task SeedWinnerQuoteAsync( + TestApplicationDbContext db, PurchaseEvaluation pe, decimal thanhTien) + { + var pes = new PurchaseEvaluationSupplier + { + Id = Guid.NewGuid(), + PurchaseEvaluationId = pe.Id, + SupplierId = Guid.NewGuid(), + Order = 0, + IsWinner = true, + }; + var detail = new PurchaseEvaluationDetail + { + Id = Guid.NewGuid(), + PurchaseEvaluationId = pe.Id, + GroupCode = "A.I", + GroupName = "Bê tông", + NoiDung = "Concrete", + Order = 0, + }; + db.PurchaseEvaluationSuppliers.Add(pes); + db.PurchaseEvaluationDetails.Add(detail); + db.PurchaseEvaluationQuotes.Add(new PurchaseEvaluationQuote + { + Id = Guid.NewGuid(), + PurchaseEvaluationDetailId = detail.Id, + PurchaseEvaluationSupplierId = pes.Id, + ThanhTien = thanhTien, + IsSelected = true, + }); + await db.SaveChangesAsync(CancellationToken.None); + } + + // Peer PE cùng cặp cho accumulator (lũy kế phiếu TRƯỚC). CreatedAt override thủ công + // (TestApplicationDbContext KHÔNG wire audit interceptor) để < CreatedAt phiếu đang xét. + private static async Task SeedPeerAsync( + TestApplicationDbContext db, Guid projectId, Guid wiId, + PurchaseEvaluationPhase phase, decimal? budgetPeriod, DateTime createdAt, + decimal? winnerQuote, string code) + { + var peer = new PurchaseEvaluation + { + Id = Guid.NewGuid(), + Type = PurchaseEvaluationType.DuyetNcc, + Phase = phase, + MaPhieu = code, + TenGoiThau = "Peer " + code, + ProjectId = projectId, + WorkItemId = wiId, + DrafterUserId = Guid.NewGuid(), + BudgetPeriodAmount = budgetPeriod, + }; + db.PurchaseEvaluations.Add(peer); + await db.SaveChangesAsync(CancellationToken.None); + peer.CreatedAt = createdAt; // accumulator dùng CreatedAt < this + await db.SaveChangesAsync(CancellationToken.None); + if (winnerQuote is decimal wq) + await SeedWinnerQuoteAsync(db, peer, wq); + } + + // Workflow V2 1 Bước 1 Cấp KHÔNG finalize-flag KHÔNG threshold → approve slot cuối = + // nhánh all-steps-done (:933). Khác SeedFinalizeWorkflowAsync (AllowApproverFinalize=true, :861). + private static async Task SeedPlainWorkflowAsync( + TestApplicationDbContext db, Guid approverUserId) + { + var wf = new ApprovalWorkflow + { + Code = "QT-BF-PLAIN", + Version = 1, + ApplicableType = ApprovalWorkflowApplicableType.DuyetNcc, + Name = "QT budget-freeze all-steps", + IsActive = true, + IsUserSelectable = true, + }; + var step = new ApprovalWorkflowStep { ApprovalWorkflowId = wf.Id, Order = 1, Name = "Bước 1" }; + step.Levels.Add(new ApprovalWorkflowLevel + { + ApprovalWorkflowStepId = step.Id, + Order = 1, + Name = "Cấp 1", + ApproverUserId = approverUserId, + }); + wf.Steps.Add(step); + db.ApprovalWorkflows.Add(wf); + await db.SaveChangesAsync(CancellationToken.None); + return wf; + } + + // Workflow V2 1 Bước 1 Cấp CCM + CeoApprovalThreshold → CCM tích uỷ-quyền finalize + // (nhánh :900). Cấp KHÔNG AllowApproverFinalize (mặc định) → rơi xuống ccm-delegation. + private static async Task SeedCcmThresholdWorkflowAsync( + TestApplicationDbContext db, Guid ccmUserId, decimal ceoThreshold) + { + var wf = new ApprovalWorkflow + { + Code = "QT-BF-CCM", + Version = 1, + ApplicableType = ApprovalWorkflowApplicableType.DuyetNcc, + Name = "QT budget-freeze CCM threshold", + IsActive = true, + IsUserSelectable = true, + CeoApprovalThreshold = ceoThreshold, + }; + var step = new ApprovalWorkflowStep { ApprovalWorkflowId = wf.Id, Order = 1, Name = "Bước 1" }; + step.Levels.Add(new ApprovalWorkflowLevel + { + ApprovalWorkflowStepId = step.Id, + Order = 1, + Name = "Cấp 1 (CCM)", + ApproverUserId = ccmUserId, + }); + wf.Steps.Add(step); + db.ApprovalWorkflows.Add(wf); + await db.SaveChangesAsync(CancellationToken.None); + return wf; + } + + // Seed cặp (full pair 6 cột) + workflow level-finalize + PE ChoDuyet → finalize qua service → + // trả PE ĐÃ DaDuyet (snapshot đã chốt). Reuse cho T5/T6/T7(frozen). withWorkItem/seedPair tuỳ chọn. + private static async Task<(PurchaseEvaluation pe, Guid drafter, Project project, WorkItem wi)> + SeedAndFinalizeAsync( + PurchaseEvaluationWorkflowService svc, IdentityFixture fix, TestApplicationDbContext db, + string suffix, bool withWorkItem = true, bool seedPair = true) + { + var (project, wi) = await SeedProjectAndWorkItemAsync(db, suffix); + Guid? wiId = withWorkItem ? wi.Id : null; + if (seedPair && withWorkItem) + await SeedPairAsync(db, project.Id, wi.Id, + PairProInitial, PairProAdjust, PairProNote, PairCcmInitial, PairCcmAdjust, PairCcmNote); + + var approver = (await fix.CreateUserAsync($"appr-{suffix}@bf.test", "Approver", null, Array.Empty())).Id; + var drafter = (await fix.CreateUserAsync($"drafter-{suffix}@bf.test", "Drafter", null, Array.Empty())).Id; + var wf = await SeedFinalizeWorkflowAsync(db, approver, $"QT-BF-V2-{suffix}"); + + var pe = BuildPeAtFinalizeSlot(wf.Id, project.Id, wiId, drafter, $"PE-BF-{suffix}"); + db.PurchaseEvaluations.Add(pe); + await db.SaveChangesAsync(CancellationToken.None); + await ApproveWithPriceAsync(svc, pe, approver, Array.Empty()); + pe.Phase.Should().Be(PurchaseEvaluationPhase.DaDuyet, "tiền đề helper: PE phải ở DaDuyet qua finalize path"); + return (pe, drafter, project, wi); + } + + // ===================================================================== + // T1 — LEVEL-FINALIZE (:861) chốt ĐỦ 11 cột snapshot: 6 cột pair (PRO/CCM Initial+ + // Adjustment+Note) + 4 cột lũy kế (accumulator, theo 2 peer seed) + SnapshotAt. + // (T0 chỉ assert 2 số — T1 phủ toàn bộ bộ chụp.) + // ===================================================================== + [Fact] + public async Task LevelFinalize_SnapshotsAll11Columns_FromPairAndAccumulator() + { + var (svc, fix, db) = CreateService(); + using (fix) + { + var (project, wi) = await SeedProjectAndWorkItemAsync(db, "T1"); + await SeedPairAsync(db, project.Id, wi.Id, + PairProInitial, PairProAdjust, PairProNote, PairCcmInitial, PairCcmAdjust, PairCcmNote); + + // 2 peer cùng cặp (CreatedAt sớm hơn) → lũy kế xác định: + // peer DaDuyet: budget 100tr (PrevSubmitted) + winner quote 90tr (PrevSelected). + // peer ChoDuyet: budget 50tr (PrevSubmitted, KHÔNG PrevSelected). + var baseT = new DateTime(2026, 7, 17, 8, 0, 0, DateTimeKind.Utc); + await SeedPeerAsync(db, project.Id, wi.Id, PurchaseEvaluationPhase.DaDuyet, + budgetPeriod: 100_000_000m, createdAt: baseT.AddDays(-3), winnerQuote: 90_000_000m, code: "PE-BF-T1-DD"); + await SeedPeerAsync(db, project.Id, wi.Id, PurchaseEvaluationPhase.ChoDuyet, + budgetPeriod: 50_000_000m, createdAt: baseT.AddDays(-2), winnerQuote: null, code: "PE-BF-T1-CD"); + + var approver = (await fix.CreateUserAsync("appr@t1.test", "Approver", null, Array.Empty())).Id; + var drafter = (await fix.CreateUserAsync("drafter@t1.test", "Drafter", null, Array.Empty())).Id; + var wf = await SeedFinalizeWorkflowAsync(db, approver); + + var pe = BuildPeAtFinalizeSlot(wf.Id, project.Id, wi.Id, drafter, "PE-BF-T1"); + db.PurchaseEvaluations.Add(pe); + await db.SaveChangesAsync(CancellationToken.None); + pe.CreatedAt = baseT; // muộn hơn 2 peer → cả 2 lọt lũy kế + await db.SaveChangesAsync(CancellationToken.None); + + await ApproveWithPriceAsync(svc, pe, approver, Array.Empty()); + pe.Phase.Should().Be(PurchaseEvaluationPhase.DaDuyet); + + var s = await db.PurchaseEvaluations.AsNoTracking().SingleAsync(x => x.Id == pe.Id); + // 6 cột pair (NAMING: Pro* = PRO; Ccm* = CCM). + s.ApprovedBudgetProInitialAmount.Should().Be(PairProInitial, "chốt PRO ban hành từ pair"); + s.ApprovedBudgetProAdjustmentAmount.Should().Be(PairProAdjust, "chốt PRO hiệu chỉnh (ÂM) từ pair"); + s.ApprovedBudgetProNote.Should().Be(PairProNote, "chốt ghi chú PRO từ pair"); + s.ApprovedBudgetCcmInitialAmount.Should().Be(PairCcmInitial, "chốt CCM ban hành từ pair.InitialAmount"); + s.ApprovedBudgetCcmAdjustmentAmount.Should().Be(PairCcmAdjust, "chốt CCM hiệu chỉnh (ÂM) từ pair.AdjustmentAmount"); + s.ApprovedBudgetCcmNote.Should().Be(PairCcmNote, "chốt ghi chú CCM từ pair.CcmNote"); + // 4 cột lũy kế từ accumulator. + s.ApprovedBudgetPrevSubmittedTotal.Should().Be(150_000_000m, "peer DaDuyet 100tr + ChoDuyet 50tr"); + s.ApprovedBudgetPrevSubmittedCount.Should().Be(2); + s.ApprovedBudgetPrevSelectedTotal.Should().Be(90_000_000m, "chỉ peer DaDuyet có winner quote"); + s.ApprovedBudgetPrevSelectedCount.Should().Be(1); + // Cờ đã-chốt. + s.ApprovedBudgetSnapshotAt.Should().NotBeNull("SnapshotAt LUÔN set khi finalize = cờ đã chốt"); + } + } + + // ===================================================================== + // T2 — CCM-THRESHOLD delegation (:900): CCM tích uỷ-quyền + gói < ngưỡng CEO → + // DaDuyet qua nhánh delegation → snapshot ngân sách set (pair cols + SnapshotAt). + // ===================================================================== + [Fact] + public async Task CcmThresholdFinalize_SetsBudgetSnapshot() + { + var (svc, fix, db) = CreateService(); + using (fix) + { + var (project, wi) = await SeedProjectAndWorkItemAsync(db, "T2"); + await SeedPairAsync(db, project.Id, wi.Id, + PairProInitial, null, null, PairCcmInitial, null, null); + + var ccm = (await fix.CreateUserAsync("ccm@t2.test", "CCM", null, new[] { AppRoles.CostControl })).Id; + var wf = await SeedCcmThresholdWorkflowAsync(db, ccm, ceoThreshold: 1_000_000_000m); + + var pe = BuildPeAtFinalizeSlot(wf.Id, project.Id, wi.Id, Guid.NewGuid(), "PE-BF-T2"); + db.PurchaseEvaluations.Add(pe); + await db.SaveChangesAsync(CancellationToken.None); + // Gói 400tr < ngưỡng 1 tỷ (winnerQuoteTotal = SUM quote IsSelected của phiếu). + await SeedWinnerQuoteAsync(db, pe, ValidApprovedPrice); + + await svc.TransitionAsync( + evaluation: pe, + targetPhase: PurchaseEvaluationPhase.ChoDuyet, + actorUserId: ccm, + actorRoles: new[] { AppRoles.CostControl }, + decision: ApprovalDecision.Approve, + comment: null, + finalizeByCcmDelegation: true, + approvedPriceAmount: ValidApprovedPrice, + approvedPriceSource: ValidApprovedSource, + ct: CancellationToken.None); + + pe.Phase.Should().Be(PurchaseEvaluationPhase.DaDuyet, "CCM uỷ-quyền + gói < ngưỡng → DaDuyet (nhánh delegation)"); + + var s = await db.PurchaseEvaluations.AsNoTracking().SingleAsync(x => x.Id == pe.Id); + s.ApprovedBudgetSnapshotAt.Should().NotBeNull("nhánh CCM-threshold PHẢI chốt snapshot"); + s.ApprovedBudgetProInitialAmount.Should().Be(PairProInitial, "chốt PRO từ pair tại nhánh delegation"); + s.ApprovedBudgetCcmInitialAmount.Should().Be(PairCcmInitial, "chốt CCM từ pair tại nhánh delegation"); + } + } + + // ===================================================================== + // T3 — ALL-STEPS-DONE (:933): 1 Bước 1 Cấp KHÔNG finalize-flag → approve slot cuối → + // terminal DaDuyet nhánh all-steps → snapshot set. + // ===================================================================== + [Fact] + public async Task AllStepsDoneFinalize_SetsBudgetSnapshot() + { + var (svc, fix, db) = CreateService(); + using (fix) + { + var (project, wi) = await SeedProjectAndWorkItemAsync(db, "T3"); + await SeedPairAsync(db, project.Id, wi.Id, + PairProInitial, PairProAdjust, PairProNote, PairCcmInitial, PairCcmAdjust, PairCcmNote); + + var approver = (await fix.CreateUserAsync("appr@t3.test", "Approver", null, Array.Empty())).Id; + var wf = await SeedPlainWorkflowAsync(db, approver); // AllowApproverFinalize=false + + var pe = BuildPeAtFinalizeSlot(wf.Id, project.Id, wi.Id, Guid.NewGuid(), "PE-BF-T3"); + db.PurchaseEvaluations.Add(pe); + await db.SaveChangesAsync(CancellationToken.None); + + await ApproveWithPriceAsync(svc, pe, approver, Array.Empty()); + + pe.Phase.Should().Be(PurchaseEvaluationPhase.DaDuyet, "slot cuối, hết bước → terminal DaDuyet (all-steps)"); + var s = await db.PurchaseEvaluations.AsNoTracking().SingleAsync(x => x.Id == pe.Id); + s.ApprovedBudgetSnapshotAt.Should().NotBeNull("nhánh all-steps-done PHẢI chốt snapshot"); + s.ApprovedBudgetProInitialAmount.Should().Be(PairProInitial); + s.ApprovedBudgetCcmInitialAmount.Should().Be(PairCcmInitial); + } + } + + // ===================================================================== + // T4 — V1 LEGACY (:1108): phiếu pin WorkflowDefinitionId (V1 flat) đi hết steps → + // nhánh V1 → snapshot set NHƯNG ApprovedPriceAmount KHÔNG set (V1 không có luồng giá-chốt). + // ===================================================================== + [Fact] + public async Task V1LegacyFinalize_SetsBudgetSnapshot_WithoutApprovedPrice() + { + var (svc, fix, db) = CreateService(); + using (fix) + { + var (project, wi) = await SeedProjectAndWorkItemAsync(db, "T4"); + await SeedPairAsync(db, project.Id, wi.Id, + PairProInitial, PairProAdjust, PairProNote, PairCcmInitial, PairCcmAdjust, PairCcmNote); + + var approver = (await fix.CreateUserAsync("appr@t4.test", "Approver V1", null, Array.Empty())).Id; + + // V1 workflow definition: 1 step, approver = explicit User (match qua userManager). + var def = new PurchaseEvaluationWorkflowDefinition + { + Id = Guid.NewGuid(), + Code = "PE-V1-BF", + Version = 1, + EvaluationType = PurchaseEvaluationType.DuyetNcc, + Name = "V1 legacy budget-freeze", + IsActive = true, + }; + var step = new PurchaseEvaluationWorkflowStep + { + Id = Guid.NewGuid(), + PurchaseEvaluationWorkflowDefinitionId = def.Id, + Order = 1, + Phase = PurchaseEvaluationPhase.ChoDuyet, + Name = "Bước 1 (V1)", + }; + step.Approvers.Add(new PurchaseEvaluationWorkflowStepApprover + { + Id = Guid.NewGuid(), + PurchaseEvaluationWorkflowStepId = step.Id, + Kind = WorkflowApproverKind.User, + AssignmentValue = approver.ToString(), + }); + def.Steps.Add(step); + db.PurchaseEvaluationWorkflowDefinitions.Add(def); + await db.SaveChangesAsync(CancellationToken.None); + + // PE pin V1 (WorkflowDefinitionId) — ApprovalWorkflowId=null → route ApproveV1LegacyAsync. + var pe = new PurchaseEvaluation + { + Id = Guid.NewGuid(), + Type = PurchaseEvaluationType.DuyetNcc, + Phase = PurchaseEvaluationPhase.ChoDuyet, + MaPhieu = "PE-BF-T4", + TenGoiThau = "Gói thầu V1 legacy", + ProjectId = project.Id, + WorkItemId = wi.Id, + DrafterUserId = Guid.NewGuid(), + WorkflowDefinitionId = def.Id, + ApprovalWorkflowId = null, + CurrentWorkflowStepIndex = 0, + SlaDeadline = new DateTime(2026, 7, 24, 0, 0, 0, DateTimeKind.Utc), + }; + db.PurchaseEvaluations.Add(pe); + await db.SaveChangesAsync(CancellationToken.None); + + // V1 KHÔNG có luồng giá-chốt → KHÔNG truyền approvedPrice. + await svc.TransitionAsync( + evaluation: pe, + targetPhase: PurchaseEvaluationPhase.ChoDuyet, + actorUserId: approver, + actorRoles: Array.Empty(), + decision: ApprovalDecision.Approve, + comment: null, + ct: CancellationToken.None); + + pe.Phase.Should().Be(PurchaseEvaluationPhase.DaDuyet, "V1 legacy hết step → DaDuyet (:1108)"); + var s = await db.PurchaseEvaluations.AsNoTracking().SingleAsync(x => x.Id == pe.Id); + s.ApprovedBudgetSnapshotAt.Should().NotBeNull("V1 legacy PHẢI chốt snapshot (nhánh :1108)"); + s.ApprovedBudgetProInitialAmount.Should().Be(PairProInitial, "V1 vẫn chốt số ngân sách từ pair"); + s.ApprovedBudgetCcmInitialAmount.Should().Be(PairCcmInitial); + s.ApprovedPriceAmount.Should().BeNull("V1 legacy KHÔNG có luồng giá-chốt → ApprovedPriceAmount null"); + } + } + + // ===================================================================== + // T5 — GUARD SetPeCcmBudgetPeriod: phiếu DaDuyet → CCM VÀ Admin đều ConflictException + // (guard đặt TRƯỚC role-gate → chặn cả 2). Phiếu ChoDuyet → CCM vẫn set được (guard không lạm). + // ===================================================================== + [Fact] + public async Task SetCcmBudgetPeriodGuard_BlocksDaDuyet_ForCcmAndAdmin_AllowsChoDuyet() + { + var (svc, fix, db) = CreateService(); + using (fix) + { + // Phiếu DaDuyet THẬT qua finalize path. + var (peDone, _, _, _) = await SeedAndFinalizeAsync(svc, fix, db, "T5done"); + + var ccm = new TestCurrentUser(Guid.NewGuid(), "CCM", null, AppRoles.CostControl); + var admin = new TestCurrentUser(Guid.NewGuid(), "Admin", null, AppRoles.Admin); + + var byCcm = async () => await new SetPeCcmBudgetPeriodCommandHandler(db, ccm) + .Handle(new SetPeCcmBudgetPeriodCommand(peDone.Id, 123_000_000m), CancellationToken.None); + await byCcm.Should().ThrowAsync() + .WithMessage("*đã kết thúc*"); + + var byAdmin = async () => await new SetPeCcmBudgetPeriodCommandHandler(db, admin) + .Handle(new SetPeCcmBudgetPeriodCommand(peDone.Id, 456_000_000m), CancellationToken.None); + await byAdmin.Should().ThrowAsync() + .WithMessage("*đã kết thúc*", "guard chặn CẢ Admin (đặt TRƯỚC role-gate)"); + + // Phiếu ChoDuyet (chưa duyệt) → CCM set BÌNH THƯỜNG (guard không lạm sang phiếu chưa kết thúc). + var (project, wi) = await SeedProjectAndWorkItemAsync(db, "T5open"); + var peOpen = new PurchaseEvaluation + { + Id = Guid.NewGuid(), + Type = PurchaseEvaluationType.DuyetNcc, + Phase = PurchaseEvaluationPhase.ChoDuyet, + MaPhieu = "PE-BF-T5-OPEN", + TenGoiThau = "Gói thầu chưa duyệt", + ProjectId = project.Id, + WorkItemId = wi.Id, + DrafterUserId = Guid.NewGuid(), + }; + db.PurchaseEvaluations.Add(peOpen); + await db.SaveChangesAsync(CancellationToken.None); + + await new SetPeCcmBudgetPeriodCommandHandler(db, ccm) + .Handle(new SetPeCcmBudgetPeriodCommand(peOpen.Id, 77_000_000m), CancellationToken.None); + var reloaded = await db.PurchaseEvaluations.AsNoTracking().SingleAsync(x => x.Id == peOpen.Id); + reloaded.CcmBudgetPeriodAmount.Should().Be(77_000_000m, "phiếu ChoDuyet → CCM vẫn nhập NS kỳ này được"); + } + } + + // ===================================================================== + // T6 — GUARD AdjustBudget: phiếu DaDuyet (qua finalize) + Admin → Conflict; phiếu + // TuChoi (seed thẳng — SPEC cho phép vì guard chỉ đọc Phase) + Admin → Conflict. + // ===================================================================== + [Fact] + public async Task AdjustBudgetGuard_BlocksDaDuyetAndTuChoi_ForAdmin() + { + var (svc, fix, db) = CreateService(); + using (fix) + { + var admin = new TestCurrentUser(Guid.NewGuid(), "Admin", null, AppRoles.Admin); + + // (a) DaDuyet THẬT qua finalize → Admin adjust → Conflict. + var (peDone, _, _, _) = await SeedAndFinalizeAsync(svc, fix, db, "T6done"); + var adjDone = async () => await new AdjustPurchaseEvaluationBudgetCommandHandler(db, admin) + .Handle(new AdjustPurchaseEvaluationBudgetCommand(peDone.Id, 10_000_000m, null), CancellationToken.None); + await adjDone.Should().ThrowAsync() + .WithMessage("*đã kết thúc*", "phiếu DaDuyet → chặn CẢ Admin (guard TRƯỚC isAdmin block)"); + + // (b) TuChoi seed thẳng (không reachable qua service — S60 gỡ Từ chối). Guard chỉ đọc Phase. + var (project, wi) = await SeedProjectAndWorkItemAsync(db, "T6rej"); + var peReject = new PurchaseEvaluation + { + Id = Guid.NewGuid(), + Type = PurchaseEvaluationType.DuyetNcc, + Phase = PurchaseEvaluationPhase.TuChoi, + MaPhieu = "PE-BF-T6-REJ", + TenGoiThau = "Gói thầu bị từ chối", + ProjectId = project.Id, + WorkItemId = wi.Id, + DrafterUserId = Guid.NewGuid(), + }; + db.PurchaseEvaluations.Add(peReject); + await db.SaveChangesAsync(CancellationToken.None); + + var adjReject = async () => await new AdjustPurchaseEvaluationBudgetCommandHandler(db, admin) + .Handle(new AdjustPurchaseEvaluationBudgetCommand(peReject.Id, 5_000_000m, null), CancellationToken.None); + await adjReject.Should().ThrowAsync() + .WithMessage("*đã kết thúc*", "phiếu TuChoi cũng = kết thúc → ngân sách bất biến"); + } + } + + // ===================================================================== + // T7 — DISPLAY: nhánh else-live (phiếu ChoDuyet) → số MỚI theo pair + BudgetFrozen=false + // + CanEdit theo role; nhánh frozen (phiếu DaDuyet) → CanEditPro=CanEditCcm=false (kể cả + // Admin) + BudgetFrozen=true. + // ===================================================================== + [Fact] + public async Task BudgetSummary_LiveVsFrozen_FlagsAndEditability() + { + var (svc, fix, db) = CreateService(); + using (fix) + { + var um = fix.Services.GetRequiredService>(); + + // ---- Nhánh LIVE: phiếu ChoDuyet + pair, querier = Procurement + đồng thời drafter ---- + var (projectL, wiL) = await SeedProjectAndWorkItemAsync(db, "T7live"); + await SeedPairAsync(db, projectL.Id, wiL.Id, null, null, null, PairCcmInitial, null, null); + var proUser = await fix.CreateUserAsync("pro@t7.test", "PRO", null, new[] { AppRoles.Procurement }); + var peLive = new PurchaseEvaluation + { + Id = Guid.NewGuid(), + Type = PurchaseEvaluationType.DuyetNcc, + Phase = PurchaseEvaluationPhase.ChoDuyet, + MaPhieu = "PE-BF-T7-LIVE", + TenGoiThau = "Gói thầu live", + ProjectId = projectL.Id, + WorkItemId = wiL.Id, + DrafterUserId = proUser.Id, // drafter → authz pass + }; + db.PurchaseEvaluations.Add(peLive); + await db.SaveChangesAsync(CancellationToken.None); + + var proQuerier = new TestCurrentUser(proUser.Id, "PRO", null, AppRoles.Procurement); + var liveBundle = await new GetPurchaseEvaluationQueryHandler(db, um, proQuerier) + .Handle(new GetPurchaseEvaluationQuery(peLive.Id), CancellationToken.None); + var live = liveBundle.BudgetSummary!; + live.BudgetFrozen.Should().BeFalse("phiếu ChoDuyet → nhánh live"); + live.InitialAmount.Should().Be(PairCcmInitial, "nhánh live đọc số MỚI từ record pair"); + live.CanEditPro.Should().BeTrue("querier role Procurement → được sửa PRO khi chưa duyệt"); + live.CanEditCcm.Should().BeFalse("querier KHÔNG có role CostControl → không sửa CCM"); + + // ---- Nhánh FROZEN: phiếu DaDuyet (finalize) + querier Admin → vẫn KHÓA sửa ---- + var (peDone, _, _, _) = await SeedAndFinalizeAsync(svc, fix, db, "T7frozen"); + var admin = new TestCurrentUser(Guid.NewGuid(), "Admin", null, AppRoles.Admin); + var frozenBundle = await new GetPurchaseEvaluationQueryHandler(db, um, admin) + .Handle(new GetPurchaseEvaluationQuery(peDone.Id), CancellationToken.None); + var frozen = frozenBundle.BudgetSummary!; + frozen.BudgetFrozen.Should().BeTrue("phiếu DaDuyet + SnapshotAt != null → nhánh frozen"); + frozen.CanEditPro.Should().BeFalse("phiếu đã chốt → KHÔNG cho sửa PRO, kể cả Admin"); + frozen.CanEditCcm.Should().BeFalse("phiếu đã chốt → KHÔNG cho sửa CCM, kể cả Admin"); + frozen.InitialAmount.Should().Be(PairCcmInitial, "nhánh frozen serve CCM từ cột snapshot"); + } + } + + // ===================================================================== + // T8 — NULL-SAFE: (a) phiếu WorkItemId null finalize → không NRE, bundle.BudgetSummary=null + // NHƯNG SnapshotAt VẪN set trên entity; (b) phiếu có WorkItemId nhưng KHÔNG có pair record → + // finalize OK, frozen bs non-null với 6 cột pair null. + // ===================================================================== + [Fact] + public async Task Finalize_NullSafe_NoWorkItemAndNoPairRecord() + { + var (svc, fix, db) = CreateService(); + using (fix) + { + var um = fix.Services.GetRequiredService>(); + var admin = new TestCurrentUser(Guid.NewGuid(), "Admin", null, AppRoles.Admin); + + // (a) WorkItemId null → helper set SnapshotAt nhưng bundle.BudgetSummary null. + var (peNoWi, _, _, _) = await SeedAndFinalizeAsync(svc, fix, db, "T8nowi", withWorkItem: false, seedPair: false); + var sNoWi = await db.PurchaseEvaluations.AsNoTracking().SingleAsync(x => x.Id == peNoWi.Id); + sNoWi.ApprovedBudgetSnapshotAt.Should().NotBeNull("WorkItemId null vẫn set SnapshotAt (không NRE)"); + sNoWi.ApprovedBudgetProInitialAmount.Should().BeNull("WorkItemId null → không cặp → 10 cột null"); + var noWiBundle = await new GetPurchaseEvaluationQueryHandler(db, um, admin) + .Handle(new GetPurchaseEvaluationQuery(peNoWi.Id), CancellationToken.None); + noWiBundle.BudgetSummary.Should().BeNull("phiếu không gắn Hạng mục → BudgetSummary null"); + + // (b) WorkItemId set nhưng KHÔNG seed pair → finalize OK, frozen bs non-null, 6 pair cols null. + var (peNoPair, _, _, _) = await SeedAndFinalizeAsync(svc, fix, db, "T8nopair", withWorkItem: true, seedPair: false); + var sNoPair = await db.PurchaseEvaluations.AsNoTracking().SingleAsync(x => x.Id == peNoPair.Id); + sNoPair.ApprovedBudgetSnapshotAt.Should().NotBeNull("pair thiếu vẫn set SnapshotAt"); + sNoPair.ApprovedBudgetCcmInitialAmount.Should().BeNull("pair thiếu → 6 cột pair null"); + var noPairBundle = await new GetPurchaseEvaluationQueryHandler(db, um, admin) + .Handle(new GetPurchaseEvaluationQuery(peNoPair.Id), CancellationToken.None); + var bs = noPairBundle.BudgetSummary; + bs.Should().NotBeNull("có WorkItemId → frozen bs vẫn build (dù pair rỗng)"); + bs!.BudgetFrozen.Should().BeTrue(); + bs.ProInitialAmount.Should().BeNull("pair thiếu → FE hiển thị '—' (cột PRO null)"); + bs.InitialAmount.Should().BeNull("pair thiếu → cột CCM null"); + } + } + + // ===================================================================== + // T9 — RE-FINALIZE OVERWRITE: phiếu ĐÃ có cột snapshot CŨ (seed thẳng — SPEC cho phép để + // test semantics 'LUÔN overwrite') đi qua finalize → cột = giá trị pair HIỆN TẠI (mới), + // SnapshotAt refresh (không giữ giá trị cũ). + // ===================================================================== + [Fact] + public async Task ReFinalize_AlwaysOverwritesStaleSnapshot_RefreshesSnapshotAt() + { + var (svc, fix, db) = CreateService(); + using (fix) + { + var (project, wi) = await SeedProjectAndWorkItemAsync(db, "T9"); + // Pair MỚI: ProInitial 500tr. + await SeedPairAsync(db, project.Id, wi.Id, 500_000_000m, null, null, 600_000_000m, null, null); + + var approver = (await fix.CreateUserAsync("appr@t9.test", "Approver", null, Array.Empty())).Id; + var wf = await SeedFinalizeWorkflowAsync(db, approver); + + var pe = BuildPeAtFinalizeSlot(wf.Id, project.Id, wi.Id, Guid.NewGuid(), "PE-BF-T9"); + // Seed thẳng cột snapshot CŨ (stale) + SnapshotAt cũ để chứng minh helper LUÔN overwrite. + var staleAt = new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Utc); + pe.ApprovedBudgetProInitialAmount = 111_000_000m; // giá trị cũ SAI + pe.ApprovedBudgetCcmInitialAmount = 222_000_000m; + pe.ApprovedBudgetSnapshotAt = staleAt; + db.PurchaseEvaluations.Add(pe); + await db.SaveChangesAsync(CancellationToken.None); + + await ApproveWithPriceAsync(svc, pe, approver, Array.Empty()); + pe.Phase.Should().Be(PurchaseEvaluationPhase.DaDuyet); + + // Tracked entity: helper mutate in-place → overwrite bằng pair HIỆN TẠI. + pe.ApprovedBudgetProInitialAmount.Should().Be(500_000_000m, "LUÔN overwrite: cột = pair mới (500tr), KHÔNG giữ 111tr cũ"); + pe.ApprovedBudgetCcmInitialAmount.Should().Be(600_000_000m, "LUÔN overwrite CCM: 600tr mới, KHÔNG giữ 222tr cũ"); + pe.ApprovedBudgetSnapshotAt.Should().Be(ClockNow, "SnapshotAt refresh về thời điểm duyệt (clock), KHÔNG giữ 2020"); + pe.ApprovedBudgetSnapshotAt.Should().NotBe(staleAt); + + // Persisted. + var s = await db.PurchaseEvaluations.AsNoTracking().SingleAsync(x => x.Id == pe.Id); + s.ApprovedBudgetProInitialAmount.Should().Be(500_000_000m, "overwrite persist xuống DB"); + } + } + + // ===================================================================== + // S133 site-5 (reviewer PASS_WITH_FIXES catch — #81 indirect writer): Admin manual-override + // chuyển THẲNG → DaDuyet qua nhánh catch-all (`evaluation.Phase = targetPhase` — assignment + // qua BIẾN, không phải token enum → literal-grep 4-site không thấy) cũng PHẢI chốt snapshot. + // Thiếu → phiếu DaDuyet SnapshotAt=null → display-gate false → serve LIVE (mất bất biến). + // ===================================================================== + [Fact] + public async Task AdminOverride_DirectToDaDuyet_SetsBudgetSnapshot() + { + var (svc, fix, db) = CreateService(); + using (fix) + { + var (project, wi) = await SeedProjectAndWorkItemAsync(db, "AO"); + await SeedPairAsync(db, project.Id, wi.Id, ProAtApproval, null, null, CcmAtApproval, null, null); + + var admin = (await fix.CreateUserAsync("admin@ao.test", "Admin AO", null, new[] { AppRoles.Admin })).Id; + + // Phiếu ở DangSoanThao — KHÔNG rơi approve-step (fromPhase != ChoDuyet) → catch-all + // admin-override khi actor là Admin. + var pe = new PurchaseEvaluation + { + Id = Guid.NewGuid(), + Type = PurchaseEvaluationType.DuyetNcc, + Phase = PurchaseEvaluationPhase.DangSoanThao, + MaPhieu = "PE-BF-AO", + TenGoiThau = "Gói thầu admin-override", + ProjectId = project.Id, + WorkItemId = wi.Id, + DrafterUserId = admin, + }; + db.PurchaseEvaluations.Add(pe); + await db.SaveChangesAsync(CancellationToken.None); + + await svc.TransitionAsync( + evaluation: pe, + targetPhase: PurchaseEvaluationPhase.DaDuyet, + actorUserId: admin, + actorRoles: new[] { AppRoles.Admin }, + decision: ApprovalDecision.Approve, + comment: "admin override thẳng DaDuyet", + ct: CancellationToken.None); + + pe.Phase.Should().Be(PurchaseEvaluationPhase.DaDuyet, "admin catch-all override phải ăn"); + pe.ApprovedBudgetSnapshotAt.Should().Be(ClockNow, + "site-5: override thẳng → DaDuyet cũng phải chốt snapshot (RULE helper — reviewer S133)"); + pe.ApprovedBudgetProInitialAmount.Should().Be(ProAtApproval, "snapshot PRO từ pair lúc override"); + pe.ApprovedBudgetCcmInitialAmount.Should().Be(CcmAtApproval, "snapshot CCM từ pair lúc override"); + } + } +}