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>
This commit is contained in:
301
docs/plans/sprint_2_story_3_task_5.md
Normal file
301
docs/plans/sprint_2_story_3_task_5.md
Normal file
@@ -0,0 +1,301 @@
|
||||
---
|
||||
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
|
||||
Reference in New Issue
Block a user