[CLAUDE] Contract: chặn mọi đường tạo HĐ ngoài KHKK — YC-040 (106)=(b), chuỗi NCC→KHKK→HĐ khép kín
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 10m0s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 10m0s
Owner chốt: "(b) Chặn HẾT 7 loại — mọi HĐ đều phải từ KHKK -> Tao nghĩ nên như vậy trước đã."
BE: ContractCreationGate (1 chỗ, AllowedDirectTypes rỗng — nới = thêm loại, không sửa handler)
chặn 409 ở 2/3 write-site: POST /contracts (tạo tự do) + POST /purchase-evaluations/{id}/
create-contract (PE→HĐ 1-click nhảy cóc KHKK, di tích Phase 6). Bridge KHKK giữ nguyên
(write-site duy nhất). Gate đứng TRƯỚC mọi load — không lộ tồn tại dữ liệu. Seeder demo
là write-site miễn-trừ có khai (flag-gated).
FE ×2 app (8 lối): menu Create per-loại ẩn (Layout, policy Ct_*_Create giữ DB) · nút
"Tạo HĐ mới"/"Thêm mới" → "Tạo từ KHKK" trỏ /khkk/list (MyContracts + Dashboard +
ContractCreatePage) · ?mode=new → EmptyState chỉ đường KHKK · nút PE→HĐ ẩn (PeDetailTabs).
ContractCreatePage + PeDetailTabs giữ SHA-mirror 2 app.
Tests 697 pass + 6 skip / 0 fail (total 703): +4 gate mới (quét-trọn-enum fail-closed +
chặn-trước-NotFound + đổi message ApplicableType); 6 test multi-winner PE→HĐ chuyển
Skip-có-khai (spec di sản Mig 58, mở lại khi owner nới gate). Reviewer PASS_WITH_FIXES,
6/6 issue vá trong lượt (kể cả 3 chuỗi EmptyState/ListPanel còn mời tạo trực tiếp).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@ -0,0 +1,39 @@
|
||||
using SolutionErp.Application.Common.Exceptions;
|
||||
using SolutionErp.Application.Contracts;
|
||||
using SolutionErp.Domain.Contracts;
|
||||
|
||||
namespace SolutionErp.Infrastructure.Tests.Application;
|
||||
|
||||
// [YC-040 @S197] Câu (106) = (b) — owner: "Chặn HẾT 7 loại — mọi HĐ đều phải từ KHKK".
|
||||
// Gate = luật nghiệp-vụ CỨNG (guard ⇒ test-before-merge theo rules §7).
|
||||
//
|
||||
// Positive-control cho đường SỐNG: bridge KHKK (`CreateContractFromSigningPlanFeatures`)
|
||||
// KHÔNG gọi gate — suite `CreateContractFromSigningPlanTests` hiện có PASS nguyên vẹn
|
||||
// chính là bằng chứng bridge không bị chặn nhầm; không lặp test đó ở đây.
|
||||
public class ContractCreationGateTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(ContractType.HopDongThauPhu)] // loại thi-công phổ biến nhất
|
||||
[InlineData(ContractType.HopDongNguyenTacNCC)] // (106)(b) chặn CẢ Nguyên tắc — khác nhánh (a)
|
||||
public void EnsureDirectCreationAllowed_MoiLoai_DeuThrowConflict(ContractType type)
|
||||
{
|
||||
var act = () => ContractCreationGate.EnsureDirectCreationAllowed(type);
|
||||
|
||||
act.Should().Throw<ConflictException>()
|
||||
.WithMessage("*Kế hoạch Ký kết (KHKK)*")
|
||||
.WithMessage("*YC-040*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnsureDirectCreationAllowed_Chan_Tron_Ca7Loai()
|
||||
{
|
||||
// (106)(b) = tập cho-phép RỖNG. Quét TRỌN enum thay vì liệt tay 7 giá trị —
|
||||
// thêm ContractType mới về sau thì mặc-định cũng bị chặn (fail-closed),
|
||||
// và test này tự phủ giá trị mới, không cần nhớ sửa.
|
||||
foreach (var type in Enum.GetValues<ContractType>())
|
||||
{
|
||||
var act = () => ContractCreationGate.EnsureDirectCreationAllowed(type);
|
||||
act.Should().Throw<ConflictException>($"loại {type} phải bị chặn theo (106)(b)");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -80,13 +80,14 @@ public class CreateContractCommandApplicableTypeTests
|
||||
|
||||
var act = async () => await handler.Handle(cmd, CancellationToken.None);
|
||||
|
||||
// Note: spec BW5 mention ValidationException — thực tế ContractFeatures.cs
|
||||
// line 84 throw ConflictException ("Quy trình {Code} áp dụng cho {ApplicableType},
|
||||
// không khớp với HĐ (cần ApplicableType=Contract)."). FluentValidation
|
||||
// (line 38-50) chỉ rule field-level (MaximumLength, GreaterThanOrEqualTo),
|
||||
// KHÔNG có rule cross-table.
|
||||
// [YC-040 (106)(b) @S197] Cửa POST /contracts nay bị ContractCreationGate chặn
|
||||
// TRƯỚC mọi load/validate ⇒ forge PE-workflow còn không tới được cửa-1/4 —
|
||||
// defense NÂNG BẬC, không phải mất: validate ApplicableType vẫn sống cho đường
|
||||
// bridge KHKK (ContractWorkflowGuards, suite CreateContractFromSigningPlanTests phủ).
|
||||
// 🧊 expectation cũ giữ làm vết: ConflictException "*ApplicableType=Contract*"
|
||||
// (ContractFeatures cửa-1/4) — đúng cho tới khi gate đóng cửa này @S197.
|
||||
await act.Should().ThrowAsync<ConflictException>()
|
||||
.WithMessage("*ApplicableType=Contract*");
|
||||
.WithMessage("*Kế hoạch Ký kết (KHKK)*");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -165,7 +165,7 @@ public class CreateContractFromEvaluationMultiWinnerTests
|
||||
|
||||
// ===== (a) ≥2 winners → N HĐ, mỗi GiaTri = winner's quote total =====
|
||||
|
||||
[Fact]
|
||||
[Fact(Skip = "YC-040 (106)(b) @S197: duong PE->HD dong - ContractCreationGate chan truoc handler. Spec multi-winner giu lam di san (Mig 58/S114), mo lai khi owner noi AllowedDirectTypes.")]
|
||||
public async Task Create_TwoWinners_CreatesTwoContracts_EachGiaTriIsThatWinnerQuoteTotal()
|
||||
{
|
||||
using var fix = new IdentityFixture();
|
||||
@ -221,7 +221,7 @@ public class CreateContractFromEvaluationMultiWinnerTests
|
||||
// NAY single = SUM(Quote.IsSelected của winner), giống multi. Trước đây test này assert
|
||||
// GiaTri = 800 (budget sum); giờ = 140 (100+40 = quote ĐƯỢC CHỌN của winner0).
|
||||
|
||||
[Fact]
|
||||
[Fact(Skip = "YC-040 (106)(b) @S197: duong PE->HD dong - ContractCreationGate chan truoc handler. Spec multi-winner giu lam di san (Mig 58/S114), mo lai khi owner noi AllowedDirectTypes.")]
|
||||
public async Task Create_SingleWinner_GiaTriIsSelectedQuoteTotal_NotBudgetSum()
|
||||
{
|
||||
using var fix = new IdentityFixture();
|
||||
@ -262,7 +262,7 @@ public class CreateContractFromEvaluationMultiWinnerTests
|
||||
// Winner thắng CHỈ 1 trong 2 hạng mục (detail0 IsSelected=true, detail1 IsSelected=false)
|
||||
// → GiaTri = SUM(selected quotes) = quote detail0 THÔI, KHÁC SUM-mọi-quote-của-winner
|
||||
// (chống bug detailsSum/all-quote cũ). Chứng minh handler lọc theo Quote.IsSelected per hạng mục.
|
||||
[Fact]
|
||||
[Fact(Skip = "YC-040 (106)(b) @S197: duong PE->HD dong - ContractCreationGate chan truoc handler. Spec multi-winner giu lam di san (Mig 58/S114), mo lai khi owner noi AllowedDirectTypes.")]
|
||||
public async Task Create_SingleWinner_WinsSubsetOfDetails_GiaTriSumsOnlySelectedQuotes()
|
||||
{
|
||||
using var fix = new IdentityFixture();
|
||||
@ -299,7 +299,7 @@ public class CreateContractFromEvaluationMultiWinnerTests
|
||||
|
||||
// ===== (c) idempotency → re-call throws ConflictException =====
|
||||
|
||||
[Fact]
|
||||
[Fact(Skip = "YC-040 (106)(b) @S197: duong PE->HD dong - ContractCreationGate chan truoc handler. Spec multi-winner giu lam di san (Mig 58/S114), mo lai khi owner noi AllowedDirectTypes.")]
|
||||
public async Task Create_Again_AfterSuccess_ThrowsConflict_DaTaoHopDongRoi()
|
||||
{
|
||||
using var fix = new IdentityFixture();
|
||||
@ -329,7 +329,7 @@ public class CreateContractFromEvaluationMultiWinnerTests
|
||||
|
||||
// ===== (d) 0 winner → ConflictException (chưa chọn NCC/TP trúng thầu) =====
|
||||
|
||||
[Fact]
|
||||
[Fact(Skip = "YC-040 (106)(b) @S197: duong PE->HD dong - ContractCreationGate chan truoc handler. Spec multi-winner giu lam di san (Mig 58/S114), mo lai khi owner noi AllowedDirectTypes.")]
|
||||
public async Task Create_NoWinner_ThrowsConflict_ChuaChonTrungThau()
|
||||
{
|
||||
using var fix = new IdentityFixture();
|
||||
@ -353,7 +353,7 @@ public class CreateContractFromEvaluationMultiWinnerTests
|
||||
|
||||
// ===== (e) not DaDuyet phase → ConflictException =====
|
||||
|
||||
[Fact]
|
||||
[Fact(Skip = "YC-040 (106)(b) @S197: duong PE->HD dong - ContractCreationGate chan truoc handler. Spec multi-winner giu lam di san (Mig 58/S114), mo lai khi owner noi AllowedDirectTypes.")]
|
||||
public async Task Create_NotDaDuyet_ThrowsConflict()
|
||||
{
|
||||
using var fix = new IdentityFixture();
|
||||
@ -375,4 +375,24 @@ public class CreateContractFromEvaluationMultiWinnerTests
|
||||
|
||||
(await db.Contracts.AsNoTracking().CountAsync()).Should().Be(0);
|
||||
}
|
||||
|
||||
// [YC-040 (106)(b) @S197] Test SỐNG duy nhất của file khi cửa đóng: gate chặn TRƯỚC
|
||||
// MỌI LOAD — Pe.Id không tồn tại mà vẫn 409-KHKK (không phải NotFoundException)
|
||||
// = chứng thứ-tự guard đứng đầu handler, forge không dò được tồn-tại phiếu.
|
||||
[Fact]
|
||||
public async Task Create_GateDong_ChanTruocMoiLoad_KhongLoNotFound()
|
||||
{
|
||||
using var fix = new IdentityFixture();
|
||||
var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
|
||||
var actor = await fix.CreateUserAsync("gate-yc040@test.local", "Gate YC040",
|
||||
departmentId: null, roles: new[] { AppRoles.Drafter });
|
||||
var handler = BuildHandler(db, fix.Services, actor);
|
||||
|
||||
var act = async () => await handler.Handle(
|
||||
new CreateContractFromEvaluationCommand(Guid.NewGuid(), ContractType.HopDongThauPhu, null),
|
||||
CancellationToken.None);
|
||||
|
||||
await act.Should().ThrowAsync<ConflictException>().WithMessage("*Kế hoạch Ký kết (KHKK)*");
|
||||
(await db.Contracts.AsNoTracking().CountAsync()).Should().Be(0);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user