--- task_id: sprint_2_story_3_task_5 story: sprint_2_story_3 status: not_started estimated_hours: 3 created_date: 2025-11-05 assignee: Backend Team --- # Task 5: Add SignalR Real-Time Notifications **Story**: Story 3 - Sprint Management Module **Estimated**: 3 hours ## Description Integrate SignalR to broadcast real-time notifications for Sprint events (Created, Updated, Started, Completed, Deleted) to connected clients. ## Acceptance Criteria - [ ] Domain event handlers created for 5 Sprint events - [ ] IRealtimeNotificationService extended with Sprint methods - [ ] SignalR notifications sent to project groups - [ ] Integration tests verify notifications are sent - [ ] Frontend can receive Sprint notifications ## Implementation Details **Files to Create**: 1. **Extend Notification Service**: `colaflow-api/src/ColaFlow.Application/Common/Interfaces/IRealtimeNotificationService.cs` ```csharp public interface IRealtimeNotificationService { // ... existing methods ... // Sprint notifications Task NotifySprintCreatedAsync(Guid sprintId, string sprintName, Guid projectId); Task NotifySprintUpdatedAsync(Guid sprintId, string sprintName, Guid projectId); Task NotifySprintStartedAsync(Guid sprintId, string sprintName, Guid projectId); Task NotifySprintCompletedAsync(Guid sprintId, string sprintName, Guid projectId); Task NotifySprintDeletedAsync(Guid sprintId, string sprintName, Guid projectId); } ``` 2. **Implement Service**: `colaflow-api/src/ColaFlow.Infrastructure/SignalR/RealtimeNotificationService.cs` ```csharp public class RealtimeNotificationService : IRealtimeNotificationService { private readonly IHubContext _projectHubContext; // ... existing code ... public async Task NotifySprintCreatedAsync(Guid sprintId, string sprintName, Guid projectId) { await _projectHubContext.Clients .Group($"project-{projectId}") .SendAsync("SprintCreated", new { SprintId = sprintId, SprintName = sprintName, ProjectId = projectId, Timestamp = DateTime.UtcNow }); } public async Task NotifySprintUpdatedAsync(Guid sprintId, string sprintName, Guid projectId) { await _projectHubContext.Clients .Group($"project-{projectId}") .SendAsync("SprintUpdated", new { SprintId = sprintId, SprintName = sprintName, ProjectId = projectId, Timestamp = DateTime.UtcNow }); } public async Task NotifySprintStartedAsync(Guid sprintId, string sprintName, Guid projectId) { await _projectHubContext.Clients .Group($"project-{projectId}") .SendAsync("SprintStarted", new { SprintId = sprintId, SprintName = sprintName, ProjectId = projectId, Timestamp = DateTime.UtcNow }); } public async Task NotifySprintCompletedAsync(Guid sprintId, string sprintName, Guid projectId) { await _projectHubContext.Clients .Group($"project-{projectId}") .SendAsync("SprintCompleted", new { SprintId = sprintId, SprintName = sprintName, ProjectId = projectId, Timestamp = DateTime.UtcNow }); } public async Task NotifySprintDeletedAsync(Guid sprintId, string sprintName, Guid projectId) { await _projectHubContext.Clients .Group($"project-{projectId}") .SendAsync("SprintDeleted", new { SprintId = sprintId, SprintName = sprintName, ProjectId = projectId, Timestamp = DateTime.UtcNow }); } } ``` 3. **Domain Event Handlers**: `colaflow-api/src/ColaFlow.Application/Sprints/EventHandlers/` ```csharp // SprintCreatedEventHandler.cs public class SprintCreatedEventHandler : INotificationHandler { private readonly IRealtimeNotificationService _notificationService; private readonly ILogger _logger; public SprintCreatedEventHandler( IRealtimeNotificationService notificationService, ILogger logger) { _notificationService = notificationService; _logger = logger; } public async Task Handle(SprintCreatedEvent notification, CancellationToken cancellationToken) { try { await _notificationService.NotifySprintCreatedAsync( notification.SprintId, notification.SprintName, notification.ProjectId ); _logger.LogInformation( "Real-time notification sent for Sprint created: {SprintId}", notification.SprintId ); } catch (Exception ex) { _logger.LogError(ex, "Failed to send real-time notification for Sprint created: {SprintId}", notification.SprintId ); } } } // SprintUpdatedEventHandler.cs public class SprintUpdatedEventHandler : INotificationHandler { private readonly IRealtimeNotificationService _notificationService; public async Task Handle(SprintUpdatedEvent notification, CancellationToken cancellationToken) { // Get ProjectId from repository await _notificationService.NotifySprintUpdatedAsync( notification.SprintId, notification.SprintName, projectId // Need to get from Sprint ); } } // SprintStartedEventHandler.cs public class SprintStartedEventHandler : INotificationHandler { // Similar implementation } // SprintCompletedEventHandler.cs public class SprintCompletedEventHandler : INotificationHandler { // Similar implementation } // SprintDeletedEventHandler.cs public class SprintDeletedEventHandler : INotificationHandler { // Similar implementation } ``` 4. **Publish Domain Events**: Update Command Handlers to publish domain events ```csharp // In CreateSprintCommandHandler public async Task Handle(CreateSprintCommand request, CancellationToken cancellationToken) { var sprint = Sprint.Create(...); await _repository.AddAsync(sprint, cancellationToken); await _repository.SaveChangesAsync(cancellationToken); // Publish domain events foreach (var domainEvent in sprint.DomainEvents) { await _mediator.Publish(domainEvent, cancellationToken); } sprint.ClearDomainEvents(); return sprint.Id; } ``` **SignalR Event Types**: 1. `SprintCreated` - New sprint created 2. `SprintUpdated` - Sprint details updated 3. `SprintStarted` - Sprint status changed to Active 4. `SprintCompleted` - Sprint status changed to Completed 5. `SprintDeleted` - Sprint deleted **Frontend Integration**: ```typescript // Frontend receives notifications connection.on('SprintCreated', (data) => { console.log('Sprint created:', data); // Update UI, refresh sprint list }); connection.on('SprintStarted', (data) => { console.log('Sprint started:', data); // Update sprint status in UI }); ``` ## Technical Notes - Use MediatR to publish domain events after SaveChanges - Event handlers are fire-and-forget (don't block command execution) - Log errors if SignalR notification fails - Use project groups for targeted notifications (`project-{projectId}`) - Include timestamp in all notifications ## Testing **Integration Tests**: `colaflow-api/tests/ColaFlow.Application.IntegrationTests/Sprints/SprintSignalRTests.cs` ```csharp public class SprintSignalRTests : IntegrationTestBase { [Fact] public async Task CreateSprint_ShouldSendSprintCreatedNotification() { // Arrange var notificationService = GetMock(); var command = new CreateSprintCommand { ProjectId = TestProjectId, Name = "Sprint 1", StartDate = DateTime.UtcNow, EndDate = DateTime.UtcNow.AddDays(14) }; // Act var sprintId = await Mediator.Send(command); // Assert notificationService.Verify( s => s.NotifySprintCreatedAsync(sprintId, "Sprint 1", TestProjectId), Times.Once ); } [Fact] public async Task StartSprint_ShouldSendSprintStartedNotification() { // Arrange var sprintId = await CreateTestSprint(); var notificationService = GetMock(); // Act await Mediator.Send(new StartSprintCommand(sprintId)); // Assert notificationService.Verify( s => s.NotifySprintStartedAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once ); } } ``` --- **Created**: 2025-11-05 by Backend Agent