---
task_id: sprint_2_story_3_task_3
story: sprint_2_story_3
status: not_started
estimated_hours: 6
created_date: 2025-11-05
assignee: Backend Team
---

# Task 3: Create CQRS Commands and Queries

**Story**: Story 3 - Sprint Management Module
**Estimated**: 6 hours

## Description

Implement CQRS commands and queries for Sprint management, including Create, Update, Delete, Start, Complete, AddTask, RemoveTask, Get, and List operations.

## Acceptance Criteria

- [ ] 5 command handlers implemented
- [ ] 4 query handlers implemented
- [ ] SprintsController with 9 endpoints created
- [ ] Input validation with FluentValidation
- [ ] Unit tests for handlers
- [ ] Swagger documentation

## Implementation Details

**Commands to Implement**:
1. CreateSprintCommand
2. UpdateSprintCommand
3. DeleteSprintCommand
4. StartSprintCommand
5. CompleteSprintCommand
6. AddTaskToSprintCommand (Bonus)
7. RemoveTaskFromSprintCommand (Bonus)

**Queries to Implement**:
1. GetSprintByIdQuery
2. GetSprintsByProjectIdQuery
3. GetActiveSprintsQuery
4. GetSprintBurndownQuery

**Files to Create**:

1. **Create Sprint**: `colaflow-api/src/ColaFlow.Application/Sprints/Commands/CreateSprint/`
```csharp
// CreateSprintCommand.cs
public record CreateSprintCommand : IRequest<Guid>
{
    public Guid ProjectId { get; init; }
    public string Name { get; init; } = string.Empty;
    public string? Goal { get; init; }
    public DateTime StartDate { get; init; }
    public DateTime EndDate { get; init; }
}

// CreateSprintCommandValidator.cs
public class CreateSprintCommandValidator : AbstractValidator<CreateSprintCommand>
{
    public CreateSprintCommandValidator()
    {
        RuleFor(x => x.ProjectId)
            .NotEmpty().WithMessage("ProjectId is required");

        RuleFor(x => x.Name)
            .NotEmpty().WithMessage("Name is required")
            .MaximumLength(100).WithMessage("Name must not exceed 100 characters");

        RuleFor(x => x.Goal)
            .MaximumLength(500).WithMessage("Goal must not exceed 500 characters");

        RuleFor(x => x.StartDate)
            .NotEmpty().WithMessage("StartDate is required");

        RuleFor(x => x.EndDate)
            .NotEmpty().WithMessage("EndDate is required")
            .GreaterThan(x => x.StartDate).WithMessage("EndDate must be after StartDate");

        RuleFor(x => x)
            .Must(x => (x.EndDate - x.StartDate).TotalDays <= 30)
            .WithMessage("Sprint duration cannot exceed 30 days");
    }
}

// CreateSprintCommandHandler.cs
public class CreateSprintCommandHandler : IRequestHandler<CreateSprintCommand, Guid>
{
    private readonly ISprintRepository _repository;
    private readonly IProjectRepository _projectRepository;
    private readonly ITenantContext _tenantContext;
    private readonly ILogger<CreateSprintCommandHandler> _logger;

    public CreateSprintCommandHandler(
        ISprintRepository repository,
        IProjectRepository projectRepository,
        ITenantContext tenantContext,
        ILogger<CreateSprintCommandHandler> logger)
    {
        _repository = repository;
        _projectRepository = projectRepository;
        _tenantContext = tenantContext;
        _logger = logger;
    }

    public async Task<Guid> Handle(CreateSprintCommand request, CancellationToken cancellationToken)
    {
        // Verify project exists and belongs to tenant
        var project = await _projectRepository.GetByIdAsync(request.ProjectId, cancellationToken);
        if (project == null)
            throw new NotFoundException(nameof(Project), request.ProjectId);

        // Create sprint
        var sprint = Sprint.Create(
            _tenantContext.TenantId,
            request.ProjectId,
            request.Name,
            request.Goal,
            request.StartDate,
            request.EndDate
        );

        await _repository.AddAsync(sprint, cancellationToken);
        await _repository.SaveChangesAsync(cancellationToken);

        _logger.LogInformation("Sprint {SprintId} created for project {ProjectId}", sprint.Id, request.ProjectId);

        return sprint.Id;
    }
}
```

2. **Update Sprint**: `colaflow-api/src/ColaFlow.Application/Sprints/Commands/UpdateSprint/`
```csharp
public record UpdateSprintCommand : IRequest
{
    public Guid SprintId { get; init; }
    public string Name { get; init; } = string.Empty;
    public string? Goal { get; init; }
    public DateTime StartDate { get; init; }
    public DateTime EndDate { get; init; }
}

public class UpdateSprintCommandHandler : IRequestHandler<UpdateSprintCommand>
{
    private readonly ISprintRepository _repository;

    public async Task Handle(UpdateSprintCommand request, CancellationToken cancellationToken)
    {
        var sprint = await _repository.GetByIdAsync(request.SprintId, cancellationToken);
        if (sprint == null)
            throw new NotFoundException(nameof(Sprint), request.SprintId);

        sprint.Update(request.Name, request.Goal, request.StartDate, request.EndDate);

        _repository.Update(sprint);
        await _repository.SaveChangesAsync(cancellationToken);
    }
}
```

