[CLAUDE] PurchaseEvaluation: Mig 55 ô "Ghi chú từ CCM" ngân sách gói thầu — CCM nhập số + ghi lý do giống PRO
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m54s

- Entity PeWorkItemBudget +CcmNote (mirror ProNote, nvarchar 1000) + Mig 55 additive-nullable
- UpdatePeBudgetCcmCommand +CcmNote absolute-set, role-gate CostControl/Admin fail-closed
- DTO PeBudgetSummaryDto +CcmNote + controller BudgetCcmBody + GET mapping
- FE 2 app SHA-mirror: dòng "Ghi chú từ CCM" gate canEditCcm (sau V0/hiệu chỉnh), absolute-set đủ 3 field
- Test +5 (set CCM/Admin, null-clear, non-priv Forbidden, all-3-persist) -> 339 pass

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-06-18 19:05:10 +07:00
parent e7e99d10f2
commit 8655ebf1ba
14 changed files with 6487 additions and 15 deletions

View File

@ -80,10 +80,10 @@ public class PurchaseEvaluationsController(IMediator mediator) : ControllerBase
[HttpPut("{id:guid}/budget/ccm")]
public async Task<IActionResult> UpdateBudgetCcm(Guid id, [FromBody] BudgetCcmBody body, CancellationToken ct)
{
await mediator.Send(new UpdatePeBudgetCcmCommand(id, body.InitialAmount, body.AdjustmentAmount), ct);
await mediator.Send(new UpdatePeBudgetCcmCommand(id, body.InitialAmount, body.AdjustmentAmount, body.CcmNote), ct);
return NoContent();
}
public record BudgetCcmBody(decimal? InitialAmount, decimal? AdjustmentAmount);
public record BudgetCcmBody(decimal? InitialAmount, decimal? AdjustmentAmount, string? CcmNote);
// [S69 2026-06-17] Cờ gấp (urgent) — anh Kiệt FDC. Class [Authorize] any-auth;
// handler fine-grained Forbidden theo role (PRO=Procurement set cờ đỏ, CCM=

View File

