[CLAUDE] PurchaseEvaluation: endsBeforeCeo pointer-aware + duyệt theo khoảng Min–Max (Mig 66)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m26s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m26s
Hai chỉnh từ buổi UAT anh Kiệt FDC (S117): 1) Fix hiển thị lũy kế "duyệt rồi không bắt" — root cause là DISPLAY-LIE, không phải lũy kế bug. endsBeforeCeo cho phiếu CHƯA DaDuyet nay POINTER-AWARE: chỉ báo "kết thúc trước CEO" khi con-trỏ còn ở/trước cấp finalize. Phiếu đã đi QUA cấp finalize (approver bỏ tick "Cấp này KẾT THÚC" → trình tiếp CEO) → false: sơ đồ hết làm mờ Bước CEO + hết nói dối "Kết thúc tại Cấp X" khi phiếu đang chờ CEO. 3 site (detail in-memory + list/inbox EF-subquery). Filter lũy kế GIỮ nguyên (đúng nguyên tắc "duyệt rồi mới bắc"). BE-only — FE là consumer thuần nên tự render đúng. 2) Feature "duyệt theo KHOẢNG Min–Max": khi PRO có cả Min & Max, người duyệt cấp cuối chọn CẢ HAI (radio "PRO — cả Min và Max", source=ProMinMax) → Min ở ApprovedPriceAmount + Max ở ApprovedPriceMaxAmount (Mig 66 AddColumn nullable, no backfill, phiếu cũ nguyên vẹn). Bất-biến Max⟺ProMinMax, Min≤Max (guard ở service + validator). HĐ.GiaTri KHÔNG đọc giá chốt (= SUM báo giá NCC được chọn) → range thuần record-of-decision hiển thị, 0 tác động HĐ. FE 2-app SHA-mirror (Panel + DetailTabs). Test 495 → 501 (+1 pointer-aware repro, +5 ProMinMax). Cả 2 điều tra qua /fable-real engine-đắt (investigator-codebase deep-pass) theo yêu cầu anh Kiệt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -47,6 +47,7 @@ public class PurchaseEvaluationWorkflowService(
|
||||
bool finalizeByCcmDelegation = false,
|
||||
decimal? approvedPriceAmount = null,
|
||||
string? approvedPriceSource = null,
|
||||
decimal? approvedPriceMaxAmount = null,
|
||||
bool applyLevelFinalize = true,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
@ -288,7 +289,7 @@ public class PurchaseEvaluationWorkflowService(
|
||||
// V2 path nhận flag, V1 legacy throw nếu non-admin gọi skipToFinal=true.
|
||||
if (evaluation.ApprovalWorkflowId is Guid awId)
|
||||
{
|
||||
await ApproveV2Async(evaluation, awId, actorUserId, actorRoles, isAdmin, isSystem, comment, skipToFinal, finalizeByCcmDelegation, approvedPriceAmount, approvedPriceSource, applyLevelFinalize, ct);
|
||||
await ApproveV2Async(evaluation, awId, actorUserId, actorRoles, isAdmin, isSystem, comment, skipToFinal, finalizeByCcmDelegation, approvedPriceAmount, approvedPriceSource, approvedPriceMaxAmount, applyLevelFinalize, ct);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -659,6 +660,7 @@ public class PurchaseEvaluationWorkflowService(
|
||||
bool finalizeByCcmDelegation,
|
||||
decimal? approvedPriceAmount,
|
||||
string? approvedPriceSource,
|
||||
decimal? approvedPriceMaxAmount,
|
||||
bool applyLevelFinalize,
|
||||
CancellationToken ct)
|
||||
{
|
||||
@ -855,7 +857,7 @@ public class PurchaseEvaluationWorkflowService(
|
||||
// [S97 anh Kiệt FDC] Đánh dấu RUNTIME: phiếu kết thúc sớm tại cấp finalize (không lên CEO)
|
||||
// → EndsBeforeCeo display đọc cờ này thay vì suy từ config (phiếu opt-out lên CEO hiện đúng).
|
||||
evaluation.EndedByLevelFinalize = true;
|
||||
ApplyApprovedPriceOnFinalize(evaluation, isSystem, approvedPriceAmount, approvedPriceSource);
|
||||
ApplyApprovedPriceOnFinalize(evaluation, isSystem, approvedPriceAmount, approvedPriceSource, approvedPriceMaxAmount);
|
||||
evaluation.Phase = PurchaseEvaluationPhase.DaDuyet;
|
||||
evaluation.CurrentWorkflowStepIndex = null;
|
||||
evaluation.CurrentApprovalLevelOrder = null;
|
||||
@ -893,7 +895,7 @@ public class PurchaseEvaluationWorkflowService(
|
||||
throw new ConflictException(
|
||||
$"Giá gói {winnerQuoteTotal:N0}đ ≥ ngưỡng CEO {ceoThreshold:N0}đ — phải trình CEO duyệt, không được duyệt done miễn CEO.");
|
||||
|
||||
ApplyApprovedPriceOnFinalize(evaluation, isSystem, approvedPriceAmount, approvedPriceSource);
|
||||
ApplyApprovedPriceOnFinalize(evaluation, isSystem, approvedPriceAmount, approvedPriceSource, approvedPriceMaxAmount);
|
||||
evaluation.Phase = PurchaseEvaluationPhase.DaDuyet;
|
||||
evaluation.CurrentWorkflowStepIndex = null;
|
||||
evaluation.CurrentApprovalLevelOrder = null;
|
||||
@ -925,7 +927,7 @@ public class PurchaseEvaluationWorkflowService(
|
||||
{
|
||||
// All Steps done — terminal DaDuyet. [Mig 54 ①] người duyệt cấp cuối (CEO/NV
|
||||
// cuối) chọn giá chốt khi duyệt — bind trước khi sang DaDuyet.
|
||||
ApplyApprovedPriceOnFinalize(evaluation, isSystem, approvedPriceAmount, approvedPriceSource);
|
||||
ApplyApprovedPriceOnFinalize(evaluation, isSystem, approvedPriceAmount, approvedPriceSource, approvedPriceMaxAmount);
|
||||
evaluation.Phase = PurchaseEvaluationPhase.DaDuyet;
|
||||
evaluation.CurrentWorkflowStepIndex = null;
|
||||
evaluation.CurrentApprovalLevelOrder = null;
|
||||
@ -947,22 +949,38 @@ public class PurchaseEvaluationWorkflowService(
|
||||
// DaDuyet. Người duyệt THẬT bắt buộc chọn 1 giá (Conflict nếu thiếu) → đúng ý "CEO
|
||||
// phải chọn 1 giá làm giá chốt"; auto-approve hệ thống (SLA, isSystem) MIỄN — không
|
||||
// có người để chọn. Source ∈ {Ncc,ProMin,ProMax,Ccm}; amount = snapshot lúc duyệt.
|
||||
private static readonly string[] ValidApprovedPriceSources = { "Ncc", "ProMin", "ProMax", "Ccm" };
|
||||
// [S117 2026-07-14 — anh Kiệt FDC] +"ProMinMax" = duyệt theo KHOẢNG: Amount=Min, maxAmount=Max.
|
||||
internal const string ProMinMaxSource = "ProMinMax";
|
||||
private static readonly string[] ValidApprovedPriceSources = { "Ncc", "ProMin", "ProMax", "Ccm", ProMinMaxSource };
|
||||
private static void ApplyApprovedPriceOnFinalize(
|
||||
PurchaseEvaluation evaluation, bool isSystem, decimal? amount, string? source)
|
||||
PurchaseEvaluation evaluation, bool isSystem, decimal? amount, string? source, decimal? maxAmount = null)
|
||||
{
|
||||
if (amount is null)
|
||||
{
|
||||
if (!isSystem)
|
||||
throw new ConflictException(
|
||||
"Chọn 1 giá chốt (NCC / PRO Min / PRO Max / CCM) trước khi duyệt cấp cuối.");
|
||||
"Chọn 1 giá chốt (NCC / PRO Min / PRO Max / CCM / cả Min–Max) trước khi duyệt cấp cuối.");
|
||||
return; // auto-approve hệ thống (SLA) — không bắt chọn giá
|
||||
}
|
||||
if (source is null || !ValidApprovedPriceSources.Contains(source))
|
||||
throw new ConflictException(
|
||||
"Nguồn giá chốt không hợp lệ (phải là NCC / PRO Min / PRO Max / CCM).");
|
||||
"Nguồn giá chốt không hợp lệ (phải là NCC / PRO Min / PRO Max / CCM / cả Min–Max).");
|
||||
// [S117] "Duyệt theo khoảng" (ProMinMax): bắt buộc có cả Max, Amount=Min, Min ≤ Max.
|
||||
if (source == ProMinMaxSource)
|
||||
{
|
||||
if (maxAmount is null)
|
||||
throw new ConflictException("Duyệt theo khoảng (cả Min–Max) phải có cả giá Min và giá Max.");
|
||||
if (amount > maxAmount)
|
||||
throw new ConflictException("Giá Min không được lớn hơn giá Max khi duyệt theo khoảng.");
|
||||
}
|
||||
else if (maxAmount is not null)
|
||||
{
|
||||
// Source đơn mà kèm Max = wire-bug (FE gửi sai) → chặn, giữ bất-biến Max⟺ProMinMax.
|
||||
throw new ConflictException("Chỉ nguồn 'cả Min–Max' mới được kèm giá Max.");
|
||||
}
|
||||
evaluation.ApprovedPriceAmount = amount;
|
||||
evaluation.ApprovedPriceSource = source;
|
||||
evaluation.ApprovedPriceMaxAmount = source == ProMinMaxSource ? maxAmount : null;
|
||||
}
|
||||
|
||||
// ===== V1 legacy (Mig 21) — iterate PurchaseEvaluationWorkflowSteps =====
|
||||
|
||||
Reference in New Issue
Block a user