[CLAUDE] PurchaseEvaluation: khóa sửa giá đề xuất PRO/CCM sau khi phiếu đã gửi duyệt (anh Kiệt FDC "duyệt là fix")
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m25s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m25s
Bug (anh Kiệt/team, screenshot): drafter=PRO vẫn điền được giá đề xuất Min/Max SAU khi cấp trên (chị Trà) đã duyệt — "duyệt là fix". Root cause: UpdatePeSuggestedPricePro/CcmCommandHandler cố ý KHÔNG ràng Phase (Mig 54 "chỉnh như tài liệu sống") → sửa được mọi phase kể cả DaDuyet. Fix: - BE: +phase-guard — non-admin chỉ sửa giá đề xuất PRO/CCM khi phiếu ở Nháp (DangSoanThao) / Trả lại (TraLai); đã gửi duyệt (ChoDuyet) / đã duyệt (DaDuyet) / từ chối → ConflictException. Admin override (sửa-sai). Role-gate vẫn chạy trước. - FE (2 app): SuggestedPriceRows gate canEditPro/Ccm thêm && isEditablePhase(ev.phase) → ẩn ô sửa (read-only) khi phiếu đã gửi duyệt. Đồng bộ BE. - Test: +4 (PRO ChoDuyet->Conflict no-mutate, PRO DaDuyet->Conflict, Admin DaDuyet->override-set, CCM DaDuyet->Conflict); sửa SeedPeAsync default->DangSoanThao (spec change). 436->440 PASS. Delete phiếu sau duyệt: ĐÃ chặn sẵn (FE nút Xóa chỉ hiện DangSoanThao + BE delete-guard block non-draft) — không cần sửa. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -36,13 +36,16 @@ public class PeSuggestedPriceSetterAuthzTests
|
||||
}
|
||||
|
||||
private static async Task<PurchaseEvaluation> SeedPeAsync(
|
||||
TestApplicationDbContext db, string code = "PE-SP-001")
|
||||
TestApplicationDbContext db, string code = "PE-SP-001",
|
||||
// [S96 2026-07-01] Default DangSoanThao (Nháp) = phase editable cho setter giá đề xuất
|
||||
// sau khi thêm phase-guard "duyệt là fix". Guard tests truyền ChoDuyet/DaDuyet để chặn.
|
||||
PurchaseEvaluationPhase phase = PurchaseEvaluationPhase.DangSoanThao)
|
||||
{
|
||||
var pe = new PurchaseEvaluation
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Type = PurchaseEvaluationType.DuyetNcc,
|
||||
Phase = PurchaseEvaluationPhase.ChoDuyet,
|
||||
Phase = phase,
|
||||
MaPhieu = code,
|
||||
TenGoiThau = "Gói thầu test giá đề xuất",
|
||||
ProjectId = Guid.NewGuid(),
|
||||
@ -433,4 +436,75 @@ public class PeSuggestedPriceSetterAuthzTests
|
||||
ccmValidator.Validate(new UpdatePeSuggestedPriceCcmCommand(pe.Id, null, Note: new string('x', 1001)))
|
||||
.IsValid.Should().BeFalse("Note > 1000 ký tự vi phạm MaximumLength(1000)");
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// ===== NEW [S96 2026-07-01 — anh Kiệt FDC "duyệt là fix"] PHASE-GUARD =====
|
||||
// Giá đề xuất PRO/CCM CHỈ sửa khi phiếu Nháp (DangSoanThao) / Trả lại (TraLai).
|
||||
// Phiếu ĐÃ GỬI DUYỆT (ChoDuyet) / ĐÃ DUYỆT (DaDuyet) / TỪ CHỐI → ConflictException.
|
||||
// Admin override (sửa-sai). Đảo Mig 54 comment "KHÔNG ràng Phase". Role-gate chạy
|
||||
// TRƯỚC phase-guard (wrong-role → Forbidden; right-role + non-draft → Conflict).
|
||||
// ====================================================================
|
||||
|
||||
// 16. PRO Procurement + phiếu ĐÃ GỬI DUYỆT (ChoDuyet) → Conflict + no mutate. Đây ĐÚNG
|
||||
// bug anh Kiệt: drafter=PRO điền Min/Max SAU khi cấp trên (chị Trà) đã duyệt.
|
||||
[Fact]
|
||||
public async Task ProSetter_SubmittedPhase_ThrowsConflict_NoMutate()
|
||||
{
|
||||
using var fix = new IdentityFixture();
|
||||
var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
|
||||
var pe = await SeedPeAsync(db, "PE-SP-LOCK-1", PurchaseEvaluationPhase.ChoDuyet);
|
||||
var handler = new UpdatePeSuggestedPriceProCommandHandler(db, new FakeCurrentUser(AppRoles.Procurement));
|
||||
|
||||
var act = async () => await handler.Handle(
|
||||
new UpdatePeSuggestedPriceProCommand(pe.Id, MinPrice: 111m, MaxPrice: 111m),
|
||||
CancellationToken.None);
|
||||
|
||||
await act.Should().ThrowAsync<ConflictException>().WithMessage("*đã gửi duyệt*");
|
||||
var reloaded = await db.PurchaseEvaluations.AsNoTracking().FirstAsync(x => x.Id == pe.Id);
|
||||
reloaded.ProSuggestedMinPrice.Should().BeNull("phiếu đã gửi duyệt → KHÔNG set giá (no mutate)");
|
||||
}
|
||||
|
||||
// 17. PRO Procurement + phiếu ĐÃ DUYỆT (DaDuyet) → Conflict.
|
||||
[Fact]
|
||||
public async Task ProSetter_ApprovedPhase_ThrowsConflict()
|
||||
{
|
||||
using var fix = new IdentityFixture();
|
||||
var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
|
||||
var pe = await SeedPeAsync(db, "PE-SP-LOCK-2", PurchaseEvaluationPhase.DaDuyet);
|
||||
var handler = new UpdatePeSuggestedPriceProCommandHandler(db, new FakeCurrentUser(AppRoles.Procurement));
|
||||
|
||||
await FluentActions.Awaiting(() => handler.Handle(
|
||||
new UpdatePeSuggestedPriceProCommand(pe.Id, 1m, 2m), CancellationToken.None))
|
||||
.Should().ThrowAsync<ConflictException>();
|
||||
}
|
||||
|
||||
// 18. Admin OVERRIDE — sửa giá được kể cả phiếu ĐÃ DUYỆT (sửa-sai). Guard chỉ chặn non-admin.
|
||||
[Fact]
|
||||
public async Task ProSetter_Admin_ApprovedPhase_StillSets()
|
||||
{
|
||||
using var fix = new IdentityFixture();
|
||||
var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
|
||||
var pe = await SeedPeAsync(db, "PE-SP-LOCK-3", PurchaseEvaluationPhase.DaDuyet);
|
||||
var handler = new UpdatePeSuggestedPriceProCommandHandler(db, new FakeCurrentUser(AppRoles.Admin));
|
||||
|
||||
await handler.Handle(
|
||||
new UpdatePeSuggestedPriceProCommand(pe.Id, MinPrice: 42m, MaxPrice: null), CancellationToken.None);
|
||||
|
||||
(await db.PurchaseEvaluations.AsNoTracking().FirstAsync(x => x.Id == pe.Id))
|
||||
.ProSuggestedMinPrice.Should().Be(42m, "Admin override phase-guard (sửa-sai sau duyệt)");
|
||||
}
|
||||
|
||||
// 19. CCM CostControl + phiếu ĐÃ DUYỆT → Conflict (mirror PRO).
|
||||
[Fact]
|
||||
public async Task CcmSetter_ApprovedPhase_ThrowsConflict()
|
||||
{
|
||||
using var fix = new IdentityFixture();
|
||||
var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
|
||||
var pe = await SeedPeAsync(db, "PE-SP-LOCK-4", PurchaseEvaluationPhase.DaDuyet);
|
||||
var handler = new UpdatePeSuggestedPriceCcmCommandHandler(db, new FakeCurrentUser(AppRoles.CostControl));
|
||||
|
||||
await FluentActions.Awaiting(() => handler.Handle(
|
||||
new UpdatePeSuggestedPriceCcmCommand(pe.Id, 5m), CancellationToken.None))
|
||||
.Should().ThrowAsync<ConflictException>().WithMessage("*đã gửi duyệt*");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user