3. **Start/Complete Sprint**: `colaflow-api/src/ColaFlow.Application/Sprints/Commands/`
```csharp
// StartSprintCommand.cs
public record StartSprintCommand(Guid SprintId) : IRequest;

public class StartSprintCommandHandler : IRequestHandler<StartSprintCommand>
{
    private readonly ISprintRepository _repository;

    public async Task Handle(StartSprintCommand request, CancellationToken cancellationToken)
    {
        var sprint = await _repository.GetByIdAsync(request.SprintId, cancellationToken);
        if (sprint == null)
            throw new NotFoundException(nameof(Sprint), request.SprintId);

        sprint.Start();

        _repository.Update(sprint);
        await _repository.SaveChangesAsync(cancellationToken);
    }
}

// CompleteSprintCommand.cs
public record CompleteSprintCommand(Guid SprintId) : IRequest;

public class CompleteSprintCommandHandler : IRequestHandler<CompleteSprintCommand>
{
    private readonly ISprintRepository _repository;

    public async Task Handle(CompleteSprintCommand request, CancellationToken cancellationToken)
    {
        var sprint = await _repository.GetByIdAsync(request.SprintId, cancellationToken);
        if (sprint == null)
            throw new NotFoundException(nameof(Sprint), request.SprintId);

        sprint.Complete();

        _repository.Update(sprint);
        await _repository.SaveChangesAsync(cancellationToken);
    }
}
```

4. **Get Sprint Query**: `colaflow-api/src/ColaFlow.Application/Sprints/Queries/GetSprintById/`
```csharp
public record GetSprintByIdQuery(Guid SprintId) : IRequest<SprintDto>;

public class SprintDto
{
    public Guid Id { get; set; }
    public Guid ProjectId { get; set; }
    public string ProjectName { get; set; } = string.Empty;
    public string Name { get; set; } = string.Empty;
    public string? Goal { get; set; }
    public DateTime StartDate { get; set; }
    public DateTime EndDate { get; set; }
    public string Status { get; set; } = string.Empty;
    public int TotalTasks { get; set; }
    public int CompletedTasks { get; set; }
    public int TotalStoryPoints { get; set; }
    public int RemainingStoryPoints { get; set; }
}

public class GetSprintByIdQueryHandler : IRequestHandler<GetSprintByIdQuery, SprintDto>
{
    private readonly ISprintRepository _repository;
    private readonly IMapper _mapper;

    public async Task<SprintDto> Handle(GetSprintByIdQuery request, CancellationToken cancellationToken)
    {
        var sprint = await _repository.GetByIdAsync(request.SprintId, cancellationToken);
        if (sprint == null)
            throw new NotFoundException(nameof(Sprint), request.SprintId);

        var dto = _mapper.Map<SprintDto>(sprint);

        // Calculate statistics
        dto.TotalTasks = sprint.Tasks.Count;
        dto.CompletedTasks = sprint.Tasks.Count(t => t.Status == WorkTaskStatus.Done);
        dto.TotalStoryPoints = sprint.Tasks.Sum(t => t.StoryPoints ?? 0);
        dto.RemainingStoryPoints = sprint.Tasks.Where(t => t.Status != WorkTaskStatus.Done).Sum(t => t.StoryPoints ?? 0);

        return dto;
    }
}
```

5. **Controller**: `colaflow-api/src/ColaFlow.API/Controllers/SprintsController.cs`
```csharp
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class SprintsController : ControllerBase
{
    private readonly IMediator _mediator;

    public SprintsController(IMediator mediator)
    {
        _mediator = mediator;
    }

    [HttpPost]
    [ProducesResponseType(typeof(Guid), StatusCodes.Status201Created)]
    public async Task<IActionResult> CreateSprint([FromBody] CreateSprintCommand command)
    {
        var sprintId = await _mediator.Send(command);
        return CreatedAtAction(nameof(GetSprintById), new { id = sprintId }, sprintId);
    }

    [HttpPut("{id}")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    public async Task<IActionResult> UpdateSprint([FromRoute] Guid id, [FromBody] UpdateSprintCommand command)
    {
        if (id != command.SprintId)
            return BadRequest("SprintId mismatch");

        await _mediator.Send(command);
        return NoContent();
    }

    [HttpDelete("{id}")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    public async Task<IActionResult> DeleteSprint([FromRoute] Guid id)
    {
        await _mediator.Send(new DeleteSprintCommand(id));
        return NoContent();
    }

    [HttpGet("{id}")]
    [ProducesResponseType(typeof(SprintDto), StatusCodes.Status200OK)]
    public async Task<IActionResult> GetSprintById([FromRoute] Guid id)
    {
        var sprint = await _mediator.Send(new GetSprintByIdQuery(id));
        return Ok(sprint);
    }

    [HttpGet]
    [ProducesResponseType(typeof(List<SprintDto>), StatusCodes.Status200OK)]
    public async Task<IActionResult> GetSprints([FromQuery] Guid? projectId = null)
    {
        var sprints = projectId.HasValue
            ? await _mediator.Send(new GetSprintsByProjectIdQuery(projectId.Value))
            : await _mediator.Send(new GetActiveSprintsQuery());

        return Ok(sprints);
    }

    [HttpPost("{id}/start")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    public async Task<IActionResult> StartSprint([FromRoute] Guid id)
    {
        await _mediator.Send(new StartSprintCommand(id));
        return NoContent();
    }

    [HttpPost("{id}/complete")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    public async Task<IActionResult> CompleteSprint([FromRoute] Guid id)
    {
        await _mediator.Send(new CompleteSprintCommand(id));
        return NoContent();
    }
}
```

## Technical Notes

- Use FluentValidation for input validation
- Use AutoMapper for entity-to-DTO mapping
- Use IRequestHandler<TRequest, TResponse> for commands/queries
- Publish domain events via MediatR after SaveChanges
- Return 404 for not found entities
- Use DTOs to avoid exposing domain entities

## Testing

**Unit Tests**: Test each command/query handler independently

---

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