[CLAUDE] App+Domain+Infra+Api+FE: Notifications module end-to-end
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m43s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m43s
Domain: - Notification entity + NotificationType enum (stable ints) - Nullable RefId cho correlation (contract, user, ...) Infrastructure: - NotificationConfiguration: bảng Notifications, index theo (UserId, ReadAt) - NotificationService: ghi vào DbContext, không SaveChanges (để caller quyết định unit-of-work — đảm bảo atomic với domain mutation) - EF migration AddNotifications Application: - INotificationService (Notify + NotifyMany) - CQRS: ListMyNotifications / GetMyUnreadCount / MarkRead / MarkAllRead Api: - NotificationsController: GET /api/notifications + unread-count + mark-read Integration: - ContractWorkflowService emit notification tới Drafter khi HĐ chuyển phase (skip nếu actor chính là Drafter). Title + type theo phase đích: DaPhatHanh → ContractPublished, TuChoi → ContractRejected, khác → ContractPhaseTransition. FE: - Both NotificationBell (admin + user) dùng /api/notifications thật (thay cho derived-from-inbox MVP trước đó). 30s refetch, click mark-read, 'Đọc hết' bulk action. Foundation sẵn cho SignalR push + email outbox sau này — chỉ cần mở rộng NotificationService mà không đổi caller. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -2,15 +2,18 @@ using Microsoft.EntityFrameworkCore;
|
||||
using SolutionErp.Application.Common.Exceptions;
|
||||
using SolutionErp.Application.Common.Interfaces;
|
||||
using SolutionErp.Application.Contracts.Services;
|
||||
using SolutionErp.Application.Notifications;
|
||||
using SolutionErp.Domain.Contracts;
|
||||
using SolutionErp.Domain.Identity;
|
||||
using SolutionErp.Domain.Notifications;
|
||||
|
||||
namespace SolutionErp.Infrastructure.Services;
|
||||
|
||||
public class ContractWorkflowService(
|
||||
IApplicationDbContext db,
|
||||
IContractCodeGenerator codeGenerator,
|
||||
IDateTime dateTime) : IContractWorkflowService
|
||||
IDateTime dateTime,
|
||||
INotificationService notifications) : IContractWorkflowService
|
||||
{
|
||||
// Map (from, to) → roles được phép chuyển. Xem docs/workflow-contract.md §5.
|
||||
// Admin luôn bypass (check trong Handler trước khi gọi service).
|
||||
@ -115,6 +118,31 @@ public class ContractWorkflowService(
|
||||
ApprovedAt = dateTime.UtcNow,
|
||||
});
|
||||
|
||||
// Notify the drafter (unless they are the actor or contract has no drafter)
|
||||
if (contract.DrafterUserId is Guid drafterId && drafterId != actorUserId)
|
||||
{
|
||||
var title = targetPhase switch
|
||||
{
|
||||
ContractPhase.DaPhatHanh => $"HĐ {contract.MaHopDong ?? contract.TenHopDong} đã phát hành",
|
||||
ContractPhase.TuChoi => $"HĐ {contract.TenHopDong ?? "của bạn"} bị từ chối",
|
||||
_ => $"HĐ {contract.TenHopDong ?? contract.MaHopDong ?? ""} chuyển sang phase mới",
|
||||
};
|
||||
var type = targetPhase switch
|
||||
{
|
||||
ContractPhase.DaPhatHanh => NotificationType.ContractPublished,
|
||||
ContractPhase.TuChoi => NotificationType.ContractRejected,
|
||||
_ => NotificationType.ContractPhaseTransition,
|
||||
};
|
||||
await notifications.NotifyAsync(
|
||||
drafterId,
|
||||
type,
|
||||
title,
|
||||
description: $"{fromPhase} → {targetPhase}" + (string.IsNullOrWhiteSpace(comment) ? "" : $" · {comment}"),
|
||||
href: $"/contracts/{contract.Id}",
|
||||
refId: contract.Id,
|
||||
ct: ct);
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,59 @@
|
||||
using SolutionErp.Application.Common.Interfaces;
|
||||
using SolutionErp.Application.Notifications;
|
||||
using SolutionErp.Domain.Notifications;
|
||||
|
||||
namespace SolutionErp.Infrastructure.Services;
|
||||
|
||||
// MVP: writes directly to DbContext. Does NOT call SaveChanges — caller's unit of
|
||||
// work flushes both the domain mutation and the notification atomically.
|
||||
// Future: wrap with SignalR IHubContext push + Outbox for email dispatch.
|
||||
public class NotificationService(IApplicationDbContext db, IDateTime clock) : INotificationService
|
||||
{
|
||||
public Task NotifyAsync(
|
||||
Guid userId,
|
||||
NotificationType type,
|
||||
string title,
|
||||
string? description = null,
|
||||
string? href = null,
|
||||
Guid? refId = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
db.Notifications.Add(new Notification
|
||||
{
|
||||
UserId = userId,
|
||||
Type = type,
|
||||
Title = title,
|
||||
Description = description,
|
||||
Href = href,
|
||||
RefId = refId,
|
||||
CreatedAt = clock.UtcNow,
|
||||
});
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task NotifyManyAsync(
|
||||
IEnumerable<Guid> userIds,
|
||||
NotificationType type,
|
||||
string title,
|
||||
string? description = null,
|
||||
string? href = null,
|
||||
Guid? refId = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var now = clock.UtcNow;
|
||||
foreach (var userId in userIds.Distinct())
|
||||
{
|
||||
db.Notifications.Add(new Notification
|
||||
{
|
||||
UserId = userId,
|
||||
Type = type,
|
||||
Title = title,
|
||||
Description = description,
|
||||
Href = href,
|
||||
RefId = refId,
|
||||
CreatedAt = now,
|
||||
});
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user