[CLAUDE] PurchaseEvaluation: cờ gấp PRO/CCM + CCM duyệt-final theo ngưỡng giá trị (Mig 53) + 14 test
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m41s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m41s
Yêu cầu anh Kiệt FDC (sau họp sếp). Mig 53 AddPeUrgentAndCeoApprovalThreshold — 3 AddColumn, no new table (Mig 52→53). Rollout an toàn: cột nullable, ngưỡng null = giữ luồng duyệt cũ 100% cho tới khi admin set.
B — CCM duyệt-final theo NGƯỠNG GIÁ TRỊ ("gói CEO phân quyền theo giá trị"):
- ApprovalWorkflow += CeoApprovalThreshold (decimal?, admin nhập trong Workflow Designer).
- ApproveV2Async: actor role CostControl (CCM) + winnerQuoteTotal (tổng giá NCC được chọn) < ngưỡng → DaDuyet luôn (bỏ CEO); ≥ ngưỡng → đẩy lên CEO như cũ. Ngưỡng null = luồng tuyến tính cũ. Q4 chốt nhận diện theo ROLE người duyệt.
- reviewer PASS 0 blocker: cascade-safe (Off/role không lan), tested load-bearing (CCM dưới ngưỡng → DaDuyet skip CEO).
A — cờ gấp per-vai (visibility-only, Q3 KHÔNG đổi luồng):
- PE += IsUrgentByPro (PRO đỏ) / IsUrgentByCcm (CCM xanh).
- Endpoint PUT /purchase-evaluations/{id}/urgent role-gated (Procurement→ByPro, CostControl→ByCcm, Admin→cả 2, khác→Forbidden) + notify CEO (Director) khi MỚI bật (best-effort).
FE ×2 app: Workflow Designer ô "Ngưỡng giá trị gói CEO" (fe-admin) + PE detail nút bật/tắt cờ gấp đỏ/xanh theo role + badge GẤP + hint "giá trị gói vs ngưỡng → CCM duyệt-final/cần CEO" + PE list badge gấp.
DTO: PE detail += isUrgentByPro/Ccm + winnerQuoteTotal + ceoApprovalThreshold; list += isUrgentByPro/Ccm; workflow V2 += ceoApprovalThreshold.
+14 test (292→306): PeCcmThresholdFinalizeTests 5 (B routing) + PeUrgentToggleAuthzTests 9 (A authz). Build slnx 0/0 · npm build ×2 0 err · dotnet test 306 PASS.
C (sau duyệt xong chuyển phiếu đến dự án) — chờ anh Kiệt làm chi tiết form, CHƯA làm.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -85,6 +85,17 @@ public class PurchaseEvaluationsController(IMediator mediator) : ControllerBase
|
||||
}
|
||||
public record BudgetCcmBody(decimal? InitialAmount, decimal? AdjustmentAmount);
|
||||
|
||||
// [S69 2026-06-17] Cờ gấp (urgent) — anh Kiệt FDC. Class [Authorize] any-auth;
|
||||
// handler fine-grained Forbidden theo role (PRO=Procurement set cờ đỏ, CCM=
|
||||
// CostControl set cờ xanh, Admin cả 2). Bật → notify CEO (Director). Visibility-only.
|
||||
[HttpPut("{id:guid}/urgent")]
|
||||
public async Task<IActionResult> SetUrgent(Guid id, [FromBody] SetUrgentBody body, CancellationToken ct)
|
||||
{
|
||||
await mediator.Send(new SetPurchaseEvaluationUrgentCommand(id, body.IsUrgent), ct);
|
||||
return NoContent();
|
||||
}
|
||||
public record SetUrgentBody(bool IsUrgent);
|
||||
|
||||
[HttpPost("{id:guid}/transitions")]
|
||||
public async Task<IActionResult> Transition(Guid id, [FromBody] TransitionPeBody body, CancellationToken ct)
|
||||
{
|
||||
|
||||
@ -57,6 +57,9 @@ public record AwDefinitionDto(
|
||||
string? Description,
|
||||
bool IsActive,
|
||||
bool IsUserSelectable,
|
||||
// [S69 2026-06-17] Ngưỡng giá trị "gói CEO" — admin Designer hiển thị + edit.
|
||||
// Null = không áp ngưỡng (luồng tuyến tính cũ).
|
||||
decimal? CeoApprovalThreshold,
|
||||
// Mig 29 (S21 t5) — 6 advanced options đã MOVE per-NV: F1+F3 xuống AwLevelDto
|
||||
// (per slot Approver). Workflow-level Mig 28 dropped.
|
||||
// Mig 31 (S23 t1) — F2 cũng refactor xuống Level slot (AllowApproverSkipToFinal
|
||||
@ -153,6 +156,7 @@ public class GetAwAdminOverviewQueryHandler(
|
||||
d.Description,
|
||||
d.IsActive,
|
||||
d.IsUserSelectable,
|
||||
d.CeoApprovalThreshold, // [S69] ngưỡng gói CEO
|
||||
d.ActivatedAt,
|
||||
d.CreatedAt,
|
||||
d.Steps.OrderBy(s => s.Order).Select(s => new AwStepDto(
|
||||
@ -221,7 +225,12 @@ public record CreateAwDefinitionCommand(
|
||||
string Code,
|
||||
string Name,
|
||||
string? Description,
|
||||
List<CreateAwStepInput> Steps) : IRequest<Guid>;
|
||||
List<CreateAwStepInput> Steps,
|
||||
// [S69 2026-06-17] Ngưỡng giá trị "gói CEO" — admin nhập trong Workflow Designer.
|
||||
// CCM (CostControl) duyệt-final không cần lên CEO khi tổng giá NCC được chọn <
|
||||
// ngưỡng; ≥ ngưỡng → vẫn lên CEO (Director). NULL = không áp ngưỡng (luồng cũ).
|
||||
// Nullable passthrough — trailing-optional default backward-compat call-site cũ.
|
||||
decimal? CeoApprovalThreshold = null) : IRequest<Guid>;
|
||||
|
||||
public class CreateAwDefinitionCommandValidator : AbstractValidator<CreateAwDefinitionCommand>
|
||||
{
|
||||
@ -243,6 +252,10 @@ public class CreateAwDefinitionCommandValidator : AbstractValidator<CreateAwDefi
|
||||
RuleFor(x => x.Description).MaximumLength(1000);
|
||||
RuleFor(x => x.Steps).NotEmpty()
|
||||
.WithMessage("Quy trình phải có ít nhất 1 bước.");
|
||||
// [S69] Ngưỡng gói CEO — nếu nhập phải >= 0 (NULL = không áp ngưỡng OK).
|
||||
RuleFor(x => x.CeoApprovalThreshold).GreaterThanOrEqualTo(0)
|
||||
.When(x => x.CeoApprovalThreshold.HasValue)
|
||||
.WithMessage("Ngưỡng giá trị gói CEO không được âm.");
|
||||
RuleForEach(x => x.Steps).ChildRules(step =>
|
||||
{
|
||||
step.RuleFor(s => s.Order).GreaterThanOrEqualTo(1);
|
||||
@ -315,6 +328,7 @@ public class CreateAwDefinitionCommandHandler(IApplicationDbContext db)
|
||||
IsActive = true,
|
||||
IsUserSelectable = true, // Mig 25 — version mới mặc định cho user pick
|
||||
// Mig 29 (S21 t5) — Allow* options đã move xuống Level slot (per-NV)
|
||||
CeoApprovalThreshold = request.CeoApprovalThreshold, // [S69] ngưỡng gói CEO (nullable passthrough)
|
||||
ActivatedAt = DateTime.UtcNow,
|
||||
Steps = request.Steps.OrderBy(s => s.Order)
|
||||
.Select(s => new ApprovalWorkflowStep
|
||||
|
||||
@ -152,6 +152,7 @@ public class ListApprovedPurchaseEvaluationsQueryHandler(IApplicationDbContext d
|
||||
e.ContractId, e.SlaDeadline, e.CreatedAt, e.UpdatedAt,
|
||||
e.DrafterUserId, u != null ? u.FullName : null,
|
||||
e.DepartmentId, d != null ? d.Name : null,
|
||||
e.BudgetPeriodAmount, e.ExpectedRemainingAmount)).ToListAsync(ct);
|
||||
e.BudgetPeriodAmount, e.ExpectedRemainingAmount,
|
||||
e.IsUrgentByPro, e.IsUrgentByCcm)).ToListAsync(ct); // [S69] cờ gấp
|
||||
}
|
||||
}
|
||||
|
||||
@ -35,7 +35,11 @@ public record PurchaseEvaluationListItemDto(
|
||||
// [S61 Mig 50] 2 cột ngân sách mới (mirror detail — FE list chưa render, giữ
|
||||
// parity type PeListItem).
|
||||
decimal? BudgetPeriodAmount,
|
||||
decimal? ExpectedRemainingAmount);
|
||||
decimal? ExpectedRemainingAmount,
|
||||
// [S69 2026-06-17] Cờ gấp per-vai — FE render badge ĐỎ (PRO) / XANH (CCM) trên
|
||||
// card list + ưu tiên hiển thị. 2 cờ độc lập.
|
||||
bool IsUrgentByPro,
|
||||
bool IsUrgentByCcm);
|
||||
|
||||
public record PurchaseEvaluationSupplierDto(
|
||||
Guid Id,
|
||||
@ -230,6 +234,17 @@ public record PurchaseEvaluationDetailBundleDto(
|
||||
decimal? BudgetPeriodAmount,
|
||||
decimal? ExpectedRemainingAmount,
|
||||
PeBudgetSummaryDto? BudgetSummary,
|
||||
// [S69 2026-06-17] Cờ gấp per-vai (PRO ĐỎ / CCM XANH) — FE render badge + toggle.
|
||||
bool IsUrgentByPro,
|
||||
bool IsUrgentByCcm,
|
||||
// [S69] Tổng giá chào của đơn vị NCC/TP ĐƯỢC CHỌN (winner quote total) — SUM
|
||||
// ThanhTien các báo giá thuộc supplier-rows của SelectedSupplierId. 0 khi chưa
|
||||
// chọn. Mirror predicate submit-guard (PurchaseEvaluationWorkflowService ~:188).
|
||||
// FE so với CeoApprovalThreshold hiển thị "CCM duyệt-final" hoặc "cần CEO".
|
||||
decimal WinnerQuoteTotal,
|
||||
// [S69] Ngưỡng gói CEO của workflow đã pin (PE.ApprovalWorkflowId). Null khi
|
||||
// phiếu chưa pin workflow V2 hoặc admin chưa set ngưỡng.
|
||||
decimal? CeoApprovalThreshold,
|
||||
// Mig 23 — schema mới ApprovalWorkflowsV2 pin lúc create. Hiển thị Code +
|
||||
// Name + Version để FE show "QT-DN-V2-001 - Quy trình Duyệt NCC (v01)".
|
||||
Guid? ApprovalWorkflowId,
|
||||
|
||||
@ -588,7 +588,8 @@ public class ListPurchaseEvaluationsQueryHandler(
|
||||
x.e.ContractId, x.e.SlaDeadline, x.e.CreatedAt, x.e.UpdatedAt,
|
||||
x.e.DrafterUserId, x.u != null ? x.u.FullName : null,
|
||||
x.e.DepartmentId, x.d != null ? x.d.Name : null,
|
||||
x.e.BudgetPeriodAmount, x.e.ExpectedRemainingAmount))
|
||||
x.e.BudgetPeriodAmount, x.e.ExpectedRemainingAmount,
|
||||
x.e.IsUrgentByPro, x.e.IsUrgentByCcm))
|
||||
.ToListAsync(ct);
|
||||
|
||||
return new PagedResult<PurchaseEvaluationListItemDto>(items, total, request.Page, request.PageSize);
|
||||
@ -678,7 +679,8 @@ public class GetMyPurchaseEvaluationInboxQueryHandler(
|
||||
x.e.ContractId, x.e.SlaDeadline, x.e.CreatedAt, x.e.UpdatedAt,
|
||||
x.e.DrafterUserId, x.u != null ? x.u.FullName : null,
|
||||
x.e.DepartmentId, x.d != null ? x.d.Name : null,
|
||||
x.e.BudgetPeriodAmount, x.e.ExpectedRemainingAmount))
|
||||
x.e.BudgetPeriodAmount, x.e.ExpectedRemainingAmount,
|
||||
x.e.IsUrgentByPro, x.e.IsUrgentByCcm))
|
||||
.Take(100)
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
@ -880,6 +882,7 @@ public class GetPurchaseEvaluationQueryHandler(
|
||||
|
||||
string? awCode = null, awName = null;
|
||||
int? awVersion = null;
|
||||
decimal? awCeoThreshold = null; // [S69] ngưỡng gói CEO từ workflow pin
|
||||
ApprovalWorkflowOptionsDto? currentLevelOptions = null;
|
||||
PurchaseEvaluationCurrentApprovalDto? currentApproval = null;
|
||||
PurchaseEvaluationApprovalFlowDto? approvalFlow = null;
|
||||
@ -894,6 +897,7 @@ public class GetPurchaseEvaluationQueryHandler(
|
||||
awCode = aw.Code;
|
||||
awName = aw.Name;
|
||||
awVersion = aw.Version;
|
||||
awCeoThreshold = aw.CeoApprovalThreshold; // [S69] ngưỡng gói CEO
|
||||
|
||||
// Mig 29 (S21 t5) + Mig 30 (S22+5) + Mig 31 (S23 t1) — Resolve
|
||||
// Cấp hiện tại + populate 7 Allow* flag của slot Approver đang
|
||||
@ -1034,6 +1038,18 @@ public class GetPurchaseEvaluationQueryHandler(
|
||||
}
|
||||
}
|
||||
|
||||
// [S69] Giá trị gói = tổng ThanhTien báo giá của NCC/TP ĐƯỢC CHỌN (winner).
|
||||
// Tính từ data đã load (Suppliers + Details.Quotes) — KHÔNG query thêm. 0 khi
|
||||
// chưa chọn NCC. FE so với CeoApprovalThreshold → "CCM duyệt-final" / "cần CEO".
|
||||
var winnerSupplierRowIds = e.Suppliers
|
||||
.Where(s => s.SupplierId == e.SelectedSupplierId)
|
||||
.Select(s => s.Id)
|
||||
.ToHashSet();
|
||||
var winnerQuoteTotal = e.Details
|
||||
.SelectMany(det => det.Quotes)
|
||||
.Where(q => winnerSupplierRowIds.Contains(q.PurchaseEvaluationSupplierId))
|
||||
.Sum(q => q.ThanhTien);
|
||||
|
||||
return new PurchaseEvaluationDetailBundleDto(
|
||||
e.Id, e.MaPhieu, e.Type, e.Phase, e.TenGoiThau, e.DiaDiem, e.MoTa,
|
||||
e.HoSoLink, // [HoSoLink] hyperlink thư mục hồ sơ NAS
|
||||
@ -1045,6 +1061,7 @@ public class GetPurchaseEvaluationQueryHandler(
|
||||
e.ContractId,
|
||||
e.PaymentTerms, e.SlaDeadline, e.CreatedAt, e.UpdatedAt,
|
||||
e.BudgetPeriodAmount, e.ExpectedRemainingAmount, peBudgetSummary,
|
||||
e.IsUrgentByPro, e.IsUrgentByCcm, winnerQuoteTotal, awCeoThreshold, // [S69] cờ gấp + giá trị gói + ngưỡng CEO
|
||||
e.ApprovalWorkflowId, awCode, awName, awVersion, currentLevelOptions,
|
||||
currentApproval, approvalFlow,
|
||||
e.Suppliers
|
||||
|
||||
@ -0,0 +1,94 @@
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SolutionErp.Application.Common.Exceptions;
|
||||
using SolutionErp.Application.Common.Interfaces;
|
||||
using SolutionErp.Application.Notifications;
|
||||
using SolutionErp.Domain.Identity;
|
||||
using SolutionErp.Domain.Notifications;
|
||||
|
||||
namespace SolutionErp.Application.PurchaseEvaluations;
|
||||
|
||||
// [S69 2026-06-17] Cờ gấp (urgent) phiếu Duyệt NCC — anh Kiệt FDC. PRO (role
|
||||
// Procurement) bật/tắt cờ ĐỎ (IsUrgentByPro), CCM (role CostControl) bật/tắt cờ XANH
|
||||
// (IsUrgentByCcm), Admin set CẢ 2. Khi MỚI bật (false→true) → notify CEO (role Director)
|
||||
// để "biết gấp + xử lý sớm". Q3 chốt: VISIBILITY-ONLY — KHÔNG đổi luồng duyệt (ai duyệt
|
||||
// vẫn theo ngưỡng giá trị). Mirror cấu trúc command PE hiện có (AdjustBudget/UpsertOpinion).
|
||||
public record SetPurchaseEvaluationUrgentCommand(Guid Id, bool IsUrgent) : IRequest;
|
||||
|
||||
public class SetPurchaseEvaluationUrgentCommandValidator : AbstractValidator<SetPurchaseEvaluationUrgentCommand>
|
||||
{
|
||||
public SetPurchaseEvaluationUrgentCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.Id).NotEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
public class SetPurchaseEvaluationUrgentCommandHandler(
|
||||
IApplicationDbContext db,
|
||||
ICurrentUser currentUser,
|
||||
UserManager<User> userManager,
|
||||
INotificationService notifications) : IRequestHandler<SetPurchaseEvaluationUrgentCommand>
|
||||
{
|
||||
public async Task Handle(SetPurchaseEvaluationUrgentCommand request, CancellationToken ct)
|
||||
{
|
||||
var entity = await db.PurchaseEvaluations.FirstOrDefaultAsync(x => x.Id == request.Id, ct)
|
||||
?? throw new NotFoundException("PurchaseEvaluation", request.Id);
|
||||
|
||||
var roles = currentUser.Roles;
|
||||
var isAdmin = roles.Contains(AppRoles.Admin);
|
||||
var isPro = roles.Contains(AppRoles.Procurement);
|
||||
var isCcm = roles.Contains(AppRoles.CostControl);
|
||||
|
||||
if (!isAdmin && !isPro && !isCcm)
|
||||
throw new ForbiddenException("Chỉ PRO (Procurement) / CCM (CostControl) / Admin được đánh dấu phiếu gấp.");
|
||||
|
||||
// Snapshot để phát hiện chuyển false→true (MỚI bật gấp) → notify CEO 1 lần.
|
||||
var wasUrgent = entity.IsUrgentByPro || entity.IsUrgentByCcm;
|
||||
|
||||
// Role quyết định cờ nào: PRO → ĐỎ (ByPro), CCM → XANH (ByCcm), Admin → CẢ 2.
|
||||
if (isAdmin)
|
||||
{
|
||||
entity.IsUrgentByPro = request.IsUrgent;
|
||||
entity.IsUrgentByCcm = request.IsUrgent;
|
||||
}
|
||||
else if (isPro)
|
||||
{
|
||||
entity.IsUrgentByPro = request.IsUrgent;
|
||||
}
|
||||
else // isCcm
|
||||
{
|
||||
entity.IsUrgentByCcm = request.IsUrgent;
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
||||
// Notify CEO (Director) khi MỚI bật gấp (false→true). Best-effort — KHÔNG fail
|
||||
// toggle nếu notify lỗi (Q3 visibility-only, cờ đã lưu thành công).
|
||||
var nowUrgent = entity.IsUrgentByPro || entity.IsUrgentByCcm;
|
||||
if (request.IsUrgent && nowUrgent && !wasUrgent)
|
||||
{
|
||||
try
|
||||
{
|
||||
var directors = await userManager.GetUsersInRoleAsync(AppRoles.Director);
|
||||
var directorIds = directors.Select(u => u.Id).ToList();
|
||||
if (directorIds.Count > 0)
|
||||
{
|
||||
await notifications.NotifyManyAsync(
|
||||
directorIds,
|
||||
NotificationType.Generic,
|
||||
$"Phiếu Duyệt NCC {entity.MaPhieu ?? entity.TenGoiThau} được đánh dấu GẤP",
|
||||
"Phiếu cần được xử lý sớm.",
|
||||
$"/purchase-evaluations/{entity.Id}",
|
||||
entity.Id,
|
||||
ct);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// best-effort — nuốt lỗi notify, toggle cờ gấp đã lưu thành công.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -34,6 +34,14 @@ public class ApprovalWorkflow : BaseEntity
|
||||
// khi tạo version mới (mirror IsActive default), admin có thể unstick.
|
||||
public bool IsUserSelectable { get; set; }
|
||||
|
||||
// [S69 2026-06-17] Ngưỡng giá trị "gói CEO" — anh Kiệt FDC: CCM (role CostControl)
|
||||
// được duyệt-final KHÔNG cần lên CEO khi giá trị gói (tổng giá NCC được chọn,
|
||||
// winnerQuoteTotal) < ngưỡng này; ≥ ngưỡng → vẫn đẩy lên CEO (Director) như luồng
|
||||
// tuyến tính cũ. NULL = KHÔNG áp ngưỡng (giữ luồng cũ 100% — backward compat,
|
||||
// rollout an toàn cho tới khi admin set). Admin nhập trong Workflow Designer. So
|
||||
// sánh ở ApproveV2Async khi actor có role CostControl. decimal(18,2).
|
||||
public decimal? CeoApprovalThreshold { get; set; }
|
||||
|
||||
// Mig 28 cũ 6 column workflow-level Allow* đã DROP trong Mig 29 (S21 t5).
|
||||
// Refactor sang per-NV (per ApprovalWorkflowLevel slot + Users F2). Backfill
|
||||
// bulk SQL copy workflow → all Levels của workflow trước khi DROP — preserve
|
||||
|
||||
@ -57,6 +57,14 @@ public class PurchaseEvaluation : AuditableEntity
|
||||
// hiện tại khi pin ApprovalWorkflowId. Null khi V1 legacy hoặc terminal.
|
||||
public int? CurrentApprovalLevelOrder { get; set; }
|
||||
|
||||
// [S69 2026-06-17] Cờ gấp (urgent) — anh Kiệt FDC: PRO (role Procurement) bật cờ
|
||||
// ĐỎ, CCM (role CostControl) bật cờ XANH — 2 cờ độc lập per-vai. Gói gấp → notify
|
||||
// CEO (Director) biết gấp + xử lý sớm. VISIBILITY-ONLY: KHÔNG đổi luồng duyệt (ai
|
||||
// duyệt vẫn theo ngưỡng giá trị) — chỉ badge + thông báo + ưu tiên list. Toggle qua
|
||||
// endpoint theo role actor (PRO ↔ IsUrgentByPro, CCM ↔ IsUrgentByCcm).
|
||||
public bool IsUrgentByPro { get; set; }
|
||||
public bool IsUrgentByCcm { get; set; }
|
||||
|
||||
public List<PurchaseEvaluationSupplier> Suppliers { get; set; } = new();
|
||||
public List<PurchaseEvaluationDetail> Details { get; set; } = new();
|
||||
public List<PurchaseEvaluationQuote> Quotes { get; set; } = new();
|
||||
|
||||
@ -16,6 +16,10 @@ public class ApprovalWorkflowConfiguration : IEntityTypeConfiguration<ApprovalWo
|
||||
e.Property(x => x.Description).HasMaxLength(1000);
|
||||
e.Property(x => x.ApplicableType).HasConversion<int>();
|
||||
|
||||
// [S69 2026-06-17] Ngưỡng giá trị "gói CEO" — decimal(18,2), nullable
|
||||
// (null = không áp ngưỡng, giữ luồng tuyến tính cũ).
|
||||
e.Property(x => x.CeoApprovalThreshold).HasPrecision(18, 2);
|
||||
|
||||
e.HasIndex(x => new { x.Code, x.Version }).IsUnique();
|
||||
e.HasIndex(x => new { x.ApplicableType, x.IsActive });
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,52 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddPeUrgentAndCeoApprovalThreshold : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsUrgentByCcm",
|
||||
table: "PurchaseEvaluations",
|
||||
type: "bit",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsUrgentByPro",
|
||||
table: "PurchaseEvaluations",
|
||||
type: "bit",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<decimal>(
|
||||
name: "CeoApprovalThreshold",
|
||||
table: "ApprovalWorkflows",
|
||||
type: "decimal(18,2)",
|
||||
precision: 18,
|
||||
scale: 2,
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsUrgentByCcm",
|
||||
table: "PurchaseEvaluations");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsUrgentByPro",
|
||||
table: "PurchaseEvaluations");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CeoApprovalThreshold",
|
||||
table: "ApprovalWorkflows");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -137,6 +137,10 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||
b.Property<int>("ApplicableType")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal?>("CeoApprovalThreshold")
|
||||
.HasPrecision(18, 2)
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
@ -4624,6 +4628,12 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("IsUrgentByCcm")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("IsUrgentByPro")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("MaPhieu")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
@ -813,6 +813,46 @@ public class PurchaseEvaluationWorkflowService(
|
||||
}
|
||||
}
|
||||
|
||||
// [S69 2026-06-17] CCM duyệt-final theo NGƯỠNG GIÁ TRỊ (anh Kiệt FDC). Khi NV
|
||||
// duyệt có role CostControl (CCM) + quy trình set CeoApprovalThreshold + giá trị
|
||||
// gói (tổng giá NCC được chọn, winnerQuoteTotal) < ngưỡng → DaDuyet luôn, BỎ các
|
||||
// Bước/Cấp còn lại (CEO). Q4 chốt: nhận diện theo ROLE người duyệt. Ngưỡng null =
|
||||
// bỏ qua (luồng tuyến tính cũ — rollout an toàn). Cờ gấp KHÔNG ảnh hưởng routing
|
||||
// (visibility-only, Q3). Guard "chưa ở slot cuối" → chỉ skip-forward (nếu CCM đã
|
||||
// ở Cấp cuối Bước cuối thì normal-advance bên dưới cũng ra DaDuyet). Giả định quy
|
||||
// trình đặt CCM ngay trước CEO — UAT anh Kiệt xác nhận cấu trúc.
|
||||
if (aw.CeoApprovalThreshold is decimal ceoThreshold
|
||||
&& actorRoles.Contains(AppRoles.CostControl)
|
||||
&& !(currentIdx == steps.Count - 1 && currentLevelOrder == maxLevelOrder))
|
||||
{
|
||||
var winnerSupplierRowIds = await db.PurchaseEvaluationSuppliers.AsNoTracking()
|
||||
.Where(s => s.PurchaseEvaluationId == evaluation.Id
|
||||
&& s.SupplierId == evaluation.SelectedSupplierId)
|
||||
.Select(s => s.Id)
|
||||
.ToListAsync(ct);
|
||||
var winnerQuoteTotal = winnerSupplierRowIds.Count == 0 ? 0m
|
||||
: await db.PurchaseEvaluationQuotes.AsNoTracking()
|
||||
.Where(q => winnerSupplierRowIds.Contains(q.PurchaseEvaluationSupplierId))
|
||||
.SumAsync(q => (decimal?)q.ThanhTien, ct) ?? 0m;
|
||||
|
||||
if (winnerQuoteTotal < ceoThreshold)
|
||||
{
|
||||
evaluation.Phase = PurchaseEvaluationPhase.DaDuyet;
|
||||
evaluation.CurrentWorkflowStepIndex = null;
|
||||
evaluation.CurrentApprovalLevelOrder = null;
|
||||
evaluation.SlaDeadline = null;
|
||||
await LogTransitionAsync(
|
||||
evaluation,
|
||||
PurchaseEvaluationPhase.ChoDuyet,
|
||||
PurchaseEvaluationPhase.DaDuyet,
|
||||
actorUserId,
|
||||
ApprovalDecision.Approve,
|
||||
$"[CCM duyệt cuối — gói {winnerQuoteTotal:N0}đ < ngưỡng CEO {ceoThreshold:N0}đ, không cần CEO duyệt] {comment ?? ""}".Trim(),
|
||||
ct);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Advance: nếu còn cấp tiếp trong Step → levelOrder++; else → next Step + level 1
|
||||
if (currentLevelOrder < maxLevelOrder)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user