Project Init
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
30
colaflow-api/src/ColaFlow.API/ColaFlow.API.csproj
Normal file
30
colaflow-api/src/ColaFlow.API/ColaFlow.API.csproj
Normal file
@@ -0,0 +1,30 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.9" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Scalar.AspNetCore" Version="2.9.0" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Modules\ProjectManagement\ColaFlow.Modules.ProjectManagement.Application\ColaFlow.Modules.ProjectManagement.Application.csproj" />
|
||||
<ProjectReference Include="..\Modules\ProjectManagement\ColaFlow.Modules.ProjectManagement.Infrastructure\ColaFlow.Modules.ProjectManagement.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\Shared\ColaFlow.Shared.Kernel\ColaFlow.Shared.Kernel.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MediatR" Version="11.1.0" />
|
||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.10.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
6
colaflow-api/src/ColaFlow.API/ColaFlow.API.http
Normal file
6
colaflow-api/src/ColaFlow.API/ColaFlow.API.http
Normal file
@@ -0,0 +1,6 @@
|
||||
@ColaFlow.API_HostAddress = http://localhost:5167
|
||||
|
||||
GET {{ColaFlow.API_HostAddress}}/weatherforecast/
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
@@ -0,0 +1,62 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.DTOs;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.Commands.CreateProject;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.Queries.GetProjectById;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.Queries.GetProjects;
|
||||
|
||||
namespace ColaFlow.API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Projects API Controller
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/v1/[controller]")]
|
||||
public class ProjectsController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
public ProjectsController(IMediator mediator)
|
||||
{
|
||||
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all projects
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(List<ProjectDto>), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> GetProjects(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = new GetProjectsQuery();
|
||||
var result = await _mediator.Send(query, cancellationToken);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get project by ID
|
||||
/// </summary>
|
||||
[HttpGet("{id:guid}")]
|
||||
[ProducesResponseType(typeof(ProjectDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> GetProject(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = new GetProjectByIdQuery(id);
|
||||
var result = await _mediator.Send(query, cancellationToken);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new project
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(ProjectDto), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<IActionResult> CreateProject(
|
||||
[FromBody] CreateProjectCommand command,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = await _mediator.Send(command, cancellationToken);
|
||||
return CreatedAtAction(nameof(GetProject), new { id = result.Id }, result);
|
||||
}
|
||||
}
|
||||
46
colaflow-api/src/ColaFlow.API/Extensions/ModuleExtensions.cs
Normal file
46
colaflow-api/src/ColaFlow.API/Extensions/ModuleExtensions.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.Behaviors;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.Commands.CreateProject;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
|
||||
using ColaFlow.Modules.ProjectManagement.Infrastructure.Persistence;
|
||||
using ColaFlow.Modules.ProjectManagement.Infrastructure.Repositories;
|
||||
|
||||
namespace ColaFlow.API.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for registering modules
|
||||
/// </summary>
|
||||
public static class ModuleExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Register ProjectManagement Module
|
||||
/// </summary>
|
||||
public static IServiceCollection AddProjectManagementModule(
|
||||
this IServiceCollection services,
|
||||
IConfiguration configuration)
|
||||
{
|
||||
// Register DbContext
|
||||
var connectionString = configuration.GetConnectionString("PMDatabase");
|
||||
services.AddDbContext<PMDbContext>(options =>
|
||||
options.UseNpgsql(connectionString));
|
||||
|
||||
// Register repositories
|
||||
services.AddScoped<IProjectRepository, ProjectRepository>();
|
||||
services.AddScoped<IUnitOfWork, UnitOfWork>();
|
||||
|
||||
// Register MediatR handlers from Application assembly
|
||||
services.AddMediatR(typeof(CreateProjectCommand).Assembly);
|
||||
|
||||
// Register FluentValidation validators
|
||||
services.AddValidatorsFromAssembly(typeof(CreateProjectCommand).Assembly);
|
||||
|
||||
// Register pipeline behaviors
|
||||
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
|
||||
|
||||
Console.WriteLine("[ProjectManagement] Module registered");
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using FluentValidation;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Exceptions;
|
||||
|
||||
namespace ColaFlow.API.Middleware;
|
||||
|
||||
/// <summary>
|
||||
/// Global exception handler middleware
|
||||
/// </summary>
|
||||
public class GlobalExceptionHandlerMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly ILogger<GlobalExceptionHandlerMiddleware> _logger;
|
||||
|
||||
public GlobalExceptionHandlerMiddleware(
|
||||
RequestDelegate next,
|
||||
ILogger<GlobalExceptionHandlerMiddleware> logger)
|
||||
{
|
||||
_next = next ?? throw new ArgumentNullException(nameof(next));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _next(context);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await HandleExceptionAsync(context, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleExceptionAsync(HttpContext context, Exception exception)
|
||||
{
|
||||
context.Response.ContentType = "application/json";
|
||||
|
||||
var (statusCode, response) = exception switch
|
||||
{
|
||||
ValidationException validationEx => (
|
||||
StatusCodes.Status400BadRequest,
|
||||
new
|
||||
{
|
||||
StatusCode = StatusCodes.Status400BadRequest,
|
||||
Message = "Validation failed",
|
||||
Errors = validationEx.Errors.Select(e => new
|
||||
{
|
||||
Property = e.PropertyName,
|
||||
Message = e.ErrorMessage
|
||||
})
|
||||
}),
|
||||
DomainException domainEx => (
|
||||
StatusCodes.Status400BadRequest,
|
||||
new
|
||||
{
|
||||
StatusCode = StatusCodes.Status400BadRequest,
|
||||
Message = domainEx.Message
|
||||
}),
|
||||
NotFoundException notFoundEx => (
|
||||
StatusCodes.Status404NotFound,
|
||||
new
|
||||
{
|
||||
StatusCode = StatusCodes.Status404NotFound,
|
||||
Message = notFoundEx.Message
|
||||
}),
|
||||
_ => (
|
||||
StatusCodes.Status500InternalServerError,
|
||||
new
|
||||
{
|
||||
StatusCode = StatusCodes.Status500InternalServerError,
|
||||
Message = "An internal server error occurred"
|
||||
})
|
||||
};
|
||||
|
||||
context.Response.StatusCode = statusCode;
|
||||
|
||||
// Log with appropriate level
|
||||
if (statusCode >= 500)
|
||||
{
|
||||
_logger.LogError(exception, "Internal server error occurred: {Message}", exception.Message);
|
||||
}
|
||||
else if (statusCode >= 400)
|
||||
{
|
||||
_logger.LogWarning(exception, "Client error occurred: {Message}", exception.Message);
|
||||
}
|
||||
|
||||
var jsonResponse = JsonSerializer.Serialize(response, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
});
|
||||
|
||||
await context.Response.WriteAsync(jsonResponse);
|
||||
}
|
||||
}
|
||||
31
colaflow-api/src/ColaFlow.API/Program.cs
Normal file
31
colaflow-api/src/ColaFlow.API/Program.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using ColaFlow.API.Extensions;
|
||||
using ColaFlow.API.Middleware;
|
||||
using Scalar.AspNetCore;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Register ProjectManagement Module
|
||||
builder.Services.AddProjectManagementModule(builder.Configuration);
|
||||
|
||||
// Add controllers
|
||||
builder.Services.AddControllers();
|
||||
|
||||
// Configure OpenAPI/Scalar
|
||||
builder.Services.AddOpenApi();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Configure the HTTP request pipeline
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.MapOpenApi();
|
||||
app.MapScalarApiReference();
|
||||
}
|
||||
|
||||
// Global exception handler (should be first in pipeline)
|
||||
app.UseMiddleware<GlobalExceptionHandlerMiddleware>();
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
app.MapControllers();
|
||||
|
||||
app.Run();
|
||||
23
colaflow-api/src/ColaFlow.API/Properties/launchSettings.json
Normal file
23
colaflow-api/src/ColaFlow.API/Properties/launchSettings.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:5167",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "https://localhost:7295;http://localhost:5167",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
12
colaflow-api/src/ColaFlow.API/appsettings.Development.json
Normal file
12
colaflow-api/src/ColaFlow.API/appsettings.Development.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"PMDatabase": "Host=localhost;Port=5432;Database=colaflow_pm;Username=colaflow;Password=colaflow_dev_password"
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning",
|
||||
"Microsoft.EntityFrameworkCore": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
12
colaflow-api/src/ColaFlow.API/appsettings.json
Normal file
12
colaflow-api/src/ColaFlow.API/appsettings.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"ConnectionStrings": {
|
||||
"PMDatabase": "Host=localhost;Port=5432;Database=colaflow;Username=colaflow;Password=colaflow_dev_password"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ColaFlow.Domain\ColaFlow.Domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoMapper" Version="12.0.1" />
|
||||
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
|
||||
<PackageReference Include="FluentValidation" Version="12.0.0" />
|
||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.0.0" />
|
||||
<PackageReference Include="MediatR" Version="11.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,90 @@
|
||||
using ColaFlow.Domain.Common;
|
||||
using ColaFlow.Domain.Exceptions;
|
||||
using ColaFlow.Domain.ValueObjects;
|
||||
|
||||
namespace ColaFlow.Domain.Aggregates.ProjectAggregate;
|
||||
|
||||
/// <summary>
|
||||
/// Epic Entity (part of Project aggregate)
|
||||
/// </summary>
|
||||
public class Epic : Entity
|
||||
{
|
||||
public new EpicId Id { get; private set; }
|
||||
public string Name { get; private set; }
|
||||
public string Description { get; private set; }
|
||||
public ProjectId ProjectId { get; private set; }
|
||||
public WorkItemStatus Status { get; private set; }
|
||||
public TaskPriority Priority { get; private set; }
|
||||
|
||||
private readonly List<Story> _stories = new();
|
||||
public IReadOnlyCollection<Story> Stories => _stories.AsReadOnly();
|
||||
|
||||
public DateTime CreatedAt { get; private set; }
|
||||
public UserId CreatedBy { get; private set; }
|
||||
public DateTime? UpdatedAt { get; private set; }
|
||||
|
||||
// EF Core constructor
|
||||
private Epic()
|
||||
{
|
||||
Id = null!;
|
||||
Name = null!;
|
||||
Description = null!;
|
||||
ProjectId = null!;
|
||||
Status = null!;
|
||||
Priority = null!;
|
||||
CreatedBy = null!;
|
||||
}
|
||||
|
||||
public static Epic Create(string name, string description, ProjectId projectId, UserId createdBy)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
throw new DomainException("Epic name cannot be empty");
|
||||
|
||||
if (name.Length > 200)
|
||||
throw new DomainException("Epic name cannot exceed 200 characters");
|
||||
|
||||
return new Epic
|
||||
{
|
||||
Id = EpicId.Create(),
|
||||
Name = name,
|
||||
Description = description ?? string.Empty,
|
||||
ProjectId = projectId,
|
||||
Status = WorkItemStatus.ToDo,
|
||||
Priority = TaskPriority.Medium,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
CreatedBy = createdBy
|
||||
};
|
||||
}
|
||||
|
||||
public Story CreateStory(string title, string description, TaskPriority priority, UserId createdBy)
|
||||
{
|
||||
var story = Story.Create(title, description, this.Id, priority, createdBy);
|
||||
_stories.Add(story);
|
||||
return story;
|
||||
}
|
||||
|
||||
public void UpdateDetails(string name, string description)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
throw new DomainException("Epic name cannot be empty");
|
||||
|
||||
if (name.Length > 200)
|
||||
throw new DomainException("Epic name cannot exceed 200 characters");
|
||||
|
||||
Name = name;
|
||||
Description = description ?? string.Empty;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void UpdateStatus(WorkItemStatus newStatus)
|
||||
{
|
||||
Status = newStatus;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void UpdatePriority(TaskPriority newPriority)
|
||||
{
|
||||
Priority = newPriority;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
using ColaFlow.Domain.Common;
|
||||
using ColaFlow.Domain.Events;
|
||||
using ColaFlow.Domain.Exceptions;
|
||||
using ColaFlow.Domain.ValueObjects;
|
||||
|
||||
namespace ColaFlow.Domain.Aggregates.ProjectAggregate;
|
||||
|
||||
/// <summary>
|
||||
/// Project Aggregate Root
|
||||
/// Enforces consistency boundary for Project -> Epic -> Story -> Task hierarchy
|
||||
/// </summary>
|
||||
public class Project : AggregateRoot
|
||||
{
|
||||
public new ProjectId Id { get; private set; }
|
||||
public string Name { get; private set; }
|
||||
public string Description { get; private set; }
|
||||
public ProjectKey Key { get; private set; }
|
||||
public ProjectStatus Status { get; private set; }
|
||||
public UserId OwnerId { get; private set; }
|
||||
|
||||
private readonly List<Epic> _epics = new();
|
||||
public IReadOnlyCollection<Epic> Epics => _epics.AsReadOnly();
|
||||
|
||||
public DateTime CreatedAt { get; private set; }
|
||||
public DateTime? UpdatedAt { get; private set; }
|
||||
|
||||
// EF Core constructor
|
||||
private Project()
|
||||
{
|
||||
Id = null!;
|
||||
Name = null!;
|
||||
Description = null!;
|
||||
Key = null!;
|
||||
Status = null!;
|
||||
OwnerId = null!;
|
||||
}
|
||||
|
||||
// Factory method
|
||||
public static Project Create(string name, string description, string key, UserId ownerId)
|
||||
{
|
||||
// Validation
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
throw new DomainException("Project name cannot be empty");
|
||||
|
||||
if (name.Length > 200)
|
||||
throw new DomainException("Project name cannot exceed 200 characters");
|
||||
|
||||
var project = new Project
|
||||
{
|
||||
Id = ProjectId.Create(),
|
||||
Name = name,
|
||||
Description = description ?? string.Empty,
|
||||
Key = ProjectKey.Create(key),
|
||||
Status = ProjectStatus.Active,
|
||||
OwnerId = ownerId,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
// Raise domain event
|
||||
project.AddDomainEvent(new ProjectCreatedEvent(project.Id, project.Name, ownerId));
|
||||
|
||||
return project;
|
||||
}
|
||||
|
||||
// Business methods
|
||||
public void UpdateDetails(string name, string description)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
throw new DomainException("Project name cannot be empty");
|
||||
|
||||
if (name.Length > 200)
|
||||
throw new DomainException("Project name cannot exceed 200 characters");
|
||||
|
||||
Name = name;
|
||||
Description = description ?? string.Empty;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
AddDomainEvent(new ProjectUpdatedEvent(Id, Name, Description));
|
||||
}
|
||||
|
||||
public Epic CreateEpic(string name, string description, UserId createdBy)
|
||||
{
|
||||
if (Status == ProjectStatus.Archived)
|
||||
throw new DomainException("Cannot create epic in an archived project");
|
||||
|
||||
var epic = Epic.Create(name, description, this.Id, createdBy);
|
||||
_epics.Add(epic);
|
||||
|
||||
AddDomainEvent(new EpicCreatedEvent(epic.Id, epic.Name, this.Id));
|
||||
|
||||
return epic;
|
||||
}
|
||||
|
||||
public void Archive()
|
||||
{
|
||||
if (Status == ProjectStatus.Archived)
|
||||
throw new DomainException("Project is already archived");
|
||||
|
||||
Status = ProjectStatus.Archived;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
AddDomainEvent(new ProjectArchivedEvent(Id));
|
||||
}
|
||||
|
||||
public void Activate()
|
||||
{
|
||||
if (Status == ProjectStatus.Active)
|
||||
throw new DomainException("Project is already active");
|
||||
|
||||
Status = ProjectStatus.Active;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
using ColaFlow.Domain.Common;
|
||||
using ColaFlow.Domain.Exceptions;
|
||||
using ColaFlow.Domain.ValueObjects;
|
||||
|
||||
namespace ColaFlow.Domain.Aggregates.ProjectAggregate;
|
||||
|
||||
/// <summary>
|
||||
/// Story Entity (part of Project aggregate)
|
||||
/// </summary>
|
||||
public class Story : Entity
|
||||
{
|
||||
public new StoryId Id { get; private set; }
|
||||
public string Title { get; private set; }
|
||||
public string Description { get; private set; }
|
||||
public EpicId EpicId { get; private set; }
|
||||
public WorkItemStatus Status { get; private set; }
|
||||
public TaskPriority Priority { get; private set; }
|
||||
public decimal? EstimatedHours { get; private set; }
|
||||
public decimal? ActualHours { get; private set; }
|
||||
public UserId? AssigneeId { get; private set; }
|
||||
|
||||
private readonly List<WorkTask> _tasks = new();
|
||||
public IReadOnlyCollection<WorkTask> Tasks => _tasks.AsReadOnly();
|
||||
|
||||
public DateTime CreatedAt { get; private set; }
|
||||
public UserId CreatedBy { get; private set; }
|
||||
public DateTime? UpdatedAt { get; private set; }
|
||||
|
||||
// EF Core constructor
|
||||
private Story()
|
||||
{
|
||||
Id = null!;
|
||||
Title = null!;
|
||||
Description = null!;
|
||||
EpicId = null!;
|
||||
Status = null!;
|
||||
Priority = null!;
|
||||
CreatedBy = null!;
|
||||
}
|
||||
|
||||
public static Story Create(string title, string description, EpicId epicId, TaskPriority priority, UserId createdBy)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(title))
|
||||
throw new DomainException("Story title cannot be empty");
|
||||
|
||||
if (title.Length > 200)
|
||||
throw new DomainException("Story title cannot exceed 200 characters");
|
||||
|
||||
return new Story
|
||||
{
|
||||
Id = StoryId.Create(),
|
||||
Title = title,
|
||||
Description = description ?? string.Empty,
|
||||
EpicId = epicId,
|
||||
Status = WorkItemStatus.ToDo,
|
||||
Priority = priority,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
CreatedBy = createdBy
|
||||
};
|
||||
}
|
||||
|
||||
public WorkTask CreateTask(string title, string description, TaskPriority priority, UserId createdBy)
|
||||
{
|
||||
var task = WorkTask.Create(title, description, this.Id, priority, createdBy);
|
||||
_tasks.Add(task);
|
||||
return task;
|
||||
}
|
||||
|
||||
public void UpdateDetails(string title, string description)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(title))
|
||||
throw new DomainException("Story title cannot be empty");
|
||||
|
||||
if (title.Length > 200)
|
||||
throw new DomainException("Story title cannot exceed 200 characters");
|
||||
|
||||
Title = title;
|
||||
Description = description ?? string.Empty;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void UpdateStatus(WorkItemStatus newStatus)
|
||||
{
|
||||
Status = newStatus;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void AssignTo(UserId assigneeId)
|
||||
{
|
||||
AssigneeId = assigneeId;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void UpdateEstimate(decimal hours)
|
||||
{
|
||||
if (hours < 0)
|
||||
throw new DomainException("Estimated hours cannot be negative");
|
||||
|
||||
EstimatedHours = hours;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void LogActualHours(decimal hours)
|
||||
{
|
||||
if (hours < 0)
|
||||
throw new DomainException("Actual hours cannot be negative");
|
||||
|
||||
ActualHours = hours;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
using ColaFlow.Domain.Common;
|
||||
using ColaFlow.Domain.Exceptions;
|
||||
using ColaFlow.Domain.ValueObjects;
|
||||
|
||||
namespace ColaFlow.Domain.Aggregates.ProjectAggregate;
|
||||
|
||||
/// <summary>
|
||||
/// Task Entity (part of Project aggregate)
|
||||
/// Named "WorkTask" to avoid conflict with System.Threading.Tasks.Task
|
||||
/// </summary>
|
||||
public class WorkTask : Entity
|
||||
{
|
||||
public new TaskId Id { get; private set; }
|
||||
public string Title { get; private set; }
|
||||
public string Description { get; private set; }
|
||||
public StoryId StoryId { get; private set; }
|
||||
public WorkItemStatus Status { get; private set; }
|
||||
public TaskPriority Priority { get; private set; }
|
||||
public decimal? EstimatedHours { get; private set; }
|
||||
public decimal? ActualHours { get; private set; }
|
||||
public UserId? AssigneeId { get; private set; }
|
||||
|
||||
public DateTime CreatedAt { get; private set; }
|
||||
public UserId CreatedBy { get; private set; }
|
||||
public DateTime? UpdatedAt { get; private set; }
|
||||
|
||||
// EF Core constructor
|
||||
private WorkTask()
|
||||
{
|
||||
Id = null!;
|
||||
Title = null!;
|
||||
Description = null!;
|
||||
StoryId = null!;
|
||||
Status = null!;
|
||||
Priority = null!;
|
||||
CreatedBy = null!;
|
||||
}
|
||||
|
||||
public static WorkTask Create(string title, string description, StoryId storyId, TaskPriority priority, UserId createdBy)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(title))
|
||||
throw new DomainException("Task title cannot be empty");
|
||||
|
||||
if (title.Length > 200)
|
||||
throw new DomainException("Task title cannot exceed 200 characters");
|
||||
|
||||
return new WorkTask
|
||||
{
|
||||
Id = TaskId.Create(),
|
||||
Title = title,
|
||||
Description = description ?? string.Empty,
|
||||
StoryId = storyId,
|
||||
Status = WorkItemStatus.ToDo,
|
||||
Priority = priority,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
CreatedBy = createdBy
|
||||
};
|
||||
}
|
||||
|
||||
public void UpdateDetails(string title, string description)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(title))
|
||||
throw new DomainException("Task title cannot be empty");
|
||||
|
||||
if (title.Length > 200)
|
||||
throw new DomainException("Task title cannot exceed 200 characters");
|
||||
|
||||
Title = title;
|
||||
Description = description ?? string.Empty;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void UpdateStatus(WorkItemStatus newStatus)
|
||||
{
|
||||
Status = newStatus;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void AssignTo(UserId assigneeId)
|
||||
{
|
||||
AssigneeId = assigneeId;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void UpdatePriority(TaskPriority newPriority)
|
||||
{
|
||||
Priority = newPriority;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void UpdateEstimate(decimal hours)
|
||||
{
|
||||
if (hours < 0)
|
||||
throw new DomainException("Estimated hours cannot be negative");
|
||||
|
||||
EstimatedHours = hours;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void LogActualHours(decimal hours)
|
||||
{
|
||||
if (hours < 0)
|
||||
throw new DomainException("Actual hours cannot be negative");
|
||||
|
||||
ActualHours = hours;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
9
colaflow-api/src/ColaFlow.Domain/ColaFlow.Domain.csproj
Normal file
9
colaflow-api/src/ColaFlow.Domain/ColaFlow.Domain.csproj
Normal file
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
31
colaflow-api/src/ColaFlow.Domain/Common/AggregateRoot.cs
Normal file
31
colaflow-api/src/ColaFlow.Domain/Common/AggregateRoot.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using ColaFlow.Domain.Events;
|
||||
|
||||
namespace ColaFlow.Domain.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for all aggregate roots
|
||||
/// </summary>
|
||||
public abstract class AggregateRoot : Entity
|
||||
{
|
||||
private readonly List<DomainEvent> _domainEvents = new();
|
||||
|
||||
public IReadOnlyCollection<DomainEvent> DomainEvents => _domainEvents.AsReadOnly();
|
||||
|
||||
protected AggregateRoot() : base()
|
||||
{
|
||||
}
|
||||
|
||||
protected AggregateRoot(Guid id) : base(id)
|
||||
{
|
||||
}
|
||||
|
||||
protected void AddDomainEvent(DomainEvent domainEvent)
|
||||
{
|
||||
_domainEvents.Add(domainEvent);
|
||||
}
|
||||
|
||||
public void ClearDomainEvents()
|
||||
{
|
||||
_domainEvents.Clear();
|
||||
}
|
||||
}
|
||||
54
colaflow-api/src/ColaFlow.Domain/Common/Entity.cs
Normal file
54
colaflow-api/src/ColaFlow.Domain/Common/Entity.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
namespace ColaFlow.Domain.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for all entities
|
||||
/// </summary>
|
||||
public abstract class Entity
|
||||
{
|
||||
public Guid Id { get; protected set; }
|
||||
|
||||
protected Entity()
|
||||
{
|
||||
Id = Guid.NewGuid();
|
||||
}
|
||||
|
||||
protected Entity(Guid id)
|
||||
{
|
||||
Id = id;
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj is not Entity other)
|
||||
return false;
|
||||
|
||||
if (ReferenceEquals(this, other))
|
||||
return true;
|
||||
|
||||
if (GetType() != other.GetType())
|
||||
return false;
|
||||
|
||||
return Id == other.Id;
|
||||
}
|
||||
|
||||
public static bool operator ==(Entity? a, Entity? b)
|
||||
{
|
||||
if (a is null && b is null)
|
||||
return true;
|
||||
|
||||
if (a is null || b is null)
|
||||
return false;
|
||||
|
||||
return a.Equals(b);
|
||||
}
|
||||
|
||||
public static bool operator !=(Entity? a, Entity? b)
|
||||
{
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return Id.GetHashCode();
|
||||
}
|
||||
}
|
||||
78
colaflow-api/src/ColaFlow.Domain/Common/Enumeration.cs
Normal file
78
colaflow-api/src/ColaFlow.Domain/Common/Enumeration.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
using System.Reflection;
|
||||
|
||||
namespace ColaFlow.Domain.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for creating type-safe enumerations
|
||||
/// </summary>
|
||||
public abstract class Enumeration : IComparable
|
||||
{
|
||||
public int Id { get; private set; }
|
||||
public string Name { get; private set; }
|
||||
|
||||
protected Enumeration(int id, string name)
|
||||
{
|
||||
Id = id;
|
||||
Name = name;
|
||||
}
|
||||
|
||||
public override string ToString() => Name;
|
||||
|
||||
public static IEnumerable<T> GetAll<T>() where T : Enumeration
|
||||
{
|
||||
var fields = typeof(T).GetFields(BindingFlags.Public |
|
||||
BindingFlags.Static |
|
||||
BindingFlags.DeclaredOnly);
|
||||
|
||||
return fields.Select(f => f.GetValue(null)).Cast<T>();
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj is not Enumeration otherValue)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var typeMatches = GetType().Equals(obj.GetType());
|
||||
var valueMatches = Id.Equals(otherValue.Id);
|
||||
|
||||
return typeMatches && valueMatches;
|
||||
}
|
||||
|
||||
public override int GetHashCode() => Id.GetHashCode();
|
||||
|
||||
public static int AbsoluteDifference(Enumeration firstValue, Enumeration secondValue)
|
||||
{
|
||||
var absoluteDifference = Math.Abs(firstValue.Id - secondValue.Id);
|
||||
return absoluteDifference;
|
||||
}
|
||||
|
||||
public static T FromValue<T>(int value) where T : Enumeration
|
||||
{
|
||||
var matchingItem = Parse<T, int>(value, "value", item => item.Id == value);
|
||||
return matchingItem;
|
||||
}
|
||||
|
||||
public static T FromDisplayName<T>(string displayName) where T : Enumeration
|
||||
{
|
||||
var matchingItem = Parse<T, string>(displayName, "display name", item => item.Name == displayName);
|
||||
return matchingItem;
|
||||
}
|
||||
|
||||
private static T Parse<T, K>(K value, string description, Func<T, bool> predicate) where T : Enumeration
|
||||
{
|
||||
var matchingItem = GetAll<T>().FirstOrDefault(predicate);
|
||||
|
||||
if (matchingItem == null)
|
||||
throw new InvalidOperationException($"'{value}' is not a valid {description} in {typeof(T)}");
|
||||
|
||||
return matchingItem;
|
||||
}
|
||||
|
||||
public int CompareTo(object? other)
|
||||
{
|
||||
if (other == null) return 1;
|
||||
return Id.CompareTo(((Enumeration)other).Id);
|
||||
}
|
||||
}
|
||||
46
colaflow-api/src/ColaFlow.Domain/Common/ValueObject.cs
Normal file
46
colaflow-api/src/ColaFlow.Domain/Common/ValueObject.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
namespace ColaFlow.Domain.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for all value objects
|
||||
/// </summary>
|
||||
public abstract class ValueObject
|
||||
{
|
||||
protected abstract IEnumerable<object> GetAtomicValues();
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj == null || obj.GetType() != GetType())
|
||||
return false;
|
||||
|
||||
var other = (ValueObject)obj;
|
||||
return GetAtomicValues().SequenceEqual(other.GetAtomicValues());
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return GetAtomicValues()
|
||||
.Aggregate(1, (current, obj) =>
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
return (current * 23) + (obj?.GetHashCode() ?? 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static bool operator ==(ValueObject? a, ValueObject? b)
|
||||
{
|
||||
if (a is null && b is null)
|
||||
return true;
|
||||
|
||||
if (a is null || b is null)
|
||||
return false;
|
||||
|
||||
return a.Equals(b);
|
||||
}
|
||||
|
||||
public static bool operator !=(ValueObject? a, ValueObject? b)
|
||||
{
|
||||
return !(a == b);
|
||||
}
|
||||
}
|
||||
10
colaflow-api/src/ColaFlow.Domain/Events/DomainEvent.cs
Normal file
10
colaflow-api/src/ColaFlow.Domain/Events/DomainEvent.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace ColaFlow.Domain.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for all domain events
|
||||
/// </summary>
|
||||
public abstract record DomainEvent
|
||||
{
|
||||
public Guid EventId { get; init; } = Guid.NewGuid();
|
||||
public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
|
||||
}
|
||||
12
colaflow-api/src/ColaFlow.Domain/Events/EpicCreatedEvent.cs
Normal file
12
colaflow-api/src/ColaFlow.Domain/Events/EpicCreatedEvent.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using ColaFlow.Domain.ValueObjects;
|
||||
|
||||
namespace ColaFlow.Domain.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Event raised when an epic is created
|
||||
/// </summary>
|
||||
public sealed record EpicCreatedEvent(
|
||||
EpicId EpicId,
|
||||
string EpicName,
|
||||
ProjectId ProjectId
|
||||
) : DomainEvent;
|
||||
@@ -0,0 +1,10 @@
|
||||
using ColaFlow.Domain.ValueObjects;
|
||||
|
||||
namespace ColaFlow.Domain.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Event raised when a project is archived
|
||||
/// </summary>
|
||||
public sealed record ProjectArchivedEvent(
|
||||
ProjectId ProjectId
|
||||
) : DomainEvent;
|
||||
@@ -0,0 +1,12 @@
|
||||
using ColaFlow.Domain.ValueObjects;
|
||||
|
||||
namespace ColaFlow.Domain.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Event raised when a project is created
|
||||
/// </summary>
|
||||
public sealed record ProjectCreatedEvent(
|
||||
ProjectId ProjectId,
|
||||
string ProjectName,
|
||||
UserId CreatedBy
|
||||
) : DomainEvent;
|
||||
@@ -0,0 +1,12 @@
|
||||
using ColaFlow.Domain.ValueObjects;
|
||||
|
||||
namespace ColaFlow.Domain.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Event raised when a project is updated
|
||||
/// </summary>
|
||||
public sealed record ProjectUpdatedEvent(
|
||||
ProjectId ProjectId,
|
||||
string Name,
|
||||
string Description
|
||||
) : DomainEvent;
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace ColaFlow.Domain.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Exception type for domain layer
|
||||
/// </summary>
|
||||
public class DomainException : Exception
|
||||
{
|
||||
public DomainException()
|
||||
{ }
|
||||
|
||||
public DomainException(string message)
|
||||
: base(message)
|
||||
{ }
|
||||
|
||||
public DomainException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{ }
|
||||
}
|
||||
26
colaflow-api/src/ColaFlow.Domain/ValueObjects/EpicId.cs
Normal file
26
colaflow-api/src/ColaFlow.Domain/ValueObjects/EpicId.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using ColaFlow.Domain.Common;
|
||||
|
||||
namespace ColaFlow.Domain.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// EpicId Value Object (strongly-typed ID)
|
||||
/// </summary>
|
||||
public sealed class EpicId : ValueObject
|
||||
{
|
||||
public Guid Value { get; private set; }
|
||||
|
||||
private EpicId(Guid value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public static EpicId Create() => new EpicId(Guid.NewGuid());
|
||||
public static EpicId Create(Guid value) => new EpicId(value);
|
||||
|
||||
protected override IEnumerable<object> GetAtomicValues()
|
||||
{
|
||||
yield return Value;
|
||||
}
|
||||
|
||||
public override string ToString() => Value.ToString();
|
||||
}
|
||||
26
colaflow-api/src/ColaFlow.Domain/ValueObjects/ProjectId.cs
Normal file
26
colaflow-api/src/ColaFlow.Domain/ValueObjects/ProjectId.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using ColaFlow.Domain.Common;
|
||||
|
||||
namespace ColaFlow.Domain.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// ProjectId Value Object (strongly-typed ID)
|
||||
/// </summary>
|
||||
public sealed class ProjectId : ValueObject
|
||||
{
|
||||
public Guid Value { get; private set; }
|
||||
|
||||
private ProjectId(Guid value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public static ProjectId Create() => new ProjectId(Guid.NewGuid());
|
||||
public static ProjectId Create(Guid value) => new ProjectId(value);
|
||||
|
||||
protected override IEnumerable<object> GetAtomicValues()
|
||||
{
|
||||
yield return Value;
|
||||
}
|
||||
|
||||
public override string ToString() => Value.ToString();
|
||||
}
|
||||
38
colaflow-api/src/ColaFlow.Domain/ValueObjects/ProjectKey.cs
Normal file
38
colaflow-api/src/ColaFlow.Domain/ValueObjects/ProjectKey.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using ColaFlow.Domain.Common;
|
||||
using ColaFlow.Domain.Exceptions;
|
||||
|
||||
namespace ColaFlow.Domain.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// ProjectKey Value Object (e.g., "COLA", "FLOW")
|
||||
/// </summary>
|
||||
public sealed class ProjectKey : ValueObject
|
||||
{
|
||||
public string Value { get; private set; }
|
||||
|
||||
private ProjectKey(string value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public static ProjectKey Create(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
throw new DomainException("Project key cannot be empty");
|
||||
|
||||
if (value.Length > 10)
|
||||
throw new DomainException("Project key cannot exceed 10 characters");
|
||||
|
||||
if (!System.Text.RegularExpressions.Regex.IsMatch(value, "^[A-Z0-9]+$"))
|
||||
throw new DomainException("Project key must contain only uppercase letters and numbers");
|
||||
|
||||
return new ProjectKey(value);
|
||||
}
|
||||
|
||||
protected override IEnumerable<object> GetAtomicValues()
|
||||
{
|
||||
yield return Value;
|
||||
}
|
||||
|
||||
public override string ToString() => Value;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using ColaFlow.Domain.Common;
|
||||
|
||||
namespace ColaFlow.Domain.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// ProjectStatus Enumeration
|
||||
/// </summary>
|
||||
public sealed class ProjectStatus : Enumeration
|
||||
{
|
||||
public static readonly ProjectStatus Active = new(1, "Active");
|
||||
public static readonly ProjectStatus Archived = new(2, "Archived");
|
||||
public static readonly ProjectStatus OnHold = new(3, "On Hold");
|
||||
|
||||
private ProjectStatus(int id, string name) : base(id, name)
|
||||
{
|
||||
}
|
||||
}
|
||||
26
colaflow-api/src/ColaFlow.Domain/ValueObjects/StoryId.cs
Normal file
26
colaflow-api/src/ColaFlow.Domain/ValueObjects/StoryId.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using ColaFlow.Domain.Common;
|
||||
|
||||
namespace ColaFlow.Domain.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// StoryId Value Object (strongly-typed ID)
|
||||
/// </summary>
|
||||
public sealed class StoryId : ValueObject
|
||||
{
|
||||
public Guid Value { get; private set; }
|
||||
|
||||
private StoryId(Guid value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public static StoryId Create() => new StoryId(Guid.NewGuid());
|
||||
public static StoryId Create(Guid value) => new StoryId(value);
|
||||
|
||||
protected override IEnumerable<object> GetAtomicValues()
|
||||
{
|
||||
yield return Value;
|
||||
}
|
||||
|
||||
public override string ToString() => Value.ToString();
|
||||
}
|
||||
26
colaflow-api/src/ColaFlow.Domain/ValueObjects/TaskId.cs
Normal file
26
colaflow-api/src/ColaFlow.Domain/ValueObjects/TaskId.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using ColaFlow.Domain.Common;
|
||||
|
||||
namespace ColaFlow.Domain.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// TaskId Value Object (strongly-typed ID)
|
||||
/// </summary>
|
||||
public sealed class TaskId : ValueObject
|
||||
{
|
||||
public Guid Value { get; private set; }
|
||||
|
||||
private TaskId(Guid value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public static TaskId Create() => new TaskId(Guid.NewGuid());
|
||||
public static TaskId Create(Guid value) => new TaskId(value);
|
||||
|
||||
protected override IEnumerable<object> GetAtomicValues()
|
||||
{
|
||||
yield return Value;
|
||||
}
|
||||
|
||||
public override string ToString() => Value.ToString();
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using ColaFlow.Domain.Common;
|
||||
|
||||
namespace ColaFlow.Domain.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// TaskPriority Enumeration
|
||||
/// </summary>
|
||||
public sealed class TaskPriority : Enumeration
|
||||
{
|
||||
public static readonly TaskPriority Low = new(1, "Low");
|
||||
public static readonly TaskPriority Medium = new(2, "Medium");
|
||||
public static readonly TaskPriority High = new(3, "High");
|
||||
public static readonly TaskPriority Urgent = new(4, "Urgent");
|
||||
|
||||
private TaskPriority(int id, string name) : base(id, name)
|
||||
{
|
||||
}
|
||||
}
|
||||
26
colaflow-api/src/ColaFlow.Domain/ValueObjects/UserId.cs
Normal file
26
colaflow-api/src/ColaFlow.Domain/ValueObjects/UserId.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using ColaFlow.Domain.Common;
|
||||
|
||||
namespace ColaFlow.Domain.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// UserId Value Object (strongly-typed ID)
|
||||
/// </summary>
|
||||
public sealed class UserId : ValueObject
|
||||
{
|
||||
public Guid Value { get; private set; }
|
||||
|
||||
private UserId(Guid value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public static UserId Create() => new UserId(Guid.NewGuid());
|
||||
public static UserId Create(Guid value) => new UserId(value);
|
||||
|
||||
protected override IEnumerable<object> GetAtomicValues()
|
||||
{
|
||||
yield return Value;
|
||||
}
|
||||
|
||||
public override string ToString() => Value.ToString();
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using ColaFlow.Domain.Common;
|
||||
|
||||
namespace ColaFlow.Domain.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// WorkItemStatus Enumeration (renamed from TaskStatus to avoid conflict with System.Threading.Tasks.TaskStatus)
|
||||
/// </summary>
|
||||
public sealed class WorkItemStatus : Enumeration
|
||||
{
|
||||
public static readonly WorkItemStatus ToDo = new(1, "To Do");
|
||||
public static readonly WorkItemStatus InProgress = new(2, "In Progress");
|
||||
public static readonly WorkItemStatus InReview = new(3, "In Review");
|
||||
public static readonly WorkItemStatus Done = new(4, "Done");
|
||||
public static readonly WorkItemStatus Blocked = new(5, "Blocked");
|
||||
|
||||
private WorkItemStatus(int id, string name) : base(id, name)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ColaFlow.Domain\ColaFlow.Domain.csproj" />
|
||||
<ProjectReference Include="..\ColaFlow.Application\ColaFlow.Application.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.10" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.10">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
||||
<PackageReference Include="StackExchange.Redis" Version="2.9.32" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,46 @@
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Application.Behaviors;
|
||||
|
||||
/// <summary>
|
||||
/// Pipeline behavior for request validation using FluentValidation
|
||||
/// </summary>
|
||||
public sealed class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
|
||||
where TRequest : IRequest<TResponse>
|
||||
{
|
||||
private readonly IEnumerable<IValidator<TRequest>> _validators;
|
||||
|
||||
public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
|
||||
{
|
||||
_validators = validators;
|
||||
}
|
||||
|
||||
public async Task<TResponse> Handle(
|
||||
TRequest request,
|
||||
RequestHandlerDelegate<TResponse> next,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!_validators.Any())
|
||||
{
|
||||
return await next();
|
||||
}
|
||||
|
||||
var context = new ValidationContext<TRequest>(request);
|
||||
|
||||
var validationResults = await Task.WhenAll(
|
||||
_validators.Select(v => v.ValidateAsync(context, cancellationToken)));
|
||||
|
||||
var failures = validationResults
|
||||
.SelectMany(r => r.Errors)
|
||||
.Where(f => f != null)
|
||||
.ToList();
|
||||
|
||||
if (failures.Any())
|
||||
{
|
||||
throw new ValidationException(failures);
|
||||
}
|
||||
|
||||
return await next();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ColaFlow.Modules.ProjectManagement.Domain\ColaFlow.Modules.ProjectManagement.Domain.csproj" />
|
||||
<ProjectReference Include="..\ColaFlow.Modules.ProjectManagement.Contracts\ColaFlow.Modules.ProjectManagement.Contracts.csproj" />
|
||||
<ProjectReference Include="..\..\..\Shared\ColaFlow.Shared.Kernel\ColaFlow.Shared.Kernel.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MediatR" Version="11.1.0" />
|
||||
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="11.1.0" />
|
||||
<PackageReference Include="FluentValidation" Version="11.10.0" />
|
||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.10.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>ColaFlow.Modules.ProjectManagement.Application</AssemblyName>
|
||||
<RootNamespace>ColaFlow.Modules.ProjectManagement.Application</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,15 @@
|
||||
using MediatR;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.DTOs;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Application.Commands.CreateEpic;
|
||||
|
||||
/// <summary>
|
||||
/// Command to create a new Epic
|
||||
/// </summary>
|
||||
public sealed record CreateEpicCommand : IRequest<EpicDto>
|
||||
{
|
||||
public Guid ProjectId { get; init; }
|
||||
public string Name { get; init; } = string.Empty;
|
||||
public string Description { get; init; } = string.Empty;
|
||||
public Guid CreatedBy { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using MediatR;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.DTOs;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Exceptions;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Application.Commands.CreateEpic;
|
||||
|
||||
/// <summary>
|
||||
/// Handler for CreateEpicCommand
|
||||
/// </summary>
|
||||
public sealed class CreateEpicCommandHandler : IRequestHandler<CreateEpicCommand, EpicDto>
|
||||
{
|
||||
private readonly IProjectRepository _projectRepository;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
|
||||
public CreateEpicCommandHandler(
|
||||
IProjectRepository projectRepository,
|
||||
IUnitOfWork unitOfWork)
|
||||
{
|
||||
_projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository));
|
||||
_unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork));
|
||||
}
|
||||
|
||||
public async Task<EpicDto> Handle(CreateEpicCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// Get the project
|
||||
var projectId = ProjectId.From(request.ProjectId);
|
||||
var project = await _projectRepository.GetByIdAsync(projectId, cancellationToken);
|
||||
|
||||
if (project == null)
|
||||
throw new NotFoundException("Project", request.ProjectId);
|
||||
|
||||
// Create epic through aggregate root
|
||||
var createdById = UserId.From(request.CreatedBy);
|
||||
var epic = project.CreateEpic(request.Name, request.Description, createdById);
|
||||
|
||||
// Update project (epic is part of aggregate)
|
||||
_projectRepository.Update(project);
|
||||
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// Map to DTO
|
||||
return new EpicDto
|
||||
{
|
||||
Id = epic.Id.Value,
|
||||
Name = epic.Name,
|
||||
Description = epic.Description,
|
||||
ProjectId = epic.ProjectId.Value,
|
||||
Status = epic.Status.Value,
|
||||
Priority = epic.Priority.Value,
|
||||
CreatedBy = epic.CreatedBy.Value,
|
||||
CreatedAt = epic.CreatedAt,
|
||||
UpdatedAt = epic.UpdatedAt,
|
||||
Stories = new List<StoryDto>()
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Application.Commands.CreateEpic;
|
||||
|
||||
/// <summary>
|
||||
/// Validator for CreateEpicCommand
|
||||
/// </summary>
|
||||
public sealed class CreateEpicCommandValidator : AbstractValidator<CreateEpicCommand>
|
||||
{
|
||||
public CreateEpicCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.ProjectId)
|
||||
.NotEmpty().WithMessage("Project ID is required");
|
||||
|
||||
RuleFor(x => x.Name)
|
||||
.NotEmpty().WithMessage("Epic name is required")
|
||||
.MaximumLength(200).WithMessage("Epic name cannot exceed 200 characters");
|
||||
|
||||
RuleFor(x => x.CreatedBy)
|
||||
.NotEmpty().WithMessage("Created by user ID is required");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using MediatR;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.DTOs;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Application.Commands.CreateProject;
|
||||
|
||||
/// <summary>
|
||||
/// Command to create a new project
|
||||
/// </summary>
|
||||
public sealed record CreateProjectCommand : IRequest<ProjectDto>
|
||||
{
|
||||
public string Name { get; init; } = string.Empty;
|
||||
public string Description { get; init; } = string.Empty;
|
||||
public string Key { get; init; } = string.Empty;
|
||||
public Guid OwnerId { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
using MediatR;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.DTOs;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Exceptions;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Application.Commands.CreateProject;
|
||||
|
||||
/// <summary>
|
||||
/// Handler for CreateProjectCommand
|
||||
/// </summary>
|
||||
public sealed class CreateProjectCommandHandler : IRequestHandler<CreateProjectCommand, ProjectDto>
|
||||
{
|
||||
private readonly IProjectRepository _projectRepository;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
|
||||
public CreateProjectCommandHandler(
|
||||
IProjectRepository projectRepository,
|
||||
IUnitOfWork unitOfWork)
|
||||
{
|
||||
_projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository));
|
||||
_unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork));
|
||||
}
|
||||
|
||||
public async Task<ProjectDto> Handle(CreateProjectCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// Check if project key already exists
|
||||
var existingProject = await _projectRepository.GetByKeyAsync(request.Key, cancellationToken);
|
||||
if (existingProject != null)
|
||||
{
|
||||
throw new DomainException($"Project with key '{request.Key}' already exists");
|
||||
}
|
||||
|
||||
// Create project aggregate
|
||||
var project = Project.Create(
|
||||
request.Name,
|
||||
request.Description,
|
||||
request.Key,
|
||||
UserId.From(request.OwnerId)
|
||||
);
|
||||
|
||||
// Save to repository
|
||||
await _projectRepository.AddAsync(project, cancellationToken);
|
||||
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// Return DTO
|
||||
return MapToDto(project);
|
||||
}
|
||||
|
||||
private static ProjectDto MapToDto(Project project)
|
||||
{
|
||||
return new ProjectDto
|
||||
{
|
||||
Id = project.Id.Value,
|
||||
Name = project.Name,
|
||||
Description = project.Description,
|
||||
Key = project.Key.Value,
|
||||
Status = project.Status.Name,
|
||||
OwnerId = project.OwnerId.Value,
|
||||
CreatedAt = project.CreatedAt,
|
||||
UpdatedAt = project.UpdatedAt,
|
||||
Epics = new List<EpicDto>()
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Application.Commands.CreateProject;
|
||||
|
||||
/// <summary>
|
||||
/// Validator for CreateProjectCommand
|
||||
/// </summary>
|
||||
public sealed class CreateProjectCommandValidator : AbstractValidator<CreateProjectCommand>
|
||||
{
|
||||
public CreateProjectCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.Name)
|
||||
.NotEmpty().WithMessage("Project name is required")
|
||||
.MaximumLength(200).WithMessage("Project name cannot exceed 200 characters");
|
||||
|
||||
RuleFor(x => x.Key)
|
||||
.NotEmpty().WithMessage("Project key is required")
|
||||
.MaximumLength(20).WithMessage("Project key cannot exceed 20 characters")
|
||||
.Matches("^[A-Z0-9]+$").WithMessage("Project key must contain only uppercase letters and numbers");
|
||||
|
||||
RuleFor(x => x.OwnerId)
|
||||
.NotEmpty().WithMessage("Owner ID is required");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using MediatR;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.DTOs;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Application.Commands.UpdateEpic;
|
||||
|
||||
/// <summary>
|
||||
/// Command to update an existing Epic
|
||||
/// </summary>
|
||||
public sealed record UpdateEpicCommand : IRequest<EpicDto>
|
||||
{
|
||||
public Guid EpicId { get; init; }
|
||||
public string Name { get; init; } = string.Empty;
|
||||
public string Description { get; init; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
using MediatR;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.DTOs;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Exceptions;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Application.Commands.UpdateEpic;
|
||||
|
||||
/// <summary>
|
||||
/// Handler for UpdateEpicCommand
|
||||
/// </summary>
|
||||
public sealed class UpdateEpicCommandHandler : IRequestHandler<UpdateEpicCommand, EpicDto>
|
||||
{
|
||||
private readonly IProjectRepository _projectRepository;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
|
||||
public UpdateEpicCommandHandler(
|
||||
IProjectRepository projectRepository,
|
||||
IUnitOfWork unitOfWork)
|
||||
{
|
||||
_projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository));
|
||||
_unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork));
|
||||
}
|
||||
|
||||
public async Task<EpicDto> Handle(UpdateEpicCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// Get the project containing the epic
|
||||
var epicId = EpicId.From(request.EpicId);
|
||||
var project = await _projectRepository.GetProjectWithEpicAsync(epicId, cancellationToken);
|
||||
|
||||
if (project == null)
|
||||
throw new NotFoundException("Epic", request.EpicId);
|
||||
|
||||
// Find the epic
|
||||
var epic = project.Epics.FirstOrDefault(e => e.Id == epicId);
|
||||
if (epic == null)
|
||||
throw new NotFoundException("Epic", request.EpicId);
|
||||
|
||||
// Update epic through domain method
|
||||
epic.UpdateDetails(request.Name, request.Description);
|
||||
|
||||
// Save changes
|
||||
_projectRepository.Update(project);
|
||||
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// Map to DTO
|
||||
return new EpicDto
|
||||
{
|
||||
Id = epic.Id.Value,
|
||||
Name = epic.Name,
|
||||
Description = epic.Description,
|
||||
ProjectId = epic.ProjectId.Value,
|
||||
Status = epic.Status.Value,
|
||||
Priority = epic.Priority.Value,
|
||||
CreatedBy = epic.CreatedBy.Value,
|
||||
CreatedAt = epic.CreatedAt,
|
||||
UpdatedAt = epic.UpdatedAt,
|
||||
Stories = epic.Stories.Select(s => new StoryDto
|
||||
{
|
||||
Id = s.Id.Value,
|
||||
Title = s.Title,
|
||||
Description = s.Description,
|
||||
EpicId = s.EpicId.Value,
|
||||
Status = s.Status.Value,
|
||||
Priority = s.Priority.Value,
|
||||
EstimatedHours = s.EstimatedHours,
|
||||
ActualHours = s.ActualHours,
|
||||
AssigneeId = s.AssigneeId?.Value,
|
||||
CreatedBy = s.CreatedBy.Value,
|
||||
CreatedAt = s.CreatedAt,
|
||||
UpdatedAt = s.UpdatedAt,
|
||||
Tasks = new List<TaskDto>()
|
||||
}).ToList()
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Application.Commands.UpdateEpic;
|
||||
|
||||
/// <summary>
|
||||
/// Validator for UpdateEpicCommand
|
||||
/// </summary>
|
||||
public sealed class UpdateEpicCommandValidator : AbstractValidator<UpdateEpicCommand>
|
||||
{
|
||||
public UpdateEpicCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.EpicId)
|
||||
.NotEmpty().WithMessage("Epic ID is required");
|
||||
|
||||
RuleFor(x => x.Name)
|
||||
.NotEmpty().WithMessage("Epic name is required")
|
||||
.MaximumLength(200).WithMessage("Epic name cannot exceed 200 characters");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace ColaFlow.Modules.ProjectManagement.Application.DTOs;
|
||||
|
||||
/// <summary>
|
||||
/// Data Transfer Object for Epic
|
||||
/// </summary>
|
||||
public record EpicDto
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
public string Name { get; init; } = string.Empty;
|
||||
public string Description { get; init; } = string.Empty;
|
||||
public Guid ProjectId { get; init; }
|
||||
public string Status { get; init; } = string.Empty;
|
||||
public string Priority { get; init; } = string.Empty;
|
||||
public Guid CreatedBy { get; init; }
|
||||
public DateTime CreatedAt { get; init; }
|
||||
public DateTime? UpdatedAt { get; init; }
|
||||
public List<StoryDto> Stories { get; init; } = new();
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace ColaFlow.Modules.ProjectManagement.Application.DTOs;
|
||||
|
||||
/// <summary>
|
||||
/// Data Transfer Object for Project
|
||||
/// </summary>
|
||||
public record ProjectDto
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
public string Name { get; init; } = string.Empty;
|
||||
public string Description { get; init; } = string.Empty;
|
||||
public string Key { get; init; } = string.Empty;
|
||||
public string Status { get; init; } = string.Empty;
|
||||
public Guid OwnerId { get; init; }
|
||||
public DateTime CreatedAt { get; init; }
|
||||
public DateTime? UpdatedAt { get; init; }
|
||||
public List<EpicDto> Epics { get; init; } = new();
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace ColaFlow.Modules.ProjectManagement.Application.DTOs;
|
||||
|
||||
/// <summary>
|
||||
/// Data Transfer Object for Story
|
||||
/// </summary>
|
||||
public record StoryDto
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
public string Title { get; init; } = string.Empty;
|
||||
public string Description { get; init; } = string.Empty;
|
||||
public Guid EpicId { get; init; }
|
||||
public string Status { get; init; } = string.Empty;
|
||||
public string Priority { get; init; } = string.Empty;
|
||||
public Guid? AssigneeId { get; init; }
|
||||
public decimal? EstimatedHours { get; init; }
|
||||
public decimal? ActualHours { get; init; }
|
||||
public Guid CreatedBy { get; init; }
|
||||
public DateTime CreatedAt { get; init; }
|
||||
public DateTime? UpdatedAt { get; init; }
|
||||
public List<TaskDto> Tasks { get; init; } = new();
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace ColaFlow.Modules.ProjectManagement.Application.DTOs;
|
||||
|
||||
/// <summary>
|
||||
/// Data Transfer Object for Task
|
||||
/// </summary>
|
||||
public record TaskDto
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
public string Title { get; init; } = string.Empty;
|
||||
public string Description { get; init; } = string.Empty;
|
||||
public Guid StoryId { get; init; }
|
||||
public string Status { get; init; } = string.Empty;
|
||||
public string Priority { get; init; } = string.Empty;
|
||||
public Guid? AssigneeId { get; init; }
|
||||
public decimal? EstimatedHours { get; init; }
|
||||
public decimal? ActualHours { get; init; }
|
||||
public Guid CreatedBy { get; init; }
|
||||
public DateTime CreatedAt { get; init; }
|
||||
public DateTime? UpdatedAt { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using MediatR;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.DTOs;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Application.Queries.GetEpicById;
|
||||
|
||||
/// <summary>
|
||||
/// Query to get an Epic by its ID
|
||||
/// </summary>
|
||||
public sealed record GetEpicByIdQuery(Guid EpicId) : IRequest<EpicDto>;
|
||||
@@ -0,0 +1,9 @@
|
||||
using MediatR;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.DTOs;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Application.Queries.GetProjectById;
|
||||
|
||||
/// <summary>
|
||||
/// Query to get a project by its ID
|
||||
/// </summary>
|
||||
public sealed record GetProjectByIdQuery(Guid ProjectId) : IRequest<ProjectDto>;
|
||||
@@ -0,0 +1,92 @@
|
||||
using MediatR;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.DTOs;
|
||||
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;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Application.Queries.GetProjectById;
|
||||
|
||||
/// <summary>
|
||||
/// Handler for GetProjectByIdQuery
|
||||
/// </summary>
|
||||
public sealed class GetProjectByIdQueryHandler : IRequestHandler<GetProjectByIdQuery, ProjectDto>
|
||||
{
|
||||
private readonly IProjectRepository _projectRepository;
|
||||
|
||||
public GetProjectByIdQueryHandler(IProjectRepository projectRepository)
|
||||
{
|
||||
_projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository));
|
||||
}
|
||||
|
||||
public async Task<ProjectDto> Handle(GetProjectByIdQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var project = await _projectRepository.GetByIdAsync(
|
||||
ProjectId.From(request.ProjectId),
|
||||
cancellationToken);
|
||||
|
||||
if (project == null)
|
||||
{
|
||||
throw new DomainException($"Project with ID '{request.ProjectId}' not found");
|
||||
}
|
||||
|
||||
return MapToDto(project);
|
||||
}
|
||||
|
||||
private static ProjectDto MapToDto(Project project)
|
||||
{
|
||||
return new ProjectDto
|
||||
{
|
||||
Id = project.Id.Value,
|
||||
Name = project.Name,
|
||||
Description = project.Description,
|
||||
Key = project.Key.Value,
|
||||
Status = project.Status.Name,
|
||||
OwnerId = project.OwnerId.Value,
|
||||
CreatedAt = project.CreatedAt,
|
||||
UpdatedAt = project.UpdatedAt,
|
||||
Epics = project.Epics.Select(e => new EpicDto
|
||||
{
|
||||
Id = e.Id.Value,
|
||||
Name = e.Name,
|
||||
Description = e.Description,
|
||||
ProjectId = e.ProjectId.Value,
|
||||
Status = e.Status.Name,
|
||||
Priority = e.Priority.Name,
|
||||
CreatedBy = e.CreatedBy.Value,
|
||||
CreatedAt = e.CreatedAt,
|
||||
UpdatedAt = e.UpdatedAt,
|
||||
Stories = e.Stories.Select(s => new StoryDto
|
||||
{
|
||||
Id = s.Id.Value,
|
||||
Title = s.Title,
|
||||
Description = s.Description,
|
||||
EpicId = s.EpicId.Value,
|
||||
Status = s.Status.Name,
|
||||
Priority = s.Priority.Name,
|
||||
AssigneeId = s.AssigneeId?.Value,
|
||||
EstimatedHours = s.EstimatedHours,
|
||||
ActualHours = s.ActualHours,
|
||||
CreatedBy = s.CreatedBy.Value,
|
||||
CreatedAt = s.CreatedAt,
|
||||
UpdatedAt = s.UpdatedAt,
|
||||
Tasks = s.Tasks.Select(t => new TaskDto
|
||||
{
|
||||
Id = t.Id.Value,
|
||||
Title = t.Title,
|
||||
Description = t.Description,
|
||||
StoryId = t.StoryId.Value,
|
||||
Status = t.Status.Name,
|
||||
Priority = t.Priority.Name,
|
||||
AssigneeId = t.AssigneeId?.Value,
|
||||
EstimatedHours = t.EstimatedHours,
|
||||
ActualHours = t.ActualHours,
|
||||
CreatedBy = t.CreatedBy.Value,
|
||||
CreatedAt = t.CreatedAt,
|
||||
UpdatedAt = t.UpdatedAt
|
||||
}).ToList()
|
||||
}).ToList()
|
||||
}).ToList()
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using MediatR;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.DTOs;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Application.Queries.GetProjects;
|
||||
|
||||
/// <summary>
|
||||
/// Query to get all projects
|
||||
/// </summary>
|
||||
public sealed record GetProjectsQuery : IRequest<List<ProjectDto>>;
|
||||
@@ -0,0 +1,43 @@
|
||||
using MediatR;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.DTOs;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Application.Queries.GetProjects;
|
||||
|
||||
/// <summary>
|
||||
/// Handler for GetProjectsQuery
|
||||
/// </summary>
|
||||
public sealed class GetProjectsQueryHandler : IRequestHandler<GetProjectsQuery, List<ProjectDto>>
|
||||
{
|
||||
private readonly IProjectRepository _projectRepository;
|
||||
|
||||
public GetProjectsQueryHandler(IProjectRepository projectRepository)
|
||||
{
|
||||
_projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository));
|
||||
}
|
||||
|
||||
public async Task<List<ProjectDto>> Handle(GetProjectsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var projects = await _projectRepository.GetAllAsync(cancellationToken);
|
||||
|
||||
return projects.Select(MapToDto).ToList();
|
||||
}
|
||||
|
||||
private static ProjectDto MapToDto(Project project)
|
||||
{
|
||||
return new ProjectDto
|
||||
{
|
||||
Id = project.Id.Value,
|
||||
Name = project.Name,
|
||||
Description = project.Description,
|
||||
Key = project.Key.Value,
|
||||
Status = project.Status.Name,
|
||||
OwnerId = project.OwnerId.Value,
|
||||
CreatedAt = project.CreatedAt,
|
||||
UpdatedAt = project.UpdatedAt,
|
||||
// Don't load Epics for list view (performance)
|
||||
Epics = new List<EpicDto>()
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>ColaFlow.Modules.ProjectManagement.Contracts</AssemblyName>
|
||||
<RootNamespace>ColaFlow.Modules.ProjectManagement.Contracts</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,91 @@
|
||||
using ColaFlow.Shared.Kernel.Common;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Exceptions;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Events;
|
||||
namespace ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
|
||||
|
||||
/// <summary>
|
||||
/// Epic Entity (part of Project aggregate)
|
||||
/// </summary>
|
||||
public class Epic : Entity
|
||||
{
|
||||
public new EpicId Id { get; private set; }
|
||||
public string Name { get; private set; }
|
||||
public string Description { get; private set; }
|
||||
public ProjectId ProjectId { get; private set; }
|
||||
public WorkItemStatus Status { get; private set; }
|
||||
public TaskPriority Priority { get; private set; }
|
||||
|
||||
private readonly List<Story> _stories = new();
|
||||
public IReadOnlyCollection<Story> Stories => _stories.AsReadOnly();
|
||||
|
||||
public DateTime CreatedAt { get; private set; }
|
||||
public UserId CreatedBy { get; private set; }
|
||||
public DateTime? UpdatedAt { get; private set; }
|
||||
|
||||
// EF Core constructor
|
||||
private Epic()
|
||||
{
|
||||
Id = null!;
|
||||
Name = null!;
|
||||
Description = null!;
|
||||
ProjectId = null!;
|
||||
Status = null!;
|
||||
Priority = null!;
|
||||
CreatedBy = null!;
|
||||
}
|
||||
|
||||
public static Epic Create(string name, string description, ProjectId projectId, UserId createdBy)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
throw new DomainException("Epic name cannot be empty");
|
||||
|
||||
if (name.Length > 200)
|
||||
throw new DomainException("Epic name cannot exceed 200 characters");
|
||||
|
||||
return new Epic
|
||||
{
|
||||
Id = EpicId.Create(),
|
||||
Name = name,
|
||||
Description = description ?? string.Empty,
|
||||
ProjectId = projectId,
|
||||
Status = WorkItemStatus.ToDo,
|
||||
Priority = TaskPriority.Medium,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
CreatedBy = createdBy
|
||||
};
|
||||
}
|
||||
|
||||
public Story CreateStory(string title, string description, TaskPriority priority, UserId createdBy)
|
||||
{
|
||||
var story = Story.Create(title, description, this.Id, priority, createdBy);
|
||||
_stories.Add(story);
|
||||
return story;
|
||||
}
|
||||
|
||||
public void UpdateDetails(string name, string description)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
throw new DomainException("Epic name cannot be empty");
|
||||
|
||||
if (name.Length > 200)
|
||||
throw new DomainException("Epic name cannot exceed 200 characters");
|
||||
|
||||
Name = name;
|
||||
Description = description ?? string.Empty;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void UpdateStatus(WorkItemStatus newStatus)
|
||||
{
|
||||
Status = newStatus;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void UpdatePriority(TaskPriority newPriority)
|
||||
{
|
||||
Priority = newPriority;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
using ColaFlow.Shared.Kernel.Common;
|
||||
using ColaFlow.Shared.Kernel.Events;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Exceptions;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Events;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
|
||||
|
||||
/// <summary>
|
||||
/// Project Aggregate Root
|
||||
/// Enforces consistency boundary for Project -> Epic -> Story -> Task hierarchy
|
||||
/// </summary>
|
||||
public class Project : AggregateRoot
|
||||
{
|
||||
public new ProjectId Id { get; private set; }
|
||||
public string Name { get; private set; }
|
||||
public string Description { get; private set; }
|
||||
public ProjectKey Key { get; private set; }
|
||||
public ProjectStatus Status { get; private set; }
|
||||
public UserId OwnerId { get; private set; }
|
||||
|
||||
private readonly List<Epic> _epics = new();
|
||||
public IReadOnlyCollection<Epic> Epics => _epics.AsReadOnly();
|
||||
|
||||
public DateTime CreatedAt { get; private set; }
|
||||
public DateTime? UpdatedAt { get; private set; }
|
||||
|
||||
// EF Core constructor
|
||||
private Project()
|
||||
{
|
||||
Id = null!;
|
||||
Name = null!;
|
||||
Description = null!;
|
||||
Key = null!;
|
||||
Status = null!;
|
||||
OwnerId = null!;
|
||||
}
|
||||
|
||||
// Factory method
|
||||
public static Project Create(string name, string description, string key, UserId ownerId)
|
||||
{
|
||||
// Validation
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
throw new DomainException("Project name cannot be empty");
|
||||
|
||||
if (name.Length > 200)
|
||||
throw new DomainException("Project name cannot exceed 200 characters");
|
||||
|
||||
var project = new Project
|
||||
{
|
||||
Id = ProjectId.Create(),
|
||||
Name = name,
|
||||
Description = description ?? string.Empty,
|
||||
Key = ProjectKey.Create(key),
|
||||
Status = ProjectStatus.Active,
|
||||
OwnerId = ownerId,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
// Raise domain event
|
||||
project.AddDomainEvent(new ProjectCreatedEvent(project.Id, project.Name, ownerId));
|
||||
|
||||
return project;
|
||||
}
|
||||
|
||||
// Business methods
|
||||
public void UpdateDetails(string name, string description)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
throw new DomainException("Project name cannot be empty");
|
||||
|
||||
if (name.Length > 200)
|
||||
throw new DomainException("Project name cannot exceed 200 characters");
|
||||
|
||||
Name = name;
|
||||
Description = description ?? string.Empty;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
AddDomainEvent(new ProjectUpdatedEvent(Id, Name, Description));
|
||||
}
|
||||
|
||||
public Epic CreateEpic(string name, string description, UserId createdBy)
|
||||
{
|
||||
if (Status == ProjectStatus.Archived)
|
||||
throw new DomainException("Cannot create epic in an archived project");
|
||||
|
||||
var epic = Epic.Create(name, description, this.Id, createdBy);
|
||||
_epics.Add(epic);
|
||||
|
||||
AddDomainEvent(new EpicCreatedEvent(epic.Id, epic.Name, this.Id));
|
||||
|
||||
return epic;
|
||||
}
|
||||
|
||||
public void Archive()
|
||||
{
|
||||
if (Status == ProjectStatus.Archived)
|
||||
throw new DomainException("Project is already archived");
|
||||
|
||||
Status = ProjectStatus.Archived;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
AddDomainEvent(new ProjectArchivedEvent(Id));
|
||||
}
|
||||
|
||||
public void Activate()
|
||||
{
|
||||
if (Status == ProjectStatus.Active)
|
||||
throw new DomainException("Project is already active");
|
||||
|
||||
Status = ProjectStatus.Active;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
using ColaFlow.Shared.Kernel.Common;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Exceptions;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Events;
|
||||
namespace ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
|
||||
|
||||
/// <summary>
|
||||
/// Story Entity (part of Project aggregate)
|
||||
/// </summary>
|
||||
public class Story : Entity
|
||||
{
|
||||
public new StoryId Id { get; private set; }
|
||||
public string Title { get; private set; }
|
||||
public string Description { get; private set; }
|
||||
public EpicId EpicId { get; private set; }
|
||||
public WorkItemStatus Status { get; private set; }
|
||||
public TaskPriority Priority { get; private set; }
|
||||
public decimal? EstimatedHours { get; private set; }
|
||||
public decimal? ActualHours { get; private set; }
|
||||
public UserId? AssigneeId { get; private set; }
|
||||
|
||||
private readonly List<WorkTask> _tasks = new();
|
||||
public IReadOnlyCollection<WorkTask> Tasks => _tasks.AsReadOnly();
|
||||
|
||||
public DateTime CreatedAt { get; private set; }
|
||||
public UserId CreatedBy { get; private set; }
|
||||
public DateTime? UpdatedAt { get; private set; }
|
||||
|
||||
// EF Core constructor
|
||||
private Story()
|
||||
{
|
||||
Id = null!;
|
||||
Title = null!;
|
||||
Description = null!;
|
||||
EpicId = null!;
|
||||
Status = null!;
|
||||
Priority = null!;
|
||||
CreatedBy = null!;
|
||||
}
|
||||
|
||||
public static Story Create(string title, string description, EpicId epicId, TaskPriority priority, UserId createdBy)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(title))
|
||||
throw new DomainException("Story title cannot be empty");
|
||||
|
||||
if (title.Length > 200)
|
||||
throw new DomainException("Story title cannot exceed 200 characters");
|
||||
|
||||
return new Story
|
||||
{
|
||||
Id = StoryId.Create(),
|
||||
Title = title,
|
||||
Description = description ?? string.Empty,
|
||||
EpicId = epicId,
|
||||
Status = WorkItemStatus.ToDo,
|
||||
Priority = priority,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
CreatedBy = createdBy
|
||||
};
|
||||
}
|
||||
|
||||
public WorkTask CreateTask(string title, string description, TaskPriority priority, UserId createdBy)
|
||||
{
|
||||
var task = WorkTask.Create(title, description, this.Id, priority, createdBy);
|
||||
_tasks.Add(task);
|
||||
return task;
|
||||
}
|
||||
|
||||
public void UpdateDetails(string title, string description)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(title))
|
||||
throw new DomainException("Story title cannot be empty");
|
||||
|
||||
if (title.Length > 200)
|
||||
throw new DomainException("Story title cannot exceed 200 characters");
|
||||
|
||||
Title = title;
|
||||
Description = description ?? string.Empty;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void UpdateStatus(WorkItemStatus newStatus)
|
||||
{
|
||||
Status = newStatus;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void AssignTo(UserId assigneeId)
|
||||
{
|
||||
AssigneeId = assigneeId;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void UpdateEstimate(decimal hours)
|
||||
{
|
||||
if (hours < 0)
|
||||
throw new DomainException("Estimated hours cannot be negative");
|
||||
|
||||
EstimatedHours = hours;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void LogActualHours(decimal hours)
|
||||
{
|
||||
if (hours < 0)
|
||||
throw new DomainException("Actual hours cannot be negative");
|
||||
|
||||
ActualHours = hours;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
using ColaFlow.Shared.Kernel.Common;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Exceptions;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Events;
|
||||
namespace ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
|
||||
|
||||
/// <summary>
|
||||
/// Task Entity (part of Project aggregate)
|
||||
/// Named "WorkTask" to avoid conflict with System.Threading.Tasks.Task
|
||||
/// </summary>
|
||||
public class WorkTask : Entity
|
||||
{
|
||||
public new TaskId Id { get; private set; }
|
||||
public string Title { get; private set; }
|
||||
public string Description { get; private set; }
|
||||
public StoryId StoryId { get; private set; }
|
||||
public WorkItemStatus Status { get; private set; }
|
||||
public TaskPriority Priority { get; private set; }
|
||||
public decimal? EstimatedHours { get; private set; }
|
||||
public decimal? ActualHours { get; private set; }
|
||||
public UserId? AssigneeId { get; private set; }
|
||||
|
||||
public DateTime CreatedAt { get; private set; }
|
||||
public UserId CreatedBy { get; private set; }
|
||||
public DateTime? UpdatedAt { get; private set; }
|
||||
|
||||
// EF Core constructor
|
||||
private WorkTask()
|
||||
{
|
||||
Id = null!;
|
||||
Title = null!;
|
||||
Description = null!;
|
||||
StoryId = null!;
|
||||
Status = null!;
|
||||
Priority = null!;
|
||||
CreatedBy = null!;
|
||||
}
|
||||
|
||||
public static WorkTask Create(string title, string description, StoryId storyId, TaskPriority priority, UserId createdBy)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(title))
|
||||
throw new DomainException("Task title cannot be empty");
|
||||
|
||||
if (title.Length > 200)
|
||||
throw new DomainException("Task title cannot exceed 200 characters");
|
||||
|
||||
return new WorkTask
|
||||
{
|
||||
Id = TaskId.Create(),
|
||||
Title = title,
|
||||
Description = description ?? string.Empty,
|
||||
StoryId = storyId,
|
||||
Status = WorkItemStatus.ToDo,
|
||||
Priority = priority,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
CreatedBy = createdBy
|
||||
};
|
||||
}
|
||||
|
||||
public void UpdateDetails(string title, string description)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(title))
|
||||
throw new DomainException("Task title cannot be empty");
|
||||
|
||||
if (title.Length > 200)
|
||||
throw new DomainException("Task title cannot exceed 200 characters");
|
||||
|
||||
Title = title;
|
||||
Description = description ?? string.Empty;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void UpdateStatus(WorkItemStatus newStatus)
|
||||
{
|
||||
Status = newStatus;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void AssignTo(UserId assigneeId)
|
||||
{
|
||||
AssigneeId = assigneeId;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void UpdatePriority(TaskPriority newPriority)
|
||||
{
|
||||
Priority = newPriority;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void UpdateEstimate(decimal hours)
|
||||
{
|
||||
if (hours < 0)
|
||||
throw new DomainException("Estimated hours cannot be negative");
|
||||
|
||||
EstimatedHours = hours;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void LogActualHours(decimal hours)
|
||||
{
|
||||
if (hours < 0)
|
||||
throw new DomainException("Actual hours cannot be negative");
|
||||
|
||||
ActualHours = hours;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\Shared\ColaFlow.Shared.Kernel\ColaFlow.Shared.Kernel.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>ColaFlow.Modules.ProjectManagement.Domain</AssemblyName>
|
||||
<RootNamespace>ColaFlow.Modules.ProjectManagement.Domain</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,13 @@
|
||||
using ColaFlow.Shared.Kernel.Events;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Domain.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Event raised when an epic is created
|
||||
/// </summary>
|
||||
public sealed record EpicCreatedEvent(
|
||||
EpicId EpicId,
|
||||
string EpicName,
|
||||
ProjectId ProjectId
|
||||
) : DomainEvent;
|
||||
@@ -0,0 +1,11 @@
|
||||
using ColaFlow.Shared.Kernel.Events;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Domain.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Event raised when a project is archived
|
||||
/// </summary>
|
||||
public sealed record ProjectArchivedEvent(
|
||||
ProjectId ProjectId
|
||||
) : DomainEvent;
|
||||
@@ -0,0 +1,13 @@
|
||||
using ColaFlow.Shared.Kernel.Events;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Domain.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Event raised when a project is created
|
||||
/// </summary>
|
||||
public sealed record ProjectCreatedEvent(
|
||||
ProjectId ProjectId,
|
||||
string ProjectName,
|
||||
UserId CreatedBy
|
||||
) : DomainEvent;
|
||||
@@ -0,0 +1,13 @@
|
||||
using ColaFlow.Shared.Kernel.Events;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Domain.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Event raised when a project is updated
|
||||
/// </summary>
|
||||
public sealed record ProjectUpdatedEvent(
|
||||
ProjectId ProjectId,
|
||||
string Name,
|
||||
string Description
|
||||
) : DomainEvent;
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace ColaFlow.Modules.ProjectManagement.Domain.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Exception type for domain layer
|
||||
/// </summary>
|
||||
public class DomainException : Exception
|
||||
{
|
||||
public DomainException()
|
||||
{ }
|
||||
|
||||
public DomainException(string message)
|
||||
: base(message)
|
||||
{ }
|
||||
|
||||
public DomainException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{ }
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace ColaFlow.Modules.ProjectManagement.Domain.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Exception type for not found resources
|
||||
/// </summary>
|
||||
public class NotFoundException : Exception
|
||||
{
|
||||
public NotFoundException()
|
||||
{ }
|
||||
|
||||
public NotFoundException(string message)
|
||||
: base(message)
|
||||
{ }
|
||||
|
||||
public NotFoundException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{ }
|
||||
|
||||
public NotFoundException(string entityName, object key)
|
||||
: base($"Entity '{entityName}' with key '{key}' was not found.")
|
||||
{ }
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Domain.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository interface for Project aggregate
|
||||
/// </summary>
|
||||
public interface IProjectRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a project by its ID
|
||||
/// </summary>
|
||||
Task<Project?> GetByIdAsync(ProjectId id, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a project by its unique key
|
||||
/// </summary>
|
||||
Task<Project?> GetByKeyAsync(string key, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all projects with pagination
|
||||
/// </summary>
|
||||
Task<List<Project>> GetAllAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets project containing specific epic
|
||||
/// </summary>
|
||||
Task<Project?> GetProjectWithEpicAsync(EpicId epicId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets project containing specific story
|
||||
/// </summary>
|
||||
Task<Project?> GetProjectWithStoryAsync(StoryId storyId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets project containing specific task
|
||||
/// </summary>
|
||||
Task<Project?> GetProjectWithTaskAsync(TaskId taskId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new project
|
||||
/// </summary>
|
||||
Task AddAsync(Project project, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Updates an existing project
|
||||
/// </summary>
|
||||
void Update(Project project);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a project
|
||||
/// </summary>
|
||||
void Delete(Project project);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace ColaFlow.Modules.ProjectManagement.Domain.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Unit of Work pattern interface
|
||||
/// Coordinates the work of multiple repositories and ensures transactional consistency
|
||||
/// </summary>
|
||||
public interface IUnitOfWork
|
||||
{
|
||||
/// <summary>
|
||||
/// Saves all changes made in this unit of work to the database
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>The number of entities written to the database</returns>
|
||||
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using ColaFlow.Shared.Kernel.Common;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// EpicId Value Object (strongly-typed ID)
|
||||
/// </summary>
|
||||
public sealed class EpicId : ValueObject
|
||||
{
|
||||
public Guid Value { get; private set; }
|
||||
|
||||
private EpicId(Guid value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public static EpicId Create() => new EpicId(Guid.NewGuid());
|
||||
public static EpicId Create(Guid value) => new EpicId(value);
|
||||
public static EpicId From(Guid value) => new EpicId(value);
|
||||
|
||||
protected override IEnumerable<object> GetAtomicValues()
|
||||
{
|
||||
yield return Value;
|
||||
}
|
||||
|
||||
public override string ToString() => Value.ToString();
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using ColaFlow.Shared.Kernel.Common;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// ProjectId Value Object (strongly-typed ID)
|
||||
/// </summary>
|
||||
public sealed class ProjectId : ValueObject
|
||||
{
|
||||
public Guid Value { get; private set; }
|
||||
|
||||
private ProjectId(Guid value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public static ProjectId Create() => new ProjectId(Guid.NewGuid());
|
||||
public static ProjectId Create(Guid value) => new ProjectId(value);
|
||||
public static ProjectId From(Guid value) => new ProjectId(value);
|
||||
|
||||
protected override IEnumerable<object> GetAtomicValues()
|
||||
{
|
||||
yield return Value;
|
||||
}
|
||||
|
||||
public override string ToString() => Value.ToString();
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using ColaFlow.Shared.Kernel.Common;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Exceptions;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// ProjectKey Value Object (e.g., "COLA", "FLOW")
|
||||
/// </summary>
|
||||
public sealed class ProjectKey : ValueObject
|
||||
{
|
||||
public string Value { get; private set; }
|
||||
|
||||
private ProjectKey(string value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public static ProjectKey Create(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
throw new DomainException("Project key cannot be empty");
|
||||
|
||||
if (value.Length > 10)
|
||||
throw new DomainException("Project key cannot exceed 10 characters");
|
||||
|
||||
if (!System.Text.RegularExpressions.Regex.IsMatch(value, "^[A-Z0-9]+$"))
|
||||
throw new DomainException("Project key must contain only uppercase letters and numbers");
|
||||
|
||||
return new ProjectKey(value);
|
||||
}
|
||||
|
||||
protected override IEnumerable<object> GetAtomicValues()
|
||||
{
|
||||
yield return Value;
|
||||
}
|
||||
|
||||
public override string ToString() => Value;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using ColaFlow.Shared.Kernel.Common;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// ProjectStatus Enumeration
|
||||
/// </summary>
|
||||
public sealed class ProjectStatus : Enumeration
|
||||
{
|
||||
public static readonly ProjectStatus Active = new(1, "Active");
|
||||
public static readonly ProjectStatus Archived = new(2, "Archived");
|
||||
public static readonly ProjectStatus OnHold = new(3, "On Hold");
|
||||
|
||||
private ProjectStatus(int id, string name) : base(id, name)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using ColaFlow.Shared.Kernel.Common;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// StoryId Value Object (strongly-typed ID)
|
||||
/// </summary>
|
||||
public sealed class StoryId : ValueObject
|
||||
{
|
||||
public Guid Value { get; private set; }
|
||||
|
||||
private StoryId(Guid value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public static StoryId Create() => new StoryId(Guid.NewGuid());
|
||||
public static StoryId Create(Guid value) => new StoryId(value);
|
||||
public static StoryId From(Guid value) => new StoryId(value);
|
||||
|
||||
protected override IEnumerable<object> GetAtomicValues()
|
||||
{
|
||||
yield return Value;
|
||||
}
|
||||
|
||||
public override string ToString() => Value.ToString();
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using ColaFlow.Shared.Kernel.Common;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// TaskId Value Object (strongly-typed ID)
|
||||
/// </summary>
|
||||
public sealed class TaskId : ValueObject
|
||||
{
|
||||
public Guid Value { get; private set; }
|
||||
|
||||
private TaskId(Guid value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public static TaskId Create() => new TaskId(Guid.NewGuid());
|
||||
public static TaskId Create(Guid value) => new TaskId(value);
|
||||
public static TaskId From(Guid value) => new TaskId(value);
|
||||
|
||||
protected override IEnumerable<object> GetAtomicValues()
|
||||
{
|
||||
yield return Value;
|
||||
}
|
||||
|
||||
public override string ToString() => Value.ToString();
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using ColaFlow.Shared.Kernel.Common;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// TaskPriority Enumeration
|
||||
/// </summary>
|
||||
public sealed class TaskPriority : Enumeration
|
||||
{
|
||||
public static readonly TaskPriority Low = new(1, "Low");
|
||||
public static readonly TaskPriority Medium = new(2, "Medium");
|
||||
public static readonly TaskPriority High = new(3, "High");
|
||||
public static readonly TaskPriority Urgent = new(4, "Urgent");
|
||||
|
||||
private TaskPriority(int id, string name) : base(id, name)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using ColaFlow.Shared.Kernel.Common;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// UserId Value Object (strongly-typed ID)
|
||||
/// </summary>
|
||||
public sealed class UserId : ValueObject
|
||||
{
|
||||
public Guid Value { get; private set; }
|
||||
|
||||
private UserId(Guid value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public static UserId Create() => new UserId(Guid.NewGuid());
|
||||
public static UserId Create(Guid value) => new UserId(value);
|
||||
public static UserId From(Guid value) => new UserId(value);
|
||||
|
||||
protected override IEnumerable<object> GetAtomicValues()
|
||||
{
|
||||
yield return Value;
|
||||
}
|
||||
|
||||
public override string ToString() => Value.ToString();
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using ColaFlow.Shared.Kernel.Common;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// WorkItemStatus Enumeration (renamed from TaskStatus to avoid conflict with System.Threading.Tasks.TaskStatus)
|
||||
/// </summary>
|
||||
public sealed class WorkItemStatus : Enumeration
|
||||
{
|
||||
public static readonly WorkItemStatus ToDo = new(1, "To Do");
|
||||
public static readonly WorkItemStatus InProgress = new(2, "In Progress");
|
||||
public static readonly WorkItemStatus InReview = new(3, "In Review");
|
||||
public static readonly WorkItemStatus Done = new(4, "Done");
|
||||
public static readonly WorkItemStatus Blocked = new(5, "Blocked");
|
||||
|
||||
private WorkItemStatus(int id, string name) : base(id, name)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>ColaFlow.Modules.ProjectManagement.Infrastructure</AssemblyName>
|
||||
<RootNamespace>ColaFlow.Modules.ProjectManagement.Infrastructure</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ColaFlow.Modules.ProjectManagement.Domain\ColaFlow.Modules.ProjectManagement.Domain.csproj" />
|
||||
<ProjectReference Include="..\ColaFlow.Modules.ProjectManagement.Application\ColaFlow.Modules.ProjectManagement.Application.csproj" />
|
||||
<ProjectReference Include="..\..\..\Shared\ColaFlow.Shared.Kernel\ColaFlow.Shared.Kernel.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,298 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using ColaFlow.Modules.ProjectManagement.Infrastructure.Persistence;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Infrastructure.Migrations
|
||||
{
|
||||
[DbContext(typeof(PMDbContext))]
|
||||
[Migration("20251102220422_InitialCreate")]
|
||||
partial class InitialCreate
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("project_management")
|
||||
.HasAnnotation("ProductVersion", "9.0.0")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Epic", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("CreatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("character varying(2000)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<string>("Priority")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("ProjectId1")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.HasIndex("ProjectId");
|
||||
|
||||
b.HasIndex("ProjectId1");
|
||||
|
||||
b.ToTable("Epics", "project_management");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Project", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("character varying(2000)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<Guid>("OwnerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.HasIndex("OwnerId");
|
||||
|
||||
b.ToTable("Projects", "project_management");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Story", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<decimal?>("ActualHours")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<Guid?>("AssigneeId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("CreatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4000)
|
||||
.HasColumnType("character varying(4000)");
|
||||
|
||||
b.Property<Guid>("EpicId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<decimal?>("EstimatedHours")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<string>("Priority")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AssigneeId");
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.HasIndex("EpicId");
|
||||
|
||||
b.ToTable("Stories", "project_management");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.WorkTask", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<decimal?>("ActualHours")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<Guid?>("AssigneeId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("CreatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4000)
|
||||
.HasColumnType("character varying(4000)");
|
||||
|
||||
b.Property<decimal?>("EstimatedHours")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<string>("Priority")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<Guid>("StoryId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AssigneeId");
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.HasIndex("StoryId");
|
||||
|
||||
b.ToTable("Tasks", "project_management");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Epic", b =>
|
||||
{
|
||||
b.HasOne("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Project", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("ProjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Project", null)
|
||||
.WithMany("Epics")
|
||||
.HasForeignKey("ProjectId1");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Project", b =>
|
||||
{
|
||||
b.OwnsOne("ColaFlow.Modules.ProjectManagement.Domain.ValueObjects.ProjectKey", "Key", b1 =>
|
||||
{
|
||||
b1.Property<Guid>("ProjectId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("Value")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)")
|
||||
.HasColumnName("Key");
|
||||
|
||||
b1.HasKey("ProjectId");
|
||||
|
||||
b1.HasIndex("Value")
|
||||
.IsUnique();
|
||||
|
||||
b1.ToTable("Projects", "project_management");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("ProjectId");
|
||||
});
|
||||
|
||||
b.Navigation("Key")
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Story", b =>
|
||||
{
|
||||
b.HasOne("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Epic", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("EpicId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.WorkTask", b =>
|
||||
{
|
||||
b.HasOne("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Story", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("StoryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Project", b =>
|
||||
{
|
||||
b.Navigation("Epics");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class InitialCreate : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.EnsureSchema(
|
||||
name: "project_management");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Projects",
|
||||
schema: "project_management",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||
Description = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: false),
|
||||
Key = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
|
||||
Status = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||
OwnerId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Projects", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Epics",
|
||||
schema: "project_management",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||
Description = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: false),
|
||||
ProjectId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Status = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||
Priority = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
CreatedBy = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||
ProjectId1 = table.Column<Guid>(type: "uuid", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Epics", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Epics_Projects_ProjectId",
|
||||
column: x => x.ProjectId,
|
||||
principalSchema: "project_management",
|
||||
principalTable: "Projects",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_Epics_Projects_ProjectId1",
|
||||
column: x => x.ProjectId1,
|
||||
principalSchema: "project_management",
|
||||
principalTable: "Projects",
|
||||
principalColumn: "Id");
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Stories",
|
||||
schema: "project_management",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Title = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||
Description = table.Column<string>(type: "character varying(4000)", maxLength: 4000, nullable: false),
|
||||
EpicId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Status = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||
Priority = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||
EstimatedHours = table.Column<decimal>(type: "numeric", nullable: true),
|
||||
ActualHours = table.Column<decimal>(type: "numeric", nullable: true),
|
||||
AssigneeId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
CreatedBy = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Stories", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Stories_Epics_EpicId",
|
||||
column: x => x.EpicId,
|
||||
principalSchema: "project_management",
|
||||
principalTable: "Epics",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Tasks",
|
||||
schema: "project_management",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Title = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||
Description = table.Column<string>(type: "character varying(4000)", maxLength: 4000, nullable: false),
|
||||
StoryId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Status = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||
Priority = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||
EstimatedHours = table.Column<decimal>(type: "numeric", nullable: true),
|
||||
ActualHours = table.Column<decimal>(type: "numeric", nullable: true),
|
||||
AssigneeId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
CreatedBy = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Tasks", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Tasks_Stories_StoryId",
|
||||
column: x => x.StoryId,
|
||||
principalSchema: "project_management",
|
||||
principalTable: "Stories",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Epics_CreatedAt",
|
||||
schema: "project_management",
|
||||
table: "Epics",
|
||||
column: "CreatedAt");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Epics_ProjectId",
|
||||
schema: "project_management",
|
||||
table: "Epics",
|
||||
column: "ProjectId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Epics_ProjectId1",
|
||||
schema: "project_management",
|
||||
table: "Epics",
|
||||
column: "ProjectId1");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Projects_CreatedAt",
|
||||
schema: "project_management",
|
||||
table: "Projects",
|
||||
column: "CreatedAt");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Projects_Key",
|
||||
schema: "project_management",
|
||||
table: "Projects",
|
||||
column: "Key",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Projects_OwnerId",
|
||||
schema: "project_management",
|
||||
table: "Projects",
|
||||
column: "OwnerId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Stories_AssigneeId",
|
||||
schema: "project_management",
|
||||
table: "Stories",
|
||||
column: "AssigneeId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Stories_CreatedAt",
|
||||
schema: "project_management",
|
||||
table: "Stories",
|
||||
column: "CreatedAt");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Stories_EpicId",
|
||||
schema: "project_management",
|
||||
table: "Stories",
|
||||
column: "EpicId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Tasks_AssigneeId",
|
||||
schema: "project_management",
|
||||
table: "Tasks",
|
||||
column: "AssigneeId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Tasks_CreatedAt",
|
||||
schema: "project_management",
|
||||
table: "Tasks",
|
||||
column: "CreatedAt");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Tasks_StoryId",
|
||||
schema: "project_management",
|
||||
table: "Tasks",
|
||||
column: "StoryId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Tasks",
|
||||
schema: "project_management");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Stories",
|
||||
schema: "project_management");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Epics",
|
||||
schema: "project_management");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Projects",
|
||||
schema: "project_management");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using ColaFlow.Modules.ProjectManagement.Infrastructure.Persistence;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Infrastructure.Migrations
|
||||
{
|
||||
[DbContext(typeof(PMDbContext))]
|
||||
partial class PMDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("project_management")
|
||||
.HasAnnotation("ProductVersion", "9.0.0")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Epic", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("CreatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("character varying(2000)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<string>("Priority")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("ProjectId1")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.HasIndex("ProjectId");
|
||||
|
||||
b.HasIndex("ProjectId1");
|
||||
|
||||
b.ToTable("Epics", "project_management");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Project", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("character varying(2000)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<Guid>("OwnerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.HasIndex("OwnerId");
|
||||
|
||||
b.ToTable("Projects", "project_management");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Story", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<decimal?>("ActualHours")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<Guid?>("AssigneeId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("CreatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4000)
|
||||
.HasColumnType("character varying(4000)");
|
||||
|
||||
b.Property<Guid>("EpicId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<decimal?>("EstimatedHours")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<string>("Priority")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AssigneeId");
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.HasIndex("EpicId");
|
||||
|
||||
b.ToTable("Stories", "project_management");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.WorkTask", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<decimal?>("ActualHours")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<Guid?>("AssigneeId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("CreatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4000)
|
||||
.HasColumnType("character varying(4000)");
|
||||
|
||||
b.Property<decimal?>("EstimatedHours")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<string>("Priority")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<Guid>("StoryId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AssigneeId");
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.HasIndex("StoryId");
|
||||
|
||||
b.ToTable("Tasks", "project_management");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Epic", b =>
|
||||
{
|
||||
b.HasOne("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Project", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("ProjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Project", null)
|
||||
.WithMany("Epics")
|
||||
.HasForeignKey("ProjectId1");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Project", b =>
|
||||
{
|
||||
b.OwnsOne("ColaFlow.Modules.ProjectManagement.Domain.ValueObjects.ProjectKey", "Key", b1 =>
|
||||
{
|
||||
b1.Property<Guid>("ProjectId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("Value")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)")
|
||||
.HasColumnName("Key");
|
||||
|
||||
b1.HasKey("ProjectId");
|
||||
|
||||
b1.HasIndex("Value")
|
||||
.IsUnique();
|
||||
|
||||
b1.ToTable("Projects", "project_management");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("ProjectId");
|
||||
});
|
||||
|
||||
b.Navigation("Key")
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Story", b =>
|
||||
{
|
||||
b.HasOne("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Epic", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("EpicId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.WorkTask", b =>
|
||||
{
|
||||
b.HasOne("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Story", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("StoryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Project", b =>
|
||||
{
|
||||
b.Navigation("Epics");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
using ColaFlow.Shared.Kernel.Common;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Infrastructure.Persistence.Configurations;
|
||||
|
||||
/// <summary>
|
||||
/// Entity configuration for Epic entity
|
||||
/// </summary>
|
||||
public class EpicConfiguration : IEntityTypeConfiguration<Epic>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Epic> builder)
|
||||
{
|
||||
builder.ToTable("Epics");
|
||||
|
||||
// Primary key
|
||||
builder.HasKey("Id");
|
||||
|
||||
// Id conversion
|
||||
builder.Property(e => e.Id)
|
||||
.HasConversion(
|
||||
id => id.Value,
|
||||
value => EpicId.From(value))
|
||||
.IsRequired()
|
||||
.ValueGeneratedNever();
|
||||
|
||||
// ProjectId (foreign key)
|
||||
builder.Property(e => e.ProjectId)
|
||||
.HasConversion(
|
||||
id => id.Value,
|
||||
value => ProjectId.From(value))
|
||||
.IsRequired();
|
||||
|
||||
// Basic properties
|
||||
builder.Property(e => e.Name)
|
||||
.HasMaxLength(200)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(e => e.Description)
|
||||
.HasMaxLength(2000);
|
||||
|
||||
// Status enumeration
|
||||
builder.Property(e => e.Status)
|
||||
.HasConversion(
|
||||
s => s.Name,
|
||||
name => Enumeration.FromDisplayName<WorkItemStatus>(name))
|
||||
.HasMaxLength(50)
|
||||
.IsRequired();
|
||||
|
||||
// Priority enumeration
|
||||
builder.Property(e => e.Priority)
|
||||
.HasConversion(
|
||||
p => p.Name,
|
||||
name => Enumeration.FromDisplayName<TaskPriority>(name))
|
||||
.HasMaxLength(50)
|
||||
.IsRequired();
|
||||
|
||||
// CreatedBy conversion
|
||||
builder.Property(e => e.CreatedBy)
|
||||
.HasConversion(
|
||||
id => id.Value,
|
||||
value => UserId.From(value))
|
||||
.IsRequired();
|
||||
|
||||
// Timestamps
|
||||
builder.Property(e => e.CreatedAt)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(e => e.UpdatedAt);
|
||||
|
||||
// Ignore navigation properties (DDD pattern - access through aggregate)
|
||||
builder.Ignore(e => e.Stories);
|
||||
|
||||
// Foreign key relationship to Project
|
||||
builder.HasOne<Project>()
|
||||
.WithMany()
|
||||
.HasForeignKey(e => e.ProjectId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
// Indexes
|
||||
builder.HasIndex(e => e.ProjectId);
|
||||
builder.HasIndex(e => e.CreatedAt);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
using ColaFlow.Shared.Kernel.Common;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Infrastructure.Persistence.Configurations;
|
||||
|
||||
/// <summary>
|
||||
/// Entity configuration for Project aggregate root
|
||||
/// </summary>
|
||||
public class ProjectConfiguration : IEntityTypeConfiguration<Project>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Project> builder)
|
||||
{
|
||||
builder.ToTable("Projects");
|
||||
|
||||
// Primary key
|
||||
builder.HasKey(p => p.Id);
|
||||
|
||||
// Id conversion (StronglyTypedId to Guid)
|
||||
builder.Property(p => p.Id)
|
||||
.HasConversion(
|
||||
id => id.Value,
|
||||
value => ProjectId.From(value))
|
||||
.IsRequired()
|
||||
.ValueGeneratedNever();
|
||||
|
||||
// Basic properties
|
||||
builder.Property(p => p.Name)
|
||||
.HasMaxLength(200)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(p => p.Description)
|
||||
.HasMaxLength(2000);
|
||||
|
||||
// ProjectKey as owned value object
|
||||
builder.OwnsOne(p => p.Key, kb =>
|
||||
{
|
||||
kb.Property(k => k.Value)
|
||||
.HasColumnName("Key")
|
||||
.HasMaxLength(20)
|
||||
.IsRequired();
|
||||
|
||||
kb.HasIndex(k => k.Value).IsUnique();
|
||||
});
|
||||
|
||||
// Status enumeration (stored as string)
|
||||
builder.Property(p => p.Status)
|
||||
.HasConversion(
|
||||
s => s.Name,
|
||||
name => Enumeration.FromDisplayName<ProjectStatus>(name))
|
||||
.HasMaxLength(50)
|
||||
.IsRequired();
|
||||
|
||||
// OwnerId conversion
|
||||
builder.Property(p => p.OwnerId)
|
||||
.HasConversion(
|
||||
id => id.Value,
|
||||
value => UserId.From(value))
|
||||
.IsRequired();
|
||||
|
||||
// Timestamps
|
||||
builder.Property(p => p.CreatedAt)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(p => p.UpdatedAt);
|
||||
|
||||
// Relationships - Epics collection (owned by aggregate)
|
||||
// Note: We don't expose this as navigation property in DDD, epics are accessed through repository
|
||||
|
||||
// Indexes for performance
|
||||
builder.HasIndex(p => p.CreatedAt);
|
||||
builder.HasIndex(p => p.OwnerId);
|
||||
|
||||
// Ignore DomainEvents (handled separately)
|
||||
builder.Ignore(p => p.DomainEvents);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
using ColaFlow.Shared.Kernel.Common;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Infrastructure.Persistence.Configurations;
|
||||
|
||||
/// <summary>
|
||||
/// Entity configuration for Story entity
|
||||
/// </summary>
|
||||
public class StoryConfiguration : IEntityTypeConfiguration<Story>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Story> builder)
|
||||
{
|
||||
builder.ToTable("Stories");
|
||||
|
||||
// Primary key
|
||||
builder.HasKey("Id");
|
||||
|
||||
// Id conversion
|
||||
builder.Property(s => s.Id)
|
||||
.HasConversion(
|
||||
id => id.Value,
|
||||
value => StoryId.From(value))
|
||||
.IsRequired()
|
||||
.ValueGeneratedNever();
|
||||
|
||||
// EpicId (foreign key)
|
||||
builder.Property(s => s.EpicId)
|
||||
.HasConversion(
|
||||
id => id.Value,
|
||||
value => EpicId.From(value))
|
||||
.IsRequired();
|
||||
|
||||
// Basic properties
|
||||
builder.Property(s => s.Title)
|
||||
.HasMaxLength(200)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(s => s.Description)
|
||||
.HasMaxLength(4000);
|
||||
|
||||
// Status enumeration
|
||||
builder.Property(s => s.Status)
|
||||
.HasConversion(
|
||||
st => st.Name,
|
||||
name => Enumeration.FromDisplayName<WorkItemStatus>(name))
|
||||
.HasMaxLength(50)
|
||||
.IsRequired();
|
||||
|
||||
// Priority enumeration
|
||||
builder.Property(s => s.Priority)
|
||||
.HasConversion(
|
||||
p => p.Name,
|
||||
name => Enumeration.FromDisplayName<TaskPriority>(name))
|
||||
.HasMaxLength(50)
|
||||
.IsRequired();
|
||||
|
||||
// CreatedBy conversion
|
||||
builder.Property(s => s.CreatedBy)
|
||||
.HasConversion(
|
||||
id => id.Value,
|
||||
value => UserId.From(value))
|
||||
.IsRequired();
|
||||
|
||||
// AssigneeId (optional)
|
||||
builder.Property(s => s.AssigneeId)
|
||||
.HasConversion(
|
||||
id => id != null ? id.Value : (Guid?)null,
|
||||
value => value.HasValue ? UserId.From(value.Value) : null);
|
||||
|
||||
// Effort tracking
|
||||
builder.Property(s => s.EstimatedHours);
|
||||
builder.Property(s => s.ActualHours);
|
||||
|
||||
// Timestamps
|
||||
builder.Property(s => s.CreatedAt)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(s => s.UpdatedAt);
|
||||
|
||||
// Ignore navigation properties (DDD pattern - access through aggregate)
|
||||
builder.Ignore(s => s.Tasks);
|
||||
|
||||
// Foreign key relationship to Epic
|
||||
builder.HasOne<Epic>()
|
||||
.WithMany()
|
||||
.HasForeignKey(s => s.EpicId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
// Indexes
|
||||
builder.HasIndex(s => s.EpicId);
|
||||
builder.HasIndex(s => s.AssigneeId);
|
||||
builder.HasIndex(s => s.CreatedAt);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
using ColaFlow.Shared.Kernel.Common;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Infrastructure.Persistence.Configurations;
|
||||
|
||||
/// <summary>
|
||||
/// Entity configuration for WorkTask entity
|
||||
/// </summary>
|
||||
public class WorkTaskConfiguration : IEntityTypeConfiguration<WorkTask>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<WorkTask> builder)
|
||||
{
|
||||
builder.ToTable("Tasks");
|
||||
|
||||
// Primary key
|
||||
builder.HasKey("Id");
|
||||
|
||||
// Id conversion
|
||||
builder.Property(t => t.Id)
|
||||
.HasConversion(
|
||||
id => id.Value,
|
||||
value => TaskId.From(value))
|
||||
.IsRequired()
|
||||
.ValueGeneratedNever();
|
||||
|
||||
// StoryId (foreign key)
|
||||
builder.Property(t => t.StoryId)
|
||||
.HasConversion(
|
||||
id => id.Value,
|
||||
value => StoryId.From(value))
|
||||
.IsRequired();
|
||||
|
||||
// Basic properties
|
||||
builder.Property(t => t.Title)
|
||||
.HasMaxLength(200)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(t => t.Description)
|
||||
.HasMaxLength(4000);
|
||||
|
||||
// Status enumeration
|
||||
builder.Property(t => t.Status)
|
||||
.HasConversion(
|
||||
s => s.Name,
|
||||
name => Enumeration.FromDisplayName<WorkItemStatus>(name))
|
||||
.HasMaxLength(50)
|
||||
.IsRequired();
|
||||
|
||||
// Priority enumeration
|
||||
builder.Property(t => t.Priority)
|
||||
.HasConversion(
|
||||
p => p.Name,
|
||||
name => Enumeration.FromDisplayName<TaskPriority>(name))
|
||||
.HasMaxLength(50)
|
||||
.IsRequired();
|
||||
|
||||
// CreatedBy conversion
|
||||
builder.Property(t => t.CreatedBy)
|
||||
.HasConversion(
|
||||
id => id.Value,
|
||||
value => UserId.From(value))
|
||||
.IsRequired();
|
||||
|
||||
// AssigneeId (optional)
|
||||
builder.Property(t => t.AssigneeId)
|
||||
.HasConversion(
|
||||
id => id != null ? id.Value : (Guid?)null,
|
||||
value => value.HasValue ? UserId.From(value.Value) : null);
|
||||
|
||||
// Effort tracking
|
||||
builder.Property(t => t.EstimatedHours);
|
||||
builder.Property(t => t.ActualHours);
|
||||
|
||||
// Timestamps
|
||||
builder.Property(t => t.CreatedAt)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(t => t.UpdatedAt);
|
||||
|
||||
// Foreign key relationship to Story
|
||||
builder.HasOne<Story>()
|
||||
.WithMany()
|
||||
.HasForeignKey(t => t.StoryId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
// Indexes
|
||||
builder.HasIndex(t => t.StoryId);
|
||||
builder.HasIndex(t => t.AssigneeId);
|
||||
builder.HasIndex(t => t.CreatedAt);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System.Reflection;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Infrastructure.Persistence;
|
||||
|
||||
/// <summary>
|
||||
/// Project Management Module DbContext
|
||||
/// </summary>
|
||||
public class PMDbContext : DbContext
|
||||
{
|
||||
public PMDbContext(DbContextOptions<PMDbContext> options) : base(options)
|
||||
{
|
||||
}
|
||||
|
||||
public DbSet<Project> Projects => Set<Project>();
|
||||
public DbSet<Epic> Epics => Set<Epic>();
|
||||
public DbSet<Story> Stories => Set<Story>();
|
||||
public DbSet<WorkTask> Tasks => Set<WorkTask>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
// Set default schema for this module (must be before configurations)
|
||||
modelBuilder.HasDefaultSchema("project_management");
|
||||
|
||||
// Apply all entity configurations from this assembly
|
||||
modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
|
||||
using ColaFlow.Shared.Kernel.Common;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Infrastructure.Persistence;
|
||||
|
||||
/// <summary>
|
||||
/// Unit of Work implementation for ProjectManagement module
|
||||
/// </summary>
|
||||
public class UnitOfWork : IUnitOfWork
|
||||
{
|
||||
private readonly PMDbContext _context;
|
||||
|
||||
public UnitOfWork(PMDbContext context)
|
||||
{
|
||||
_context = context ?? throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
public async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Dispatch domain events before saving
|
||||
await DispatchDomainEventsAsync(cancellationToken);
|
||||
|
||||
// Save changes to database
|
||||
return await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private async Task DispatchDomainEventsAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// Get all entities with domain events
|
||||
var domainEntities = _context.ChangeTracker
|
||||
.Entries<AggregateRoot>()
|
||||
.Where(x => x.Entity.DomainEvents.Any())
|
||||
.Select(x => x.Entity)
|
||||
.ToList();
|
||||
|
||||
// Get all domain events
|
||||
var domainEvents = domainEntities
|
||||
.SelectMany(x => x.DomainEvents)
|
||||
.ToList();
|
||||
|
||||
// Clear domain events from entities
|
||||
domainEntities.ForEach(entity => entity.ClearDomainEvents());
|
||||
|
||||
// TODO: Dispatch domain events to handlers
|
||||
// This will be implemented when we add MediatR
|
||||
// For now, we just clear the events
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
using ColaFlow.Modules.ProjectManagement.Infrastructure.Persistence;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Infrastructure.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Project repository implementation using EF Core
|
||||
/// </summary>
|
||||
public class ProjectRepository : IProjectRepository
|
||||
{
|
||||
private readonly PMDbContext _context;
|
||||
|
||||
public ProjectRepository(PMDbContext context)
|
||||
{
|
||||
_context = context ?? throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
public async Task<Project?> GetByIdAsync(ProjectId id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Projects
|
||||
.Include(p => p.Epics)
|
||||
.FirstOrDefaultAsync(p => p.Id == id, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<Project?> GetByKeyAsync(string key, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Projects
|
||||
.FirstOrDefaultAsync(p => p.Key.Value == key, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<List<Project>> GetAllAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Projects
|
||||
.OrderByDescending(p => p.CreatedAt)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task AddAsync(Project project, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _context.Projects.AddAsync(project, cancellationToken);
|
||||
}
|
||||
|
||||
public void Update(Project project)
|
||||
{
|
||||
_context.Projects.Update(project);
|
||||
}
|
||||
|
||||
public void Delete(Project project)
|
||||
{
|
||||
_context.Projects.Remove(project);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using ColaFlow.Shared.Kernel.Modules;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.Behaviors;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.Commands.CreateProject;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
|
||||
using ColaFlow.Modules.ProjectManagement.Infrastructure.Persistence;
|
||||
using ColaFlow.Modules.ProjectManagement.Infrastructure.Repositories;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement;
|
||||
|
||||
/// <summary>
|
||||
/// Project Management Module
|
||||
/// Responsible for managing projects, epics, stories, and tasks.
|
||||
/// </summary>
|
||||
public class ProjectManagementModule : IModule
|
||||
{
|
||||
public string Name => "ProjectManagement";
|
||||
|
||||
public void RegisterServices(IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
// Register DbContext
|
||||
var connectionString = configuration.GetConnectionString("PMDatabase");
|
||||
services.AddDbContext<PMDbContext>(options =>
|
||||
options.UseNpgsql(connectionString));
|
||||
|
||||
// Register repositories
|
||||
services.AddScoped<IProjectRepository, ProjectRepository>();
|
||||
services.AddScoped<IUnitOfWork, UnitOfWork>();
|
||||
|
||||
// Register MediatR handlers from Application assembly
|
||||
services.AddMediatR(cfg =>
|
||||
{
|
||||
cfg.RegisterServicesFromAssembly(typeof(CreateProjectCommand).Assembly);
|
||||
});
|
||||
|
||||
// Register FluentValidation validators
|
||||
services.AddValidatorsFromAssembly(typeof(CreateProjectCommand).Assembly);
|
||||
|
||||
// Register pipeline behaviors
|
||||
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
|
||||
|
||||
Console.WriteLine($"[{Name}] Module services registered");
|
||||
}
|
||||
|
||||
public void ConfigureApplication(IApplicationBuilder app)
|
||||
{
|
||||
// Configure module-specific middleware if needed
|
||||
Console.WriteLine($"[{Name}] Module application configured");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,31 @@
|
||||
using ColaFlow.Shared.Kernel.Events;
|
||||
|
||||
namespace ColaFlow.Shared.Kernel.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for all aggregate roots
|
||||
/// </summary>
|
||||
public abstract class AggregateRoot : Entity
|
||||
{
|
||||
private readonly List<DomainEvent> _domainEvents = new();
|
||||
|
||||
public IReadOnlyCollection<DomainEvent> DomainEvents => _domainEvents.AsReadOnly();
|
||||
|
||||
protected AggregateRoot() : base()
|
||||
{
|
||||
}
|
||||
|
||||
protected AggregateRoot(Guid id) : base(id)
|
||||
{
|
||||
}
|
||||
|
||||
protected void AddDomainEvent(DomainEvent domainEvent)
|
||||
{
|
||||
_domainEvents.Add(domainEvent);
|
||||
}
|
||||
|
||||
public void ClearDomainEvents()
|
||||
{
|
||||
_domainEvents.Clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
namespace ColaFlow.Shared.Kernel.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for all entities
|
||||
/// </summary>
|
||||
public abstract class Entity
|
||||
{
|
||||
public Guid Id { get; protected set; }
|
||||
|
||||
protected Entity()
|
||||
{
|
||||
Id = Guid.NewGuid();
|
||||
}
|
||||
|
||||
protected Entity(Guid id)
|
||||
{
|
||||
Id = id;
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj is not Entity other)
|
||||
return false;
|
||||
|
||||
if (ReferenceEquals(this, other))
|
||||
return true;
|
||||
|
||||
if (GetType() != other.GetType())
|
||||
return false;
|
||||
|
||||
return Id == other.Id;
|
||||
}
|
||||
|
||||
public static bool operator ==(Entity? a, Entity? b)
|
||||
{
|
||||
if (a is null && b is null)
|
||||
return true;
|
||||
|
||||
if (a is null || b is null)
|
||||
return false;
|
||||
|
||||
return a.Equals(b);
|
||||
}
|
||||
|
||||
public static bool operator !=(Entity? a, Entity? b)
|
||||
{
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return Id.GetHashCode();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using System.Reflection;
|
||||
|
||||
namespace ColaFlow.Shared.Kernel.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for creating type-safe enumerations
|
||||
/// </summary>
|
||||
public abstract class Enumeration : IComparable
|
||||
{
|
||||
public int Id { get; private set; }
|
||||
public string Name { get; private set; }
|
||||
|
||||
protected Enumeration(int id, string name)
|
||||
{
|
||||
Id = id;
|
||||
Name = name;
|
||||
}
|
||||
|
||||
public override string ToString() => Name;
|
||||
|
||||
public static IEnumerable<T> GetAll<T>() where T : Enumeration
|
||||
{
|
||||
var fields = typeof(T).GetFields(BindingFlags.Public |
|
||||
BindingFlags.Static |
|
||||
BindingFlags.DeclaredOnly);
|
||||
|
||||
return fields.Select(f => f.GetValue(null)).Cast<T>();
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj is not Enumeration otherValue)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var typeMatches = GetType().Equals(obj.GetType());
|
||||
var valueMatches = Id.Equals(otherValue.Id);
|
||||
|
||||
return typeMatches && valueMatches;
|
||||
}
|
||||
|
||||
public override int GetHashCode() => Id.GetHashCode();
|
||||
|
||||
public static int AbsoluteDifference(Enumeration firstValue, Enumeration secondValue)
|
||||
{
|
||||
var absoluteDifference = Math.Abs(firstValue.Id - secondValue.Id);
|
||||
return absoluteDifference;
|
||||
}
|
||||
|
||||
public static T FromValue<T>(int value) where T : Enumeration
|
||||
{
|
||||
var matchingItem = Parse<T, int>(value, "value", item => item.Id == value);
|
||||
return matchingItem;
|
||||
}
|
||||
|
||||
public static T FromDisplayName<T>(string displayName) where T : Enumeration
|
||||
{
|
||||
var matchingItem = Parse<T, string>(displayName, "display name", item => item.Name == displayName);
|
||||
return matchingItem;
|
||||
}
|
||||
|
||||
private static T Parse<T, K>(K value, string description, Func<T, bool> predicate) where T : Enumeration
|
||||
{
|
||||
var matchingItem = GetAll<T>().FirstOrDefault(predicate);
|
||||
|
||||
if (matchingItem == null)
|
||||
throw new InvalidOperationException($"'{value}' is not a valid {description} in {typeof(T)}");
|
||||
|
||||
return matchingItem;
|
||||
}
|
||||
|
||||
public int CompareTo(object? other)
|
||||
{
|
||||
if (other == null) return 1;
|
||||
return Id.CompareTo(((Enumeration)other).Id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
namespace ColaFlow.Shared.Kernel.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for all value objects
|
||||
/// </summary>
|
||||
public abstract class ValueObject
|
||||
{
|
||||
protected abstract IEnumerable<object> GetAtomicValues();
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj == null || obj.GetType() != GetType())
|
||||
return false;
|
||||
|
||||
var other = (ValueObject)obj;
|
||||
return GetAtomicValues().SequenceEqual(other.GetAtomicValues());
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return GetAtomicValues()
|
||||
.Aggregate(1, (current, obj) =>
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
return (current * 23) + (obj?.GetHashCode() ?? 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static bool operator ==(ValueObject? a, ValueObject? b)
|
||||
{
|
||||
if (a is null && b is null)
|
||||
return true;
|
||||
|
||||
if (a is null || b is null)
|
||||
return false;
|
||||
|
||||
return a.Equals(b);
|
||||
}
|
||||
|
||||
public static bool operator !=(ValueObject? a, ValueObject? b)
|
||||
{
|
||||
return !(a == b);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace ColaFlow.Shared.Kernel.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for all domain events
|
||||
/// </summary>
|
||||
public abstract record DomainEvent
|
||||
{
|
||||
public Guid EventId { get; init; } = Guid.NewGuid();
|
||||
public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace ColaFlow.Shared.Kernel.Modules;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the contract for a modular component in ColaFlow.
|
||||
/// Each module is responsible for registering its own services and configuring its middleware.
|
||||
/// </summary>
|
||||
public interface IModule
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the unique name of the module.
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Registers module-specific services with the dependency injection container.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection to register services with.</param>
|
||||
/// <param name="configuration">The application configuration.</param>
|
||||
void RegisterServices(IServiceCollection services, IConfiguration configuration);
|
||||
|
||||
/// <summary>
|
||||
/// Configures module-specific middleware and application behaviors.
|
||||
/// </summary>
|
||||
/// <param name="app">The application builder.</param>
|
||||
void ConfigureApplication(IApplicationBuilder app);
|
||||
}
|
||||
Reference in New Issue
Block a user