[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
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:
@ -115,6 +115,18 @@ public class PurchaseEvaluationsController(IMediator mediator) : ControllerBase
|
||||
}
|
||||
public record SuggestedPriceCcmBody(decimal? CcmPrice, string? Note);
|
||||
|
||||
// [Mig 59 2026-06-24 — anh Kiệt FDC / CCM] "NS kỳ này" cột CCM (Section B THỰC HIỆN
|
||||
// tách 3 cột Dự án|PRO|CCM). PRO dùng /budget-adjust sẵn có; CCM nhập cột riêng.
|
||||
// Class [Authorize] any-auth; handler fine-grained Forbidden (CCM=CostControl | Admin).
|
||||
// Absolute-set (null=clear). Budget=0 hợp lệ.
|
||||
[HttpPatch("{id:guid}/budget/ccm-period")]
|
||||
public async Task<IActionResult> SetCcmBudgetPeriod(Guid id, [FromBody] CcmBudgetPeriodBody body, CancellationToken ct)
|
||||
{
|
||||
await mediator.Send(new SetPeCcmBudgetPeriodCommand(id, body.CcmBudgetPeriodAmount), ct);
|
||||
return NoContent();
|
||||
}
|
||||
public record CcmBudgetPeriodBody(decimal? CcmBudgetPeriodAmount);
|
||||
|
||||
[HttpPost("{id:guid}/transitions")]
|
||||
public async Task<IActionResult> Transition(Guid id, [FromBody] TransitionPeBody body, CancellationToken ct)
|
||||
{
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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á
|
||||
|
||||
@ -40,6 +40,14 @@ public class PurchaseEvaluation : AuditableEntity
|
||||
public decimal? BudgetPeriodAmount { get; set; }
|
||||
public decimal? ExpectedRemainingAmount { get; set; }
|
||||
|
||||
// [Mig 59 2026-06-24 — anh Kiệt FDC / CCM] "NS kỳ này" của CỘT CCM (Section B
|
||||
// THỰC HIỆN tách 3 cột Dự án|PRO|CCM). PRO dùng BudgetPeriodAmount sẵn có; CCM
|
||||
// nhập cột riêng độc-lập, đối chiếu full-CCM = (InitialAmount+AdjustmentAmount)
|
||||
// ở PeWorkItemBudget. Nhập qua endpoint riêng role-gate Admin|CostControl,
|
||||
// absolute-set (null=clear). Budget=0 hợp lệ. KHÔNG submit-guard (guard chỉ trên
|
||||
// BudgetPeriodAmount của PRO).
|
||||
public decimal? CcmBudgetPeriodAmount { get; set; }
|
||||
|
||||
// Smart reject (Phase 9 — Migration 16): Phase nguồn khi reject. Drafter
|
||||
// sửa lại + trình lại → quay về RejectedFromPhase thay vì đi tuần tự.
|
||||
public PurchaseEvaluationPhase? RejectedFromPhase { get; set; }
|
||||
|
||||
@ -24,6 +24,9 @@ public class PurchaseEvaluationConfiguration : IEntityTypeConfiguration<Purchase
|
||||
// [S61 Mig 50] 2 cột ngân sách mới thay BudgetManual* — precision giữ (18,2).
|
||||
b.Property(x => x.BudgetPeriodAmount).HasPrecision(18, 2);
|
||||
b.Property(x => x.ExpectedRemainingAmount).HasPrecision(18, 2);
|
||||
// [Mig 59 2026-06-24] "NS kỳ này" cột CCM (Section B 3 cột) — decimal(18,2)
|
||||
// nullable, mirror BudgetPeriodAmount. Nhập role-gate Admin|CostControl.
|
||||
b.Property(x => x.CcmBudgetPeriodAmount).HasColumnType("decimal(18,2)");
|
||||
// [Mig 54 2026-06-18] Giá đề xuất PRO/CCM + giá chốt khi duyệt — precision (18,2)
|
||||
// mirror các cột tiền khác. ApprovedPriceSource nvarchar(20): Ncc/ProMin/ProMax/Ccm.
|
||||
b.Property(x => x.ProSuggestedMinPrice).HasPrecision(18, 2);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,28 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddCcmBudgetPeriodToPurchaseEvaluation : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<decimal>(
|
||||
name: "CcmBudgetPeriodAmount",
|
||||
table: "PurchaseEvaluations",
|
||||
type: "decimal(18,2)",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CcmBudgetPeriodAmount",
|
||||
table: "PurchaseEvaluations");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -4611,6 +4611,9 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||
.HasPrecision(18, 2)
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<decimal?>("CcmBudgetPeriodAmount")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<decimal?>("CcmSuggestedPrice")
|
||||
.HasPrecision(18, 2)
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
Reference in New Issue
Block a user