[CLAUDE] Contract: K1 SP-002 danh muc loai HD - ContractCatalogEntries (Mig 70) + seed 86 + CQRS + FE kind-5 x2 app + 8 test
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 6m31s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 6m31s
Gate: reviewer PASS-WITH-FLAGS 9f (sub-reviewer-gate-k1.md). Xu ly: F4 IncludeInactive mac-dinh-loai (spec K1.c) + test T5b; F7 comment 5-kind; F1 OG-6 doi nhan soat-SAU @PAUSE-2 (seeder skip-if-exists giu sua-tay CRUD qua restart - update-branch co y KHONG co); F3 authz mirror khuon Catalogs, gioi-han khai: 4 policy ContractCatalog.* sinh ra nhung chua endpoint nao tieu thu (display-layer khac API-layer, #82); F6 ten index IX_ giu EF-default (NIT). Suite 598/0 (45D+553I). Mig 70 ap SQL Server that OK. Drift DELTA: MenuKeys.All 55->56, policies 220->224, bang 96->97. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@ -24,6 +24,8 @@ public interface IApplicationDbContext
|
||||
DbSet<MaterialItem> MaterialItems { get; }
|
||||
DbSet<ServiceItem> ServiceItems { get; }
|
||||
DbSet<WorkItem> WorkItems { get; }
|
||||
// [K1 S164 — Mig 70] Danh mục Hợp đồng SOL-CCM-SP-002 (86 dòng × 8 nhóm duyệt)
|
||||
DbSet<ContractCatalogEntry> ContractCatalogEntries { get; }
|
||||
DbSet<MenuItem> MenuItems { get; }
|
||||
DbSet<Permission> Permissions { get; }
|
||||
DbSet<User> Users { get; }
|
||||
|
||||
@ -0,0 +1,226 @@
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SolutionErp.Application.Common.Exceptions;
|
||||
using SolutionErp.Application.Common.Interfaces;
|
||||
using SolutionErp.Domain.Master.Catalogs;
|
||||
|
||||
namespace SolutionErp.Application.Master.Catalogs;
|
||||
|
||||
// [K1 — S164, Mig 70] CRUD CQRS cho "Danh mục Hợp đồng" (SOL-CCM-SP-002, 86 dòng).
|
||||
// File RIÊNG cạnh CatalogsFeatures.cs (mega-file 4 catalog cũ) vì shape khác hẳn:
|
||||
// có GroupCode + ApprovalGroup (1-8 = nhóm duyệt N1..N8) + 2 cột vai-ký dạng TEXT.
|
||||
//
|
||||
// Endpoints (ContractCatalogController, route `api/catalogs/contract-catalog`):
|
||||
// GET /api/catalogs/contract-catalog — list (filter groupCode/approvalGroup/q;
|
||||
// mặc định CHỈ trả IsActive=true — spec K1.c `IncludeInactive?` mặc-định-loại;
|
||||
// truyền includeInactive=true để thấy cả dòng ngừng hiệu lực — gate-K1 F4 @S165)
|
||||
// GET /api/catalogs/contract-catalog/{id} — get by id
|
||||
// POST /api/catalogs/contract-catalog — create (Admin)
|
||||
// PUT /api/catalogs/contract-catalog/{id} — update (Admin)
|
||||
// DELETE /api/catalogs/contract-catalog/{id} — soft delete (Admin)
|
||||
//
|
||||
// KHÔNG paging: 86 dòng, dùng chung cho trang quản trị + picker chọn hạng mục (K2).
|
||||
// Dup-Code check chạy trên ACTIVE-set (query filter !IsDeleted) — khớp UNIQUE index
|
||||
// filtered `[IsDeleted] = 0` ⇒ Code của row đã xoá mềm được tái sử dụng (gotcha #57).
|
||||
|
||||
// ===== DTO =====
|
||||
|
||||
public record ContractCatalogEntryDto(
|
||||
Guid Id,
|
||||
string Code,
|
||||
string TenVi,
|
||||
string? TenEn,
|
||||
string GroupCode,
|
||||
int ApprovalGroup,
|
||||
string SignerRole,
|
||||
string? InitialsChain,
|
||||
string? NduqNote,
|
||||
bool IsActive,
|
||||
int SortOrder);
|
||||
|
||||
// ===== List =====
|
||||
|
||||
public record ListContractCatalogQuery(
|
||||
string? GroupCode = null,
|
||||
int? ApprovalGroup = null,
|
||||
bool IncludeInactive = false,
|
||||
string? Q = null) : IRequest<List<ContractCatalogEntryDto>>;
|
||||
|
||||
public class ListContractCatalogHandler(IApplicationDbContext db)
|
||||
: IRequestHandler<ListContractCatalogQuery, List<ContractCatalogEntryDto>>
|
||||
{
|
||||
public async Task<List<ContractCatalogEntryDto>> Handle(ListContractCatalogQuery req, CancellationToken ct)
|
||||
{
|
||||
var q = db.ContractCatalogEntries.AsNoTracking().AsQueryable();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(req.GroupCode))
|
||||
q = q.Where(x => x.GroupCode == req.GroupCode);
|
||||
if (req.ApprovalGroup is int g)
|
||||
q = q.Where(x => x.ApprovalGroup == g);
|
||||
if (!req.IncludeInactive)
|
||||
q = q.Where(x => x.IsActive);
|
||||
if (!string.IsNullOrWhiteSpace(req.Q))
|
||||
{
|
||||
var s = req.Q.ToLower();
|
||||
q = q.Where(x => x.Code.ToLower().Contains(s)
|
||||
|| x.TenVi.ToLower().Contains(s)
|
||||
|| (x.TenEn != null && x.TenEn.ToLower().Contains(s)));
|
||||
}
|
||||
|
||||
return await q.OrderBy(x => x.GroupCode).ThenBy(x => x.SortOrder).ThenBy(x => x.Code)
|
||||
.Select(x => new ContractCatalogEntryDto(
|
||||
x.Id, x.Code, x.TenVi, x.TenEn, x.GroupCode, x.ApprovalGroup,
|
||||
x.SignerRole, x.InitialsChain, x.NduqNote, x.IsActive, x.SortOrder))
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Get by id =====
|
||||
|
||||
public record GetContractCatalogEntryQuery(Guid Id) : IRequest<ContractCatalogEntryDto>;
|
||||
|
||||
public class GetContractCatalogEntryHandler(IApplicationDbContext db)
|
||||
: IRequestHandler<GetContractCatalogEntryQuery, ContractCatalogEntryDto>
|
||||
{
|
||||
public async Task<ContractCatalogEntryDto> Handle(GetContractCatalogEntryQuery req, CancellationToken ct)
|
||||
=> await db.ContractCatalogEntries.AsNoTracking()
|
||||
.Where(x => x.Id == req.Id)
|
||||
.Select(x => new ContractCatalogEntryDto(
|
||||
x.Id, x.Code, x.TenVi, x.TenEn, x.GroupCode, x.ApprovalGroup,
|
||||
x.SignerRole, x.InitialsChain, x.NduqNote, x.IsActive, x.SortOrder))
|
||||
.FirstOrDefaultAsync(ct)
|
||||
?? throw new NotFoundException("ContractCatalogEntry", req.Id);
|
||||
}
|
||||
|
||||
// ===== Create =====
|
||||
|
||||
public record CreateContractCatalogEntryCommand(
|
||||
string Code,
|
||||
string TenVi,
|
||||
string? TenEn,
|
||||
string GroupCode,
|
||||
int ApprovalGroup,
|
||||
string SignerRole,
|
||||
string? InitialsChain,
|
||||
string? NduqNote,
|
||||
bool IsActive,
|
||||
int SortOrder) : IRequest<Guid>;
|
||||
|
||||
// MaxLength khớp ĐÚNG EF config (ContractCatalogEntryConfiguration.cs:18-24) —
|
||||
// EF là source of truth, KHÔNG lấy số từ spec.
|
||||
public class CreateContractCatalogEntryValidator : AbstractValidator<CreateContractCatalogEntryCommand>
|
||||
{
|
||||
public CreateContractCatalogEntryValidator()
|
||||
{
|
||||
RuleFor(x => x.Code).NotEmpty().MaximumLength(20);
|
||||
RuleFor(x => x.TenVi).NotEmpty().MaximumLength(500);
|
||||
RuleFor(x => x.TenEn).MaximumLength(500);
|
||||
RuleFor(x => x.GroupCode).NotEmpty().MaximumLength(10);
|
||||
RuleFor(x => x.ApprovalGroup).InclusiveBetween(1, 8)
|
||||
.WithMessage("Nhóm duyệt phải từ 1 đến 8 (N1..N8).");
|
||||
RuleFor(x => x.SignerRole).NotEmpty().MaximumLength(100);
|
||||
RuleFor(x => x.InitialsChain).MaximumLength(300);
|
||||
RuleFor(x => x.NduqNote).MaximumLength(500);
|
||||
}
|
||||
}
|
||||
|
||||
public class CreateContractCatalogEntryHandler(IApplicationDbContext db)
|
||||
: IRequestHandler<CreateContractCatalogEntryCommand, Guid>
|
||||
{
|
||||
public async Task<Guid> Handle(CreateContractCatalogEntryCommand req, CancellationToken ct)
|
||||
{
|
||||
if (await db.ContractCatalogEntries.AnyAsync(x => x.Code == req.Code, ct))
|
||||
throw new ConflictException($"Mã '{req.Code}' đã tồn tại.");
|
||||
|
||||
var entity = new ContractCatalogEntry
|
||||
{
|
||||
Code = req.Code,
|
||||
TenVi = req.TenVi,
|
||||
TenEn = req.TenEn,
|
||||
GroupCode = req.GroupCode,
|
||||
ApprovalGroup = req.ApprovalGroup,
|
||||
SignerRole = req.SignerRole,
|
||||
InitialsChain = req.InitialsChain,
|
||||
NduqNote = req.NduqNote,
|
||||
IsActive = req.IsActive,
|
||||
SortOrder = req.SortOrder,
|
||||
};
|
||||
db.ContractCatalogEntries.Add(entity);
|
||||
await db.SaveChangesAsync(ct);
|
||||
return entity.Id;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Update =====
|
||||
|
||||
public record UpdateContractCatalogEntryCommand(
|
||||
Guid Id,
|
||||
string Code,
|
||||
string TenVi,
|
||||
string? TenEn,
|
||||
string GroupCode,
|
||||
int ApprovalGroup,
|
||||
string SignerRole,
|
||||
string? InitialsChain,
|
||||
string? NduqNote,
|
||||
bool IsActive,
|
||||
int SortOrder) : IRequest;
|
||||
|
||||
public class UpdateContractCatalogEntryValidator : AbstractValidator<UpdateContractCatalogEntryCommand>
|
||||
{
|
||||
public UpdateContractCatalogEntryValidator()
|
||||
{
|
||||
RuleFor(x => x.Id).NotEmpty();
|
||||
RuleFor(x => x.Code).NotEmpty().MaximumLength(20);
|
||||
RuleFor(x => x.TenVi).NotEmpty().MaximumLength(500);
|
||||
RuleFor(x => x.TenEn).MaximumLength(500);
|
||||
RuleFor(x => x.GroupCode).NotEmpty().MaximumLength(10);
|
||||
RuleFor(x => x.ApprovalGroup).InclusiveBetween(1, 8)
|
||||
.WithMessage("Nhóm duyệt phải từ 1 đến 8 (N1..N8).");
|
||||
RuleFor(x => x.SignerRole).NotEmpty().MaximumLength(100);
|
||||
RuleFor(x => x.InitialsChain).MaximumLength(300);
|
||||
RuleFor(x => x.NduqNote).MaximumLength(500);
|
||||
}
|
||||
}
|
||||
|
||||
public class UpdateContractCatalogEntryHandler(IApplicationDbContext db)
|
||||
: IRequestHandler<UpdateContractCatalogEntryCommand>
|
||||
{
|
||||
public async Task Handle(UpdateContractCatalogEntryCommand req, CancellationToken ct)
|
||||
{
|
||||
var entity = await db.ContractCatalogEntries.FirstOrDefaultAsync(x => x.Id == req.Id, ct)
|
||||
?? throw new NotFoundException("ContractCatalogEntry", req.Id);
|
||||
|
||||
if (entity.Code != req.Code && await db.ContractCatalogEntries.AnyAsync(x => x.Code == req.Code, ct))
|
||||
throw new ConflictException($"Mã '{req.Code}' đã tồn tại.");
|
||||
|
||||
entity.Code = req.Code;
|
||||
entity.TenVi = req.TenVi;
|
||||
entity.TenEn = req.TenEn;
|
||||
entity.GroupCode = req.GroupCode;
|
||||
entity.ApprovalGroup = req.ApprovalGroup;
|
||||
entity.SignerRole = req.SignerRole;
|
||||
entity.InitialsChain = req.InitialsChain;
|
||||
entity.NduqNote = req.NduqNote;
|
||||
entity.IsActive = req.IsActive;
|
||||
entity.SortOrder = req.SortOrder;
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Delete (soft) =====
|
||||
|
||||
public record DeleteContractCatalogEntryCommand(Guid Id) : IRequest;
|
||||
|
||||
public class DeleteContractCatalogEntryHandler(IApplicationDbContext db)
|
||||
: IRequestHandler<DeleteContractCatalogEntryCommand>
|
||||
{
|
||||
public async Task Handle(DeleteContractCatalogEntryCommand req, CancellationToken ct)
|
||||
{
|
||||
var entity = await db.ContractCatalogEntries.FirstOrDefaultAsync(x => x.Id == req.Id, ct)
|
||||
?? throw new NotFoundException("ContractCatalogEntry", req.Id);
|
||||
db.ContractCatalogEntries.Remove(entity); // Soft delete via AuditingInterceptor
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user