[CLAUDE] PurchaseEvaluation: Section B (THỰC HIỆN) tách 3 cột per-cột kỳ-này (Mig 59, anh Kiệt FDC / CCM)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m8s

Section B "Tổng hợp ngân sách trình ký" tách 3 cột Dự án|PRO|CCM giống Section A —
mỗi cột nhập "NS kỳ này" RIÊNG + tính thực-hiện độc-lập (lũy-kế / còn-lại / % /
so-sánh) theo ngân-sách-full của cột đó:
- PRO: budgetPeriodAmount (có sẵn) đối chiếu proFull
- CCM: ccmBudgetPeriodAmount (MỚI, Mig 59) đối chiếu ccmFull — CCM nhập (canEditCcm)
- Dự án: — (chưa wire, như A)
Số thực-tế dùng chung mọi cột (NCC đề xuất, lũy kế lịch sử). Submit-guard giữ trên
budgetPeriodAmount (không đụng).

Mig 59 AddCcmBudgetPeriodToPurchaseEvaluation (1 cột nullable, no backfill). NEW
endpoint PATCH /budget/ccm-period role-gate Admin|CostControl (fail-closed). FE
Section B rewrite <BudgetRow> list -> <table> 3 cột (colCalc per-cột), 2 app SHA-mirror.
Test 395->402 (+7 authz setter). Build sạch BE + 2 FE. Workflow wf_aafb18fe-d9d
(BE->FE->test + 3-lane review: math PASS, authz PASS, mirror self-gated identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-06-24 16:06:59 +07:00
parent f1b10e9c33
commit 9b1ee0c3a5
14 changed files with 7121 additions and 397 deletions

View File

@ -239,6 +239,10 @@ public record PurchaseEvaluationDetailBundleDto(
// (module Budget XÓA HẲN, data BudgetManualAmount migrate → BudgetPeriodAmount).
decimal? BudgetPeriodAmount,
decimal? ExpectedRemainingAmount,
// [Mig 59 2026-06-24] "NS kỳ này" cột CCM (Section B THỰC HIỆN tách 3 cột) —
// PRO=BudgetPeriodAmount sẵn có, CCM=cột này độc-lập. Nhập qua PATCH
// /budget/ccm-period role-gate Admin|CostControl.
decimal? CcmBudgetPeriodAmount,
PeBudgetSummaryDto? BudgetSummary,
// [S69 2026-06-17] Cờ gấp per-vai (PRO ĐỎ / CCM XANH) — FE render badge + toggle.
bool IsUrgentByPro,

View File

@ -141,3 +141,59 @@ public class UpdatePeSuggestedPriceCcmCommandHandler(
await db.SaveChangesAsync(ct);
}
}
// ===== CCM — "NS kỳ này" cột CCM (Section B THỰC HIỆN tách 3 cột Dự án|PRO|CCM) =====
// [Mig 59 2026-06-24 — anh Kiệt FDC / CCM] PRO dùng BudgetPeriodAmount sẵn có (luồng
// /budget-adjust). CCM nhập "NS kỳ này" RIÊNG ở cột mình → tính thực-hiện độc-lập.
// Authz mirror UpdatePeSuggestedPriceCcm: controller [Authorize] any-auth, handler
// ForbiddenException fail-closed TRƯỚC mọi side-effect (S56 #5). Budget=0 hợp lệ
// (GreaterThanOrEqualTo(0).When(HasValue)). Absolute-set (null=clear). KHÔNG
// submit-guard (guard chỉ trên BudgetPeriodAmount của PRO, KHÔNG đụng — gotcha #73).
public record SetPeCcmBudgetPeriodCommand(
Guid Id,
decimal? CcmBudgetPeriodAmount) : IRequest;
public class SetPeCcmBudgetPeriodCommandValidator : AbstractValidator<SetPeCcmBudgetPeriodCommand>
{
public SetPeCcmBudgetPeriodCommandValidator()
{
RuleFor(x => x.CcmBudgetPeriodAmount).GreaterThanOrEqualTo(0) // 0 hợp lệ
.When(x => x.CcmBudgetPeriodAmount.HasValue);
}
}
public class SetPeCcmBudgetPeriodCommandHandler(
IApplicationDbContext db,
ICurrentUser currentUser) : IRequestHandler<SetPeCcmBudgetPeriodCommand>
{
public async Task Handle(SetPeCcmBudgetPeriodCommand request, CancellationToken ct)
{
var pe = await db.PurchaseEvaluations.FirstOrDefaultAsync(x => x.Id == request.Id, ct)
?? throw new NotFoundException("PurchaseEvaluation", request.Id);
// Fail-closed TRƯỚC mọi side-effect.
if (!currentUser.Roles.Contains(AppRoles.Admin)
&& !currentUser.Roles.Contains(AppRoles.CostControl))
{
throw new ForbiddenException(
"Chỉ Phòng Kiểm soát Chi phí (CCM) hoặc Admin được nhập NS kỳ này (CCM).");
}
var oldVal = pe.CcmBudgetPeriodAmount;
pe.CcmBudgetPeriodAmount = request.CcmBudgetPeriodAmount; // absolute-set (null = clear)
db.PurchaseEvaluationChangelogs.Add(new PurchaseEvaluationChangelog
{
PurchaseEvaluationId = pe.Id,
EntityType = PurchaseEvaluationEntityType.Header,
Action = ChangelogAction.Update,
PhaseAtChange = pe.Phase,
UserId = currentUser.UserId,
UserName = currentUser.FullName ?? currentUser.Email,
Summary = $"NS kỳ này (CCM): {oldVal?.ToString("N0") ?? "(trống)"}đ → {request.CcmBudgetPeriodAmount?.ToString("N0") ?? "(trống)"}đ",
});
await db.SaveChangesAsync(ct);
}
}

View File

@ -1107,7 +1107,7 @@ public class GetPurchaseEvaluationQueryHandler(
e.SelectedSupplierId, selectedSupplier?.Name,
e.ContractId,
e.PaymentTerms, e.SlaDeadline, e.CreatedAt, e.UpdatedAt,
e.BudgetPeriodAmount, e.ExpectedRemainingAmount, peBudgetSummary,
e.BudgetPeriodAmount, e.ExpectedRemainingAmount, e.CcmBudgetPeriodAmount, peBudgetSummary,
e.IsUrgentByPro, e.IsUrgentByCcm, winnerQuoteTotal, awCeoThreshold, // [S69] cờ gấp + giá trị gói + ngưỡng CEO
e.ProSuggestedMinPrice, e.ProSuggestedMaxPrice, e.CcmSuggestedPrice, // [Mig 54] giá đề xuất PRO/CCM
e.ProSuggestedPriceNote, e.CcmSuggestedPriceNote, // [Mig 57] ghi chú giải thích giá