[CLAUDE] PurchaseEvaluation: mark-read noti "cần duyệt" cũ khi phiếu rời cấp (fix chuông stale — anh Kiệt FDC)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m3s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m3s
anh Kiệt "chuông không bỏ ra được" — noti "Phiếu cần bạn duyệt" của approver (kể cả CEO) KHÔNG tự xóa khi phiếu đã xong/chuyển bởi người khác (duyệt-thay tại Cấp CEO, co-approver OR-of-N, finalize) → chuông đầy phiếu thừa. Fix: trong LogTransitionAsync (chạy MỌI transition), TRƯỚC block notify cấp mới → mark-read (ReadAt) tất cả noti Generic + RefId=phiếu còn unread. Phiếu rời cấp (finalize/advance/return/reject) → noti cũ tự sạch; approver cấp MỚI nhận noti fresh (tạo sau mark-read). Best-effort try/catch, V2-only, ExecuteUpdateAsync. KHÔNG đổi target (noti vẫn báo đúng lúc), chỉ thu-hồi khi stale. Test: +test #7 (finalize → noti CEO cũ mark-read). 435→436 PASS. BE-only, no migration. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -5,6 +5,7 @@ using SolutionErp.Application.Common.Exceptions;
|
||||
using SolutionErp.Domain.ApprovalWorkflowsV2;
|
||||
using SolutionErp.Domain.Contracts; // ApprovalDecision enum (shared HĐ/PE)
|
||||
using SolutionErp.Domain.Identity;
|
||||
using SolutionErp.Domain.Notifications;
|
||||
using SolutionErp.Domain.PurchaseEvaluations;
|
||||
using SolutionErp.Infrastructure.Services;
|
||||
using SolutionErp.Infrastructure.Tests.Common;
|
||||
@ -366,4 +367,49 @@ public class PeApproverFinalizeTests
|
||||
opinions.Should().ContainSingle("approver ký cấp mình rồi mới đẩy lên CEO");
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// 7. ⭐ STALE-NOTI CLEAR (S96 2026-07-01 — anh Kiệt FDC "chuông không bỏ ra được") —
|
||||
// phiếu tới Cấp CEO → CEO có noti "cần bạn duyệt" (Generic + RefId=phiếu). Khi phiếu
|
||||
// FINALIZE (duyệt-thay/kết-thúc hộ tại đây) → noti CŨ của CEO = stale → TỰ mark-read
|
||||
// (ReadAt set) để chuông sạch. Trực-tiếp vá "noti cần-duyệt không tự bỏ khi phiếu xong".
|
||||
// =====================================================================
|
||||
[Fact]
|
||||
public async Task ApproveV2_Finalize_MarksStalePendingNotificationRead()
|
||||
{
|
||||
var (svc, fix, db, _) = CreateService();
|
||||
using (fix)
|
||||
{
|
||||
var approver = (await fix.CreateUserAsync("appr7@af.test", "Approver", null, Array.Empty<string>())).Id;
|
||||
var ceo = (await fix.CreateUserAsync("ceo7@af.test", "CEO", null, new[] { AppRoles.Director })).Id;
|
||||
|
||||
var wf = await SeedWorkflowAsync(db, new[]
|
||||
{
|
||||
new[] { (approver, true) }, // slot cấu hình KẾT THÚC — approver duyệt-thay hộ CEO
|
||||
new[] { (ceo, false) },
|
||||
});
|
||||
|
||||
var pe = BuildPeAtApprovalSlot(wf.Id, stepIdx: 0, levelOrder: 1, code: "PE-AF-007");
|
||||
db.PurchaseEvaluations.Add(pe);
|
||||
// Giả-lập noti CŨ "cần bạn duyệt" của CEO cho phiếu này (RefId=phiếu, Generic, chưa đọc).
|
||||
db.Notifications.Add(new Notification
|
||||
{
|
||||
UserId = ceo,
|
||||
Type = NotificationType.Generic,
|
||||
Title = "Phiếu cần bạn duyệt: PE-AF-007",
|
||||
RefId = pe.Id,
|
||||
ReadAt = null,
|
||||
});
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
await ApproveAsync(svc, pe, approver, Array.Empty<string>(),
|
||||
approvedPriceAmount: ValidApprovedPrice, approvedPriceSource: ValidApprovedSource);
|
||||
|
||||
pe.Phase.Should().Be(PurchaseEvaluationPhase.DaDuyet, "finalize");
|
||||
var ceoNotif = await db.Notifications.AsNoTracking()
|
||||
.FirstAsync(n => n.UserId == ceo && n.RefId == pe.Id);
|
||||
ceoNotif.ReadAt.Should().NotBeNull(
|
||||
"phiếu finalize → noti 'cần bạn duyệt' cũ của CEO tự mark-read, chuông sạch (không còn phiếu thừa)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user