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>
8.5 KiB
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:
- 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);
}
- 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);
}
}
- 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);
}
}
- 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);
- 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;
}
- 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