diff --git a/DadmehrGostar.sln b/DadmehrGostar.sln
index 342cf0ef..04c70593 100644
--- a/DadmehrGostar.sln
+++ b/DadmehrGostar.sln
@@ -89,6 +89,9 @@ EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BackgroundInstitutionContract.Task", "BackgroundInstitutionContract\BackgroundInstitutionContract.Task\BackgroundInstitutionContract.Task.csproj", "{F78FBB92-294B-88BA-168D-F0C578B0D7D6}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ProgramManager", "ProgramManager", "{67AFF7B6-4C4F-464C-A90D-9BDB644D83A9}"
+ ProjectSection(SolutionItems) = preProject
+ ProgramManager\TASKCHAT_ARCHITECTURE.md = ProgramManager\TASKCHAT_ARCHITECTURE.md
+ EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{48F6F6A5-7340-42F8-9216-BEB7A4B7D5A1}"
EndProject
diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Entities/TaskChatMessage.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Entities/TaskChatMessage.cs
new file mode 100644
index 00000000..b132ac62
--- /dev/null
+++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Entities/TaskChatMessage.cs
@@ -0,0 +1,200 @@
+using GozareshgirProgramManager.Domain._Common;
+using GozareshgirProgramManager.Domain._Common.Exceptions;
+using GozareshgirProgramManager.Domain.TaskChatAgg.Events;
+using MessageType = GozareshgirProgramManager.Domain.TaskChatAgg.Enums.MessageType;
+
+namespace GozareshgirProgramManager.Domain.TaskChatAgg.Entities;
+
+///
+/// پیام چت تسک - Aggregate Root
+/// هر کسی که به تسک دسترسی داشته باشد میتواند پیامها را ببیند و ارسال کند
+/// نیازی به مدیریت گروه و ممبر نیست چون دسترسی از طریق خود تسک کنترل میشود
+///
+public class TaskChatMessage : EntityBase
+{
+ private TaskChatMessage()
+ {
+ }
+
+ public TaskChatMessage(Guid taskId, long senderUserId, MessageType messageType, string? textContent = null)
+ {
+ TaskId = taskId;
+ SenderUserId = senderUserId;
+ MessageType = messageType;
+ TextContent = textContent;
+ IsEdited = false;
+ IsDeleted = false;
+ IsPinned = false;
+
+ ValidateMessage();
+ AddDomainEvent(new TaskChatMessageSentEvent(Id, taskId, senderUserId, messageType));
+ }
+
+ // Reference به Task (Foreign Key فقط - بدون Navigation Property برای جلوگیری از coupling)
+ public Guid TaskId { get; private set; }
+
+ public long SenderUserId { get; private set; }
+
+ public MessageType MessageType { get; private set; }
+
+ // محتوای متنی (برای پیامهای Text و Caption برای فایل/تصویر)
+ public string? TextContent { get; private set; }
+
+ // اطلاعات فایل (برای پیامهای File, Voice, Image, Video)
+ public string? FileUrl { get; private set; }
+ public string? FileName { get; private set; }
+ public long? FileSizeBytes { get; private set; }
+ public string? FileMimeType { get; private set; }
+
+ // اطلاعات صوت (برای Voice)
+ public int? VoiceDurationSeconds { get; private set; }
+
+ // پیام Reply
+ public Guid? ReplyToMessageId { get; private set; }
+ public TaskChatMessage? ReplyToMessage { get; private set; }
+
+ // وضعیت پیام
+ public bool IsEdited { get; private set; }
+ public DateTime? EditedDate { get; private set; }
+ public bool IsDeleted { get; private set; }
+ public DateTime? DeletedDate { get; private set; }
+ public bool IsPinned { get; private set; }
+ public DateTime? PinnedDate { get; private set; }
+ public long? PinnedByUserId { get; private set; }
+
+ private void ValidateMessage()
+ {
+ if (MessageType == MessageType.Text && string.IsNullOrWhiteSpace(TextContent))
+ {
+ throw new BadRequestException("پیام متنی نمیتواند خالی باشد");
+ }
+
+ if ((MessageType == MessageType.File || MessageType == MessageType.Voice ||
+ MessageType == MessageType.Image || MessageType == MessageType.Video)
+ && string.IsNullOrWhiteSpace(FileUrl))
+ {
+ throw new BadRequestException("آدرس فایل نمیتواند خالی باشد");
+ }
+
+ if (MessageType == MessageType.Voice && VoiceDurationSeconds == null)
+ {
+ throw new BadRequestException("مدت زمان صدا باید مشخص شود");
+ }
+ }
+
+ public void SetFileInfo(string fileUrl, string fileName, long fileSizeBytes, string mimeType)
+ {
+ if (MessageType != MessageType.File && MessageType != MessageType.Image &&
+ MessageType != MessageType.Video && MessageType != MessageType.Voice)
+ {
+ throw new BadRequestException("فقط میتوان برای پیامهای فایل، تصویر، ویدیو و صدا اطلاعات فایل تنظیم کرد");
+ }
+
+ FileUrl = fileUrl;
+ FileName = fileName;
+ FileSizeBytes = fileSizeBytes;
+ FileMimeType = mimeType;
+ }
+
+ public void SetVoiceDuration(int durationSeconds)
+ {
+ if (MessageType != MessageType.Voice)
+ {
+ throw new BadRequestException("فقط میتوان برای پیامهای صوتی مدت زمان تنظیم کرد");
+ }
+
+ if (durationSeconds <= 0)
+ {
+ throw new BadRequestException("مدت زمان صدا باید بیشتر از صفر باشد");
+ }
+
+ VoiceDurationSeconds = durationSeconds;
+ }
+
+ public void EditMessage(string newTextContent, long editorUserId)
+ {
+ if (IsDeleted)
+ {
+ throw new BadRequestException("نمیتوان پیام حذف شده را ویرایش کرد");
+ }
+
+ if (editorUserId != SenderUserId)
+ {
+ throw new BadRequestException("فقط فرستنده میتواند پیام را ویرایش کند");
+ }
+
+ if (MessageType != MessageType.Text)
+ {
+ throw new BadRequestException("فقط پیامهای متنی قابل ویرایش هستند");
+ }
+
+ if (string.IsNullOrWhiteSpace(newTextContent))
+ {
+ throw new BadRequestException("محتوای پیام نمیتواند خالی باشد");
+ }
+
+ TextContent = newTextContent;
+ IsEdited = true;
+ EditedDate = DateTime.Now;
+ AddDomainEvent(new TaskChatMessageEditedEvent(Id, TaskId, editorUserId));
+ }
+
+ public void DeleteMessage(long deleterUserId)
+ {
+ if (IsDeleted)
+ {
+ throw new BadRequestException("پیام قبلاً حذف شده است");
+ }
+
+ if (deleterUserId != SenderUserId)
+ {
+ throw new BadRequestException("فقط فرستنده میتواند پیام را حذف کند");
+ }
+
+ IsDeleted = true;
+ DeletedDate = DateTime.Now;
+ AddDomainEvent(new TaskChatMessageDeletedEvent(Id, TaskId, deleterUserId));
+ }
+
+ public void PinMessage(long pinnerUserId)
+ {
+ if (IsDeleted)
+ {
+ throw new BadRequestException("نمیتوان پیام حذف شده را پین کرد");
+ }
+
+ if (IsPinned)
+ {
+ throw new BadRequestException("این پیام قبلاً پین شده است");
+ }
+
+ IsPinned = true;
+ PinnedDate = DateTime.Now;
+ PinnedByUserId = pinnerUserId;
+ AddDomainEvent(new TaskChatMessagePinnedEvent(Id, TaskId, pinnerUserId));
+ }
+
+ public void UnpinMessage(long unpinnerUserId)
+ {
+ if (!IsPinned)
+ {
+ throw new BadRequestException("این پیام پین نشده است");
+ }
+
+ IsPinned = false;
+ PinnedDate = null;
+ PinnedByUserId = null;
+ AddDomainEvent(new TaskChatMessageUnpinnedEvent(Id, TaskId, unpinnerUserId));
+ }
+
+ public void SetReplyTo(Guid replyToMessageId)
+ {
+ ReplyToMessageId = replyToMessageId;
+ }
+
+ public bool IsSentBy(long userId)
+ {
+ return SenderUserId == userId;
+ }
+}
+
diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Enums/MessageType.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Enums/MessageType.cs
new file mode 100644
index 00000000..4b30dcd4
--- /dev/null
+++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Enums/MessageType.cs
@@ -0,0 +1,14 @@
+namespace GozareshgirProgramManager.Domain.TaskChatAgg.Enums;
+
+///
+/// نوع پیام در چت تسک
+///
+public enum MessageType
+{
+ Text = 1, // پیام متنی
+ File = 2, // فایل (اسناد، PDF، و غیره)
+ Image = 3, // تصویر
+ Voice = 4, // پیام صوتی
+ Video = 5, // ویدیو
+}
+
diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Events/TaskChatEvents.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Events/TaskChatEvents.cs
new file mode 100644
index 00000000..9493fa55
--- /dev/null
+++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Events/TaskChatEvents.cs
@@ -0,0 +1,31 @@
+using GozareshgirProgramManager.Domain._Common;
+using GozareshgirProgramManager.Domain.TaskChatAgg.Enums;
+
+namespace GozareshgirProgramManager.Domain.TaskChatAgg.Events;
+
+// Message Events
+public record TaskChatMessageSentEvent(Guid MessageId, Guid TaskId, long SenderUserId, MessageType MessageType) : IDomainEvent
+{
+ public DateTime OccurredOn { get; init; } = DateTime.Now;
+}
+
+public record TaskChatMessageEditedEvent(Guid MessageId, Guid TaskId, long EditorUserId) : IDomainEvent
+{
+ public DateTime OccurredOn { get; init; } = DateTime.Now;
+}
+
+public record TaskChatMessageDeletedEvent(Guid MessageId, Guid TaskId, long DeleterUserId) : IDomainEvent
+{
+ public DateTime OccurredOn { get; init; } = DateTime.Now;
+}
+
+public record TaskChatMessagePinnedEvent(Guid MessageId, Guid TaskId, long PinnerUserId) : IDomainEvent
+{
+ public DateTime OccurredOn { get; init; } = DateTime.Now;
+}
+
+public record TaskChatMessageUnpinnedEvent(Guid MessageId, Guid TaskId, long UnpinnerUserId) : IDomainEvent
+{
+ public DateTime OccurredOn { get; init; } = DateTime.Now;
+}
+
diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Repositories/ITaskChatMessageRepository.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Repositories/ITaskChatMessageRepository.cs
new file mode 100644
index 00000000..8ac9b3b8
--- /dev/null
+++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/TaskChatAgg/Repositories/ITaskChatMessageRepository.cs
@@ -0,0 +1,75 @@
+using GozareshgirProgramManager.Domain.TaskChatAgg.Entities;
+
+namespace GozareshgirProgramManager.Domain.TaskChatAgg.Repositories;
+
+///
+/// Repository برای مدیریت پیامهای چت تسک
+///
+public interface ITaskChatMessageRepository
+{
+ ///
+ /// دریافت پیام بر اساس شناسه
+ ///
+ Task GetByIdAsync(Guid messageId);
+
+ ///
+ /// دریافت لیست پیامهای یک تسک (با صفحهبندی)
+ ///
+ Task> GetTaskMessagesAsync(Guid taskId, int pageNumber, int pageSize);
+
+ ///
+ /// دریافت تعداد کل پیامهای یک تسک
+ ///
+ Task GetTaskMessageCountAsync(Guid taskId);
+
+ ///
+ /// دریافت پیامهای پین شده یک تسک
+ ///
+ Task> GetPinnedMessagesAsync(Guid taskId);
+
+ ///
+ /// دریافت آخرین پیام یک تسک
+ ///
+ Task GetLastMessageAsync(Guid taskId);
+
+ ///
+ /// جستجو در پیامهای یک تسک
+ ///
+ Task> SearchMessagesAsync(Guid taskId, string searchText, int pageNumber, int pageSize);
+
+ ///
+ /// دریافت پیامهای یک کاربر خاص در یک تسک
+ ///
+ Task> GetUserMessagesAsync(Guid taskId, long userId, int pageNumber, int pageSize);
+
+ ///
+ /// دریافت پیامهای با فایل (تصویر، ویدیو، فایل و...)
+ ///
+ Task> GetMediaMessagesAsync(Guid taskId, int pageNumber, int pageSize);
+
+ ///
+ /// اضافه کردن پیام جدید
+ ///
+ Task AddAsync(TaskChatMessage message);
+
+ ///
+ /// بهروزرسانی پیام
+ ///
+ Task UpdateAsync(TaskChatMessage message);
+
+ ///
+ /// حذف فیزیکی پیام (در صورت نیاز - معمولاً استفاده نمیشود)
+ ///
+ Task DeleteAsync(TaskChatMessage message);
+
+ ///
+ /// ذخیره تغییرات
+ ///
+ Task SaveChangesAsync();
+
+ ///
+ /// بررسی وجود پیام
+ ///
+ Task ExistsAsync(Guid messageId);
+}
+