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>
This commit is contained in:
Yaojia Wang
2025-11-04 22:56:31 +01:00
parent d6cf86a4da
commit ebb56cc9f8
19 changed files with 4030 additions and 0 deletions

View File

@@ -0,0 +1,289 @@
---
task_id: sprint_2_story_3_task_1
story: sprint_2_story_3
status: not_started
estimated_hours: 5
created_date: 2025-11-05
assignee: Backend Team
---
# Task 1: Create Sprint Aggregate Root and Domain Events
**Story**: Story 3 - Sprint Management Module
**Estimated**: 5 hours
## Description
Design and implement the Sprint aggregate root with proper domain logic, business rules validation, and domain events for sprint lifecycle management.
## Acceptance Criteria
- [ ] Sprint entity created with all required properties
- [ ] Domain events defined (SprintCreated, SprintUpdated, SprintStarted, SprintCompleted, SprintDeleted)
- [ ] Business logic for status transitions implemented
- [ ] Validation rules enforced (dates, status)
- [ ] Unit tests for domain logic
## Implementation Details
**Files to Create**:
1. **Sprint Entity**: `colaflow-api/src/ColaFlow.Domain/Entities/Sprint.cs`
```csharp
public class Sprint
{
public Guid Id { get; private set; }
public Guid TenantId { get; private set; }
public Guid ProjectId { get; private set; }
public string Name { get; private set; } = string.Empty;
public string? Goal { get; private set; }
public DateTime StartDate { get; private set; }
public DateTime EndDate { get; private set; }
public SprintStatus Status { get; private set; }
public DateTime CreatedAt { get; private set; }
public DateTime? UpdatedAt { get; private set; }
// Navigation properties
public Project Project { get; private set; } = null!;
public ICollection<WorkTask> Tasks { get; private set; } = new List<WorkTask>();
// Domain events
private readonly List<DomainEvent> _domainEvents = new();
public IReadOnlyList<DomainEvent> DomainEvents => _domainEvents.AsReadOnly();
// Factory method
public static Sprint Create(Guid tenantId, Guid projectId, string name, string? goal,
DateTime startDate, DateTime endDate)
{
ValidateDates(startDate, endDate);
var sprint = new Sprint
{
Id = Guid.NewGuid(),
TenantId = tenantId,
ProjectId = projectId,
Name = name,
Goal = goal,
StartDate = startDate,
EndDate = endDate,
Status = SprintStatus.Planned,
CreatedAt = DateTime.UtcNow
};
sprint.AddDomainEvent(new SprintCreatedEvent(sprint.Id, sprint.Name, sprint.ProjectId));
return sprint;
}
// Business logic methods
public void Update(string name, string? goal, DateTime startDate, DateTime endDate)
{
ValidateDates(startDate, endDate);
if (Status == SprintStatus.Completed)
throw new InvalidOperationException("Cannot update a completed sprint");
Name = name;
Goal = goal;
StartDate = startDate;
EndDate = endDate;
UpdatedAt = DateTime.UtcNow;
AddDomainEvent(new SprintUpdatedEvent(Id, Name));
}
public void Start()
{
if (Status != SprintStatus.Planned)
throw new InvalidOperationException($"Cannot start sprint in {Status} status");
if (DateTime.UtcNow < StartDate)
throw new InvalidOperationException("Cannot start sprint before start date");
Status = SprintStatus.Active;
UpdatedAt = DateTime.UtcNow;
AddDomainEvent(new SprintStartedEvent(Id, Name));
}
public void Complete()
{
if (Status != SprintStatus.Active)
throw new InvalidOperationException($"Cannot complete sprint in {Status} status");
Status = SprintStatus.Completed;
UpdatedAt = DateTime.UtcNow;
AddDomainEvent(new SprintCompletedEvent(Id, Name, Tasks.Count));
}
public void AddTask(WorkTask task)
{
if (Status == SprintStatus.Completed)
throw new InvalidOperationException("Cannot add tasks to a completed sprint");
if (!Tasks.Contains(task))
{
Tasks.Add(task);
UpdatedAt = DateTime.UtcNow;
}
}
public void RemoveTask(WorkTask task)
{
if (Status == SprintStatus.Completed)
throw new InvalidOperationException("Cannot remove tasks from a completed sprint");
Tasks.Remove(task);
UpdatedAt = DateTime.UtcNow;
}
private static void ValidateDates(DateTime startDate, DateTime endDate)
{
if (endDate <= startDate)
throw new ArgumentException("End date must be after start date");
if ((endDate - startDate).TotalDays > 30)
throw new ArgumentException("Sprint duration cannot exceed 30 days");
}
private void AddDomainEvent(DomainEvent @event)
{
_domainEvents.Add(@event);
}
public void ClearDomainEvents()
{
_domainEvents.Clear();
}
}
public enum SprintStatus
{
Planned = 0,
Active = 1,
Completed = 2
}
```
2. **Domain Events**: `colaflow-api/src/ColaFlow.Domain/Events/Sprint/`
```csharp
// SprintCreatedEvent.cs
public record SprintCreatedEvent(Guid SprintId, string SprintName, Guid ProjectId) : DomainEvent;
// SprintUpdatedEvent.cs
public record SprintUpdatedEvent(Guid SprintId, string SprintName) : DomainEvent;
// SprintStartedEvent.cs
public record SprintStartedEvent(Guid SprintId, string SprintName) : DomainEvent;
// SprintCompletedEvent.cs
public record SprintCompletedEvent(Guid SprintId, string SprintName, int TaskCount) : DomainEvent;
// SprintDeletedEvent.cs
public record SprintDeletedEvent(Guid SprintId, string SprintName) : DomainEvent;
```
## Technical Notes
**Business Rules**:
- Sprint duration: 1-30 days (typical Scrum 2-4 weeks)
- Status transitions: Planned → Active → Completed (one-way)
- Cannot update/delete completed sprints
- Cannot add/remove tasks from completed sprints
- End date must be after start date
**Validation**:
- Name: Required, max 100 characters
- Goal: Optional, max 500 characters
- Dates: StartDate < EndDate, duration <= 30 days
## Testing
**Unit Tests**: `colaflow-api/tests/ColaFlow.Domain.Tests/Entities/SprintTests.cs`
```csharp
public class SprintTests
{
[Fact]
public void Create_ShouldCreateValidSprint()
{
// Arrange
var tenantId = Guid.NewGuid();
var projectId = Guid.NewGuid();
var startDate = DateTime.UtcNow;
var endDate = startDate.AddDays(14);
// Act
var sprint = Sprint.Create(tenantId, projectId, "Sprint 1", "Complete Feature X", startDate, endDate);
// Assert
Assert.NotEqual(Guid.Empty, sprint.Id);
Assert.Equal("Sprint 1", sprint.Name);
Assert.Equal(SprintStatus.Planned, sprint.Status);
Assert.Single(sprint.DomainEvents); // SprintCreatedEvent
}
[Fact]
public void Create_ShouldThrowException_WhenEndDateBeforeStartDate()
{
// Arrange
var startDate = DateTime.UtcNow;
var endDate = startDate.AddDays(-1);
// Act & Assert
Assert.Throws<ArgumentException>(() =>
Sprint.Create(Guid.NewGuid(), Guid.NewGuid(), "Sprint 1", null, startDate, endDate));
}
[Fact]
public void Start_ShouldChangeStatusToActive()
{
// Arrange
var sprint = CreateTestSprint();
// Act
sprint.Start();
// Assert
Assert.Equal(SprintStatus.Active, sprint.Status);
Assert.Contains(sprint.DomainEvents, e => e is SprintStartedEvent);
}
[Fact]
public void Complete_ShouldThrowException_WhenNotActive()
{
// Arrange
var sprint = CreateTestSprint(); // Status = Planned
// Act & Assert
Assert.Throws<InvalidOperationException>(() => sprint.Complete());
}
[Fact]
public void AddTask_ShouldThrowException_WhenSprintCompleted()
{
// Arrange
var sprint = CreateTestSprint();
sprint.Start();
sprint.Complete();
// Act & Assert
Assert.Throws<InvalidOperationException>(() => sprint.AddTask(new WorkTask()));
}
private Sprint CreateTestSprint()
{
return Sprint.Create(
Guid.NewGuid(),
Guid.NewGuid(),
"Test Sprint",
"Test Goal",
DateTime.UtcNow,
DateTime.UtcNow.AddDays(14)
);
}
}
```
---
**Created**: 2025-11-05 by Backend Agent