[CLAUDE] App+Domain+Infra+Api+FE: Notifications module end-to-end
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:
pqhuy1987
2026-04-21 15:24:09 +07:00
parent 6c0e20649a
commit 49c0ddc8f4
17 changed files with 1619 additions and 110 deletions

View File

@ -0,0 +1,35 @@
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SolutionErp.Application.Notifications;
using SolutionErp.Application.Notifications.Dtos;
namespace SolutionErp.Api.Controllers;
[ApiController]
[Route("api/notifications")]
[Authorize]
public class NotificationsController(IMediator mediator) : ControllerBase
{
[HttpGet]
public async Task<ActionResult<IReadOnlyList<NotificationDto>>> List(
[FromQuery] bool unreadOnly = false,
[FromQuery] int limit = 50,
CancellationToken ct = default)
=> Ok(await mediator.Send(new ListMyNotificationsQuery(unreadOnly, limit), ct));
[HttpGet("unread-count")]
public async Task<ActionResult<int>> UnreadCount(CancellationToken ct)
=> Ok(await mediator.Send(new GetMyUnreadCountQuery(), ct));
[HttpPost("{id:guid}/read")]
public async Task<IActionResult> MarkRead(Guid id, CancellationToken ct)
{
await mediator.Send(new MarkNotificationReadCommand(id), ct);
return NoContent();
}
[HttpPost("read-all")]
public async Task<ActionResult<int>> MarkAllRead(CancellationToken ct)
=> Ok(await mediator.Send(new MarkAllNotificationsReadCommand(), ct));
}