Files
ColaFlow/docs/plans/sprint_2_story_3_task_1.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.2 KiB

task_id, story, status, estimated_hours, created_date, assignee
task_id story status estimated_hours created_date assignee
sprint_2_story_3_task_1 sprint_2_story_3 not_started 5 2025-11-05 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
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
}
  1. Domain Events: colaflow-api/src/ColaFlow.Domain/Events/Sprint/
// 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

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