Project Init
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user