---
task_id: sprint_2_story_3_task_2
story: sprint_2_story_3
status: not_started
estimated_hours: 4
created_date: 2025-11-05
assignee: Backend Team
---

# Task 2: Implement Sprint Repository and EF Core Configuration

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

## Description

Create the Sprint repository pattern implementation with EF Core configuration, including multi-tenant isolation, database migration, and indexes for query optimization.

## Acceptance Criteria

- [ ] ISprintRepository interface defined
- [ ] SprintRepository implementation with multi-tenant filtering
- [ ] EF Core entity configuration created
- [ ] Database migration generated and applied
- [ ] Indexes created for performance
- [ ] Unit tests for repository methods

## Implementation Details

**Files to Create**:

1. **Repository Interface**: `colaflow-api/src/ColaFlow.Domain/Repositories/ISprintRepository.cs`
```csharp
public interface ISprintRepository
{
    Task<Sprint?> GetByIdAsync(Guid sprintId, CancellationToken cancellationToken = default);
    Task<List<Sprint>> GetByProjectIdAsync(Guid projectId, CancellationToken cancellationToken = default);
    Task<List<Sprint>> GetActiveSprintsAsync(CancellationToken cancellationToken = default);
    Task<Sprint?> GetActiveSprintForProjectAsync(Guid projectId, CancellationToken cancellationToken = default);
    Task AddAsync(Sprint sprint, CancellationToken cancellationToken = default);
    void Update(Sprint sprint);
    void Delete(Sprint sprint);
    Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
}
```

2. **Repository Implementation**: `colaflow-api/src/ColaFlow.Infrastructure/Repositories/SprintRepository.cs`
```csharp
public class SprintRepository : ISprintRepository
{
    private readonly ColaFlowDbContext _context;
    private readonly ITenantContext _tenantContext;

    public SprintRepository(ColaFlowDbContext context, ITenantContext tenantContext)
    {
        _context = context;
        _tenantContext = tenantContext;
    }

    public async Task<Sprint?> GetByIdAsync(Guid sprintId, CancellationToken cancellationToken = default)
    {
        var tenantId = _tenantContext.TenantId;
        return await _context.Sprints
            .Include(s => s.Project)
            .Include(s => s.Tasks)
            .FirstOrDefaultAsync(s => s.TenantId == tenantId && s.Id == sprintId, cancellationToken);
    }

    public async Task<List<Sprint>> GetByProjectIdAsync(Guid projectId, CancellationToken cancellationToken = default)
    {
        var tenantId = _tenantContext.TenantId;
        return await _context.Sprints
            .Include(s => s.Tasks)
            .Where(s => s.TenantId == tenantId && s.ProjectId == projectId)
            .OrderByDescending(s => s.StartDate)
            .AsNoTracking()
            .ToListAsync(cancellationToken);
    }

    public async Task<List<Sprint>> GetActiveSprintsAsync(CancellationToken cancellationToken = default)
    {
        var tenantId = _tenantContext.TenantId;
        return await _context.Sprints
            .Include(s => s.Project)
            .Include(s => s.Tasks)
            .Where(s => s.TenantId == tenantId && s.Status == SprintStatus.Active)
            .AsNoTracking()
            .ToListAsync(cancellationToken);
    }

    public async Task<Sprint?> GetActiveSprintForProjectAsync(Guid projectId, CancellationToken cancellationToken = default)
    {
        var tenantId = _tenantContext.TenantId;
        return await _context.Sprints
            .Include(s => s.Tasks)
            .FirstOrDefaultAsync(s => s.TenantId == tenantId &&
                                     s.ProjectId == projectId &&
                                     s.Status == SprintStatus.Active,
                                cancellationToken);
    }

    public async Task AddAsync(Sprint sprint, CancellationToken cancellationToken = default)
    {
        await _context.Sprints.AddAsync(sprint, cancellationToken);
    }

    public void Update(Sprint sprint)
    {
        _context.Sprints.Update(sprint);
    }

    public void Delete(Sprint sprint)
    {
        _context.Sprints.Remove(sprint);
    }

    public async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
    {
        return await _context.SaveChangesAsync(cancellationToken);
    }
}
```

