diff --git a/colaflow-api/src/ColaFlow.API/EventHandlers/SprintEventHandlers.cs b/colaflow-api/src/ColaFlow.API/EventHandlers/SprintEventHandlers.cs new file mode 100644 index 0000000..5654249 --- /dev/null +++ b/colaflow-api/src/ColaFlow.API/EventHandlers/SprintEventHandlers.cs @@ -0,0 +1,139 @@ +using MediatR; +using ColaFlow.API.Services; +using ColaFlow.Modules.ProjectManagement.Domain.Events; +using Microsoft.Extensions.Logging; + +namespace ColaFlow.API.EventHandlers; + +/// +/// Handles Sprint domain events and sends SignalR notifications +/// +public class SprintEventHandlers : + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler +{ + private readonly IRealtimeNotificationService _notificationService; + private readonly ILogger _logger; + private readonly IHttpContextAccessor _httpContextAccessor; + + public SprintEventHandlers( + IRealtimeNotificationService notificationService, + ILogger logger, + IHttpContextAccessor httpContextAccessor) + { + _notificationService = notificationService ?? throw new ArgumentNullException(nameof(notificationService)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor)); + } + + public async Task Handle(SprintCreatedEvent notification, CancellationToken cancellationToken) + { + try + { + var tenantId = GetCurrentTenantId(); + await _notificationService.NotifySprintCreated( + tenantId, + notification.ProjectId, + notification.SprintId, + notification.SprintName + ); + _logger.LogInformation("Sprint created notification sent: {SprintId}", notification.SprintId); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to send Sprint created notification: {SprintId}", notification.SprintId); + } + } + + public async Task Handle(SprintUpdatedEvent notification, CancellationToken cancellationToken) + { + try + { + var tenantId = GetCurrentTenantId(); + await _notificationService.NotifySprintUpdated( + tenantId, + notification.ProjectId, + notification.SprintId, + notification.SprintName + ); + _logger.LogInformation("Sprint updated notification sent: {SprintId}", notification.SprintId); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to send Sprint updated notification: {SprintId}", notification.SprintId); + } + } + + public async Task Handle(SprintStartedEvent notification, CancellationToken cancellationToken) + { + try + { + var tenantId = GetCurrentTenantId(); + await _notificationService.NotifySprintStarted( + tenantId, + notification.ProjectId, + notification.SprintId, + notification.SprintName + ); + _logger.LogInformation("Sprint started notification sent: {SprintId}", notification.SprintId); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to send Sprint started notification: {SprintId}", notification.SprintId); + } + } + + public async Task Handle(SprintCompletedEvent notification, CancellationToken cancellationToken) + { + try + { + var tenantId = GetCurrentTenantId(); + await _notificationService.NotifySprintCompleted( + tenantId, + notification.ProjectId, + notification.SprintId, + notification.SprintName + ); + _logger.LogInformation("Sprint completed notification sent: {SprintId}", notification.SprintId); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to send Sprint completed notification: {SprintId}", notification.SprintId); + } + } + + public async Task Handle(SprintDeletedEvent notification, CancellationToken cancellationToken) + { + try + { + var tenantId = GetCurrentTenantId(); + await _notificationService.NotifySprintDeleted( + tenantId, + notification.ProjectId, + notification.SprintId, + notification.SprintName + ); + _logger.LogInformation("Sprint deleted notification sent: {SprintId}", notification.SprintId); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to send Sprint deleted notification: {SprintId}", notification.SprintId); + } + } + + private Guid GetCurrentTenantId() + { + var tenantIdClaim = _httpContextAccessor?.HttpContext?.User + .FindFirst("tenant_id")?.Value; + + if (Guid.TryParse(tenantIdClaim, out var tenantId) && tenantId != Guid.Empty) + { + return tenantId; + } + + return Guid.Empty; // Default for non-HTTP contexts + } +} diff --git a/colaflow-api/src/ColaFlow.API/Services/IRealtimeNotificationService.cs b/colaflow-api/src/ColaFlow.API/Services/IRealtimeNotificationService.cs index c016422..b41f24d 100644 --- a/colaflow-api/src/ColaFlow.API/Services/IRealtimeNotificationService.cs +++ b/colaflow-api/src/ColaFlow.API/Services/IRealtimeNotificationService.cs @@ -30,6 +30,13 @@ public interface IRealtimeNotificationService Task NotifyIssueDeleted(Guid tenantId, Guid projectId, Guid issueId); Task NotifyIssueStatusChanged(Guid tenantId, Guid projectId, Guid issueId, string oldStatus, string newStatus); + // Sprint notifications + Task NotifySprintCreated(Guid tenantId, Guid projectId, Guid sprintId, string sprintName); + Task NotifySprintUpdated(Guid tenantId, Guid projectId, Guid sprintId, string sprintName); + Task NotifySprintStarted(Guid tenantId, Guid projectId, Guid sprintId, string sprintName); + Task NotifySprintCompleted(Guid tenantId, Guid projectId, Guid sprintId, string sprintName); + Task NotifySprintDeleted(Guid tenantId, Guid projectId, Guid sprintId, string sprintName); + // User-level notifications Task NotifyUser(Guid userId, string message, string type = "info"); Task NotifyUsersInTenant(Guid tenantId, string message, string type = "info"); diff --git a/colaflow-api/src/ColaFlow.API/Services/RealtimeNotificationService.cs b/colaflow-api/src/ColaFlow.API/Services/RealtimeNotificationService.cs index 9a72106..0b652bf 100644 --- a/colaflow-api/src/ColaFlow.API/Services/RealtimeNotificationService.cs +++ b/colaflow-api/src/ColaFlow.API/Services/RealtimeNotificationService.cs @@ -202,6 +202,118 @@ public class RealtimeNotificationService : IRealtimeNotificationService }); } + // Sprint notifications + public async Task NotifySprintCreated(Guid tenantId, Guid projectId, Guid sprintId, string sprintName) + { + var projectGroupName = $"project-{projectId}"; + var tenantGroupName = $"tenant-{tenantId}"; + + _logger.LogInformation("Notifying sprint {SprintId} created in project {ProjectId}", sprintId, projectId); + + await _projectHubContext.Clients.Group(projectGroupName).SendAsync("SprintCreated", new + { + SprintId = sprintId, + SprintName = sprintName, + ProjectId = projectId, + Timestamp = DateTime.UtcNow + }); + + await _projectHubContext.Clients.Group(tenantGroupName).SendAsync("SprintCreated", new + { + SprintId = sprintId, + SprintName = sprintName, + ProjectId = projectId, + Timestamp = DateTime.UtcNow + }); + } + + public async Task NotifySprintUpdated(Guid tenantId, Guid projectId, Guid sprintId, string sprintName) + { + var projectGroupName = $"project-{projectId}"; + + _logger.LogInformation("Notifying sprint {SprintId} updated", sprintId); + + await _projectHubContext.Clients.Group(projectGroupName).SendAsync("SprintUpdated", new + { + SprintId = sprintId, + SprintName = sprintName, + ProjectId = projectId, + Timestamp = DateTime.UtcNow + }); + } + + public async Task NotifySprintStarted(Guid tenantId, Guid projectId, Guid sprintId, string sprintName) + { + var projectGroupName = $"project-{projectId}"; + var tenantGroupName = $"tenant-{tenantId}"; + + _logger.LogInformation("Notifying sprint {SprintId} started", sprintId); + + await _projectHubContext.Clients.Group(projectGroupName).SendAsync("SprintStarted", new + { + SprintId = sprintId, + SprintName = sprintName, + ProjectId = projectId, + Timestamp = DateTime.UtcNow + }); + + await _projectHubContext.Clients.Group(tenantGroupName).SendAsync("SprintStarted", new + { + SprintId = sprintId, + SprintName = sprintName, + ProjectId = projectId, + Timestamp = DateTime.UtcNow + }); + } + + public async Task NotifySprintCompleted(Guid tenantId, Guid projectId, Guid sprintId, string sprintName) + { + var projectGroupName = $"project-{projectId}"; + var tenantGroupName = $"tenant-{tenantId}"; + + _logger.LogInformation("Notifying sprint {SprintId} completed", sprintId); + + await _projectHubContext.Clients.Group(projectGroupName).SendAsync("SprintCompleted", new + { + SprintId = sprintId, + SprintName = sprintName, + ProjectId = projectId, + Timestamp = DateTime.UtcNow + }); + + await _projectHubContext.Clients.Group(tenantGroupName).SendAsync("SprintCompleted", new + { + SprintId = sprintId, + SprintName = sprintName, + ProjectId = projectId, + Timestamp = DateTime.UtcNow + }); + } + + public async Task NotifySprintDeleted(Guid tenantId, Guid projectId, Guid sprintId, string sprintName) + { + var projectGroupName = $"project-{projectId}"; + var tenantGroupName = $"tenant-{tenantId}"; + + _logger.LogInformation("Notifying sprint {SprintId} deleted", sprintId); + + await _projectHubContext.Clients.Group(projectGroupName).SendAsync("SprintDeleted", new + { + SprintId = sprintId, + SprintName = sprintName, + ProjectId = projectId, + Timestamp = DateTime.UtcNow + }); + + await _projectHubContext.Clients.Group(tenantGroupName).SendAsync("SprintDeleted", new + { + SprintId = sprintId, + SprintName = sprintName, + ProjectId = projectId, + Timestamp = DateTime.UtcNow + }); + } + public async Task NotifyUser(Guid userId, string message, string type = "info") { var userConnectionId = $"user-{userId}"; diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/UpdateSprint/UpdateSprintCommandHandler.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/UpdateSprint/UpdateSprintCommandHandler.cs index a5bf17a..cd7e44e 100644 --- a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/UpdateSprint/UpdateSprintCommandHandler.cs +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/UpdateSprint/UpdateSprintCommandHandler.cs @@ -4,6 +4,7 @@ using ColaFlow.Modules.ProjectManagement.Application.Common.Interfaces; using ColaFlow.Modules.ProjectManagement.Domain.Repositories; using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects; using ColaFlow.Modules.ProjectManagement.Domain.Exceptions; +using ColaFlow.Modules.ProjectManagement.Domain.Events; namespace ColaFlow.Modules.ProjectManagement.Application.Commands.UpdateSprint; @@ -12,11 +13,13 @@ namespace ColaFlow.Modules.ProjectManagement.Application.Commands.UpdateSprint; /// public sealed class UpdateSprintCommandHandler( IApplicationDbContext context, - IUnitOfWork unitOfWork) + IUnitOfWork unitOfWork, + IMediator mediator) : IRequestHandler { private readonly IApplicationDbContext _context = context ?? throw new ArgumentNullException(nameof(context)); private readonly IUnitOfWork _unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork)); + private readonly IMediator _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); public async Task Handle(UpdateSprintCommand request, CancellationToken cancellationToken) { @@ -39,6 +42,9 @@ public sealed class UpdateSprintCommandHandler( // Save changes await _unitOfWork.SaveChangesAsync(cancellationToken); + // Publish domain event + await _mediator.Publish(new SprintUpdatedEvent(sprint.Id.Value, sprint.Name, sprint.ProjectId.Value), cancellationToken); + return Unit.Value; } }