--- 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 Tasks { get; private set; } = new List(); // Domain events private readonly List _domainEvents = new(); public IReadOnlyList 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(() => 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(() => sprint.Complete()); } [Fact] public void AddTask_ShouldThrowException_WhenSprintCompleted() { // Arrange var sprint = CreateTestSprint(); sprint.Start(); sprint.Complete(); // Act & Assert Assert.Throws(() => 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