# Day 15-16 Implementation Roadmap **Date**: 2025-11-05 至 2025-11-06 **Focus**: Epic/Story/Task Hierarchy Implementation **Module**: Issue Management Module (Enhancement) **Architecture Decision**: ADR-035 --- ## Overview Day 15-16 will implement parent-child hierarchy support in the existing Issue Management Module, enabling Epic → Story → Task relationships without breaking existing functionality. **Key Principles**: - Enhance, don't replace - Backward compatible - Zero breaking changes - Multi-tenant secure by default - Performance first --- ## Day 15: Database & Domain Layer (6-8 hours) ### Morning Session (3-4 hours): Database Design #### Task 1.1: Create Database Migration (1-1.5h) **Owner**: Backend Developer **Steps**: ```csharp // File: colaflow-api/src/Modules/IssueManagement/Infrastructure/Persistence/Migrations/ // 20251105_AddIssueHierarchy.cs public partial class AddIssueHierarchy : Migration { protected override void Up(MigrationBuilder migrationBuilder) { // Add ParentIssueId column migrationBuilder.AddColumn( name: "parent_issue_id", table: "issues", type: "uuid", nullable: true); // Add foreign key constraint migrationBuilder.AddForeignKey( name: "fk_issues_parent", table: "issues", column: "parent_issue_id", principalTable: "issues", principalColumn: "id", onDelete: ReferentialAction.SetNull); // Prevent cascade delete // Add index for performance migrationBuilder.CreateIndex( name: "ix_issues_parent_issue_id", table: "issues", column: "parent_issue_id", filter: "parent_issue_id IS NOT NULL"); // Partial index } protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropForeignKey( name: "fk_issues_parent", table: "issues"); migrationBuilder.DropIndex( name: "ix_issues_parent_issue_id", table: "issues"); migrationBuilder.DropColumn( name: "parent_issue_id", table: "issues"); } } ``` **Checklist**: - [ ] Create migration file - [ ] Test migration: `dotnet ef migrations add AddIssueHierarchy` - [ ] Apply to dev DB: `dotnet ef database update` - [ ] Verify column added: `\d+ issues` in psql - [ ] Verify index created - [ ] Test backward compatibility (existing queries still work) **Acceptance Criteria**: - Migration runs without errors - `parent_issue_id` column is nullable (existing data unaffected) - Foreign key prevents invalid parent references - Index improves query performance --- #### Task 1.2: Update EF Core Configuration (0.5-1h) **Owner**: Backend Developer **Steps**: ```csharp // File: colaflow-api/src/Modules/IssueManagement/Infrastructure/Persistence/Configurations/IssueConfiguration.cs public class IssueConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { // Existing configuration... // NEW: Hierarchy configuration builder.Property(i => i.ParentIssueId) .HasColumnName("parent_issue_id") .IsRequired(false); builder.HasOne(i => i.ParentIssue) .WithMany(i => i.ChildIssues) .HasForeignKey(i => i.ParentIssueId) .OnDelete(DeleteBehavior.SetNull) // When parent deleted, set child's parent to NULL .IsRequired(false); builder.HasMany(i => i.ChildIssues) .WithOne(i => i.ParentIssue) .HasForeignKey(i => i.ParentIssueId) .OnDelete(DeleteBehavior.SetNull); } } ``` **Checklist**: - [ ] Update `IssueConfiguration.cs` - [ ] Verify EF Core generates correct SQL - [ ] Test lazy loading disabled (no N+1 queries) - [ ] Test eager loading works: `.Include(i => i.ChildIssues)` **Acceptance Criteria**: - EF Core configuration matches database schema - Navigation properties work correctly - Delete behavior is SetNull (not Cascade) --- ### Afternoon Session (3-4 hours): Domain Logic #### Task 1.3: Update Issue Entity (2-3h) **Owner**: Backend Developer **Steps**: ```csharp // File: colaflow-api/src/Modules/IssueManagement/Domain/Entities/Issue.cs public class Issue : TenantEntity, IAggregateRoot { // Existing properties... public IssueType Type { get; private set; } public string Title { get; private set; } public IssueStatus Status { get; private set; } // NEW: Hierarchy support public Guid? ParentIssueId { get; private set; } public virtual Issue? ParentIssue { get; private set; } public virtual ICollection ChildIssues { get; private set; } = new List(); // NEW: Hierarchy methods public Result SetParent(Issue parent) { // Validation 1: Same tenant if (parent.TenantId != this.TenantId) return Result.Failure("Cannot link issues across tenants"); // Validation 2: Valid hierarchy rules if (!IsValidHierarchy(parent)) return Result.Failure($"{parent.Type} cannot be parent of {this.Type}"); // Validation 3: Prevent circular dependency if (WouldCreateCircularDependency(parent)) return Result.Failure("Circular dependency detected"); // Validation 4: Depth limit if (parent.GetDepth() >= 2) // Max 3 levels (0, 1, 2) return Result.Failure("Maximum hierarchy depth exceeded (3 levels)"); // Set parent ParentIssueId = parent.Id; ParentIssue = parent; // Raise domain event AddDomainEvent(new IssueHierarchyChangedEvent( issueId: this.Id, newParentId: parent.Id, oldParentId: null )); return Result.Success(); } public Result RemoveParent() { if (!ParentIssueId.HasValue) return Result.Failure("Issue does not have a parent"); var oldParentId = ParentIssueId.Value; ParentIssueId = null; ParentIssue = null; // Raise domain event AddDomainEvent(new IssueHierarchyChangedEvent( issueId: this.Id, newParentId: null, oldParentId: oldParentId )); return Result.Success(); } // NEW: Validation rules private bool IsValidHierarchy(Issue parent) { return (parent.Type, this.Type) switch { (IssueType.Epic, IssueType.Story) => true, (IssueType.Story, IssueType.Task) => true, (IssueType.Story, IssueType.Bug) => true, _ => false }; } private bool WouldCreateCircularDependency(Issue proposedParent) { var current = proposedParent; var visitedIds = new HashSet { this.Id }; int depth = 0; int maxDepth = 10; // Safety limit while (current != null && depth < maxDepth) { if (visitedIds.Contains(current.Id)) return true; // Circular dependency detected visitedIds.Add(current.Id); current = current.ParentIssue; depth++; } return false; } public int GetDepth() { int depth = 0; var current = this.ParentIssue; while (current != null && depth < 10) { depth++; current = current.ParentIssue; } return depth; } public List GetAncestorIds() { var ancestors = new List(); var current = this.ParentIssue; while (current != null && ancestors.Count < 10) { ancestors.Add(current.Id); current = current.ParentIssue; } return ancestors; } } ``` **Checklist**: - [ ] Add hierarchy properties (ParentIssueId, ParentIssue, ChildIssues) - [ ] Implement `SetParent` method with 4 validations - [ ] Implement `RemoveParent` method - [ ] Add `IsValidHierarchy` validation logic - [ ] Add `WouldCreateCircularDependency` check - [ ] Add `GetDepth` method - [ ] Add `GetAncestorIds` method - [ ] Ensure domain events are raised **Acceptance Criteria**: - All hierarchy rules enforced (Epic → Story → Task) - Circular dependency prevented - Max depth 3 levels enforced - Tenant isolation maintained - Domain events raised on hierarchy changes --- #### Task 1.4: Create Domain Events (0.5-1h) **Owner**: Backend Developer **Steps**: ```csharp // File: colaflow-api/src/Modules/IssueManagement/Domain/Events/IssueHierarchyChangedEvent.cs public class IssueHierarchyChangedEvent : DomainEvent { public Guid IssueId { get; } public Guid? NewParentId { get; } public Guid? OldParentId { get; } public IssueHierarchyChangedEvent( Guid issueId, Guid? newParentId, Guid? oldParentId) { IssueId = issueId; NewParentId = newParentId; OldParentId = oldParentId; } } ``` **Checklist**: - [ ] Create `IssueHierarchyChangedEvent.cs` - [ ] Add event handler (if needed for future use) - [ ] Test event is raised when parent set/removed **Acceptance Criteria**: - Event is raised on hierarchy changes - Event contains correct issueId, newParentId, oldParentId --- #### Task 1.5: Unit Tests for Domain Logic (1-1.5h) **Owner**: Backend Developer **Steps**: ```csharp // File: colaflow-api/tests/Modules/IssueManagement.Domain.Tests/IssueHierarchyTests.cs public class IssueHierarchyTests { [Fact] public void SetParent_ValidEpicToStory_ShouldSucceed() { // Arrange var epic = CreateIssue(IssueType.Epic); var story = CreateIssue(IssueType.Story); // Act var result = story.SetParent(epic); // Assert result.IsSuccess.Should().BeTrue(); story.ParentIssueId.Should().Be(epic.Id); } [Fact] public void SetParent_InvalidTaskToEpic_ShouldFail() { // Arrange var epic = CreateIssue(IssueType.Epic); var task = CreateIssue(IssueType.Task); // Act var result = task.SetParent(epic); // Assert result.IsSuccess.Should().BeFalse(); result.Error.Should().Contain("cannot be parent"); } [Fact] public void SetParent_CircularDependency_ShouldFail() { // Arrange var epic = CreateIssue(IssueType.Epic); var story = CreateIssue(IssueType.Story); story.SetParent(epic); // Act - Try to set epic's parent as story (circular) var result = epic.SetParent(story); // Assert result.IsSuccess.Should().BeFalse(); result.Error.Should().Contain("Circular dependency"); } [Fact] public void SetParent_CrossTenant_ShouldFail() { // Arrange var epicTenant1 = CreateIssue(IssueType.Epic, tenantId: Guid.NewGuid()); var storyTenant2 = CreateIssue(IssueType.Story, tenantId: Guid.NewGuid()); // Act var result = storyTenant2.SetParent(epicTenant1); // Assert result.IsSuccess.Should().BeFalse(); result.Error.Should().Contain("across tenants"); } [Fact] public void SetParent_MaxDepthExceeded_ShouldFail() { // Arrange var epic = CreateIssue(IssueType.Epic); var story1 = CreateIssue(IssueType.Story); var story2 = CreateIssue(IssueType.Story); var task = CreateIssue(IssueType.Task); story1.SetParent(epic); // Depth 1 story2.SetParent(story1); // Depth 2 task.SetParent(story2); // Depth 3 // Act - Try to add one more level (would be depth 4) var extraTask = CreateIssue(IssueType.Task); var result = extraTask.SetParent(task); // Assert result.IsSuccess.Should().BeFalse(); result.Error.Should().Contain("Maximum hierarchy depth"); } [Fact] public void GetDepth_ThreeLevels_ShouldReturnTwo() { // Arrange var epic = CreateIssue(IssueType.Epic); // Depth 0 var story = CreateIssue(IssueType.Story); var task = CreateIssue(IssueType.Task); story.SetParent(epic); // Depth 1 task.SetParent(story); // Depth 2 // Act var depth = task.GetDepth(); // Assert depth.Should().Be(2); } [Fact] public void RemoveParent_HasParent_ShouldSucceed() { // Arrange var epic = CreateIssue(IssueType.Epic); var story = CreateIssue(IssueType.Story); story.SetParent(epic); // Act var result = story.RemoveParent(); // Assert result.IsSuccess.Should().BeTrue(); story.ParentIssueId.Should().BeNull(); } [Fact] public void SetParent_RaisesHierarchyChangedEvent() { // Arrange var epic = CreateIssue(IssueType.Epic); var story = CreateIssue(IssueType.Story); // Act story.SetParent(epic); // Assert var domainEvent = story.DomainEvents .OfType() .FirstOrDefault(); domainEvent.Should().NotBeNull(); domainEvent.IssueId.Should().Be(story.Id); domainEvent.NewParentId.Should().Be(epic.Id); } private Issue CreateIssue( IssueType type, Guid? tenantId = null) { return Issue.Create( tenantId: tenantId ?? Guid.NewGuid(), projectId: Guid.NewGuid(), type: type, title: $"Test {type}", description: "Test description", priority: IssuePriority.Medium, createdBy: Guid.NewGuid() ); } } ``` **Checklist**: - [ ] Test valid hierarchy: Epic → Story, Story → Task - [ ] Test invalid hierarchy: Task → Story, Epic → Task - [ ] Test circular dependency prevention - [ ] Test cross-tenant rejection - [ ] Test max depth enforcement (3 levels) - [ ] Test `GetDepth` method - [ ] Test `RemoveParent` method - [ ] Test domain events raised - [ ] All 10+ tests passing **Acceptance Criteria**: - 100% code coverage for hierarchy logic - All edge cases tested - Tests run in < 1 second --- ## Day 16: Application & API Layer (6-8 hours) ### Morning Session (3-4 hours): Commands & Queries #### Task 2.1: Create Commands (1.5-2h) **Owner**: Backend Developer **Files to Create**: **1. AddChildIssueCommand.cs** ```csharp // File: colaflow-api/src/Modules/IssueManagement/Application/Commands/AddChildIssueCommand.cs public record AddChildIssueCommand( Guid ParentIssueId, Guid ChildIssueId ) : IRequest>; public class AddChildIssueCommandHandler : IRequestHandler> { private readonly IIssueRepository _issueRepository; private readonly ITenantContextAccessor _tenantContext; private readonly IMapper _mapper; public AddChildIssueCommandHandler( IIssueRepository issueRepository, ITenantContextAccessor tenantContext, IMapper mapper) { _issueRepository = issueRepository; _tenantContext = tenantContext; _mapper = mapper; } public async Task> Handle( AddChildIssueCommand request, CancellationToken cancellationToken) { var tenantId = _tenantContext.GetTenantId(); // Get parent issue (with tenant filter) var parent = await _issueRepository.GetByIdAsync( request.ParentIssueId, cancellationToken); if (parent == null) return Result.Failure("Parent issue not found"); // Get child issue (with tenant filter) var child = await _issueRepository.GetByIdAsync( request.ChildIssueId, cancellationToken); if (child == null) return Result.Failure("Child issue not found"); // Set parent (domain logic validates) var result = child.SetParent(parent); if (!result.IsSuccess) return Result.Failure(result.Error); // Save changes await _issueRepository.UpdateAsync(child, cancellationToken); await _issueRepository.UnitOfWork.SaveChangesAsync(cancellationToken); // Return updated child issue var dto = _mapper.Map(child); return Result.Success(dto); } } ``` **2. RemoveChildIssueCommand.cs** ```csharp // File: colaflow-api/src/Modules/IssueManagement/Application/Commands/RemoveChildIssueCommand.cs public record RemoveChildIssueCommand( Guid IssueId ) : IRequest; public class RemoveChildIssueCommandHandler : IRequestHandler { private readonly IIssueRepository _issueRepository; private readonly ITenantContextAccessor _tenantContext; public RemoveChildIssueCommandHandler( IIssueRepository issueRepository, ITenantContextAccessor tenantContext) { _issueRepository = issueRepository; _tenantContext = tenantContext; } public async Task Handle( RemoveChildIssueCommand request, CancellationToken cancellationToken) { var tenantId = _tenantContext.GetTenantId(); // Get issue (with tenant filter) var issue = await _issueRepository.GetByIdAsync( request.IssueId, cancellationToken); if (issue == null) return Result.Failure("Issue not found"); // Remove parent var result = issue.RemoveParent(); if (!result.IsSuccess) return result; // Save changes await _issueRepository.UpdateAsync(issue, cancellationToken); await _issueRepository.UnitOfWork.SaveChangesAsync(cancellationToken); return Result.Success(); } } ``` **Checklist**: - [ ] Create `AddChildIssueCommand.cs` + handler - [ ] Create `RemoveChildIssueCommand.cs` + handler - [ ] Add tenant context validation - [ ] Add authorization checks (future: role-based) - [ ] Add FluentValidation rules - [ ] Test command handlers (unit tests) **Acceptance Criteria**: - Commands follow CQRS pattern - Tenant isolation enforced in handlers - Domain validation errors propagated correctly - Unit tests passing --- #### Task 2.2: Create Queries (1-1.5h) **Owner**: Backend Developer **Files to Create**: **1. GetIssueHierarchyQuery.cs** ```csharp // File: colaflow-api/src/Modules/IssueManagement/Application/Queries/GetIssueHierarchyQuery.cs public record GetIssueHierarchyQuery( Guid IssueId ) : IRequest>; public class GetIssueHierarchyQueryHandler : IRequestHandler> { private readonly IIssueRepository _issueRepository; private readonly ITenantContextAccessor _tenantContext; private readonly IMapper _mapper; public GetIssueHierarchyQueryHandler( IIssueRepository issueRepository, ITenantContextAccessor tenantContext, IMapper mapper) { _issueRepository = issueRepository; _tenantContext = tenantContext; _mapper = mapper; } public async Task> Handle( GetIssueHierarchyQuery request, CancellationToken cancellationToken) { var tenantId = _tenantContext.GetTenantId(); // Get issue with full hierarchy (using CTE for performance) var hierarchy = await _issueRepository.GetHierarchyAsync( request.IssueId, cancellationToken); if (hierarchy == null) return Result.Failure("Issue not found"); // Map to DTO var dto = _mapper.Map(hierarchy); return Result.Success(dto); } } // DTO public class IssueHierarchyDto { public Guid Id { get; set; } public string Title { get; set; } public IssueType Type { get; set; } public IssueStatus Status { get; set; } public int Depth { get; set; } public List Children { get; set; } = new(); } ``` **2. GetChildIssuesQuery.cs** ```csharp // File: colaflow-api/src/Modules/IssueManagement/Application/Queries/GetChildIssuesQuery.cs public record GetChildIssuesQuery( Guid ParentIssueId ) : IRequest>>; public class GetChildIssuesQueryHandler : IRequestHandler>> { private readonly IIssueRepository _issueRepository; private readonly ITenantContextAccessor _tenantContext; private readonly IMapper _mapper; public GetChildIssuesQueryHandler( IIssueRepository issueRepository, ITenantContextAccessor tenantContext, IMapper mapper) { _issueRepository = issueRepository; _tenantContext = tenantContext; _mapper = mapper; } public async Task>> Handle( GetChildIssuesQuery request, CancellationToken cancellationToken) { var tenantId = _tenantContext.GetTenantId(); // Get children (single level) var children = await _issueRepository.GetChildrenAsync( request.ParentIssueId, cancellationToken); // Map to DTOs var dtos = _mapper.Map>(children); return Result>.Success(dtos); } } ``` **Checklist**: - [ ] Create `GetIssueHierarchyQuery.cs` + handler - [ ] Create `GetChildIssuesQuery.cs` + handler - [ ] Implement repository methods (GetHierarchyAsync, GetChildrenAsync) - [ ] Use PostgreSQL CTE for recursive queries - [ ] Add query performance tests (< 50ms for 100+ issues) - [ ] Test tenant isolation in queries **Acceptance Criteria**: - Queries return correct hierarchy data - Performance < 50ms for 100+ issues - Tenant isolation enforced - No N+1 query problems --- ### Afternoon Session (3-4 hours): API Endpoints #### Task 2.3: Add API Endpoints (1.5-2h) **Owner**: Backend Developer **File to Update**: ```csharp // File: colaflow-api/src/Modules/IssueManagement/Api/Controllers/IssuesController.cs [ApiController] [Route("api/[controller]")] [Authorize] public class IssuesController : ControllerBase { private readonly IMediator _mediator; public IssuesController(IMediator mediator) { _mediator = mediator; } // Existing endpoints... /// /// Add a child issue to a parent issue /// [HttpPost("{parentId}/add-child")] [ProducesResponseType(typeof(IssueDto), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task AddChildIssue( [FromRoute] Guid parentId, [FromBody] AddChildIssueRequest request) { var command = new AddChildIssueCommand(parentId, request.ChildIssueId); var result = await _mediator.Send(command); if (!result.IsSuccess) return BadRequest(result.Error); return Ok(result.Value); } /// /// Remove parent from an issue /// [HttpDelete("{issueId}/remove-parent")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task RemoveParent( [FromRoute] Guid issueId) { var command = new RemoveChildIssueCommand(issueId); var result = await _mediator.Send(command); if (!result.IsSuccess) return BadRequest(result.Error); return NoContent(); } /// /// Get issue hierarchy (full tree) /// [HttpGet("{issueId}/hierarchy")] [ProducesResponseType(typeof(IssueHierarchyDto), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task GetHierarchy( [FromRoute] Guid issueId) { var query = new GetIssueHierarchyQuery(issueId); var result = await _mediator.Send(query); if (!result.IsSuccess) return NotFound(result.Error); return Ok(result.Value); } /// /// Get direct children of an issue /// [HttpGet("{issueId}/children")] [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] public async Task GetChildren( [FromRoute] Guid issueId) { var query = new GetChildIssuesQuery(issueId); var result = await _mediator.Send(query); return Ok(result.Value); } } // Request DTO public class AddChildIssueRequest { public Guid ChildIssueId { get; set; } } ``` **Checklist**: - [ ] Add `POST /api/issues/{parentId}/add-child` endpoint - [ ] Add `DELETE /api/issues/{issueId}/remove-parent` endpoint - [ ] Add `GET /api/issues/{issueId}/hierarchy` endpoint - [ ] Add `GET /api/issues/{issueId}/children` endpoint - [ ] Add Swagger documentation (XML comments) - [ ] Add authorization attributes - [ ] Test endpoints with Postman/cURL **Acceptance Criteria**: - All 4 new endpoints working - Swagger documentation complete - Authorization enforced - Proper HTTP status codes (200, 400, 404) --- #### Task 2.4: Implement Repository Methods (1-1.5h) **Owner**: Backend Developer **File to Update**: ```csharp // File: colaflow-api/src/Modules/IssueManagement/Infrastructure/Persistence/Repositories/IssueRepository.cs public class IssueRepository : IIssueRepository { private readonly IssueManagementDbContext _context; public IssueRepository(IssueManagementDbContext context) { _context = context; } // Existing methods... public async Task GetHierarchyAsync( Guid issueId, CancellationToken cancellationToken = default) { // Use PostgreSQL CTE for recursive query var sql = @" WITH RECURSIVE hierarchy AS ( -- Base case: Root issue SELECT id, tenant_id, parent_issue_id, title, type, status, priority, 0 AS depth FROM issues WHERE id = @issueId AND tenant_id = @tenantId UNION ALL -- Recursive case: Children SELECT i.id, i.tenant_id, i.parent_issue_id, i.title, i.type, i.status, i.priority, h.depth + 1 FROM issues i INNER JOIN hierarchy h ON i.parent_issue_id = h.id WHERE i.tenant_id = @tenantId AND h.depth < 3 -- Max depth limit ) SELECT * FROM hierarchy ORDER BY depth, title; "; var tenantId = _context.TenantId; // From DbContext var issues = await _context.Issues .FromSqlRaw(sql, new { issueId, tenantId }) .ToListAsync(cancellationToken); if (!issues.Any()) return null; // Build hierarchy tree return BuildHierarchyTree(issues); } public async Task> GetChildrenAsync( Guid parentIssueId, CancellationToken cancellationToken = default) { return await _context.Issues .Where(i => i.ParentIssueId == parentIssueId) .OrderBy(i => i.CreatedAt) .ToListAsync(cancellationToken); } private IssueHierarchy BuildHierarchyTree(List flatList) { var lookup = flatList.ToDictionary(i => i.Id); var root = flatList.First(i => i.Depth == 0); foreach (var issue in flatList.Where(i => i.ParentIssueId.HasValue)) { if (lookup.TryGetValue(issue.ParentIssueId.Value, out var parent)) { parent.ChildIssues.Add(issue); } } return new IssueHierarchy { RootIssue = root, AllIssues = flatList, TotalCount = flatList.Count }; } } // Interface public interface IIssueRepository : IRepository { // Existing methods... Task GetHierarchyAsync(Guid issueId, CancellationToken cancellationToken = default); Task> GetChildrenAsync(Guid parentIssueId, CancellationToken cancellationToken = default); } // Helper class public class IssueHierarchy { public Issue RootIssue { get; set; } public List AllIssues { get; set; } public int TotalCount { get; set; } } ``` **Checklist**: - [ ] Implement `GetHierarchyAsync` using PostgreSQL CTE - [ ] Implement `GetChildrenAsync` method - [ ] Add `BuildHierarchyTree` helper method - [ ] Test query performance (< 50ms) - [ ] Test tenant isolation in raw SQL - [ ] Test with 100+ issues in hierarchy **Acceptance Criteria**: - CTE query returns correct hierarchy - Performance < 50ms for 100+ issues - Tenant filter applied correctly - Max depth limit enforced (3 levels) --- #### Task 2.5: Integration Tests (1.5-2h) **Owner**: Backend Developer + QA **File to Create**: ```csharp // File: colaflow-api/tests/Modules/IssueManagement.IntegrationTests/IssueHierarchyIntegrationTests.cs public class IssueHierarchyIntegrationTests : IClassFixture> { private readonly HttpClient _client; private readonly IServiceScope _scope; public IssueHierarchyIntegrationTests(WebApplicationFactory factory) { _client = factory.CreateClient(); _scope = factory.Services.CreateScope(); } [Fact] public async Task AddChildIssue_ValidHierarchy_ShouldSucceed() { // Arrange var epic = await CreateIssueAsync(IssueType.Epic, "Epic 1"); var story = await CreateIssueAsync(IssueType.Story, "Story 1"); var request = new AddChildIssueRequest { ChildIssueId = story.Id }; // Act var response = await _client.PostAsJsonAsync( $"/api/issues/{epic.Id}/add-child", request); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); var updatedStory = await response.Content.ReadFromJsonAsync(); updatedStory.ParentIssueId.Should().Be(epic.Id); } [Fact] public async Task AddChildIssue_InvalidHierarchy_ShouldReturn400() { // Arrange var task = await CreateIssueAsync(IssueType.Task, "Task 1"); var epic = await CreateIssueAsync(IssueType.Epic, "Epic 1"); var request = new AddChildIssueRequest { ChildIssueId = epic.Id }; // Act - Try to set Task as parent of Epic (invalid) var response = await _client.PostAsJsonAsync( $"/api/issues/{task.Id}/add-child", request); // Assert response.StatusCode.Should().Be(HttpStatusCode.BadRequest); } [Fact] public async Task AddChildIssue_CircularDependency_ShouldReturn400() { // Arrange var epic = await CreateIssueAsync(IssueType.Epic, "Epic 1"); var story = await CreateIssueAsync(IssueType.Story, "Story 1"); await _client.PostAsJsonAsync( $"/api/issues/{epic.Id}/add-child", new { ChildIssueId = story.Id }); // Act - Try to set Epic's parent as Story (circular) var response = await _client.PostAsJsonAsync( $"/api/issues/{story.Id}/add-child", new { ChildIssueId = epic.Id }); // Assert response.StatusCode.Should().Be(HttpStatusCode.BadRequest); } [Fact] public async Task AddChildIssue_CrossTenant_ShouldReturn404() { // Arrange var epicTenant1 = await CreateIssueAsync(IssueType.Epic, "Epic T1", tenantId: Guid.NewGuid()); // Switch to tenant 2 SetTenantContext(Guid.NewGuid()); var storyTenant2 = await CreateIssueAsync(IssueType.Story, "Story T2"); // Act - Try to link across tenants var response = await _client.PostAsJsonAsync( $"/api/issues/{epicTenant1.Id}/add-child", new { ChildIssueId = storyTenant2.Id }); // Assert response.StatusCode.Should().Be(HttpStatusCode.NotFound); // Parent not found in tenant 2 } [Fact] public async Task GetHierarchy_ThreeLevels_ShouldReturnFullTree() { // Arrange var epic = await CreateIssueAsync(IssueType.Epic, "Epic 1"); var story = await CreateIssueAsync(IssueType.Story, "Story 1"); var task = await CreateIssueAsync(IssueType.Task, "Task 1"); await AddChildAsync(epic.Id, story.Id); await AddChildAsync(story.Id, task.Id); // Act var response = await _client.GetAsync($"/api/issues/{epic.Id}/hierarchy"); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); var hierarchy = await response.Content.ReadFromJsonAsync(); hierarchy.Id.Should().Be(epic.Id); hierarchy.Children.Should().HaveCount(1); hierarchy.Children[0].Id.Should().Be(story.Id); hierarchy.Children[0].Children.Should().HaveCount(1); hierarchy.Children[0].Children[0].Id.Should().Be(task.Id); } [Fact] public async Task GetChildren_ParentWithTwoChildren_ShouldReturnBoth() { // Arrange var epic = await CreateIssueAsync(IssueType.Epic, "Epic 1"); var story1 = await CreateIssueAsync(IssueType.Story, "Story 1"); var story2 = await CreateIssueAsync(IssueType.Story, "Story 2"); await AddChildAsync(epic.Id, story1.Id); await AddChildAsync(epic.Id, story2.Id); // Act var response = await _client.GetAsync($"/api/issues/{epic.Id}/children"); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); var children = await response.Content.ReadFromJsonAsync>(); children.Should().HaveCount(2); children.Should().Contain(c => c.Id == story1.Id); children.Should().Contain(c => c.Id == story2.Id); } [Fact] public async Task RemoveParent_HasParent_ShouldSucceed() { // Arrange var epic = await CreateIssueAsync(IssueType.Epic, "Epic 1"); var story = await CreateIssueAsync(IssueType.Story, "Story 1"); await AddChildAsync(epic.Id, story.Id); // Act var response = await _client.DeleteAsync($"/api/issues/{story.Id}/remove-parent"); // Assert response.StatusCode.Should().Be(HttpStatusCode.NoContent); // Verify parent removed var children = await GetChildrenAsync(epic.Id); children.Should().BeEmpty(); } [Fact] public async Task HierarchyQuery_Performance_ShouldBeLessThan50ms() { // Arrange - Create large hierarchy (100+ issues) var epic = await CreateIssueAsync(IssueType.Epic, "Epic 1"); for (int i = 0; i < 10; i++) { var story = await CreateIssueAsync(IssueType.Story, $"Story {i}"); await AddChildAsync(epic.Id, story.Id); for (int j = 0; j < 10; j++) { var task = await CreateIssueAsync(IssueType.Task, $"Task {i}-{j}"); await AddChildAsync(story.Id, task.Id); } } // Act var stopwatch = Stopwatch.StartNew(); var response = await _client.GetAsync($"/api/issues/{epic.Id}/hierarchy"); stopwatch.Stop(); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); stopwatch.ElapsedMilliseconds.Should().BeLessThan(50); // Performance requirement } // Helper methods... private async Task CreateIssueAsync(IssueType type, string title, Guid? tenantId = null) { /* ... */ } private async Task AddChildAsync(Guid parentId, Guid childId) { /* ... */ } private async Task> GetChildrenAsync(Guid parentId) { /* ... */ } private void SetTenantContext(Guid tenantId) { /* ... */ } } ``` **Checklist**: - [ ] Test valid hierarchy (Epic → Story → Task) - [ ] Test invalid hierarchy (Task → Epic) - [ ] Test circular dependency prevention - [ ] Test cross-tenant isolation - [ ] Test GetHierarchy endpoint - [ ] Test GetChildren endpoint - [ ] Test RemoveParent endpoint - [ ] Test performance (< 50ms for 100+ issues) - [ ] All 10+ integration tests passing **Acceptance Criteria**: - All integration tests pass (10+/10+) - Performance tests pass (< 50ms) - Multi-tenant isolation verified - No N+1 query issues --- ## Day 16 Evening: Frontend Integration (Optional, 2-3h) ### Task 3.1: Update Kanban Board (1-1.5h) **Owner**: Frontend Developer **Files to Update**: ```typescript // File: colaflow-frontend/src/components/Kanban/IssueCard.tsx interface IssueCardProps { issue: Issue; onUpdate: (issue: Issue) => void; } export const IssueCard: React.FC = ({ issue, onUpdate }) => { // Existing code... // NEW: Show parent issue breadcrumb const renderParentBreadcrumb = () => { if (!issue.parentIssue) return null; return (
{issue.parentIssue.title}
); }; // NEW: Show child count const renderChildCount = () => { if (issue.childCount === 0) return null; return (
{issue.childCount} subtasks
); }; return (
{renderParentBreadcrumb()}

{issue.title}

{issue.description}

{renderChildCount()} {/* Existing code... */}
); }; ``` **Checklist**: - [ ] Display parent issue breadcrumb on cards - [ ] Display child issue count - [ ] Add "Create Child Issue" button - [ ] Test UI updates with real data **Acceptance Criteria**: - Kanban board shows hierarchy information - UI is responsive and intuitive - No performance degradation --- ## Success Criteria (Day 15-16) ### Functional Requirements - [x] Can create Epic → Story → Task hierarchy - [x] Can add/remove parent-child relationships via API - [x] Can query full hierarchy tree - [x] Hierarchy rules enforced (Epic → Story → Task only) - [x] Circular dependency prevention works - [x] Max depth 3 levels enforced ### Non-Functional Requirements - [x] Query performance < 50ms (100+ issues) - [x] Multi-tenant isolation 100% verified - [x] Backward compatible (no breaking changes) - [x] Integration tests pass rate ≥ 95% (10+/10+) - [x] API response time < 100ms ### Documentation Requirements - [x] API documentation updated (Swagger) - [x] Database schema documented - [x] ADR-035 architecture decision recorded - [x] Implementation notes for future reference --- ## Risk Mitigation ### Risk 1: Performance Issues **Mitigation**: Use PostgreSQL CTE for recursive queries, add index on parent_issue_id ### Risk 2: Multi-Tenant Security **Mitigation**: All queries filtered by TenantId, integration tests verify isolation ### Risk 3: Breaking Changes **Mitigation**: ParentIssueId is nullable, existing queries unaffected ### Risk 4: Circular Dependencies **Mitigation**: Domain logic validates before save, integration tests verify --- ## Testing Checklist ### Unit Tests (Day 15) - [ ] Domain logic: 10+ test cases - [ ] All edge cases covered - [ ] 100% code coverage for hierarchy logic ### Integration Tests (Day 16) - [ ] Valid hierarchy: Epic → Story → Task - [ ] Invalid hierarchy: Task → Epic (rejected) - [ ] Circular dependency (rejected) - [ ] Cross-tenant (rejected) - [ ] GetHierarchy endpoint - [ ] GetChildren endpoint - [ ] RemoveParent endpoint - [ ] Performance: < 50ms for 100+ issues - [ ] Multi-tenant isolation verified ### Manual Testing (Day 16 Evening) - [ ] Postman: All 4 new endpoints work - [ ] Frontend: Kanban shows hierarchy - [ ] Frontend: Create child issue works - [ ] Frontend: Real-time updates work (SignalR) --- ## Delivery Checklist ### Code Artifacts - [ ] Database migration file - [ ] Issue entity updated - [ ] 2 new commands (AddChild, RemoveParent) - [ ] 2 new queries (GetHierarchy, GetChildren) - [ ] 4 new API endpoints - [ ] Repository methods (CTE queries) - [ ] 10+ unit tests - [ ] 10+ integration tests - [ ] Frontend updates (optional) ### Documentation - [ ] API documentation (Swagger) - [ ] ADR-035 architecture decision - [ ] M1_REMAINING_TASKS.md updated - [ ] Database schema documented - [ ] Performance test results ### Git Commits - [ ] Day 15 AM: Database migration + EF Core config - [ ] Day 15 PM: Domain logic + unit tests - [ ] Day 16 AM: Commands + queries - [ ] Day 16 PM: API endpoints + integration tests - [ ] Day 16 Evening: Frontend integration (optional) --- ## Next Steps (Day 17) ### If On Schedule - Start Audit Log System Phase 1 (Day 17-23) - Run full regression test suite - Code review for hierarchy feature ### If Behind Schedule - Focus on P0 features only (skip frontend integration) - Defer performance optimization to Day 17 - Request additional development time --- ## Contact & Escalation **Technical Questions**: Backend Agent, Architect Agent **Requirements Clarification**: Product Manager Agent **Testing Issues**: QA Agent **Progress Updates**: Main Coordinator Agent --- **Document Version**: 1.0 **Last Updated**: 2025-11-04 **Next Review**: End of Day 16 (2025-11-06)