Files
ColaFlow/docs/plans/sprint_2_story_3_task_2.md
Yaojia Wang ebb56cc9f8 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>
2025-11-04 22:56:31 +01:00

8.5 KiB

task_id, story, status, estimated_hours, created_date, assignee
task_id story status estimated_hours created_date assignee
sprint_2_story_3_task_2 sprint_2_story_3 not_started 4 2025-11-05 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
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);
}
  1. Repository Implementation: colaflow-api/src/ColaFlow.Infrastructure/Repositories/SprintRepository.cs
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);
    }
}
  1. EF Core Configuration: colaflow-api/src/ColaFlow.Infrastructure/Data/Configurations/SprintConfiguration.cs
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);
    }
}
  1. Database Migration: Create migration
cd colaflow-api/src/ColaFlow.Infrastructure
dotnet ef migrations add AddSprintEntity --project ../ColaFlow.Infrastructure --startup-project ../../ColaFlow.API

Migration will create:

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);
  1. Update WorkTask: Add SprintId foreign key
// In WorkTask entity
public Guid? SprintId { get; private set; }

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

public void RemoveFromSprint()
{
    SprintId = null;
}
  1. DI Registration: Update colaflow-api/src/ColaFlow.Infrastructure/DependencyInjection.cs
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

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