---
task_id: sprint_2_story_3_task_4
story: sprint_2_story_3
status: not_started
estimated_hours: 4
created_date: 2025-11-05
assignee: 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**:

1. **Burndown Query**: `colaflow-api/src/ColaFlow.Application/Sprints/Queries/GetSprintBurndown/GetSprintBurndownQuery.cs`
```csharp
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; }
}
```

2. **Query Handler**: `colaflow-api/src/ColaFlow.Application/Sprints/Queries/GetSprintBurndown/GetSprintBurndownQueryHandler.cs`
```csharp
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; }
    }
}
```

3. **Add Controller Endpoint**: Update `SprintsController.cs`
```csharp
[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**:
```json
{
  "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**:
```csharp
[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
