diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 1b58eff..229797d 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -49,7 +49,10 @@ "Bash(docker info:*)", "Bash(docker:*)", "Bash(docker-compose:*)", - "Bash(Start-Sleep -Seconds 30)" + "Bash(Start-Sleep -Seconds 30)", + "Bash(Select-String -Pattern \"error|Build succeeded\")", + "Bash(Select-String -Pattern \"error|warning|succeeded\")", + "Bash(Select-Object -Last 20)" ], "deny": [], "ask": [] diff --git a/PHASE5-TEST-SUMMARY.md b/PHASE5-TEST-SUMMARY.md new file mode 100644 index 0000000..6ccdefa --- /dev/null +++ b/PHASE5-TEST-SUMMARY.md @@ -0,0 +1,160 @@ +# Phase 5: Docker E2E Testing - Executive Summary + +## Status: 🟑 PARTIAL PASS with CRITICAL BLOCKERS + +**Date:** 2025-11-04 +**Full Report:** [DOCKER-E2E-TEST-REPORT.md](./DOCKER-E2E-TEST-REPORT.md) + +--- + +## Quick Status + +| Metric | Result | +|--------|--------| +| Tests Executed | 7 of 10 (70%) | +| Tests Passed | 4 of 7 (57%) | +| Infrastructure | βœ… Functional | +| Application | ❌ Blocked | +| Critical Bugs | 4 P0 issues | +| Time to Fix | ~5 hours | + +--- + +## Critical Issues (P0) + +### πŸ”΄ BUG-001: Database Migrations Not Running +- **Impact:** Schema never created, application unusable +- **Root Cause:** No auto-migration code in Program.cs +- **Fix Time:** 2 hours +- **Fix:** Add migration execution to backend startup + +### πŸ”΄ BUG-002: Demo Data Seeding Fails +- **Impact:** No users, cannot test authentication +- **Root Cause:** Depends on BUG-001 (tables don't exist) +- **Fix Time:** N/A (fixed by BUG-001) + +### πŸ”΄ BUG-003: Placeholder Password Hash +- **Impact:** Login will fail even after migrations run +- **Root Cause:** Seed script has dummy BCrypt hash +- **Fix Time:** 30 minutes +- **Fix:** Generate real hash for `Demo@123456` + +### πŸ”΄ BUG-004: Missing Frontend Health Endpoint +- **Impact:** Container shows "unhealthy" (cosmetic) +- **Root Cause:** `/api/health` route not implemented +- **Fix Time:** 15 minutes +- **Fix:** Create `app/api/health/route.ts` + +--- + +## What Works βœ… + +1. Docker Compose orchestration +2. PostgreSQL + Redis containers +3. Backend API endpoints +4. Swagger documentation +5. Frontend Next.js app +6. Service networking +7. Startup performance (60s) + +--- + +## What's Broken ❌ + +1. Database schema (not created) +2. Demo users (don't exist) +3. Authentication (impossible) +4. Frontend health check (404) +5. PowerShell script (parse error) + +--- + +## Quick Fixes + +### Fix 1: Auto-Migrations (CRITICAL) + +**File:** `colaflow-api/src/ColaFlow.API/Program.cs` + +**Add after line 162:** + +```csharp +// Auto-apply migrations in Development +if (app.Environment.IsDevelopment()) +{ + using var scope = app.Services.CreateScope(); + var identityDb = scope.ServiceProvider.GetRequiredService(); + var projectDb = scope.ServiceProvider.GetRequiredService(); + var issueDb = scope.ServiceProvider.GetRequiredService(); + + await identityDb.Database.MigrateAsync(); + await projectDb.Database.MigrateAsync(); + await issueDb.Database.MigrateAsync(); +} +``` + +### Fix 2: Password Hash (CRITICAL) + +Generate BCrypt hash and update `scripts/seed-data.sql` lines 74, 98. + +### Fix 3: Frontend Health Check + +**Create:** `colaflow-web/app/api/health/route.ts` + +```typescript +import { NextResponse } from 'next/server'; + +export async function GET() { + return NextResponse.json({ status: 'healthy' }, { status: 200 }); +} +``` + +--- + +## Recommendation + +**Status:** πŸ”΄ DO NOT RELEASE to frontend developers yet + +**Required Actions:** +1. Fix automatic migrations (2h) +2. Fix password hashing (30m) +3. Add health endpoint (15m) +4. Update docs (1h) +5. Re-test (1h) + +**Total Time:** ~5 hours + +**Alternative:** Document known issues and proceed with manual migration workaround for Sprint 1. + +--- + +## Test Results + +| Test | Status | Notes | +|------|--------|-------| +| Clean startup | βœ… 🟑 | Containers up, app not initialized | +| API access | βœ… | All endpoints accessible | +| Demo data | ❌ | Blocked by missing schema | +| User login | ❌ | Blocked by missing users | +| Hot reload | ⏭️ | Skipped (app not functional) | +| Script params | ❌ | PowerShell parse error | +| Error handling | ⏭️ | Partially tested | +| Performance | βœ… | 60s startup (good) | +| Documentation | 🟑 | Mostly accurate, some gaps | +| Cross-platform | ⏭️ | Not tested (no Linux/Mac) | + +--- + +## Next Steps + +1. **Backend Team:** Implement auto-migrations (highest priority) +2. **Backend Team:** Fix password hash in seed script +3. **Frontend Team:** Add health check endpoint +4. **PM/QA:** Update documentation +5. **QA:** Re-run full test suite after fixes + +**ETA to Production-Ready:** 1 developer day + +--- + +**Report By:** QA Agent +**Full Report:** [DOCKER-E2E-TEST-REPORT.md](./DOCKER-E2E-TEST-REPORT.md) diff --git a/colaflow-api/src/ColaFlow.API/Controllers/SprintsController.cs b/colaflow-api/src/ColaFlow.API/Controllers/SprintsController.cs new file mode 100644 index 0000000..dab400f --- /dev/null +++ b/colaflow-api/src/ColaFlow.API/Controllers/SprintsController.cs @@ -0,0 +1,137 @@ +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using ColaFlow.Modules.ProjectManagement.Application.Commands.CreateSprint; +using ColaFlow.Modules.ProjectManagement.Application.Commands.UpdateSprint; +using ColaFlow.Modules.ProjectManagement.Application.Commands.DeleteSprint; +using ColaFlow.Modules.ProjectManagement.Application.Commands.StartSprint; +using ColaFlow.Modules.ProjectManagement.Application.Commands.CompleteSprint; +using ColaFlow.Modules.ProjectManagement.Application.Commands.AddTaskToSprint; +using ColaFlow.Modules.ProjectManagement.Application.Commands.RemoveTaskFromSprint; +using ColaFlow.Modules.ProjectManagement.Application.Queries.GetSprintById; +using ColaFlow.Modules.ProjectManagement.Application.Queries.GetSprintsByProjectId; +using ColaFlow.Modules.ProjectManagement.Application.Queries.GetActiveSprints; +using ColaFlow.Modules.ProjectManagement.Application.DTOs; + +namespace ColaFlow.API.Controllers; + +/// +/// Sprint management endpoints +/// +[ApiController] +[Route("api/v1/sprints")] +[Authorize] +public class SprintsController : ControllerBase +{ + private readonly IMediator _mediator; + + public SprintsController(IMediator mediator) + { + _mediator = mediator; + } + + /// + /// Create a new sprint + /// + [HttpPost] + public async Task> Create([FromBody] CreateSprintCommand command) + { + var result = await _mediator.Send(command); + return CreatedAtAction(nameof(GetById), new { id = result.Id }, result); + } + + /// + /// Update an existing sprint + /// + [HttpPut("{id}")] + public async Task Update(Guid id, [FromBody] UpdateSprintCommand command) + { + if (id != command.SprintId) + return BadRequest("Sprint ID mismatch"); + + await _mediator.Send(command); + return NoContent(); + } + + /// + /// Delete a sprint + /// + [HttpDelete("{id}")] + public async Task Delete(Guid id) + { + await _mediator.Send(new DeleteSprintCommand(id)); + return NoContent(); + } + + /// + /// Get sprint by ID + /// + [HttpGet("{id}")] + public async Task> GetById(Guid id) + { + var result = await _mediator.Send(new GetSprintByIdQuery(id)); + if (result == null) + return NotFound(); + return Ok(result); + } + + /// + /// Get all sprints for a project + /// + [HttpGet] + public async Task>> GetByProject([FromQuery] Guid projectId) + { + var result = await _mediator.Send(new GetSprintsByProjectIdQuery(projectId)); + return Ok(result); + } + + /// + /// Get all active sprints + /// + [HttpGet("active")] + public async Task>> GetActive() + { + var result = await _mediator.Send(new GetActiveSprintsQuery()); + return Ok(result); + } + + /// + /// Start a sprint (Planned to Active) + /// + [HttpPost("{id}/start")] + public async Task Start(Guid id) + { + await _mediator.Send(new StartSprintCommand(id)); + return NoContent(); + } + + /// + /// Complete a sprint (Active to Completed) + /// + [HttpPost("{id}/complete")] + public async Task Complete(Guid id) + { + await _mediator.Send(new CompleteSprintCommand(id)); + return NoContent(); + } + + /// + /// Add a task to a sprint + /// + [HttpPost("{id}/tasks/{taskId}")] + public async Task AddTask(Guid id, Guid taskId) + { + await _mediator.Send(new AddTaskToSprintCommand(id, taskId)); + return NoContent(); + } + + /// + /// Remove a task from a sprint + /// + [HttpDelete("{id}/tasks/{taskId}")] + public async Task RemoveTask(Guid id, Guid taskId) + { + await _mediator.Send(new RemoveTaskFromSprintCommand(id, taskId)); + return NoContent(); + } +} diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/ColaFlow.Modules.ProjectManagement.Application.csproj b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/ColaFlow.Modules.ProjectManagement.Application.csproj index dd69e17..fac8a86 100644 --- a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/ColaFlow.Modules.ProjectManagement.Application.csproj +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/ColaFlow.Modules.ProjectManagement.Application.csproj @@ -10,6 +10,7 @@ + diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/AddTaskToSprint/AddTaskToSprintCommand.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/AddTaskToSprint/AddTaskToSprintCommand.cs new file mode 100644 index 0000000..4933572 --- /dev/null +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/AddTaskToSprint/AddTaskToSprintCommand.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace ColaFlow.Modules.ProjectManagement.Application.Commands.AddTaskToSprint; + +/// +/// Command to add a task to a sprint +/// +public sealed record AddTaskToSprintCommand(Guid SprintId, Guid TaskId) : IRequest; diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/AddTaskToSprint/AddTaskToSprintCommandHandler.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/AddTaskToSprint/AddTaskToSprintCommandHandler.cs new file mode 100644 index 0000000..8768a26 --- /dev/null +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/AddTaskToSprint/AddTaskToSprintCommandHandler.cs @@ -0,0 +1,47 @@ +using MediatR; +using Microsoft.EntityFrameworkCore; +using ColaFlow.Modules.ProjectManagement.Application.Common.Interfaces; +using ColaFlow.Modules.ProjectManagement.Domain.Repositories; +using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects; +using ColaFlow.Modules.ProjectManagement.Domain.Exceptions; + +namespace ColaFlow.Modules.ProjectManagement.Application.Commands.AddTaskToSprint; + +/// +/// Handler for AddTaskToSprintCommand +/// +public sealed class AddTaskToSprintCommandHandler( + IApplicationDbContext context, + IUnitOfWork unitOfWork) + : IRequestHandler +{ + private readonly IApplicationDbContext _context = context ?? throw new ArgumentNullException(nameof(context)); + private readonly IUnitOfWork _unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork)); + + public async Task Handle(AddTaskToSprintCommand request, CancellationToken cancellationToken) + { + // Get sprint with tracking + var sprintId = SprintId.From(request.SprintId); + var sprint = await _context.Sprints + .FirstOrDefaultAsync(s => s.Id == sprintId, cancellationToken); + + if (sprint == null) + throw new NotFoundException("Sprint", request.SprintId); + + // Verify task exists + var taskId = TaskId.From(request.TaskId); + var taskExists = await _context.Tasks + .AnyAsync(t => t.Id == taskId, cancellationToken); + + if (!taskExists) + throw new NotFoundException("Task", request.TaskId); + + // Add task to sprint + sprint.AddTask(taskId); + + // Save changes + await _unitOfWork.SaveChangesAsync(cancellationToken); + + return Unit.Value; + } +} diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/CompleteSprint/CompleteSprintCommand.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/CompleteSprint/CompleteSprintCommand.cs new file mode 100644 index 0000000..cbe57fb --- /dev/null +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/CompleteSprint/CompleteSprintCommand.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace ColaFlow.Modules.ProjectManagement.Application.Commands.CompleteSprint; + +/// +/// Command to complete a Sprint (Active β†’ Completed) +/// +public sealed record CompleteSprintCommand(Guid SprintId) : IRequest; diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/CompleteSprint/CompleteSprintCommandHandler.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/CompleteSprint/CompleteSprintCommandHandler.cs new file mode 100644 index 0000000..8d49853 --- /dev/null +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/CompleteSprint/CompleteSprintCommandHandler.cs @@ -0,0 +1,39 @@ +using MediatR; +using Microsoft.EntityFrameworkCore; +using ColaFlow.Modules.ProjectManagement.Application.Common.Interfaces; +using ColaFlow.Modules.ProjectManagement.Domain.Repositories; +using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects; +using ColaFlow.Modules.ProjectManagement.Domain.Exceptions; + +namespace ColaFlow.Modules.ProjectManagement.Application.Commands.CompleteSprint; + +/// +/// Handler for CompleteSprintCommand +/// +public sealed class CompleteSprintCommandHandler( + IApplicationDbContext context, + IUnitOfWork unitOfWork) + : IRequestHandler +{ + private readonly IApplicationDbContext _context = context ?? throw new ArgumentNullException(nameof(context)); + private readonly IUnitOfWork _unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork)); + + public async Task Handle(CompleteSprintCommand request, CancellationToken cancellationToken) + { + // Get sprint with tracking + var sprintId = SprintId.From(request.SprintId); + var sprint = await _context.Sprints + .FirstOrDefaultAsync(s => s.Id == sprintId, cancellationToken); + + if (sprint == null) + throw new NotFoundException("Sprint", request.SprintId); + + // Complete sprint (business rules enforced in domain) + sprint.Complete(); + + // Save changes + await _unitOfWork.SaveChangesAsync(cancellationToken); + + return Unit.Value; + } +} diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/CreateSprint/CreateSprintCommand.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/CreateSprint/CreateSprintCommand.cs new file mode 100644 index 0000000..41af891 --- /dev/null +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/CreateSprint/CreateSprintCommand.cs @@ -0,0 +1,17 @@ +using MediatR; +using ColaFlow.Modules.ProjectManagement.Application.DTOs; + +namespace ColaFlow.Modules.ProjectManagement.Application.Commands.CreateSprint; + +/// +/// Command to create a new Sprint +/// +public sealed record CreateSprintCommand : IRequest +{ + public Guid ProjectId { get; init; } + public string Name { get; init; } = string.Empty; + public string? Goal { get; init; } + public DateTime StartDate { get; init; } + public DateTime EndDate { get; init; } + public Guid CreatedBy { get; init; } +} diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/CreateSprint/CreateSprintCommandHandler.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/CreateSprint/CreateSprintCommandHandler.cs new file mode 100644 index 0000000..a4446c4 --- /dev/null +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/CreateSprint/CreateSprintCommandHandler.cs @@ -0,0 +1,76 @@ +using MediatR; +using ColaFlow.Modules.ProjectManagement.Application.DTOs; +using ColaFlow.Modules.ProjectManagement.Application.Common.Interfaces; +using ColaFlow.Modules.ProjectManagement.Domain.Repositories; +using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects; +using ColaFlow.Modules.ProjectManagement.Domain.Exceptions; +using ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate; +using ColaFlow.Modules.ProjectManagement.Domain.Events; + +namespace ColaFlow.Modules.ProjectManagement.Application.Commands.CreateSprint; + +/// +/// Handler for CreateSprintCommand +/// +public sealed class CreateSprintCommandHandler( + IProjectRepository projectRepository, + IApplicationDbContext context, + IUnitOfWork unitOfWork, + IMediator mediator) + : IRequestHandler +{ + private readonly IProjectRepository _projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository)); + private readonly IApplicationDbContext _context = context ?? throw new ArgumentNullException(nameof(context)); + private readonly IUnitOfWork _unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork)); + private readonly IMediator _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); + + public async Task Handle(CreateSprintCommand request, CancellationToken cancellationToken) + { + // Verify project exists (Global Query Filter ensures tenant isolation) + var projectId = ProjectId.From(request.ProjectId); + var project = await _projectRepository.GetByIdAsync(projectId, cancellationToken); + + if (project == null) + throw new NotFoundException("Project", request.ProjectId); + + // Create sprint + var createdById = UserId.From(request.CreatedBy); + var sprint = Sprint.Create( + project.TenantId, + projectId, + request.Name, + request.Goal, + request.StartDate, + request.EndDate, + createdById + ); + + // Add to context + await _context.Sprints.AddAsync(sprint, cancellationToken); + await _unitOfWork.SaveChangesAsync(cancellationToken); + + // Publish domain event + await _mediator.Publish(new SprintCreatedEvent(sprint.Id.Value, sprint.Name, projectId.Value), cancellationToken); + + // Map to DTO + return new SprintDto + { + Id = sprint.Id.Value, + ProjectId = sprint.ProjectId.Value, + ProjectName = project.Name, + Name = sprint.Name, + Goal = sprint.Goal, + StartDate = sprint.StartDate, + EndDate = sprint.EndDate, + Status = sprint.Status.Name, + TotalTasks = 0, + CompletedTasks = 0, + TotalStoryPoints = 0, + RemainingStoryPoints = 0, + TaskIds = new List(), + CreatedAt = sprint.CreatedAt, + CreatedBy = sprint.CreatedBy.Value, + UpdatedAt = sprint.UpdatedAt + }; + } +} diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/CreateSprint/CreateSprintCommandValidator.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/CreateSprint/CreateSprintCommandValidator.cs new file mode 100644 index 0000000..5a38ccb --- /dev/null +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/CreateSprint/CreateSprintCommandValidator.cs @@ -0,0 +1,38 @@ +using FluentValidation; + +namespace ColaFlow.Modules.ProjectManagement.Application.Commands.CreateSprint; + +/// +/// Validator for CreateSprintCommand +/// +public sealed class CreateSprintCommandValidator : AbstractValidator +{ + public CreateSprintCommandValidator() + { + RuleFor(x => x.ProjectId) + .NotEmpty().WithMessage("ProjectId is required"); + + RuleFor(x => x.Name) + .NotEmpty().WithMessage("Name is required") + .MaximumLength(200).WithMessage("Name must not exceed 200 characters"); + + RuleFor(x => x.Goal) + .MaximumLength(1000).WithMessage("Goal must not exceed 1000 characters"); + + RuleFor(x => x.StartDate) + .NotEmpty().WithMessage("StartDate is required"); + + RuleFor(x => x.EndDate) + .NotEmpty().WithMessage("EndDate is required") + .GreaterThan(x => x.StartDate).WithMessage("EndDate must be after StartDate"); + + RuleFor(x => x) + .Must(x => (x.EndDate - x.StartDate).TotalDays >= 1) + .WithMessage("Sprint duration must be at least 1 day") + .Must(x => (x.EndDate - x.StartDate).TotalDays <= 30) + .WithMessage("Sprint duration cannot exceed 30 days"); + + RuleFor(x => x.CreatedBy) + .NotEmpty().WithMessage("CreatedBy is required"); + } +} diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/DeleteSprint/DeleteSprintCommand.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/DeleteSprint/DeleteSprintCommand.cs new file mode 100644 index 0000000..f88a12f --- /dev/null +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/DeleteSprint/DeleteSprintCommand.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace ColaFlow.Modules.ProjectManagement.Application.Commands.DeleteSprint; + +/// +/// Command to delete a Sprint +/// +public sealed record DeleteSprintCommand(Guid SprintId) : IRequest; diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/DeleteSprint/DeleteSprintCommandHandler.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/DeleteSprint/DeleteSprintCommandHandler.cs new file mode 100644 index 0000000..0fb2799 --- /dev/null +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/DeleteSprint/DeleteSprintCommandHandler.cs @@ -0,0 +1,44 @@ +using MediatR; +using Microsoft.EntityFrameworkCore; +using ColaFlow.Modules.ProjectManagement.Application.Common.Interfaces; +using ColaFlow.Modules.ProjectManagement.Domain.Repositories; +using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects; +using ColaFlow.Modules.ProjectManagement.Domain.Exceptions; +using ColaFlow.Modules.ProjectManagement.Domain.Enums; + +namespace ColaFlow.Modules.ProjectManagement.Application.Commands.DeleteSprint; + +/// +/// Handler for DeleteSprintCommand +/// +public sealed class DeleteSprintCommandHandler( + IApplicationDbContext context, + IUnitOfWork unitOfWork) + : IRequestHandler +{ + private readonly IApplicationDbContext _context = context ?? throw new ArgumentNullException(nameof(context)); + private readonly IUnitOfWork _unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork)); + + public async Task Handle(DeleteSprintCommand request, CancellationToken cancellationToken) + { + // Get sprint with tracking + var sprintId = SprintId.From(request.SprintId); + var sprint = await _context.Sprints + .FirstOrDefaultAsync(s => s.Id == sprintId, cancellationToken); + + if (sprint == null) + throw new NotFoundException("Sprint", request.SprintId); + + // Business rule: Cannot delete Active sprints + if (sprint.Status.Name == SprintStatus.Active.Name) + throw new DomainException("Cannot delete an active sprint. Please complete it first."); + + // Remove sprint + _context.Sprints.Remove(sprint); + + // Save changes + await _unitOfWork.SaveChangesAsync(cancellationToken); + + return Unit.Value; + } +} diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/RemoveTaskFromSprint/RemoveTaskFromSprintCommand.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/RemoveTaskFromSprint/RemoveTaskFromSprintCommand.cs new file mode 100644 index 0000000..727d86e --- /dev/null +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/RemoveTaskFromSprint/RemoveTaskFromSprintCommand.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace ColaFlow.Modules.ProjectManagement.Application.Commands.RemoveTaskFromSprint; + +/// +/// Command to remove a task from a sprint +/// +public sealed record RemoveTaskFromSprintCommand(Guid SprintId, Guid TaskId) : IRequest; diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/RemoveTaskFromSprint/RemoveTaskFromSprintCommandHandler.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/RemoveTaskFromSprint/RemoveTaskFromSprintCommandHandler.cs new file mode 100644 index 0000000..33bf872 --- /dev/null +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/RemoveTaskFromSprint/RemoveTaskFromSprintCommandHandler.cs @@ -0,0 +1,40 @@ +using MediatR; +using Microsoft.EntityFrameworkCore; +using ColaFlow.Modules.ProjectManagement.Application.Common.Interfaces; +using ColaFlow.Modules.ProjectManagement.Domain.Repositories; +using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects; +using ColaFlow.Modules.ProjectManagement.Domain.Exceptions; + +namespace ColaFlow.Modules.ProjectManagement.Application.Commands.RemoveTaskFromSprint; + +/// +/// Handler for RemoveTaskFromSprintCommand +/// +public sealed class RemoveTaskFromSprintCommandHandler( + IApplicationDbContext context, + IUnitOfWork unitOfWork) + : IRequestHandler +{ + private readonly IApplicationDbContext _context = context ?? throw new ArgumentNullException(nameof(context)); + private readonly IUnitOfWork _unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork)); + + public async Task Handle(RemoveTaskFromSprintCommand request, CancellationToken cancellationToken) + { + // Get sprint with tracking + var sprintId = SprintId.From(request.SprintId); + var sprint = await _context.Sprints + .FirstOrDefaultAsync(s => s.Id == sprintId, cancellationToken); + + if (sprint == null) + throw new NotFoundException("Sprint", request.SprintId); + + // Remove task from sprint + var taskId = TaskId.From(request.TaskId); + sprint.RemoveTask(taskId); + + // Save changes + await _unitOfWork.SaveChangesAsync(cancellationToken); + + return Unit.Value; + } +} diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/StartSprint/StartSprintCommand.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/StartSprint/StartSprintCommand.cs new file mode 100644 index 0000000..328162f --- /dev/null +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/StartSprint/StartSprintCommand.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace ColaFlow.Modules.ProjectManagement.Application.Commands.StartSprint; + +/// +/// Command to start a Sprint (Planned β†’ Active) +/// +public sealed record StartSprintCommand(Guid SprintId) : IRequest; diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/StartSprint/StartSprintCommandHandler.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/StartSprint/StartSprintCommandHandler.cs new file mode 100644 index 0000000..6158e73 --- /dev/null +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/StartSprint/StartSprintCommandHandler.cs @@ -0,0 +1,39 @@ +using MediatR; +using Microsoft.EntityFrameworkCore; +using ColaFlow.Modules.ProjectManagement.Application.Common.Interfaces; +using ColaFlow.Modules.ProjectManagement.Domain.Repositories; +using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects; +using ColaFlow.Modules.ProjectManagement.Domain.Exceptions; + +namespace ColaFlow.Modules.ProjectManagement.Application.Commands.StartSprint; + +/// +/// Handler for StartSprintCommand +/// +public sealed class StartSprintCommandHandler( + IApplicationDbContext context, + IUnitOfWork unitOfWork) + : IRequestHandler +{ + private readonly IApplicationDbContext _context = context ?? throw new ArgumentNullException(nameof(context)); + private readonly IUnitOfWork _unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork)); + + public async Task Handle(StartSprintCommand request, CancellationToken cancellationToken) + { + // Get sprint with tracking + var sprintId = SprintId.From(request.SprintId); + var sprint = await _context.Sprints + .FirstOrDefaultAsync(s => s.Id == sprintId, cancellationToken); + + if (sprint == null) + throw new NotFoundException("Sprint", request.SprintId); + + // Start sprint (business rules enforced in domain) + sprint.Start(); + + // Save changes + await _unitOfWork.SaveChangesAsync(cancellationToken); + + return Unit.Value; + } +} diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/UpdateSprint/UpdateSprintCommand.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/UpdateSprint/UpdateSprintCommand.cs new file mode 100644 index 0000000..4a3a49f --- /dev/null +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/UpdateSprint/UpdateSprintCommand.cs @@ -0,0 +1,15 @@ +using MediatR; + +namespace ColaFlow.Modules.ProjectManagement.Application.Commands.UpdateSprint; + +/// +/// Command to update an existing Sprint +/// +public sealed record UpdateSprintCommand : IRequest +{ + public Guid SprintId { get; init; } + public string Name { get; init; } = string.Empty; + public string? Goal { get; init; } + public DateTime StartDate { get; init; } + public DateTime EndDate { get; init; } +} diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/UpdateSprint/UpdateSprintCommandHandler.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/UpdateSprint/UpdateSprintCommandHandler.cs new file mode 100644 index 0000000..a5bf17a --- /dev/null +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/UpdateSprint/UpdateSprintCommandHandler.cs @@ -0,0 +1,44 @@ +using MediatR; +using Microsoft.EntityFrameworkCore; +using ColaFlow.Modules.ProjectManagement.Application.Common.Interfaces; +using ColaFlow.Modules.ProjectManagement.Domain.Repositories; +using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects; +using ColaFlow.Modules.ProjectManagement.Domain.Exceptions; + +namespace ColaFlow.Modules.ProjectManagement.Application.Commands.UpdateSprint; + +/// +/// Handler for UpdateSprintCommand +/// +public sealed class UpdateSprintCommandHandler( + IApplicationDbContext context, + IUnitOfWork unitOfWork) + : IRequestHandler +{ + private readonly IApplicationDbContext _context = context ?? throw new ArgumentNullException(nameof(context)); + private readonly IUnitOfWork _unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork)); + + public async Task Handle(UpdateSprintCommand request, CancellationToken cancellationToken) + { + // Get sprint with tracking + var sprintId = SprintId.From(request.SprintId); + var sprint = await _context.Sprints + .FirstOrDefaultAsync(s => s.Id == sprintId, cancellationToken); + + if (sprint == null) + throw new NotFoundException("Sprint", request.SprintId); + + // Update sprint details + sprint.UpdateDetails( + request.Name, + request.Goal, + request.StartDate, + request.EndDate + ); + + // Save changes + await _unitOfWork.SaveChangesAsync(cancellationToken); + + return Unit.Value; + } +} diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/UpdateSprint/UpdateSprintCommandValidator.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/UpdateSprint/UpdateSprintCommandValidator.cs new file mode 100644 index 0000000..3238955 --- /dev/null +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/UpdateSprint/UpdateSprintCommandValidator.cs @@ -0,0 +1,38 @@ +using FluentValidation; + +namespace ColaFlow.Modules.ProjectManagement.Application.Commands.UpdateSprint; + +/// +/// Validator for UpdateSprintCommand +/// +public sealed class UpdateSprintCommandValidator : AbstractValidator +{ + public UpdateSprintCommandValidator() + { + RuleFor(x => x.SprintId) + .NotEmpty() + .WithMessage("Sprint ID is required"); + + RuleFor(x => x.Name) + .NotEmpty() + .WithMessage("Sprint name is required") + .MaximumLength(200) + .WithMessage("Sprint name cannot exceed 200 characters"); + + RuleFor(x => x.StartDate) + .NotEmpty() + .WithMessage("Start date is required"); + + RuleFor(x => x.EndDate) + .NotEmpty() + .WithMessage("End date is required") + .GreaterThan(x => x.StartDate) + .WithMessage("End date must be after start date"); + + RuleFor(x => x) + .Must(x => (x.EndDate - x.StartDate).Days <= 30) + .WithMessage("Sprint duration cannot exceed 30 days") + .Must(x => (x.EndDate - x.StartDate).Days >= 1) + .WithMessage("Sprint duration must be at least 1 day"); + } +} diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Common/Interfaces/IApplicationDbContext.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Common/Interfaces/IApplicationDbContext.cs new file mode 100644 index 0000000..976aba9 --- /dev/null +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Common/Interfaces/IApplicationDbContext.cs @@ -0,0 +1,20 @@ +using Microsoft.EntityFrameworkCore; +using ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate; +using ColaFlow.Modules.ProjectManagement.Domain.Entities; + +namespace ColaFlow.Modules.ProjectManagement.Application.Common.Interfaces; + +/// +/// Application database context interface for direct access to DbSets +/// +public interface IApplicationDbContext +{ + DbSet Projects { get; } + DbSet Epics { get; } + DbSet Stories { get; } + DbSet Tasks { get; } + DbSet Sprints { get; } + DbSet AuditLogs { get; } + + Task SaveChangesAsync(CancellationToken cancellationToken = default); +} diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/DTOs/SprintDto.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/DTOs/SprintDto.cs new file mode 100644 index 0000000..adbaa9f --- /dev/null +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/DTOs/SprintDto.cs @@ -0,0 +1,24 @@ +namespace ColaFlow.Modules.ProjectManagement.Application.DTOs; + +/// +/// Sprint Data Transfer Object +/// +public class SprintDto +{ + public Guid Id { get; set; } + public Guid ProjectId { get; set; } + public string ProjectName { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public string? Goal { get; set; } + public DateTime StartDate { get; set; } + public DateTime EndDate { get; set; } + public string Status { get; set; } = string.Empty; + public int TotalTasks { get; set; } + public int CompletedTasks { get; set; } + public int TotalStoryPoints { get; set; } + public int RemainingStoryPoints { get; set; } + public List TaskIds { get; set; } = new(); + public DateTime CreatedAt { get; set; } + public Guid CreatedBy { get; set; } + public DateTime? UpdatedAt { get; set; } +} diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/GetActiveSprints/GetActiveSprintsQuery.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/GetActiveSprints/GetActiveSprintsQuery.cs new file mode 100644 index 0000000..4dc655b --- /dev/null +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/GetActiveSprints/GetActiveSprintsQuery.cs @@ -0,0 +1,9 @@ +using MediatR; +using ColaFlow.Modules.ProjectManagement.Application.DTOs; + +namespace ColaFlow.Modules.ProjectManagement.Application.Queries.GetActiveSprints; + +/// +/// Query to get all active sprints +/// +public sealed record GetActiveSprintsQuery : IRequest>; diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/GetActiveSprints/GetActiveSprintsQueryHandler.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/GetActiveSprints/GetActiveSprintsQueryHandler.cs new file mode 100644 index 0000000..922ab8c --- /dev/null +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/GetActiveSprints/GetActiveSprintsQueryHandler.cs @@ -0,0 +1,39 @@ +using MediatR; +using ColaFlow.Modules.ProjectManagement.Application.DTOs; +using ColaFlow.Modules.ProjectManagement.Domain.Repositories; + +namespace ColaFlow.Modules.ProjectManagement.Application.Queries.GetActiveSprints; + +/// +/// Handler for GetActiveSprintsQuery +/// +public sealed class GetActiveSprintsQueryHandler(IProjectRepository projectRepository) + : IRequestHandler> +{ + private readonly IProjectRepository _projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository)); + + public async Task> Handle(GetActiveSprintsQuery request, CancellationToken cancellationToken) + { + var sprints = await _projectRepository.GetActiveSprintsAsync(cancellationToken); + + return sprints.Select(sprint => new SprintDto + { + Id = sprint.Id.Value, + ProjectId = sprint.ProjectId.Value, + ProjectName = string.Empty, // Could join with project if needed + Name = sprint.Name, + Goal = sprint.Goal, + StartDate = sprint.StartDate, + EndDate = sprint.EndDate, + Status = sprint.Status.Name, + TotalTasks = sprint.TaskIds.Count, + CompletedTasks = 0, // TODO: Calculate from tasks + TotalStoryPoints = 0, // TODO: Calculate from tasks + RemainingStoryPoints = 0, // TODO: Calculate from tasks + TaskIds = sprint.TaskIds.Select(t => t.Value).ToList(), + CreatedAt = sprint.CreatedAt, + CreatedBy = sprint.CreatedBy.Value, + UpdatedAt = sprint.UpdatedAt + }).ToList(); + } +} diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/GetSprintById/GetSprintByIdQuery.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/GetSprintById/GetSprintByIdQuery.cs new file mode 100644 index 0000000..ddb2b3d --- /dev/null +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/GetSprintById/GetSprintByIdQuery.cs @@ -0,0 +1,9 @@ +using MediatR; +using ColaFlow.Modules.ProjectManagement.Application.DTOs; + +namespace ColaFlow.Modules.ProjectManagement.Application.Queries.GetSprintById; + +/// +/// Query to get a sprint by ID +/// +public sealed record GetSprintByIdQuery(Guid SprintId) : IRequest; diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/GetSprintById/GetSprintByIdQueryHandler.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/GetSprintById/GetSprintByIdQueryHandler.cs new file mode 100644 index 0000000..269f621 --- /dev/null +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/GetSprintById/GetSprintByIdQueryHandler.cs @@ -0,0 +1,47 @@ +using MediatR; +using ColaFlow.Modules.ProjectManagement.Application.DTOs; +using ColaFlow.Modules.ProjectManagement.Domain.Repositories; +using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects; + +namespace ColaFlow.Modules.ProjectManagement.Application.Queries.GetSprintById; + +/// +/// Handler for GetSprintByIdQuery +/// +public sealed class GetSprintByIdQueryHandler(IProjectRepository projectRepository) + : IRequestHandler +{ + private readonly IProjectRepository _projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository)); + + public async Task Handle(GetSprintByIdQuery request, CancellationToken cancellationToken) + { + var sprintId = SprintId.From(request.SprintId); + var sprint = await _projectRepository.GetSprintByIdReadOnlyAsync(sprintId, cancellationToken); + + if (sprint == null) + return null; + + // Get project name + var project = await _projectRepository.GetByIdAsync(sprint.ProjectId, cancellationToken); + + return new SprintDto + { + Id = sprint.Id.Value, + ProjectId = sprint.ProjectId.Value, + ProjectName = project?.Name ?? string.Empty, + Name = sprint.Name, + Goal = sprint.Goal, + StartDate = sprint.StartDate, + EndDate = sprint.EndDate, + Status = sprint.Status.Name, + TotalTasks = sprint.TaskIds.Count, + CompletedTasks = 0, // TODO: Calculate from tasks + TotalStoryPoints = 0, // TODO: Calculate from tasks + RemainingStoryPoints = 0, // TODO: Calculate from tasks + TaskIds = sprint.TaskIds.Select(t => t.Value).ToList(), + CreatedAt = sprint.CreatedAt, + CreatedBy = sprint.CreatedBy.Value, + UpdatedAt = sprint.UpdatedAt + }; + } +} diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/GetSprintsByProjectId/GetSprintsByProjectIdQuery.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/GetSprintsByProjectId/GetSprintsByProjectIdQuery.cs new file mode 100644 index 0000000..351d5d9 --- /dev/null +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/GetSprintsByProjectId/GetSprintsByProjectIdQuery.cs @@ -0,0 +1,9 @@ +using MediatR; +using ColaFlow.Modules.ProjectManagement.Application.DTOs; + +namespace ColaFlow.Modules.ProjectManagement.Application.Queries.GetSprintsByProjectId; + +/// +/// Query to get all sprints for a project +/// +public sealed record GetSprintsByProjectIdQuery(Guid ProjectId) : IRequest>; diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/GetSprintsByProjectId/GetSprintsByProjectIdQueryHandler.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/GetSprintsByProjectId/GetSprintsByProjectIdQueryHandler.cs new file mode 100644 index 0000000..0612978 --- /dev/null +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/GetSprintsByProjectId/GetSprintsByProjectIdQueryHandler.cs @@ -0,0 +1,45 @@ +using MediatR; +using ColaFlow.Modules.ProjectManagement.Application.DTOs; +using ColaFlow.Modules.ProjectManagement.Domain.Repositories; +using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects; + +namespace ColaFlow.Modules.ProjectManagement.Application.Queries.GetSprintsByProjectId; + +/// +/// Handler for GetSprintsByProjectIdQuery +/// +public sealed class GetSprintsByProjectIdQueryHandler(IProjectRepository projectRepository) + : IRequestHandler> +{ + private readonly IProjectRepository _projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository)); + + public async Task> Handle(GetSprintsByProjectIdQuery request, CancellationToken cancellationToken) + { + var projectId = ProjectId.From(request.ProjectId); + var sprints = await _projectRepository.GetSprintsByProjectIdAsync(projectId, cancellationToken); + + // Get project name + var project = await _projectRepository.GetByIdAsync(projectId, cancellationToken); + var projectName = project?.Name ?? string.Empty; + + return sprints.Select(sprint => new SprintDto + { + Id = sprint.Id.Value, + ProjectId = sprint.ProjectId.Value, + ProjectName = projectName, + Name = sprint.Name, + Goal = sprint.Goal, + StartDate = sprint.StartDate, + EndDate = sprint.EndDate, + Status = sprint.Status.Name, + TotalTasks = sprint.TaskIds.Count, + CompletedTasks = 0, // TODO: Calculate from tasks + TotalStoryPoints = 0, // TODO: Calculate from tasks + RemainingStoryPoints = 0, // TODO: Calculate from tasks + TaskIds = sprint.TaskIds.Select(t => t.Value).ToList(), + CreatedAt = sprint.CreatedAt, + CreatedBy = sprint.CreatedBy.Value, + UpdatedAt = sprint.UpdatedAt + }).ToList(); + } +} diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Domain/Repositories/IUnitOfWork.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Domain/Repositories/IUnitOfWork.cs index 25dec89..32e3f20 100644 --- a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Domain/Repositories/IUnitOfWork.cs +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Domain/Repositories/IUnitOfWork.cs @@ -12,4 +12,12 @@ public interface IUnitOfWork /// Cancellation token /// The number of entities written to the database Task SaveChangesAsync(CancellationToken cancellationToken = default); + + /// + /// Gets the DbContext for direct access to DbSets + /// Note: Returns object to avoid EF Core dependency in Domain layer + /// Cast to concrete DbContext type in Application layer + /// + /// The DbContext instance + object GetDbContext(); } diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Persistence/PMDbContext.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Persistence/PMDbContext.cs index 01100a9..c80c172 100644 --- a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Persistence/PMDbContext.cs +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Persistence/PMDbContext.cs @@ -1,6 +1,7 @@ using System.Reflection; using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; +using ColaFlow.Modules.ProjectManagement.Application.Common.Interfaces; using ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate; using ColaFlow.Modules.ProjectManagement.Domain.Entities; using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects; @@ -10,7 +11,7 @@ namespace ColaFlow.Modules.ProjectManagement.Infrastructure.Persistence; /// /// Project Management Module DbContext /// -public class PMDbContext : DbContext +public class PMDbContext : DbContext, IApplicationDbContext { private readonly IHttpContextAccessor _httpContextAccessor; diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Persistence/UnitOfWork.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Persistence/UnitOfWork.cs index cff5916..a6f003b 100644 --- a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Persistence/UnitOfWork.cs +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Persistence/UnitOfWork.cs @@ -1,3 +1,4 @@ +using Microsoft.EntityFrameworkCore; using ColaFlow.Modules.ProjectManagement.Domain.Repositories; using ColaFlow.Shared.Kernel.Common; @@ -19,6 +20,11 @@ public class UnitOfWork(PMDbContext context) : IUnitOfWork return await _context.SaveChangesAsync(cancellationToken); } + public object GetDbContext() + { + return _context; + } + private async Task DispatchDomainEventsAsync(CancellationToken cancellationToken) { // Get all entities with domain events diff --git a/colaflow-api/src/Modules/ProjectManagement/ProjectManagementModule.cs b/colaflow-api/src/Modules/ProjectManagement/ProjectManagementModule.cs index 6f53033..45827e4 100644 --- a/colaflow-api/src/Modules/ProjectManagement/ProjectManagementModule.cs +++ b/colaflow-api/src/Modules/ProjectManagement/ProjectManagementModule.cs @@ -41,6 +41,9 @@ public class ProjectManagementModule : IModule .AddInterceptors(auditInterceptor); }); + // Register IApplicationDbContext + services.AddScoped(sp => sp.GetRequiredService()); + // Register repositories services.AddScoped(); services.AddScoped(); diff --git a/docs/reports/DOCKER-VERIFICATION-REPORT-DAY18.md b/docs/reports/DOCKER-VERIFICATION-REPORT-DAY18.md new file mode 100644 index 0000000..a4926fe --- /dev/null +++ b/docs/reports/DOCKER-VERIFICATION-REPORT-DAY18.md @@ -0,0 +1,398 @@ +# Docker Environment Verification Report - Day 18 + +**Date**: 2025-11-05 +**QA Engineer**: QA Agent +**Test Objective**: Verify all P0 Bug fixes and validate Docker environment readiness +**Test Scope**: Complete environment reset, rebuild, and validation + +--- + +## Executive Summary + +### CRITICAL STATUS: BLOCKER FOUND πŸ”΄ + +**Result**: **FAILED - NO GO** + +The verification testing discovered a **CRITICAL P0 BLOCKER** that prevents the Docker environment from building and deploying. While the previously reported bugs (BUG-001, BUG-003, BUG-004) have been addressed in source code, **new compilation errors** were introduced in the Sprint management code, blocking the entire build process. + +### Key Findings + +| Status | Finding | Severity | Impact | +|--------|---------|----------|--------| +| πŸ”΄ FAILED | Backend compilation errors in Sprint code | **P0 - BLOCKER** | Docker build fails completely | +| ⚠️ PARTIAL | BUG-001 fix present in code but not verified (blocked) | P0 | Cannot verify until build succeeds | +| ⚠️ PARTIAL | BUG-003 fix present in seed data (not verified) | P0 | Cannot verify until build succeeds | +| ⚠️ PARTIAL | BUG-004 fix present in frontend (not verified) | P0 | Cannot verify until build succeeds | + +--- + +## NEW BUG REPORT: BUG-005 + +### BUG-005: Backend Compilation Failure - Sprint Command Handlers + +**Severity**: πŸ”΄ **CRITICAL (P0 - BLOCKER)** +**Priority**: **P0 - Fix Immediately** +**Impact**: Docker build fails completely, environment cannot be deployed + +#### Description +The Docker build process fails with compilation errors in the ProjectManagement module Sprint command handlers. This is a **regression** introduced in recent code changes. + +#### Compilation Errors + +**Error 1**: `CreateSprintCommandHandler.cs` Line 47 +``` +error CS1061: 'IUnitOfWork' does not contain a definition for 'GetDbContext' +and no accessible extension method 'GetDbContext' accepting a first argument +of type 'IUnitOfWork' could be found +``` + +**File**: `colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/CreateSprint/CreateSprintCommandHandler.cs` + +**Problematic Code** (Line 47): +```csharp +await _unitOfWork.GetDbContext().Sprints.AddAsync(sprint, cancellationToken); +``` + +**Error 2**: `UpdateSprintCommandHandler.cs` Line 28 +``` +error CS1061: 'Project' does not contain a definition for 'Sprints' +and no accessible extension method 'Sprints' accepting a first argument +of type 'Project' could be found +``` + +**File**: `colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/UpdateSprint/UpdateSprintCommandHandler.cs` + +**Problematic Code** (Line 28): +```csharp +var sprint = project.Sprints.FirstOrDefault(s => s.Id.Value == request.SprintId); +``` + +#### Root Cause Analysis + +**Error 1 Root Cause**: +- `IUnitOfWork` interface does not expose a `GetDbContext()` method +- This violates the **Repository Pattern** and **Unit of Work Pattern** +- The handler should not access the DbContext directly +- **Solution**: Use a repository pattern (e.g., `ISprintRepository`) or add the Sprint entity through the appropriate aggregate root + +**Error 2 Root Cause**: +- The `Project` domain entity does not have a `Sprints` navigation property +- This suggests Sprint is being treated as a child entity of Project aggregate +- However, the current domain model does not include this relationship +- **Solution**: Either: + 1. Add `Sprints` collection to `Project` aggregate (if Sprint is part of Project aggregate) + 2. OR treat Sprint as a separate aggregate root with its own repository + 3. OR use `IProjectRepository.GetProjectWithSprintAsync()` correctly + +#### Impact Assessment + +- **Development**: ❌ Complete blocker - no containers can be built +- **Testing**: ❌ Cannot perform any Docker testing +- **Deployment**: ❌ Environment cannot be deployed +- **Frontend Development**: ❌ Backend API unavailable +- **Sprint Scope**: 🚨 **CRITICAL** - Blocks all M1 Sprint 1 deliverables + +#### Recommended Fix + +**Option 1: Use Sprint Repository (Recommended)** +```csharp +// CreateSprintCommandHandler.cs Line 47 +// Replace: +await _unitOfWork.GetDbContext().Sprints.AddAsync(sprint, cancellationToken); + +// With: +await _sprintRepository.AddAsync(sprint, cancellationToken); +await _unitOfWork.SaveChangesAsync(cancellationToken); +``` + +**Option 2: Fix Domain Model** +- Ensure `Project` aggregate includes `Sprints` collection if Sprint is truly a child entity +- Update `UpdateSprintCommandHandler` to correctly navigate Project β†’ Sprints + +**Immediate Action Required**: +1. Backend team to fix compilation errors IMMEDIATELY +2. Rebuild Docker images +3. Re-run full verification test suite + +--- + +## Test Environment + +### Environment Setup +- **OS**: Windows 11 +- **Docker**: Docker Desktop (latest) +- **Docker Compose**: Version 2.x +- **Test Date**: 2025-11-05 00:12-00:15 UTC+01:00 + +### Test Procedure +1. βœ… Complete environment cleanup: `docker-compose down -v` +2. βœ… Docker system cleanup: `docker system prune -f` (reclaimed 2.8GB) +3. ❌ Docker image rebuild: `docker-compose build --no-cache` **FAILED** + +--- + +## Test 1: Complete Environment Reset and Startup + +### Test Objective +Verify that Docker environment can be completely reset and restarted with automatic migrations. + +### Steps Executed +```powershell +# Step 1: Stop and remove all containers and volumes +docker-compose down -v +# Result: SUCCESS βœ… + +# Step 2: Clean Docker system +docker system prune -f +# Result: SUCCESS βœ… (Reclaimed 2.8GB) + +# Step 3: Rebuild images without cache +docker-compose build --no-cache +# Result: FAILED ❌ - Compilation errors +``` + +### Expected Result +- All services rebuild successfully +- API container includes updated Program.cs with migration code +- No compilation errors + +### Actual Result +❌ **FAILED**: Backend compilation failed with 2 errors in Sprint command handlers + +### Evidence +``` +Build FAILED. + +/src/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/CreateSprint/CreateSprintCommandHandler.cs(47,27): +error CS1061: 'IUnitOfWork' does not contain a definition for 'GetDbContext' ... + +/src/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/UpdateSprint/UpdateSprintCommandHandler.cs(28,30): +error CS1061: 'Project' does not contain a definition for 'Sprints' ... + + 0 Warning(s) + 2 Error(s) + +Time Elapsed 00:00:06.88 +``` + +### Status +πŸ”΄ **BLOCKED**: Cannot proceed with remaining tests until compilation errors are fixed + +--- + +## Tests NOT Executed (Blocked) + +The following tests were planned but could not be executed due to the P0 blocker: + +### Test 2: Database Schema Verification ⚠️ BLOCKED +- **Objective**: Verify all EF Core migrations created database tables correctly +- **Status**: Cannot execute - containers not running + +### Test 3: Demo Data Verification ⚠️ BLOCKED +- **Objective**: Verify seed data including BCrypt password hashes (BUG-003 fix) +- **Status**: Cannot execute - database not initialized + +### Test 4: Container Health Status ⚠️ BLOCKED +- **Objective**: Verify all containers report "healthy" status +- **Status**: Cannot execute - containers not built + +### Test 5: Frontend Health Check Endpoint ⚠️ BLOCKED +- **Objective**: Verify BUG-004 fix (health check endpoint) +- **Status**: Cannot execute - frontend container not running + +### Test 6: User Login Functionality ⚠️ BLOCKED +- **Objective**: Verify BUG-003 fix (login with real BCrypt hash) +- **Status**: Cannot execute - API not available + +### Test 7: Auto-Migration Verification ⚠️ BLOCKED +- **Objective**: Verify BUG-001 fix (automatic database migration) +- **Status**: Cannot execute - API not built + +--- + +## Analysis: Previously Reported Bug Fixes + +### BUG-001: Database Auto-Migration βœ… FIX PRESENT (Not Verified) + +**Status**: Code fix is present in `Program.cs` but NOT verified due to blocker + +**Evidence**: +- File: `colaflow-api/src/ColaFlow.API/Program.cs` (Lines 204-248) +- Migration code added to Program.cs: + ```csharp + if (app.Environment.IsDevelopment()) + { + app.Logger.LogInformation("Running in Development mode, applying database migrations..."); + // ... migration code ... + app.Logger.LogInformation("βœ… Identity module migrations applied successfully"); + app.Logger.LogInformation("βœ… ProjectManagement module migrations applied successfully"); + } + ``` + +**Verification Status**: ⚠️ **Cannot verify** - Docker image not built with this code + +--- + +### BUG-003: Password Hash Fix βœ… FIX PRESENT (Not Verified) + +**Status**: Fix is present in seed data but NOT verified due to blocker + +**Evidence**: +- File: `scripts/seed-data.sql` +- Real BCrypt hashes added (reported by backend team) +- Password: `Demo@123456` + +**Verification Status**: ⚠️ **Cannot verify** - Database not seeded + +--- + +### BUG-004: Frontend Health Check βœ… FIX PRESENT (Not Verified) + +**Status**: Fix is present in frontend code but NOT verified due to blocker + +**Evidence**: +- File: `colaflow-web/app/api/health/route.ts` (reported by frontend team) +- Health check endpoint implemented + +**Verification Status**: ⚠️ **Cannot verify** - Frontend container not running + +--- + +## Quality Gate Assessment + +### Release Criteria Evaluation + +| Criteria | Target | Actual | Status | +|----------|--------|--------|--------| +| P0/P1 Bugs | 0 | **1 NEW P0** + 3 unverified | πŸ”΄ FAIL | +| Test Pass Rate | β‰₯ 95% | 0% (0/7 tests) | πŸ”΄ FAIL | +| Code Coverage | β‰₯ 80% | N/A - Cannot measure | πŸ”΄ FAIL | +| Container Health | All healthy | Cannot verify | πŸ”΄ FAIL | +| Build Success | 100% | **0% (Build fails)** | πŸ”΄ FAIL | + +### Go/No-Go Decision + +**Decision**: πŸ”΄ **NO GO - CRITICAL BLOCKER** + +**Justification**: +1. **P0 BLOCKER**: Backend code does not compile +2. **Zero tests passed**: No verification possible +3. **Regression**: New errors introduced in Sprint code +4. **Impact**: Complete development halt - no Docker environment available + +--- + +## Critical Issues Summary + +### P0 Blockers (Must Fix Immediately) + +1. **BUG-005**: Backend compilation failure in Sprint command handlers + - **Impact**: Complete build failure + - **Owner**: Backend Team + - **ETA**: IMMEDIATE (< 2 hours) + +### P0 Issues (Cannot Verify Until Blocker Fixed) + +2. **BUG-001**: Database auto-migration (fix present, not verified) +3. **BUG-003**: Password hash fix (fix present, not verified) +4. **BUG-004**: Frontend health check (fix present, not verified) + +--- + +## Recommendations + +### Immediate Actions (Next 2 Hours) + +1. **Backend Team**: + - ⚠️ Fix `CreateSprintCommandHandler.cs` Line 47 (GetDbContext issue) + - ⚠️ Fix `UpdateSprintCommandHandler.cs` Line 28 (Sprints navigation property) + - ⚠️ Run `dotnet build` locally to verify compilation + - ⚠️ Commit and push fixes immediately + +2. **QA Team**: + - ⏸️ Wait for backend fixes + - ⏸️ Re-run full verification suite after fixes + - ⏸️ Generate updated verification report + +3. **Coordinator**: + - 🚨 Escalate BUG-005 to highest priority + - 🚨 Block all other work until blocker is resolved + - 🚨 Schedule emergency bug fix session + +### Code Quality Actions + +1. **Add Pre-commit Hooks**: + - Run `dotnet build` before allowing commits + - Prevent compilation errors from reaching main branch + +2. **CI/CD Pipeline**: + - Add automated build checks on pull requests + - Block merge if build fails + +3. **Code Review**: + - Review Sprint command handlers for architectural issues + - Ensure proper use of Repository and Unit of Work patterns + +### Process Improvements + +1. **Build Verification**: Always run `dotnet build` before claiming fix complete +2. **Integration Testing**: Run Docker build as part of CI/CD +3. **Regression Prevention**: Add automated tests for Sprint CRUD operations + +--- + +## Next Steps + +### Step 1: Fix BUG-005 (CRITICAL) +- **Owner**: Backend Team +- **Priority**: P0 - Immediate +- **ETA**: < 2 hours + +### Step 2: Re-run Verification (After Fix) +- **Owner**: QA Team +- **Duration**: 1 hour +- **Scope**: Full 7-test suite + +### Step 3: Generate Final Report +- **Owner**: QA Team +- **Deliverable**: Updated verification report with Go/No-Go decision + +--- + +## Appendix A: Test Environment Details + +### Docker Compose Services +- `postgres`: PostgreSQL 17 database +- `postgres-test`: Test database instance +- `redis`: Redis cache +- `colaflow-api`: Backend API (.NET 9) +- `colaflow-web`: Frontend (Next.js 15) + +### Volumes +- `postgres_data`: Persistent database storage +- `redis_data`: Persistent cache storage + +### Networks +- `colaflow-network`: Internal Docker network + +--- + +## Appendix B: Related Documents + +- Original Bug Reports: (in project history) +- BUG-001 Fix: `colaflow-api/src/ColaFlow.API/Program.cs` +- BUG-003 Fix: `scripts/seed-data.sql` +- BUG-004 Fix: `colaflow-web/app/api/health/route.ts` +- BUG-005 Files: + - `colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/CreateSprint/CreateSprintCommandHandler.cs` + - `colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/UpdateSprint/UpdateSprintCommandHandler.cs` + +--- + +**Report Generated**: 2025-11-05 00:15 UTC+01:00 +**QA Engineer**: QA Agent +**Status**: πŸ”΄ BLOCKER - NO GO + +--- + +**IMMEDIATE ESCALATION REQUIRED**: This report must be reviewed by the Coordinator and Backend Team immediately. All M1 Sprint 1 deliverables are blocked until BUG-005 is resolved.