---
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<ProjectHub> _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<SprintCreatedEvent>
{
    private readonly IRealtimeNotificationService _notificationService;
    private readonly ILogger<SprintCreatedEventHandler> _logger;

    public SprintCreatedEventHandler(
        IRealtimeNotificationService notificationService,
        ILogger<SprintCreatedEventHandler> 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<SprintUpdatedEvent>
{
    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<SprintStartedEvent>
{
    // Similar implementation
}

// SprintCompletedEventHandler.cs
public class SprintCompletedEventHandler : INotificationHandler<SprintCompletedEvent>
{
    // Similar implementation
}

// SprintDeletedEventHandler.cs
public class SprintDeletedEventHandler : INotificationHandler<SprintDeletedEvent>
{
    // Similar implementation
}
```

4. **Publish Domain Events**: Update Command Handlers to publish domain events

```csharp
// In CreateSprintCommandHandler
public async Task<Guid> 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<IRealtimeNotificationService>();
        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<IRealtimeNotificationService>();

        // Act
        await Mediator.Send(new StartSprintCommand(sprintId));

        // Assert
        notificationService.Verify(
            s => s.NotifySprintStartedAsync(It.IsAny<Guid>(), It.IsAny<string>(), It.IsAny<Guid>()),
            Times.Once
        );
    }
}
```

---

**Created**: 2025-11-05 by Backend Agent
