[CLAUDE] PurchaseEvaluation: ngan sach goi thau theo Excel anh Kiet - bang tong hop 2 block + nhap theo role PRO/CCM + xoa module Budget cu (Mig 50)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m31s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m31s
- Mig 50 ReplaceBudgetModuleWithPeWorkItemBudgets: bang moi PeWorkItemBudgets (1 record/cap Du an x Hang muc, UNIQUE filtered [IsDeleted]=0) + drop 5 bang Budget cu + PE/Contracts drop BudgetId + backfill BudgetManualAmount->BudgetPeriodAmount TRUOC DropColumn (phieu UAT giu so) + DELETE menu/permission Bg_* IN-list children-first
- BE: PUT {id}/budget/pro (role Procurement) + {id}/budget/ccm (role CostControl, Adjustment cho phep AM) fail-closed Forbidden-truoc-side-effect + EnsureTrackedAsync race-safe (catch unique -> re-fetch winner, loi khac rethrow) + auto-create record khi tao phieu + budgetSummary DTO (luy ke trinh-truoc/chon-thau-truoc/de-xuat-ky-nay + full fallback du-tru-PRO + canEdit flags) + submit-guard (3) doi predicate BudgetPeriodAmount -> "chua nhap Ngan sach ky nay" + PATCH budget-adjust absolute-set 2 field moi + Contract GIU BudgetManual* (HD nhap tay khong doi) + ke thua HD map BudgetPeriodAmount
- FE x2 app SHA256 identical: bang "TONG HOP NGAN SACH TRINH KY" block A (full dam + ban hanh + V0 hieu chinh + du tru PRO + ghi chu, editable theo canEditPro/canEditCcm) + block B 9 dong cong thuc Excel (5=1+3, 6=2+4, 7=full-5, 8 tu nhap default 7, 9=4+8) + to mau vuot ngan sach #C00000 / am do / red-soft row8>row7 + "Chua chon" khi count=0 + banner phieu chua gan Hang muc + o "Ngan sach ky nay" o create/header + XOA pages/components/types budgets + routes + menuKeys + Layout staticMap 4-place
- Tests: +22 PeWorkItemBudgetTests (auto-create x3, ensure/race x2, authz matrix PRO x5 + CCM x3, budgetSummary aggregates x5, adjust x4) - 14 BudgetPolicyTests xoa theo module - 1 test via-BudgetId -> 263 PASS (45 Domain + 218 Infra, 0 fail)
- database-agent advise adopted: khong FK vat ly PE/Contracts->Budgets (DropColumn khong can DropForeignKey) + DropIndex truoc DropColumn (SQL 5074) + IN-list thay LIKE Bg_% (underscore wildcard + miss root) + khong Serializable wrap (nested-tx conflict codegen)
- Reviewer PASS-with-minor 0 blocker (verdict-first survived); 2 minor da sua truoc commit (comment adjustMut absolute-set + dead key budgetId); note: F4 approver-edit-budget UI entry tam drafter-only, BE van cho approver scope - cho UAT anh Kiet
- Scaffold-bug caught: EF tu sinh RenameColumn BudgetManualAmount->ExpectedRemainingAmount (SAI semantics) -> thay bang Add+UPDATE+Drop
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@ -1,100 +0,0 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SolutionErp.Application.Budgets;
|
||||
using SolutionErp.Application.Budgets.Dtos;
|
||||
using SolutionErp.Application.Common.Models;
|
||||
using SolutionErp.Domain.Budgets;
|
||||
using SolutionErp.Domain.Contracts;
|
||||
|
||||
namespace SolutionErp.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/budgets")]
|
||||
[Authorize]
|
||||
public class BudgetsController(IMediator mediator) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<PagedResult<BudgetListItemDto>>> List(
|
||||
[FromQuery] int page = 1, [FromQuery] int pageSize = 20,
|
||||
[FromQuery] string? search = null, [FromQuery] bool sortDesc = true,
|
||||
[FromQuery] BudgetPhase? phase = null,
|
||||
[FromQuery] Guid? projectId = null,
|
||||
[FromQuery] int? namNganSach = null,
|
||||
CancellationToken ct = default)
|
||||
=> Ok(await mediator.Send(new ListBudgetsQuery(phase, projectId, namNganSach)
|
||||
{ Page = page, PageSize = pageSize, Search = search, SortDesc = sortDesc }, ct));
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
public async Task<ActionResult<BudgetDetailBundleDto>> Get(Guid id, CancellationToken ct)
|
||||
=> Ok(await mediator.Send(new GetBudgetQuery(id), ct));
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<object>> Create([FromBody] CreateBudgetCommand cmd, CancellationToken ct)
|
||||
{
|
||||
var id = await mediator.Send(cmd, ct);
|
||||
return CreatedAtAction(nameof(Get), new { id }, new { id });
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateBudgetDraftCommand cmd, CancellationToken ct)
|
||||
{
|
||||
if (id != cmd.Id) return BadRequest(new { detail = "ID không khớp" });
|
||||
await mediator.Send(cmd, ct);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}/transitions")]
|
||||
public async Task<IActionResult> Transition(Guid id, [FromBody] TransitionBudgetBody body, CancellationToken ct)
|
||||
{
|
||||
await mediator.Send(new TransitionBudgetCommand(id, body.TargetPhase, body.Decision, body.Comment), ct);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpDelete("{id:guid}")]
|
||||
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
|
||||
{
|
||||
await mediator.Send(new DeleteBudgetCommand(id), ct);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}/details")]
|
||||
public async Task<ActionResult<object>> AddDetail(Guid id, [FromBody] BudgetDetailBody body, CancellationToken ct)
|
||||
{
|
||||
var newId = await mediator.Send(new AddBudgetDetailCommand(
|
||||
id, body.GroupCode, body.GroupName, body.ItemCode, body.NoiDung, body.DonViTinh,
|
||||
body.KhoiLuong, body.DonGia, body.ThanhTien, body.GhiChu), ct);
|
||||
return Ok(new { id = newId });
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}/details/{detailId:guid}")]
|
||||
public async Task<IActionResult> UpdateDetail(Guid id, Guid detailId, [FromBody] BudgetDetailBody body, CancellationToken ct)
|
||||
{
|
||||
await mediator.Send(new UpdateBudgetDetailCommand(
|
||||
id, detailId, body.GroupCode, body.GroupName, body.ItemCode, body.NoiDung, body.DonViTinh,
|
||||
body.KhoiLuong, body.DonGia, body.ThanhTien, body.GhiChu), ct);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpDelete("{id:guid}/details/{detailId:guid}")]
|
||||
public async Task<IActionResult> DeleteDetail(Guid id, Guid detailId, CancellationToken ct)
|
||||
{
|
||||
await mediator.Send(new DeleteBudgetDetailCommand(id, detailId), ct);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}/changelogs")]
|
||||
public async Task<List<BudgetChangelogDto>> GetChangelogs(Guid id, CancellationToken ct)
|
||||
=> await mediator.Send(new ListBudgetChangelogsQuery(id), ct);
|
||||
|
||||
// 2-stage department approval list (Phase 9 — Migration 16).
|
||||
[HttpGet("{id:guid}/department-approvals")]
|
||||
public async Task<ActionResult<List<BudgetDepartmentApprovalDto>>> ListDepartmentApprovals(
|
||||
Guid id, CancellationToken ct)
|
||||
=> Ok(await mediator.Send(new ListBudgetDepartmentApprovalsQuery(id), ct));
|
||||
}
|
||||
|
||||
public record TransitionBudgetBody(BudgetPhase TargetPhase, ApprovalDecision Decision, string? Comment);
|
||||
public record BudgetDetailBody(
|
||||
string GroupCode, string GroupName, string? ItemCode, string NoiDung,
|
||||
string? DonViTinh, decimal KhoiLuong, decimal DonGia, decimal ThanhTien, string? GhiChu);
|
||||
@ -55,15 +55,35 @@ public class PurchaseEvaluationsController(IMediator mediator) : ControllerBase
|
||||
|
||||
// S22+4 — Feature 2: Section "Điều chỉnh ngân sách" tách endpoint riêng.
|
||||
// Cho phép Drafter (DangSoanThao/TraLai) OR Approver currentLevel (ChoDuyet)
|
||||
// OR Admin adjust Budget* fields. Handler kiểm phase + actor scope.
|
||||
// OR Admin. [S61 Mig 50] Body đổi sang 2 field mới (NS kỳ này + dự kiến còn
|
||||
// lại) — BudgetId/BudgetManual* cũ DROP cùng module Budget. Absolute-set.
|
||||
[HttpPatch("{id:guid}/budget-adjust")]
|
||||
public async Task<IActionResult> AdjustBudget(Guid id, [FromBody] AdjustBudgetBody body, CancellationToken ct)
|
||||
{
|
||||
await mediator.Send(new AdjustPurchaseEvaluationBudgetCommand(
|
||||
id, body.BudgetId, body.BudgetManualName, body.BudgetManualAmount), ct);
|
||||
id, body.BudgetPeriodAmount, body.ExpectedRemainingAmount), ct);
|
||||
return NoContent();
|
||||
}
|
||||
public record AdjustBudgetBody(Guid? BudgetId, string? BudgetManualName, decimal? BudgetManualAmount);
|
||||
public record AdjustBudgetBody(decimal? BudgetPeriodAmount, decimal? ExpectedRemainingAmount);
|
||||
|
||||
// [S61 Mig 50] Ngân sách gói thầu per cặp (Dự án, Hạng mục) — nhập theo ROLE.
|
||||
// Class [Authorize] any-auth; handler fine-gained Forbidden (PRO=Procurement,
|
||||
// CCM=CostControl, Admin cả 2 — pattern AssignItTicketHandler S54).
|
||||
[HttpPut("{id:guid}/budget/pro")]
|
||||
public async Task<IActionResult> UpdateBudgetPro(Guid id, [FromBody] BudgetProBody body, CancellationToken ct)
|
||||
{
|
||||
await mediator.Send(new UpdatePeBudgetProCommand(id, body.ProEstimateAmount, body.ProNote), ct);
|
||||
return NoContent();
|
||||
}
|
||||
public record BudgetProBody(decimal? ProEstimateAmount, string? ProNote);
|
||||
|
||||
[HttpPut("{id:guid}/budget/ccm")]
|
||||
public async Task<IActionResult> UpdateBudgetCcm(Guid id, [FromBody] BudgetCcmBody body, CancellationToken ct)
|
||||
{
|
||||
await mediator.Send(new UpdatePeBudgetCcmCommand(id, body.InitialAmount, body.AdjustmentAmount), ct);
|
||||
return NoContent();
|
||||
}
|
||||
public record BudgetCcmBody(decimal? InitialAmount, decimal? AdjustmentAmount);
|
||||
|
||||
[HttpPost("{id:guid}/transitions")]
|
||||
public async Task<IActionResult> Transition(Guid id, [FromBody] TransitionPeBody body, CancellationToken ct)
|
||||
|
||||
Reference in New Issue
Block a user