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:
289
docs/plans/sprint_2_story_3_task_1.md
Normal file
289
docs/plans/sprint_2_story_3_task_1.md
Normal 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
|
||||
Reference in New Issue
Block a user