[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:
@ -284,6 +284,7 @@ public record PurchaseEvaluationDetailBundleDto(
|
||||
string? CcmSuggestedPriceNote,
|
||||
decimal? ApprovedPriceAmount,
|
||||
string? ApprovedPriceSource,
|
||||
decimal? ApprovedPriceMaxAmount, // [S117] Max của khoảng khi source=ProMinMax (duyệt theo Min–Max)
|
||||
bool CanEditProSuggestedPrice,
|
||||
bool CanEditCcmSuggestedPrice,
|
||||
// Mig 23 — schema mới ApprovalWorkflowsV2 pin lúc create. Hiển thị Code +
|
||||
|
||||
@ -479,6 +479,8 @@ public record TransitionPurchaseEvaluationCommand(
|
||||
bool FinalizeByCcmDelegation = false,
|
||||
decimal? ApprovedPriceAmount = null,
|
||||
string? ApprovedPriceSource = null,
|
||||
// [S117 2026-07-14 — anh Kiệt FDC] Max của khoảng khi source="ProMinMax" (duyệt theo Min–Max).
|
||||
decimal? ApprovedPriceMaxAmount = null,
|
||||
// [S96 2026-07-01 — anh Kiệt FDC] false = người duyệt BỎ tick "Cấp này KẾT THÚC" → vẫn trình CEO.
|
||||
bool ApplyLevelFinalize = true) : IRequest;
|
||||
|
||||
@ -501,9 +503,20 @@ public class TransitionPurchaseEvaluationCommandValidator : AbstractValidator<Tr
|
||||
RuleFor(x => x.ApprovedPriceAmount).GreaterThanOrEqualTo(0)
|
||||
.When(x => x.ApprovedPriceAmount.HasValue);
|
||||
RuleFor(x => x.ApprovedPriceSource)
|
||||
.Must(s => s is "Ncc" or "ProMin" or "ProMax" or "Ccm")
|
||||
.Must(s => s is "Ncc" or "ProMin" or "ProMax" or "Ccm" or "ProMinMax")
|
||||
.When(x => x.ApprovedPriceSource is not null)
|
||||
.WithMessage("Nguồn giá chốt phải là Ncc/ProMin/ProMax/Ccm.");
|
||||
.WithMessage("Nguồn giá chốt phải là Ncc/ProMin/ProMax/Ccm/ProMinMax.");
|
||||
// [S117] Max của khoảng ≥ 0; chỉ kèm khi source=ProMinMax (bất-biến Max⟺ProMinMax); Min ≤ Max.
|
||||
// Bắt-buộc-khi-duyệt-cuối vẫn enforce ở service (biết nhánh DaDuyet) — validator chặn rác.
|
||||
RuleFor(x => x.ApprovedPriceMaxAmount).GreaterThanOrEqualTo(0)
|
||||
.When(x => x.ApprovedPriceMaxAmount.HasValue);
|
||||
RuleFor(x => x.ApprovedPriceMaxAmount)
|
||||
.Null()
|
||||
.When(x => x.ApprovedPriceSource != "ProMinMax")
|
||||
.WithMessage("Chỉ nguồn 'cả Min–Max' (ProMinMax) mới được kèm giá Max.");
|
||||
RuleFor(x => x.ApprovedPriceMaxAmount)
|
||||
.Must((cmd, max) => max is null || cmd.ApprovedPriceAmount is null || cmd.ApprovedPriceAmount <= max)
|
||||
.WithMessage("Giá Min không được lớn hơn giá Max khi duyệt theo khoảng.");
|
||||
}
|
||||
}
|
||||
|
||||
@ -533,6 +546,7 @@ public class TransitionPurchaseEvaluationCommandHandler(
|
||||
request.FinalizeByCcmDelegation,
|
||||
request.ApprovedPriceAmount,
|
||||
request.ApprovedPriceSource,
|
||||
request.ApprovedPriceMaxAmount,
|
||||
request.ApplyLevelFinalize,
|
||||
ct);
|
||||
}
|
||||
@ -642,12 +656,18 @@ public class ListPurchaseEvaluationsQueryHandler(
|
||||
// (mirror winner-names — EF-safe; record-ctor projection KHÔNG cho biến trung
|
||||
// gian nên không gộp tuple). lv.Step!.Name = simple navigation chắc translate.
|
||||
// ApprovalWorkflowId==null → subquery rỗng → false/null (V1 tự đúng).
|
||||
// [S97] runtime: phiếu DaDuyet đọc EndedByLevelFinalize (finalize sớm THẬT);
|
||||
// chưa xong → config-subquery. Phiếu-đã-lên-CEO KHÔNG hiện badge "Kết thúc" sai.
|
||||
// [S97] runtime: phiếu DaDuyet đọc EndedByLevelFinalize (finalize sớm THẬT).
|
||||
// [S117] phiếu chưa xong POINTER-AWARE: config-subquery lọc thêm cấp finalize
|
||||
// ở-hoặc-SAU con-trỏ (rank cấp = Step.Order-1, Order tuần-tự s+1 bởi seed/Designer).
|
||||
// Phiếu đã QUA cấp finalize (approver opt-out → lên CEO) → rỗng → false, KHÔNG badge
|
||||
// "Kết thúc" sai khi đang chờ CEO. Con-trỏ null (Nháp) → ?? 0/1 = mọi cấp reachable.
|
||||
x.e.Phase == PurchaseEvaluationPhase.DaDuyet
|
||||
? x.e.EndedByLevelFinalize
|
||||
: (from lv in db.ApprovalWorkflowLevels
|
||||
where lv.AllowApproverFinalize && lv.Step!.ApprovalWorkflowId == x.e.ApprovalWorkflowId
|
||||
&& (lv.Step!.Order - 1 > (x.e.CurrentWorkflowStepIndex ?? 0)
|
||||
|| (lv.Step!.Order - 1 == (x.e.CurrentWorkflowStepIndex ?? 0)
|
||||
&& lv.Order >= (x.e.CurrentApprovalLevelOrder ?? 1)))
|
||||
select 1).Any(),
|
||||
(from lv in db.ApprovalWorkflowLevels
|
||||
where lv.AllowApproverFinalize && lv.Step!.ApprovalWorkflowId == x.e.ApprovalWorkflowId
|
||||
@ -762,12 +782,17 @@ public class GetMyPurchaseEvaluationInboxQueryHandler(
|
||||
// [S89 Mig 58] "Duyệt là KẾT THÚC (không trình CEO)" — 3 subquery riêng mirror
|
||||
// winner-names (EF-safe). lv.Step!.Name simple navigation. ApprovalWorkflowId
|
||||
// ==null → subquery rỗng → false/null (V1 tự đúng).
|
||||
// [S97] runtime: phiếu DaDuyet đọc EndedByLevelFinalize (finalize sớm THẬT);
|
||||
// chưa xong → config-subquery. Phiếu-đã-lên-CEO KHÔNG hiện badge "Kết thúc" sai.
|
||||
// [S97] runtime: phiếu DaDuyet đọc EndedByLevelFinalize (finalize sớm THẬT).
|
||||
// [S117] phiếu chưa xong POINTER-AWARE: config-subquery lọc thêm cấp finalize
|
||||
// ở-hoặc-SAU con-trỏ (rank = Step.Order-1). Phiếu đã QUA cấp finalize → lên CEO →
|
||||
// rỗng → false, KHÔNG badge "Kết thúc" sai (mirror List :647).
|
||||
x.e.Phase == PurchaseEvaluationPhase.DaDuyet
|
||||
? x.e.EndedByLevelFinalize
|
||||
: (from lv in db.ApprovalWorkflowLevels
|
||||
where lv.AllowApproverFinalize && lv.Step!.ApprovalWorkflowId == x.e.ApprovalWorkflowId
|
||||
&& (lv.Step!.Order - 1 > (x.e.CurrentWorkflowStepIndex ?? 0)
|
||||
|| (lv.Step!.Order - 1 == (x.e.CurrentWorkflowStepIndex ?? 0)
|
||||
&& lv.Order >= (x.e.CurrentApprovalLevelOrder ?? 1)))
|
||||
select 1).Any(),
|
||||
(from lv in db.ApprovalWorkflowLevels
|
||||
where lv.AllowApproverFinalize && lv.Step!.ApprovalWorkflowId == x.e.ApprovalWorkflowId
|
||||
@ -1146,12 +1171,20 @@ public class GetPurchaseEvaluationQueryHandler(
|
||||
{
|
||||
var finalizeLevel = finalizeStep.Levels
|
||||
.Where(l => l.AllowApproverFinalize).OrderBy(l => l.Order).FirstOrDefault();
|
||||
// [S97] endsBeforeCeo theo RUNTIME cho phiếu DaDuyet (đọc EndedByLevelFinalize):
|
||||
// phiếu đi HẾT lên CEO (approver opt-out finalize) → false, KHÔNG hiện "không qua
|
||||
// CEO". Phiếu chưa xong (ChoDuyet…) giữ heads-up config (còn cấp finalize phía trước).
|
||||
// [S97] endsBeforeCeo theo RUNTIME cho phiếu DaDuyet (đọc EndedByLevelFinalize).
|
||||
// [S117 anh Kiệt — bug "duyệt rồi không bắt lũy kế"] Phiếu CHƯA DaDuyet nay
|
||||
// POINTER-AWARE: heads-up "kết thúc trước CEO" CHỈ khi CÒN cấp finalize ở-hoặc-SAU
|
||||
// con-trỏ hiện tại (còn có thể finalize). Phiếu đã đi QUA mọi cấp finalize (approver
|
||||
// opt-out S97 → trình tiếp CEO) → false: KHÔNG làm mờ Bước sau + KHÔNG nói dối
|
||||
// "Kết thúc tại Cấp X" khi phiếu đang nằm inbox CEO (A/018: user tưởng đã duyệt
|
||||
// xong nên trách "lũy kế không bắt"). Con-trỏ null (Nháp/TraLai) → giữ config như cũ.
|
||||
var anyReachableFinalize = currentIdx is int finCi && currentLevel is int finCl
|
||||
? steps.Select((s, si) => (s, si)).Any(t => t.si >= finCi
|
||||
&& t.s.Levels.Any(l => l.AllowApproverFinalize && (t.si > finCi || l.Order >= finCl)))
|
||||
: true;
|
||||
endsBeforeCeo = phase == PurchaseEvaluationPhase.DaDuyet
|
||||
? e.EndedByLevelFinalize
|
||||
: true;
|
||||
: anyReachableFinalize;
|
||||
finalizeStepName = finalizeStep.DepartmentId is Guid fsd
|
||||
&& stepDeptNames.TryGetValue(fsd, out var fsdn) ? fsdn : finalizeStep.Name;
|
||||
finalizeLevelName = finalizeLevel?.Name;
|
||||
@ -1208,7 +1241,7 @@ public class GetPurchaseEvaluationQueryHandler(
|
||||
e.IsUrgentByPro, e.IsUrgentByCcm, winnerQuoteTotal, awCeoThreshold, // [S69] cờ gấp + giá trị gói + ngưỡng CEO
|
||||
e.ProSuggestedMinPrice, e.ProSuggestedMaxPrice, e.CcmSuggestedPrice, // [Mig 54] giá đề xuất PRO/CCM
|
||||
e.ProSuggestedPriceNote, e.CcmSuggestedPriceNote, // [Mig 57] ghi chú giải thích giá
|
||||
e.ApprovedPriceAmount, e.ApprovedPriceSource, // [Mig 54] giá chốt người duyệt chọn
|
||||
e.ApprovedPriceAmount, e.ApprovedPriceSource, e.ApprovedPriceMaxAmount, // [Mig 54 + S117] giá chốt (+ Max khoảng ProMinMax)
|
||||
canEditProSuggested, canEditCcmSuggested, // [Mig 54] capability role-gate
|
||||
e.ApprovalWorkflowId, awCode, awName, awVersion, currentLevelOptions,
|
||||
currentApproval, approvalFlow,
|
||||
|
||||
@ -32,6 +32,9 @@ public interface IPurchaseEvaluationWorkflowService
|
||||
bool finalizeByCcmDelegation = false,
|
||||
decimal? approvedPriceAmount = null,
|
||||
string? approvedPriceSource = null,
|
||||
// [S117 2026-07-14 — anh Kiệt FDC] Max của khoảng khi source="ProMinMax" (duyệt theo Min–Max).
|
||||
// null = giá đơn (giữ nguyên Mig 54); != null ⟺ source=="ProMinMax", Amount=Min.
|
||||
decimal? approvedPriceMaxAmount = null,
|
||||
// [S96 2026-07-01 — anh Kiệt FDC] Người duyệt BỎ tick "Cấp này KẾT THÚC quy trình"
|
||||
// (AllowApproverFinalize) → false = KHÔNG finalize, vẫn trình tiếp CEO/cấp sau.
|
||||
// Default true = giữ hành vi cũ (cấp cấu hình finalize → auto kết thúc). Đảo S89 auto→opt-out.
|
||||
|
||||
Reference in New Issue
Block a user