[CLAUDE] Contract: W7 KHKK GD4 "Hop dong cung" 1-moc — HardCopiesPage x2 + ?type filter + hasSealedCopy badge + 3 test PIN (suite 590/0)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m50s

- BE: ListContractsQuery +ContractType? type (filter server-side F-C1/F-05) + DTO HasSealedCopy derive Attachments.Any(purpose=SealedCopy=3) — append-cuoi default false, 0 policy moi, 0 migration, attachment endpoints 0-diff (O-A giu)
- FE x2 SHA-PAIR: trang /hard-copies (list phase=9 hardcode + badge + panel upload multipart purpose=3 + Y kien cap duyet readonly) + 7 leaf Hdc_* re-point ?type=1..7 (coming-soon 0/0, menu 142 giu)
- Tests: ContractHardCopyTests 3 PIN (phase-only + type-filter Total-server-side + badge derive purpose=3 no-IsDeleted) — probe vi-phan 5/5 phan biet ban hong; suite 587->590/0
- Review: PASS-co-dieu-kien (FLAG-1 untracked da gate; FLAG-2/3 comment da va, SHA-pair C7DCC20B giu; +31/-4 errata BE self-report)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-07-30 00:17:52 +07:00
parent e16fa14ebd
commit 3119cb9104
12 changed files with 1358 additions and 18 deletions

View File

@ -20,8 +20,11 @@ public class ContractsController(IMediator mediator) : ControllerBase
[FromQuery] ContractPhase? phase = null,
[FromQuery] Guid? supplierId = null,
[FromQuery] Guid? projectId = null,
// [W7 S161] Lọc theo loại HĐ cho 7 leaf bản-cứng `Hdc_*`: `?type={1..7}&phase=9`.
// Nullable ⇒ bỏ trống = giữ nguyên hành vi cũ (0 breaking cho caller hiện có).
[FromQuery] ContractType? type = null,
CancellationToken ct = default)
=> Ok(await mediator.Send(new ListContractsQuery(phase, supplierId, projectId) { Page = page, PageSize = pageSize, Search = search, SortDesc = sortDesc }, ct));
=> Ok(await mediator.Send(new ListContractsQuery(phase, supplierId, projectId, type) { Page = page, PageSize = pageSize, Search = search, SortDesc = sortDesc }, ct));
// [S159-đợt5] Màn "Đã xóa" HĐ (chỉ-xem) — mirror PE /deleted (S155). Read-only,
// policy root Contracts.Read (chặt hơn [Authorize] trần của class — không phá gì).

View File

@ -279,10 +279,14 @@ public class AddCommentCommandHandler(
// ========== LIST contracts (admin view) ==========
// [W7 S161] `Type` append-CUỐI (positional) — call-site controller dựng bằng vị-trí
// `new ListContractsQuery(phase, supplierId, projectId, type)`, chèn giữa là đổi nghĩa
// 3 tham số cũ. Khuôn lấy nguyên từ `ListDeletedContractsQuery(:408)` đã có sẵn.
public record ListContractsQuery(
ContractPhase? Phase = null,
Guid? SupplierId = null,
Guid? ProjectId = null) : PagedRequest, IRequest<PagedResult<ContractListItemDto>>;
Guid? ProjectId = null,
ContractType? Type = null) : PagedRequest, IRequest<PagedResult<ContractListItemDto>>;
public class ListContractsQueryHandler(IApplicationDbContext db, ICurrentUser currentUser)
: IRequestHandler<ListContractsQuery, PagedResult<ContractListItemDto>>
@ -317,6 +321,10 @@ public class ListContractsQueryHandler(IApplicationDbContext db, ICurrentUser cu
if (request.Phase is not null) q = q.Where(x => x.c.Phase == request.Phase);
if (request.SupplierId is not null) q = q.Where(x => x.c.SupplierId == request.SupplierId);
if (request.ProjectId is not null) q = q.Where(x => x.c.ProjectId == request.ProjectId);
// [W7 S161] Lọc theo loại HĐ — server-side, ĐẶT TRONG khối filter (SAU khối IDOR
// `:298-315`) ⇒ thu hẹp tập đã-được-phép, KHÔNG mở thêm gì. FE 7 leaf `Hdc_*` gọi
// `?type={1..7}&phase=9`. Mirror `ListDeletedContractsQueryHandler:434`.
if (request.Type is not null) q = q.Where(x => x.c.Type == request.Type);
if (!string.IsNullOrWhiteSpace(request.Search))
{
@ -336,7 +344,15 @@ public class ListContractsQueryHandler(IApplicationDbContext db, ICurrentUser cu
x.c.Id, x.c.MaHopDong, x.c.TenHopDong, x.c.Type, x.c.Phase,
x.c.SupplierId, x.s.Name,
x.c.ProjectId, x.p.Name,
x.c.GiaTri, x.c.SlaDeadline, x.c.CreatedAt))
x.c.GiaTri, x.c.SlaDeadline, x.c.CreatedAt,
// [W7 S161] Cờ "đã có bản cứng" — dịch thành EXISTS(...) trên
// `ContractAttachments`, 0 round-trip thêm.
// 🔴 CỐ Ý KHÔNG có vế `!a.IsDeleted`: `ContractAttachment : BaseEntity`
// (ContractAttachment.cs:13) — BaseEntity KHÔNG có `IsDeleted`, và
// `ContractAttachmentConfiguration` (ContractConfiguration.cs:78-93) KHÔNG
// có `HasQueryFilter`. Xoá đính kèm là HARD delete
// (`ContractAttachmentFeatures.cs:143` `.Remove(att)`) ⇒ row còn = file còn.
x.c.Attachments.Any(a => a.Purpose == AttachmentPurpose.SealedCopy)))
.ToListAsync(ct);
return new PagedResult<ContractListItemDto>(items, total, request.Page, request.PageSize);

View File

@ -14,7 +14,15 @@ public record ContractListItemDto(
string ProjectName,
decimal GiaTri,
DateTime? SlaDeadline,
DateTime CreatedAt);
DateTime CreatedAt,
// [W7 S161] Cờ "đã có bản cứng" — append-CUỐI + CÓ DEFAULT có chủ đích.
// 🔴 Default BẮT BUỘC: DTO này còn 2 call-site dựng bằng 12 arg
// (`ContractFeatures.cs` ListDeleted + Inbox) — bỏ default là vỡ compile 2 chỗ đó.
// Hệ quả KHAI THẲNG: cờ CHỈ được derive ở `ListContractsQueryHandler` (màn danh sách
// HĐ, nơi FE gắn badge); ở màn "Đã xóa" + "Hộp thư" nó luôn `false` vì KHÔNG derive
// — cố ý giữ scope W7 tối thiểu, KHÔNG phải bỏ sót.
// Nguồn sự-thật của cờ = có ≥1 `ContractAttachment.Purpose == SealedCopy(3)`.
bool HasSealedCopy = false);
public record ContractDetailDto(
Guid Id,