Files
ColaFlow/docs/plans/sprint_2_story_3_task_5.md
Yaojia Wang ebb56cc9f8 feat(backend): Create Sprint 2 backend Stories and Tasks
Created detailed implementation plans for Sprint 2 backend work:

Story 1: Audit Log Foundation (Phase 1)
- Task 1: Design AuditLog database schema and create migration
- Task 2: Create AuditLog entity and Repository
- Task 3: Implement EF Core SaveChangesInterceptor
- Task 4: Write unit tests for audit logging
- Task 5: Integrate with ProjectManagement Module

Story 2: Audit Log Core Features (Phase 2)
- Task 1: Implement Changed Fields Detection (JSON Diff)
- Task 2: Integrate User Context Tracking
- Task 3: Add Multi-Tenant Isolation
- Task 4: Implement Audit Query API
- Task 5: Write Integration Tests

Story 3: Sprint Management Module
- Task 1: Create Sprint Aggregate Root and Domain Events
- Task 2: Implement Sprint Repository and EF Core Configuration
- Task 3: Create CQRS Commands and Queries
- Task 4: Implement Burndown Chart Calculation
- Task 5: Add SignalR Real-Time Notifications
- Task 6: Write Integration Tests

Total: 3 Stories, 16 Tasks, 24 Story Points (8+8+8)
Estimated Duration: 10-12 days

All tasks include:
- Detailed technical implementation guidance
- Code examples and file paths
- Testing requirements (>= 90% coverage)
- Performance benchmarks (< 5ms audit overhead)
- Multi-tenant security validation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 22:56:31 +01:00

8.9 KiB

task_id, story, status, estimated_hours, created_date, assignee
task_id story status estimated_hours created_date assignee
sprint_2_story_3_task_5 sprint_2_story_3 not_started 3 2025-11-05 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
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);
}
  1. Implement Service: colaflow-api/src/ColaFlow.Infrastructure/SignalR/RealtimeNotificationService.cs
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
            });
    }
}
  1. Domain Event Handlers: colaflow-api/src/ColaFlow.Application/Sprints/EventHandlers/
// 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
}
  1. Publish Domain Events: Update Command Handlers to publish domain events
// 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:

// 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

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