@ -297,6 +297,7 @@ public record PeBudgetSummaryDto(
string? ProNote,
decimal? InitialAmount,
decimal? AdjustmentAmount,
string? CcmNote,
decimal FullAmount,
bool FullIsEstimate,
bool CanEditPro,

View File

@ -12,7 +12,7 @@ namespace SolutionErp.Application.PurchaseEvaluations;
// [S61 Mig 50] 2 handler nhập ngân sách gói thầu theo ROLE (anh Kiệt chốt):
// - PRO (Procurement | Admin): ProEstimateAmount (dự trù lần đầu) + ProNote.
// - CCM (CostControl | Admin): InitialAmount ("Ban hành lần đầu") +
// AdjustmentAmount ("V0/hiệu chỉnh tăng giảm" — cho phép ÂM).
// AdjustmentAmount ("V0/hiệu chỉnh tăng giảm" — cho phép ÂM) + CcmNote (Mig 55).
// Authz pattern AssignItTicketHandler S54: controller [Authorize] any-auth,
// handler fine-grained ForbiddenException fail-closed (Forbidden TRƯỚC mọi
// side-effect — S56 #5). KHÔNG ràng Phase (CCM "nhập trong khi duyệt" theo lời
@ -126,7 +126,8 @@ public class UpdatePeBudgetProCommandHandler(
public record UpdatePeBudgetCcmCommand(
Guid PeId,
decimal? InitialAmount,
decimal? AdjustmentAmount) : IRequest;
decimal? AdjustmentAmount,
string? CcmNote) : IRequest;
public class UpdatePeBudgetCcmCommandValidator : AbstractValidator<UpdatePeBudgetCcmCommand>
{
@ -135,6 +136,7 @@ public class UpdatePeBudgetCcmCommandValidator : AbstractValidator<UpdatePeBudge
RuleFor(x => x.InitialAmount).GreaterThanOrEqualTo(0)
.When(x => x.InitialAmount.HasValue);
// AdjustmentAmount KHÔNG ràng dấu — "hiệu chỉnh tăng giảm" cho phép ÂM.
RuleFor(x => x.CcmNote).MaximumLength(1000);
}
}
@ -161,14 +163,18 @@ public class UpdatePeBudgetCcmCommandHandler(
var oldInitial = rec.InitialAmount;
var oldAdjustment = rec.AdjustmentAmount;
var oldCcmNote = rec.CcmNote;
rec.InitialAmount = request.InitialAmount; // absolute-set (null = clear)
rec.AdjustmentAmount = request.AdjustmentAmount;
rec.CcmNote = request.CcmNote;
var parts = new List<string>();
if (oldInitial != request.InitialAmount)
parts.Add($"ban hành lần đầu {oldInitial?.ToString("N0") ?? "(trống)"}đ → {request.InitialAmount?.ToString("N0") ?? "(trống)"}đ");
if (oldAdjustment != request.AdjustmentAmount)
parts.Add($"V0/hiệu chỉnh {oldAdjustment?.ToString("N0") ?? "(trống)"}đ → {request.AdjustmentAmount?.ToString("N0") ?? "(trống)"}đ");
if (oldCcmNote != request.CcmNote)
parts.Add("ghi chú CCM cập nhật");
db.PurchaseEvaluationChangelogs.Add(new PurchaseEvaluationChangelog
{

View File

@ -851,7 +851,7 @@ public class GetPurchaseEvaluationQueryHandler(
peBudgetSummary = new PeBudgetSummaryDto(
pairRec?.Id, pairRec?.ProEstimateAmount, pairRec?.ProNote,
pairRec?.InitialAmount, pairRec?.AdjustmentAmount,
pairRec?.InitialAmount, pairRec?.AdjustmentAmount, pairRec?.CcmNote,
fullAmount, !hasCcm,
canEditPro, canEditCcm,
prevSubmittedTotal, prevSubmittedCount,

View File

@ -13,7 +13,7 @@ namespace SolutionErp.Domain.PurchaseEvaluations;
// Quyền nhập theo ROLE (anh Kiệt chốt S61):
// - PRO (Procurement): ProEstimateAmount (dự trù lần đầu) + ProNote.
// - CCM (CostControl): InitialAmount (Ban hành lần đầu) + AdjustmentAmount
// (NS V0 hiệu chỉnh tăng/giảm — cho phép ÂM).
// (NS V0 hiệu chỉnh tăng/giảm — cho phép ÂM) + CcmNote (ghi chú CCM, Mig 55).
//
// "Ngân sách full gói thầu" KHÔNG lưu cột — BE compute:
// full = (InitialAmount ?? 0) + (AdjustmentAmount ?? 0);
@ -28,4 +28,5 @@ public class PeWorkItemBudget : AuditableEntity
public string? ProNote { get; set; } // "Ghi chú từ PRO"
public decimal? InitialAmount { get; set; } // CCM "Ngân sách Ban hành lần đầu" (đ)
public decimal? AdjustmentAmount { get; set; } // CCM "NS V0/hiệu chỉnh tăng giảm" (đ, cho phép ÂM)
public string? CcmNote { get; set; } // [Mig 55] "Ghi chú từ CCM" — CCM ghi lý do/nguồn số (mirror ProNote)
}

View File

@ -20,6 +20,7 @@ public class PeWorkItemBudgetConfiguration : IEntityTypeConfiguration<PeWorkItem
b.Property(x => x.InitialAmount).HasPrecision(18, 2);
b.Property(x => x.AdjustmentAmount).HasPrecision(18, 2);
b.Property(x => x.ProNote).HasMaxLength(1000);
b.Property(x => x.CcmNote).HasMaxLength(1000);
b.HasIndex(x => new { x.ProjectId, x.WorkItemId })
.IsUnique()

View File

@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SolutionErp.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddCcmNoteToPeWorkItemBudget : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "CcmNote",
table: "PeWorkItemBudgets",
type: "nvarchar(1000)",
maxLength: 1000,
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "CcmNote",
table: "PeWorkItemBudgets");
}
}
}

View File

@ -4523,6 +4523,10 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
.HasPrecision(18, 2)
.HasColumnType("decimal(18,2)");
b.Property<string>("CcmNote")
.HasMaxLength(1000)
.HasColumnType("nvarchar(1000)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");