Files
ColaFlow/docs/plans/DAY-15-16-IMPLEMENTATION-ROADMAP.md
Yaojia Wang 08b317e789
Some checks failed
Code Coverage / Generate Coverage Report (push) Has been cancelled
Tests / Run Tests (9.0.x) (push) Has been cancelled
Tests / Docker Build Test (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
Add trace files.
2025-11-04 23:28:56 +01:00

1439 lines
41 KiB
Markdown

# 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<Guid?>(
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<Issue>
{
public void Configure(EntityTypeBuilder<Issue> 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<Issue> ChildIssues { get; private set; } = new List<Issue>();
// 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<Guid> { 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<Guid> GetAncestorIds()
{
var ancestors = new List<Guid>();
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<IssueHierarchyChangedEvent>()
.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<Result<IssueDto>>;
public class AddChildIssueCommandHandler : IRequestHandler<AddChildIssueCommand, Result<IssueDto>>
{
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<Result<IssueDto>> 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<IssueDto>.Failure("Parent issue not found");
// Get child issue (with tenant filter)
var child = await _issueRepository.GetByIdAsync(
request.ChildIssueId,
cancellationToken);
if (child == null)
return Result<IssueDto>.Failure("Child issue not found");
// Set parent (domain logic validates)
var result = child.SetParent(parent);
if (!result.IsSuccess)
return Result<IssueDto>.Failure(result.Error);
// Save changes
await _issueRepository.UpdateAsync(child, cancellationToken);
await _issueRepository.UnitOfWork.SaveChangesAsync(cancellationToken);
// Return updated child issue
var dto = _mapper.Map<IssueDto>(child);
return Result<IssueDto>.Success(dto);
}
}
```
**2. RemoveChildIssueCommand.cs**
```csharp
// File: colaflow-api/src/Modules/IssueManagement/Application/Commands/RemoveChildIssueCommand.cs
public record RemoveChildIssueCommand(
Guid IssueId
) : IRequest<Result>;
public class RemoveChildIssueCommandHandler : IRequestHandler<RemoveChildIssueCommand, Result>
{
private readonly IIssueRepository _issueRepository;
private readonly ITenantContextAccessor _tenantContext;
public RemoveChildIssueCommandHandler(
IIssueRepository issueRepository,
ITenantContextAccessor tenantContext)
{
_issueRepository = issueRepository;
_tenantContext = tenantContext;
}
public async Task<Result> 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<Result<IssueHierarchyDto>>;
public class GetIssueHierarchyQueryHandler : IRequestHandler<GetIssueHierarchyQuery, Result<IssueHierarchyDto>>
{
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<Result<IssueHierarchyDto>> 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<IssueHierarchyDto>.Failure("Issue not found");
// Map to DTO
var dto = _mapper.Map<IssueHierarchyDto>(hierarchy);
return Result<IssueHierarchyDto>.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<IssueHierarchyDto> Children { get; set; } = new();
}
```
**2. GetChildIssuesQuery.cs**
```csharp
// File: colaflow-api/src/Modules/IssueManagement/Application/Queries/GetChildIssuesQuery.cs
public record GetChildIssuesQuery(
Guid ParentIssueId
) : IRequest<Result<List<IssueDto>>>;
public class GetChildIssuesQueryHandler : IRequestHandler<GetChildIssuesQuery, Result<List<IssueDto>>>
{
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<Result<List<IssueDto>>> 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<List<IssueDto>>(children);
return Result<List<IssueDto>>.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...
/// <summary>
/// Add a child issue to a parent issue
/// </summary>
[HttpPost("{parentId}/add-child")]
[ProducesResponseType(typeof(IssueDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> 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);
}
/// <summary>
/// Remove parent from an issue
/// </summary>
[HttpDelete("{issueId}/remove-parent")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> RemoveParent(
[FromRoute] Guid issueId)
{
var command = new RemoveChildIssueCommand(issueId);
var result = await _mediator.Send(command);
if (!result.IsSuccess)
return BadRequest(result.Error);
return NoContent();
}
/// <summary>
/// Get issue hierarchy (full tree)
/// </summary>
[HttpGet("{issueId}/hierarchy")]
[ProducesResponseType(typeof(IssueHierarchyDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> 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);
}
/// <summary>
/// Get direct children of an issue
/// </summary>
[HttpGet("{issueId}/children")]
[ProducesResponseType(typeof(List<IssueDto>), StatusCodes.Status200OK)]
public async Task<IActionResult> 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<IssueHierarchy?> 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<List<Issue>> 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<Issue> 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<Issue>
{
// Existing methods...
Task<IssueHierarchy?> GetHierarchyAsync(Guid issueId, CancellationToken cancellationToken = default);
Task<List<Issue>> GetChildrenAsync(Guid parentIssueId, CancellationToken cancellationToken = default);
}
// Helper class
public class IssueHierarchy
{
public Issue RootIssue { get; set; }
public List<Issue> 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<WebApplicationFactory<Program>>
{
private readonly HttpClient _client;
private readonly IServiceScope _scope;
public IssueHierarchyIntegrationTests(WebApplicationFactory<Program> 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<IssueDto>();
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<IssueHierarchyDto>();
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<List<IssueDto>>();
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<IssueDto> CreateIssueAsync(IssueType type, string title, Guid? tenantId = null) { /* ... */ }
private async Task AddChildAsync(Guid parentId, Guid childId) { /* ... */ }
private async Task<List<IssueDto>> 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<IssueCardProps> = ({ issue, onUpdate }) => {
// Existing code...
// NEW: Show parent issue breadcrumb
const renderParentBreadcrumb = () => {
if (!issue.parentIssue) return null;
return (
<div className="text-xs text-gray-500 mb-2">
<Link to={`/issues/${issue.parentIssue.id}`}>
<IconType type={issue.parentIssue.type} />
{issue.parentIssue.title}
</Link>
</div>
);
};
// NEW: Show child count
const renderChildCount = () => {
if (issue.childCount === 0) return null;
return (
<div className="text-xs text-blue-600 mt-2">
<IconChildIssues />
{issue.childCount} subtasks
</div>
);
};
return (
<div className="issue-card">
{renderParentBreadcrumb()}
<h3>{issue.title}</h3>
<p>{issue.description}</p>
{renderChildCount()}
{/* Existing code... */}
</div>
);
};
```
**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)