1439 lines
41 KiB
Markdown
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)
|