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>
9.6 KiB
9.6 KiB
task_id, story, status, estimated_hours, created_date, assignee
| task_id | story | status | estimated_hours | created_date | assignee |
|---|---|---|---|---|---|
| sprint_2_story_3_task_4 | sprint_2_story_3 | not_started | 4 | 2025-11-05 | Backend Team |
Task 4: Implement Burndown Chart Calculation
Story: Story 3 - Sprint Management Module Estimated: 4 hours
Description
Implement burndown chart data calculation to track sprint progress. Calculate daily remaining story points and provide data for visualization.
Acceptance Criteria
- GetSprintBurndownQuery implemented
- Daily remaining story points calculated
- Ideal burndown line calculated
- Actual burndown line calculated
- BurndownDto with chart data created
- Unit tests for calculation logic
Implementation Details
Files to Create:
- Burndown Query:
colaflow-api/src/ColaFlow.Application/Sprints/Queries/GetSprintBurndown/GetSprintBurndownQuery.cs
public record GetSprintBurndownQuery(Guid SprintId) : IRequest<BurndownDto>;
public class BurndownDto
{
public Guid SprintId { get; set; }
public string SprintName { get; set; } = string.Empty;
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
public int TotalStoryPoints { get; set; }
public int RemainingStoryPoints { get; set; }
public List<BurndownDataPoint> IdealBurndown { get; set; } = new();
public List<BurndownDataPoint> ActualBurndown { get; set; } = new();
public double CompletionPercentage { get; set; }
}
public class BurndownDataPoint
{
public DateTime Date { get; set; }
public int StoryPoints { get; set; }
}
- Query Handler:
colaflow-api/src/ColaFlow.Application/Sprints/Queries/GetSprintBurndown/GetSprintBurndownQueryHandler.cs
public class GetSprintBurndownQueryHandler : IRequestHandler<GetSprintBurndownQuery, BurndownDto>
{
private readonly ISprintRepository _sprintRepository;
private readonly IAuditLogRepository _auditLogRepository; // For historical data
private readonly ILogger<GetSprintBurndownQueryHandler> _logger;
public GetSprintBurndownQueryHandler(
ISprintRepository sprintRepository,
IAuditLogRepository auditLogRepository,
ILogger<GetSprintBurndownQueryHandler> logger)
{
_sprintRepository = sprintRepository;
_auditLogRepository = auditLogRepository;
_logger = logger;
}
public async Task<BurndownDto> Handle(GetSprintBurndownQuery request, CancellationToken cancellationToken)
{
var sprint = await _sprintRepository.GetByIdAsync(request.SprintId, cancellationToken);
if (sprint == null)
throw new NotFoundException(nameof(Sprint), request.SprintId);
var totalStoryPoints = sprint.Tasks.Sum(t => t.StoryPoints ?? 0);
var remainingStoryPoints = sprint.Tasks
.Where(t => t.Status != WorkTaskStatus.Done)
.Sum(t => t.StoryPoints ?? 0);
var burndownDto = new BurndownDto
{
SprintId = sprint.Id,
SprintName = sprint.Name,
StartDate = sprint.StartDate,
EndDate = sprint.EndDate,
TotalStoryPoints = totalStoryPoints,
RemainingStoryPoints = remainingStoryPoints,
CompletionPercentage = totalStoryPoints > 0
? ((totalStoryPoints - remainingStoryPoints) / (double)totalStoryPoints) * 100
: 0
};
// Calculate ideal burndown
burndownDto.IdealBurndown = CalculateIdealBurndown(
sprint.StartDate,
sprint.EndDate,
totalStoryPoints
);
// Calculate actual burndown
burndownDto.ActualBurndown = await CalculateActualBurndownAsync(
sprint,
totalStoryPoints,
cancellationToken
);
return burndownDto;
}
private List<BurndownDataPoint> CalculateIdealBurndown(
DateTime startDate,
DateTime endDate,
int totalStoryPoints)
{
var idealBurndown = new List<BurndownDataPoint>();
var sprintDuration = (endDate - startDate).Days;
if (sprintDuration <= 0)
return idealBurndown;
var dailyBurnRate = totalStoryPoints / (double)sprintDuration;
for (int day = 0; day <= sprintDuration; day++)
{
var date = startDate.AddDays(day);
var remainingPoints = (int)Math.Round(totalStoryPoints - (day * dailyBurnRate));
idealBurndown.Add(new BurndownDataPoint
{
Date = date,
StoryPoints = Math.Max(0, remainingPoints)
});
}
return idealBurndown;
}
private async Task<List<BurndownDataPoint>> CalculateActualBurndownAsync(
Sprint sprint,
int totalStoryPoints,
CancellationToken cancellationToken)
{
var actualBurndown = new List<BurndownDataPoint>();
// Start with total story points
actualBurndown.Add(new BurndownDataPoint
{
Date = sprint.StartDate,
StoryPoints = totalStoryPoints
});
// Get task completion history from audit logs
var taskCompletions = await GetTaskCompletionHistoryAsync(sprint.Id, cancellationToken);
var currentDate = sprint.StartDate.Date;
var endDate = sprint.Status == SprintStatus.Completed
? sprint.EndDate.Date
: DateTime.UtcNow.Date;
var remainingPoints = totalStoryPoints;
while (currentDate <= endDate)
{
currentDate = currentDate.AddDays(1);
// Get tasks completed on this day
var completedOnDay = taskCompletions
.Where(tc => tc.CompletedDate.Date == currentDate)
.Sum(tc => tc.StoryPoints);
remainingPoints -= completedOnDay;
actualBurndown.Add(new BurndownDataPoint
{
Date = currentDate,
StoryPoints = Math.Max(0, remainingPoints)
});
}
return actualBurndown;
}
private async Task<List<TaskCompletion>> GetTaskCompletionHistoryAsync(
Guid sprintId,
CancellationToken cancellationToken)
{
// Query audit logs for task status changes to "Done"
// This gives us historical completion data for accurate burndown
// For MVP, use current task status
// TODO: Implement audit log query for historical data in Phase 2
var sprint = await _sprintRepository.GetByIdAsync(sprintId, cancellationToken);
if (sprint == null) return new List<TaskCompletion>();
return sprint.Tasks
.Where(t => t.Status == WorkTaskStatus.Done)
.Select(t => new TaskCompletion
{
TaskId = t.Id,
StoryPoints = t.StoryPoints ?? 0,
CompletedDate = t.UpdatedAt ?? DateTime.UtcNow // Approximation for MVP
})
.ToList();
}
private class TaskCompletion
{
public Guid TaskId { get; set; }
public int StoryPoints { get; set; }
public DateTime CompletedDate { get; set; }
}
}
- Add Controller Endpoint: Update
SprintsController.cs
[HttpGet("{id}/burndown")]
[ProducesResponseType(typeof(BurndownDto), StatusCodes.Status200OK)]
public async Task<IActionResult> GetSprintBurndown([FromRoute] Guid id)
{
var burndown = await _mediator.Send(new GetSprintBurndownQuery(id));
return Ok(burndown);
}
Example Response:
{
"sprintId": "abc-123",
"sprintName": "Sprint 1",
"startDate": "2025-11-01",
"endDate": "2025-11-14",
"totalStoryPoints": 50,
"remainingStoryPoints": 20,
"completionPercentage": 60,
"idealBurndown": [
{ "date": "2025-11-01", "storyPoints": 50 },
{ "date": "2025-11-02", "storyPoints": 46 },
{ "date": "2025-11-03", "storyPoints": 42 },
...
{ "date": "2025-11-14", "storyPoints": 0 }
],
"actualBurndown": [
{ "date": "2025-11-01", "storyPoints": 50 },
{ "date": "2025-11-02", "storyPoints": 50 },
{ "date": "2025-11-03", "storyPoints": 45 },
{ "date": "2025-11-04", "storyPoints": 38 },
...
{ "date": "2025-11-05", "storyPoints": 20 }
]
}
Technical Notes
Ideal Burndown:
- Linear decrease from total story points to 0
- Formula:
Remaining = Total - (Day * DailyBurnRate) - DailyBurnRate =
TotalStoryPoints / SprintDuration
Actual Burndown:
- Based on real task completions
- Uses audit logs for historical accuracy (Phase 2)
- MVP uses task UpdatedAt timestamp (approximation)
Future Enhancements (Phase 2):
- Query audit logs for exact task completion dates
- Handle scope changes (tasks added/removed mid-sprint)
- Velocity trend analysis
- Sprint capacity planning
Testing
Unit Tests:
[Fact]
public async Task GetSprintBurndown_ShouldCalculateIdealBurndown()
{
// Arrange
var sprint = CreateTestSprint(
startDate: new DateTime(2025, 11, 1),
endDate: new DateTime(2025, 11, 14),
totalStoryPoints: 50
);
// Act
var burndown = await _handler.Handle(new GetSprintBurndownQuery(sprint.Id), default);
// Assert
Assert.Equal(15, burndown.IdealBurndown.Count); // 14 days + 1
Assert.Equal(50, burndown.IdealBurndown.First().StoryPoints);
Assert.Equal(0, burndown.IdealBurndown.Last().StoryPoints);
}
[Fact]
public async Task GetSprintBurndown_ShouldCalculateCompletionPercentage()
{
// Arrange
var sprint = CreateTestSprintWithTasks(totalPoints: 100, completedPoints: 60);
// Act
var burndown = await _handler.Handle(new GetSprintBurndownQuery(sprint.Id), default);
// Assert
Assert.Equal(60, burndown.CompletionPercentage);
}
Created: 2025-11-05 by Backend Agent