[CLAUDE] Contract: K7 SP-002 bridge KHKK->HD hoi-2-khoa + Line.ContractId + grant seeder (gate PASS-WITH-FLAGS 10, F-8 fixed)
Some checks failed
Deploy SOLUTION_ERP / build-deploy (push) Has been cancelled

BE: POST {id}/create-contract (Contracts.Create AND KeHoachKyKet.Read) + guard 4-ve IsActive/IsUserSelectable
+ Line.ContractId write-site DAU TIEN + list-DTO ContractIds + SeedContractCreateAccessAsync (Drafter/
Procurement/Admin upgrade-only, cau III default cho ratify). FE x2 app SHA-pair 3/3: nut bridge gate 2-khoa
+ dialog khuon-san + cay GD3/GD4 doc TAP contractIds (union pe.contractId legacy). Test +17 (12 bridge T1-T12
+ 5 authz) suite 639/0; ngoai-le CreateContract trong AuthorizePolicyRegressionTests (#85).
Gate F-8 fixed truoc commit: neo so-dong :2516 chet-luc-land -> neo ten ham (3 cho) + 13/13->12/13 theo do
prod + khai chu-dich CanRead + sua tro-nham-ham. No mang theo (0-blocker): MAJOR-race-2-request-dong-thoi
(chong-trung in-memory, phuong an unique/RowVersion = nhip sau) + DraftData-khong-carry + a11y/O(n.m) FE.
UAT-NOI-TRUOC: nut sang sau restart + user RE-LOGIN (menu cache); acceptance seeder log = 0 added + 2 upgraded.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-08-01 13:04:49 +07:00
parent c97f948a7b
commit aaed699228
13 changed files with 2241 additions and 46 deletions

View File

@ -5,6 +5,7 @@ using SolutionErp.Application.Common.Models;
using SolutionErp.Application.ContractSigningPlans;
using SolutionErp.Application.ContractSigningPlans.Services;
using SolutionErp.Domain.ContractSigningPlans;
using SolutionErp.Domain.Contracts; // ContractType (cầu GĐ2 → GĐ3, K7)
namespace SolutionErp.Api.Controllers;
@ -213,6 +214,30 @@ public class ContractSigningPlansController(
=> Ok(await workflow.TransitionAsync(id, body.Action, body.Comment,
body.ApplyLevelFinalize ?? true, ct));
// ===================== Cầu GĐ2 → GĐ3 (K7 — S167) =====================
// 🔴 [vá-1 spec-cụm-3 §②-1] AUTHZ = HỘI 2 KHOÁ, cả 2 vế đều phải chặn được:
// vế 1 (class, dòng :28) `KeHoachKyKet.Read` — không được xem kế hoạch thì không được
// lấy dòng của nó đi tạo HĐ;
// vế 2 (action, ngay dưới) `Contracts.Create` — tài nguyên SINH RA là Contract ⇒ khoá phải
// là khoá của thứ được tạo (gotcha #85:
// gate bằng đúng key policy của endpoint).
// ASP.NET Core cộng dồn [Authorize] class + action theo AND ⇒ thiếu 1 trong 2 = 403.
// ⚠️ `Contracts.Create` trước S167 KHÔNG role nào có (seeder `SeedAllRolesReviewReadPermissionsAsync`
// đặt `CanCreate = isPe` ⇒ false cho key `Contracts` — grep tên hàm, ĐỪNG neo số dòng: file này
// dài ra là số thối [gate-K7 F-8 bắt neo cũ sai ngay lúc land]); `SeedContractCreateAccessAsync` cấp cho
// Drafter + Procurement + Admin — grant đó ăn @restart, KHÔNG phải deploy FE.
// ⚠️ KHÔNG động tới `POST /api/contracts` (`ContractsController` `[Authorize]` trần, #82 treo
// chủ đích): siết ở đây là siết CỬA MỚI, không phải sửa cửa cũ.
[HttpPost("{id:guid}/create-contract")]
[Authorize(Policy = "Contracts.Create")]
public async Task<ActionResult<CreateContractFromSigningPlanResult>> CreateContract(
Guid id, [FromBody] CreateContractFromSigningPlanBody body, CancellationToken ct)
{
var result = await mediator.Send(new CreateContractFromSigningPlanCommand(
id, body.LineIds ?? [], body.ContractType, body.ApprovalWorkflowId, body.TenHopDong), ct);
return Created($"/api/contracts/{result.ContractId}", result);
}
// ========================= Body records =========================
// PUT header: KHÔNG mang `Id` trong body (id lấy từ route) — tránh 2 nguồn sự thật.
// Mọi field nullable = null-safe: client không gửi ⇒ giữ giá trị cũ (#73).
@ -231,4 +256,13 @@ public class ContractSigningPlansController(
string Action,
string? Comment = null,
bool? ApplyLevelFinalize = null);
// [K7 S167] Cầu → HĐ. `PlanId` lấy từ route (không nhận trong body — 1 nguồn sự thật).
// `LineIds` nullable ở BODY để client gửi thiếu ra 400 của validator ("chọn ít nhất 1 dòng")
// thay vì 500 NullReference ở tầng dưới.
public record CreateContractFromSigningPlanBody(
List<Guid>? LineIds,
ContractType ContractType,
Guid ApprovalWorkflowId,
string? TenHopDong = null);
}