3. **EF Core Configuration**: `colaflow-api/src/ColaFlow.Infrastructure/Data/Configurations/SprintConfiguration.cs`
```csharp
public class SprintConfiguration : IEntityTypeConfiguration<Sprint>
{
    public void Configure(EntityTypeBuilder<Sprint> builder)
    {
        builder.ToTable("Sprints");

        builder.HasKey(s => s.Id);

        // Properties
        builder.Property(s => s.Name)
            .IsRequired()
            .HasMaxLength(100);

        builder.Property(s => s.Goal)
            .HasMaxLength(500);

        builder.Property(s => s.StartDate)
            .IsRequired();

        builder.Property(s => s.EndDate)
            .IsRequired();

        builder.Property(s => s.Status)
            .IsRequired()
            .HasConversion<string>(); // Store as string for readability

        builder.Property(s => s.TenantId)
            .IsRequired();

        builder.Property(s => s.CreatedAt)
            .IsRequired();

        builder.Property(s => s.UpdatedAt);

        // Relationships
        builder.HasOne(s => s.Project)
            .WithMany()
            .HasForeignKey(s => s.ProjectId)
            .OnDelete(DeleteBehavior.Cascade);

        builder.HasMany(s => s.Tasks)
            .WithOne()
            .HasForeignKey("SprintId")
            .OnDelete(DeleteBehavior.SetNull);

        // Indexes
        builder.HasIndex(s => new { s.TenantId, s.ProjectId });
        builder.HasIndex(s => new { s.TenantId, s.Status });
        builder.HasIndex(s => s.StartDate);

        // Multi-tenant global query filter
        builder.HasQueryFilter(s => s.TenantId == Guid.Empty); // Replaced at runtime by TenantContext

        // Ignore domain events (not persisted)
        builder.Ignore(s => s.DomainEvents);
    }
}
```

4. **Database Migration**: Create migration
```bash
cd colaflow-api/src/ColaFlow.Infrastructure
dotnet ef migrations add AddSprintEntity --project ../ColaFlow.Infrastructure --startup-project ../../ColaFlow.API
```

**Migration will create**:
```sql
CREATE TABLE Sprints (
    Id UUID PRIMARY KEY,
    TenantId UUID NOT NULL,
    ProjectId UUID NOT NULL,
    Name VARCHAR(100) NOT NULL,
    Goal VARCHAR(500),
    StartDate TIMESTAMP NOT NULL,
    EndDate TIMESTAMP NOT NULL,
    Status VARCHAR(20) NOT NULL,
    CreatedAt TIMESTAMP NOT NULL,
    UpdatedAt TIMESTAMP,

    CONSTRAINT FK_Sprints_Projects FOREIGN KEY (ProjectId) REFERENCES Projects(Id) ON DELETE CASCADE,
    CONSTRAINT FK_Sprints_Tenants FOREIGN KEY (TenantId) REFERENCES Tenants(Id) ON DELETE CASCADE
);

CREATE INDEX IX_Sprints_TenantId_ProjectId ON Sprints(TenantId, ProjectId);
CREATE INDEX IX_Sprints_TenantId_Status ON Sprints(TenantId, Status);
CREATE INDEX IX_Sprints_StartDate ON Sprints(StartDate);
```

5. **Update WorkTask**: Add SprintId foreign key
```csharp
// In WorkTask entity
public Guid? SprintId { get; private set; }

public void AssignToSprint(Guid sprintId)
{
    SprintId = sprintId;
}

public void RemoveFromSprint()
{
    SprintId = null;
}
```

6. **DI Registration**: Update `colaflow-api/src/ColaFlow.Infrastructure/DependencyInjection.cs`
```csharp
services.AddScoped<ISprintRepository, SprintRepository>();
```

## Technical Notes

- Use composite indexes for multi-tenant queries
- Store Status as string for readability in database
- Use `AsNoTracking()` for read-only queries (performance)
- Global query filter enforces multi-tenant isolation
- Tasks relationship uses SetNull (tasks remain when sprint deleted)

## Testing

**Unit Tests**: `colaflow-api/tests/ColaFlow.Infrastructure.Tests/Repositories/SprintRepositoryTests.cs`
```csharp
public class SprintRepositoryTests
{
    [Fact]
    public async Task GetByIdAsync_ShouldReturnSprint_WhenExists()
    {
        // Arrange
        var (context, tenantContext, repository) = CreateTestContext();
        var sprint = CreateTestSprint(tenantContext.TenantId);
        await context.Sprints.AddAsync(sprint);
        await context.SaveChangesAsync();

        // Act
        var result = await repository.GetByIdAsync(sprint.Id);

        // Assert
        Assert.NotNull(result);
        Assert.Equal(sprint.Id, result.Id);
    }

    [Fact]
    public async Task GetByProjectIdAsync_ShouldReturnOnlyCurrentTenantSprints()
    {
        // Test multi-tenant isolation
        // ...
    }
}
```

---

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