feat: Implement Public Message Management Commands and Queries

- Add GetUserPackageStatusQueryValidator for user package status validation.
- Create ArchiveMessageCommand and ArchiveMessageCommandHandler for archiving public messages.
- Implement ArchiveMessageCommandValidator to validate message ID.
- Introduce PublishMessageCommand and PublishMessageCommandHandler for publishing messages.
- Add PublishMessageCommandValidator for validating publish message requests.
- Implement GetPublicMessageQuery and GetPublicMessageQueryHandler for retrieving public messages.
- Create GetPublicMessageQueryValidator for validating public message requests.
- Add ApplyDiscountToOrderCommand and ApplyDiscountToOrderCommandHandler for applying discounts to orders.
- Implement ApplyDiscountToOrderCommandValidator for validating discount application requests.
- Create UpdateOrderStatusCommand and UpdateOrderStatusCommandHandler for changing order statuses.
- Implement UpdateOrderStatusCommandValidator for validating order status updates.
- Add CalculateOrderPVQuery and CalculateOrderPVQueryHandler for calculating order PV.
- Implement CalculateOrderPVQueryValidator for validating PV calculation requests.
- Create GetOrdersByDateRangeQuery and GetOrdersByDateRangeQueryHandler for retrieving orders by date range.
- Implement GetOrdersByDateRangeQueryValidator for validating date range queries.
- Add PublicMessage entity to represent public messages in the system.
- Implement PublicMessageService for handling public message operations via gRPC.
This commit is contained in:
masoodafar-web
2025-12-04 03:43:19 +03:30
parent 84f642e900
commit ba6d74fe35
45 changed files with 1777 additions and 22 deletions

View File

@@ -1,5 +1,4 @@
using CMSMicroservice.Domain.Entities.Payment;
using CMSMicroservice.Domain.Entities.Message;
using CMSMicroservice.Domain.Entities.Order;
using CMSMicroservice.Domain.Entities.DiscountShop;

View File

@@ -0,0 +1,22 @@
using MediatR;
namespace CMSMicroservice.Application.PackageCQ.Commands.PurchaseGoldenPackage;
/// <summary>
/// خرید پکیج طلایی (شروع فرآیند پرداخت)
/// </summary>
public record PurchaseGoldenPackageCommand : IRequest<PurchaseGoldenPackageResponseDto>
{
public long UserId { get; init; }
public long PackageId { get; init; }
public string ReturnUrl { get; init; } = string.Empty;
}
public class PurchaseGoldenPackageResponseDto
{
public bool Success { get; set; }
public string Message { get; set; } = string.Empty;
public long OrderId { get; set; }
public string PaymentGatewayUrl { get; set; } = string.Empty;
public string TrackingCode { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,88 @@
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Enums;
using MediatR;
namespace CMSMicroservice.Application.PackageCQ.Commands.PurchaseGoldenPackage;
public class PurchaseGoldenPackageCommandHandler : IRequestHandler<PurchaseGoldenPackageCommand, PurchaseGoldenPackageResponseDto>
{
private readonly IApplicationDbContext _context;
private readonly IPaymentGatewayService _paymentGateway;
private readonly ILogger<PurchaseGoldenPackageCommandHandler> _logger;
public PurchaseGoldenPackageCommandHandler(
IApplicationDbContext context,
IPaymentGatewayService paymentGateway,
ILogger<PurchaseGoldenPackageCommandHandler> logger)
{
_context = context;
_paymentGateway = paymentGateway;
_logger = logger;
}
public async Task<PurchaseGoldenPackageResponseDto> Handle(PurchaseGoldenPackageCommand request, CancellationToken cancellationToken)
{
// TODO: پیاده‌سازی خرید پکیج طلایی
//
// 1. پیدا کردن کاربر و بررسی شرایط:
// - var user = await _context.Users
// .Include(u => u.UserOrders)
// .FirstOrDefaultAsync(u => u.Id == request.UserId, cancellationToken)
// - if (user == null) throw new NotFoundException("کاربر یافت نشد")
// - if (user.PackagePurchaseMethod != PackagePurchaseMethod.None)
// throw new InvalidOperationException("شما قبلاً پکیج طلایی خریداری کرده‌اید")
//
// 2. پیدا کردن پکیج:
// - var package = await _context.Packages
// .FirstOrDefaultAsync(p => p.Id == request.PackageId && p.IsAvailable, cancellationToken)
// - if (package == null) throw new NotFoundException("پکیج یافت نشد")
// - if (package.Name != "طلایی")
// throw new InvalidOperationException("فقط پکیج طلایی قابل خرید است")
//
// 3. ایجاد سفارش:
// - var order = new UserOrder {
// UserId = user.Id,
// OrderNumber = GenerateOrderNumber(), // مثلاً "ORD" + DateTime.UtcNow.Ticks
// TotalPrice = package.Price, // 56,000,000
// Status = OrderStatus.Pending,
// PaymentMethod = PaymentMethod.IPG,
// OrderType = OrderType.PackagePurchase, // enum جدید
// PackageId = package.Id
// }
// - _context.UserOrders.Add(order)
// - await _context.SaveChangesAsync(cancellationToken)
//
// 4. شروع پرداخت با درگاه:
// - var paymentResult = await _paymentGateway.InitiatePaymentAsync(
// orderId: order.Id,
// amount: order.TotalPrice,
// description: $"خرید پکیج {package.Name}",
// returnUrl: request.ReturnUrl,
// cancellationToken: cancellationToken
// )
// - if (!paymentResult.Success)
// throw new InvalidOperationException($"خطا در اتصال به درگاه: {paymentResult.ErrorMessage}")
//
// 5. ذخیره اطلاعات پرداخت:
// - order.TrackingCode = paymentResult.TrackingCode
// - order.PaymentGatewayToken = paymentResult.Token
// - await _context.SaveChangesAsync(cancellationToken)
//
// 6. برگشت نتیجه:
// - _logger.LogInformation("Golden package purchase initiated for user {UserId}, order {OrderId}", user.Id, order.Id)
// - return new PurchaseGoldenPackageResponseDto {
// Success = true,
// Message = "لطفاً به درگاه پرداخت منتقل شوید",
// OrderId = order.Id,
// PaymentGatewayUrl = paymentResult.PaymentUrl,
// TrackingCode = paymentResult.TrackingCode
// }
//
// نکته 1: OrderType.PackagePurchase را به OrderType enum اضافه کنید
// نکته 2: PackageId nullable است در UserOrder - مطمئن شوید می‌توانید set کنید
// نکته 3: کاربر به صفحه paymentResult.PaymentUrl redirect می‌شود
// نکته 4: پس از پرداخت موفق، VerifyGoldenPackagePurchase فراخوانی می‌شود
throw new NotImplementedException("PurchaseGoldenPackage needs implementation");
}
}

View File

@@ -0,0 +1,23 @@
using FluentValidation;
namespace CMSMicroservice.Application.PackageCQ.Commands.PurchaseGoldenPackage;
public class PurchaseGoldenPackageCommandValidator : AbstractValidator<PurchaseGoldenPackageCommand>
{
public PurchaseGoldenPackageCommandValidator()
{
RuleFor(x => x.UserId)
.GreaterThan(0)
.WithMessage("شناسه کاربر باید بزرگتر از 0 باشد");
RuleFor(x => x.PackageId)
.GreaterThan(0)
.WithMessage("شناسه پکیج باید بزرگتر از 0 باشد");
RuleFor(x => x.ReturnUrl)
.NotEmpty()
.WithMessage("آدرس بازگشت الزامی است")
.Must(url => Uri.TryCreate(url, UriKind.Absolute, out _))
.WithMessage("آدرس بازگشت معتبر نیست");
}
}

View File

@@ -0,0 +1,23 @@
using MediatR;
namespace CMSMicroservice.Application.PackageCQ.Commands.VerifyGoldenPackagePurchase;
/// <summary>
/// تایید پرداخت پکیج طلایی (پس از بازگشت از درگاه)
/// </summary>
public record VerifyGoldenPackagePurchaseCommand : IRequest<VerifyGoldenPackagePurchaseResponseDto>
{
public long OrderId { get; init; }
public string Authority { get; init; } = string.Empty;
public string Status { get; init; } = string.Empty; // OK یا NOK
}
public class VerifyGoldenPackagePurchaseResponseDto
{
public bool Success { get; set; }
public string Message { get; set; } = string.Empty;
public long OrderId { get; set; }
public long TransactionId { get; set; }
public string ReferenceCode { get; set; } = string.Empty;
public long WalletBalance { get; set; }
}

View File

@@ -0,0 +1,115 @@
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Enums;
using MediatR;
namespace CMSMicroservice.Application.PackageCQ.Commands.VerifyGoldenPackagePurchase;
public class VerifyGoldenPackagePurchaseCommandHandler : IRequestHandler<VerifyGoldenPackagePurchaseCommand, VerifyGoldenPackagePurchaseResponseDto>
{
private readonly IApplicationDbContext _context;
private readonly IPaymentGatewayService _paymentGateway;
private readonly ILogger<VerifyGoldenPackagePurchaseCommandHandler> _logger;
public VerifyGoldenPackagePurchaseCommandHandler(
IApplicationDbContext context,
IPaymentGatewayService paymentGateway,
ILogger<VerifyGoldenPackagePurchaseCommandHandler> logger)
{
_context = context;
_paymentGateway = paymentGateway;
_logger = logger;
}
public async Task<VerifyGoldenPackagePurchaseResponseDto> Handle(VerifyGoldenPackagePurchaseCommand request, CancellationToken cancellationToken)
{
// TODO: پیاده‌سازی تایید پرداخت پکیج طلایی
//
// 1. بررسی Status از درگاه:
// - if (request.Status != "OK")
// throw new InvalidOperationException("پرداخت توسط کاربر لغو شد")
//
// 2. پیدا کردن سفارش:
// - var order = await _context.UserOrders
// .Include(o => o.User)
// .ThenInclude(u => u.UserWallet)
// .FirstOrDefaultAsync(o => o.Id == request.OrderId, cancellationToken)
// - if (order == null) throw new NotFoundException("سفارش یافت نشد")
// - if (order.Status != OrderStatus.Pending)
// throw new InvalidOperationException("این سفارش قبلاً پردازش شده است")
//
// 3. Verify با درگاه پرداخت:
// - var verifyResult = await _paymentGateway.VerifyPaymentAsync(
// authority: request.Authority,
// amount: order.TotalPrice,
// cancellationToken: cancellationToken
// )
// - if (!verifyResult.Success) {
// order.Status = OrderStatus.PaymentFailed
// order.PaymentFailureReason = verifyResult.ErrorMessage
// await _context.SaveChangesAsync(cancellationToken)
// throw new InvalidOperationException($"تایید پرداخت ناموفق: {verifyResult.ErrorMessage}")
// }
//
// 4. شارژ کیف پول:
// - var wallet = order.User.UserWallet
// - if (wallet == null) {
// wallet = new UserWallet { UserId = order.UserId, Balance = 0, DiscountBalance = 0 }
// _context.UserWallets.Add(wallet)
// }
// - wallet.Balance += order.TotalPrice // اضافه شدن 56,000,000
//
// 5. ثبت Transaction:
// - var transaction = new Transaction {
// UserId = order.UserId,
// Amount = order.TotalPrice,
// Type = TransactionType.DepositIpg,
// Status = TransactionStatus.Completed,
// Description = $"شارژ کیف پول از خرید پکیج طلایی - سفارش {order.OrderNumber}",
// ReferenceCode = verifyResult.ReferenceCode,
// OrderId = order.Id
// }
// - _context.Transactions.Add(transaction)
//
// 6. ثبت UserWalletChangeLog:
// - var changeLog = new UserWalletChangeLog {
// UserId = order.UserId,
// ChangeValue = order.TotalPrice,
// ChangeType = ChangeType.Deposit,
// CurrentBalance = wallet.Balance,
// Description = $"شارژ از خرید پکیج طلایی",
// TransactionId = transaction.Id
// }
// - _context.UserWalletChangeLogs.Add(changeLog)
//
// 7. Set PackagePurchaseMethod:
// - order.User.PackagePurchaseMethod = PackagePurchaseMethod.DirectPurchase
//
// 8. به‌روزرسانی سفارش:
// - order.Status = OrderStatus.Processing // یا Completed
// - order.PaymentStatus = PaymentStatus.Paid
// - order.PaidAt = DateTime.UtcNow
// - order.BankReferenceId = verifyResult.ReferenceCode
//
// 9. ذخیره همه تغییرات:
// - await _context.SaveChangesAsync(cancellationToken)
//
// 10. Log و برگشت:
// - _logger.LogInformation("Golden package verified for user {UserId}, order {OrderId}, wallet charged {Amount}",
// order.UserId, order.Id, order.TotalPrice)
// - return new VerifyGoldenPackagePurchaseResponseDto {
// Success = true,
// Message = "پرداخت با موفقیت تایید شد. کیف پول شما شارژ گردید",
// OrderId = order.Id,
// TransactionId = transaction.Id,
// ReferenceCode = verifyResult.ReferenceCode,
// WalletBalance = wallet.Balance
// }
//
// نکته 1: کاربر هنوز عضو باشگاه نشده - باید ActivateClubMembership صدا بزند
// نکته 2: TransactionType.DepositIpg را بررسی کنید موجود باشد
// نکته 3: در صورت خطا، سفارش به PaymentFailed تغییر وضعیت می‌دهد
// نکته 4: این فرآیند idempotent نیست - باید بررسی شود سفارش Pending باشد
throw new NotImplementedException("VerifyGoldenPackagePurchase needs implementation");
}
}

View File

@@ -0,0 +1,23 @@
using FluentValidation;
namespace CMSMicroservice.Application.PackageCQ.Commands.VerifyGoldenPackagePurchase;
public class VerifyGoldenPackagePurchaseCommandValidator : AbstractValidator<VerifyGoldenPackagePurchaseCommand>
{
public VerifyGoldenPackagePurchaseCommandValidator()
{
RuleFor(x => x.OrderId)
.GreaterThan(0)
.WithMessage("شناسه سفارش باید بزرگتر از 0 باشد");
RuleFor(x => x.Authority)
.NotEmpty()
.WithMessage("کد Authority الزامی است");
RuleFor(x => x.Status)
.NotEmpty()
.WithMessage("وضعیت پرداخت الزامی است")
.Must(s => s == "OK" || s == "NOK")
.WithMessage("وضعیت باید OK یا NOK باشد");
}
}

View File

@@ -0,0 +1,24 @@
using MediatR;
namespace CMSMicroservice.Application.PackageCQ.Queries.GetUserPackageStatus;
/// <summary>
/// دریافت وضعیت خرید پکیج کاربر
/// </summary>
public record GetUserPackageStatusQuery : IRequest<UserPackageStatusDto>
{
public long UserId { get; init; }
}
public class UserPackageStatusDto
{
public long UserId { get; set; }
public string PackagePurchaseMethod { get; set; } = string.Empty; // None, DayaLoan, DirectPurchase
public bool HasPurchasedPackage { get; set; }
public bool IsClubMemberActive { get; set; }
public long WalletBalance { get; set; }
public long DiscountBalance { get; set; }
public bool CanActivateClubMembership { get; set; }
public string? LastOrderNumber { get; set; }
public DateTime? LastPurchaseDate { get; set; }
}

View File

@@ -0,0 +1,61 @@
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Enums;
using MediatR;
namespace CMSMicroservice.Application.PackageCQ.Queries.GetUserPackageStatus;
public class GetUserPackageStatusQueryHandler : IRequestHandler<GetUserPackageStatusQuery, UserPackageStatusDto>
{
private readonly IApplicationDbContext _context;
public GetUserPackageStatusQueryHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<UserPackageStatusDto> Handle(GetUserPackageStatusQuery request, CancellationToken cancellationToken)
{
// TODO: پیاده‌سازی دریافت وضعیت پکیج کاربر
//
// 1. دریافت اطلاعات کاربر:
// - var user = await _context.Users
// .Include(u => u.UserWallet)
// .FirstOrDefaultAsync(u => u.Id == request.UserId, cancellationToken)
// - if (user == null) throw new NotFoundException("کاربر یافت نشد")
//
// 2. دریافت عضویت باشگاه:
// - var clubMembership = await _context.ClubMemberships
// .FirstOrDefaultAsync(c => c.UserId == user.Id && c.IsActive, cancellationToken)
//
// 3. دریافت آخرین سفارش پکیج:
// - var lastPackageOrder = await _context.UserOrders
// .Where(o => o.UserId == user.Id && o.PackageId != null)
// .OrderByDescending(o => o.Created)
// .FirstOrDefaultAsync(cancellationToken)
//
// 4. بررسی شرایط فعالسازی باشگاه:
// - var wallet = user.UserWallet
// - bool canActivate =
// user.PackagePurchaseMethod != PackagePurchaseMethod.None &&
// clubMembership == null &&
// wallet != null &&
// wallet.Balance >= 56_000_000
//
// 5. برگشت DTO:
// - return new UserPackageStatusDto {
// UserId = user.Id,
// PackagePurchaseMethod = user.PackagePurchaseMethod.ToString(),
// HasPurchasedPackage = user.PackagePurchaseMethod != PackagePurchaseMethod.None,
// IsClubMemberActive = clubMembership != null,
// WalletBalance = wallet?.Balance ?? 0,
// DiscountBalance = wallet?.DiscountBalance ?? 0,
// CanActivateClubMembership = canActivate,
// LastOrderNumber = lastPackageOrder?.OrderNumber,
// LastPurchaseDate = lastPackageOrder?.Created
// }
//
// نکته: این query برای UI مفید است تا وضعیت کاربر را نمایش دهد
throw new NotImplementedException("GetUserPackageStatus needs implementation");
}
}

View File

@@ -0,0 +1,13 @@
using FluentValidation;
namespace CMSMicroservice.Application.PackageCQ.Queries.GetUserPackageStatus;
public class GetUserPackageStatusQueryValidator : AbstractValidator<GetUserPackageStatusQuery>
{
public GetUserPackageStatusQueryValidator()
{
RuleFor(x => x.UserId)
.GreaterThan(0)
.WithMessage("شناسه کاربر باید بزرگتر از 0 باشد");
}
}

View File

@@ -0,0 +1,16 @@
namespace CMSMicroservice.Application.PublicMessageCQ.Commands.ArchiveMessage;
/// <summary>
/// آرشیو پیام عمومی (غیرفعال و بایگانی)
/// </summary>
public record ArchiveMessageCommand : IRequest<ArchiveMessageResponseDto>
{
public long MessageId { get; init; }
}
public class ArchiveMessageResponseDto
{
public bool Success { get; set; }
public string Message { get; set; } = string.Empty;
public DateTime? ArchivedAt { get; set; }
}

View File

@@ -0,0 +1,52 @@
using CMSMicroservice.Application.Common.Interfaces;
namespace CMSMicroservice.Application.PublicMessageCQ.Commands.ArchiveMessage;
public class ArchiveMessageCommandHandler : IRequestHandler<ArchiveMessageCommand, ArchiveMessageResponseDto>
{
private readonly IApplicationDbContext _context;
private readonly ILogger<ArchiveMessageCommandHandler> _logger;
public ArchiveMessageCommandHandler(
IApplicationDbContext context,
ILogger<ArchiveMessageCommandHandler> logger)
{
_context = context;
_logger = logger;
}
public async Task<ArchiveMessageResponseDto> Handle(ArchiveMessageCommand request, CancellationToken cancellationToken)
{
// TODO: پیاده‌سازی آرشیو پیام
// 1. پیدا کردن پیام:
// - var message = await _context.PublicMessages
// .FirstOrDefaultAsync(m => m.Id == request.MessageId, cancellationToken)
// - بررسی null و پرتاب NotFoundException
//
// 2. بررسی وضعیت:
// - اگر قبلاً آرشیو شده:
// if (message.IsArchived)
// return موفقیت با پیام "این پیام قبلاً آرشیو شده است"
//
// 3. آرشیو کردن:
// - message.IsArchived = true
// - message.IsActive = false // غیرفعال هم می‌شود
// - message.ArchivedAt = DateTime.UtcNow
//
// 4. ذخیره و Log:
// - await _context.SaveChangesAsync(cancellationToken)
// - _logger.LogInformation("Public message {MessageId} archived: {Title}", message.Id, message.Title)
//
// 5. برگشت Response:
// - return new ArchiveMessageResponseDto {
// Success = true,
// Message = "پیام با موفقیت آرشیو شد",
// ArchivedAt = message.ArchivedAt
// }
//
// نکته: پیام‌های آرشیو شده دیگر در GetActiveMessages نمایش داده نمی‌شوند
// نکته: می‌توان پیام‌های آرشیو شده را در یک query جداگانه GetArchivedMessages دریافت کرد
throw new NotImplementedException("ArchiveMessage needs implementation");
}
}

View File

@@ -0,0 +1,11 @@
namespace CMSMicroservice.Application.PublicMessageCQ.Commands.ArchiveMessage;
public class ArchiveMessageCommandValidator : AbstractValidator<ArchiveMessageCommand>
{
public ArchiveMessageCommandValidator()
{
RuleFor(x => x.MessageId)
.GreaterThan(0)
.WithMessage("شناسه پیام باید بزرگتر از 0 باشد");
}
}

View File

@@ -1,5 +1,5 @@
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Entities.Message;
using CMSMicroservice.Domain.Entities;
using MediatR;
using Microsoft.Extensions.Logging;

View File

@@ -0,0 +1,16 @@
namespace CMSMicroservice.Application.PublicMessageCQ.Commands.PublishMessage;
/// <summary>
/// انتشار پیام عمومی (فعال‌سازی)
/// </summary>
public record PublishMessageCommand : IRequest<PublishMessageResponseDto>
{
public long MessageId { get; init; }
}
public class PublishMessageResponseDto
{
public bool Success { get; set; }
public string Message { get; set; } = string.Empty;
public DateTime? PublishedAt { get; set; }
}

View File

@@ -0,0 +1,56 @@
using CMSMicroservice.Application.Common.Interfaces;
namespace CMSMicroservice.Application.PublicMessageCQ.Commands.PublishMessage;
public class PublishMessageCommandHandler : IRequestHandler<PublishMessageCommand, PublishMessageResponseDto>
{
private readonly IApplicationDbContext _context;
private readonly ILogger<PublishMessageCommandHandler> _logger;
public PublishMessageCommandHandler(
IApplicationDbContext context,
ILogger<PublishMessageCommandHandler> logger)
{
_context = context;
_logger = logger;
}
public async Task<PublishMessageResponseDto> Handle(PublishMessageCommand request, CancellationToken cancellationToken)
{
// TODO: پیاده‌سازی انتشار پیام
// 1. پیدا کردن پیام:
// - var message = await _context.PublicMessages
// .FirstOrDefaultAsync(m => m.Id == request.MessageId, cancellationToken)
// - بررسی null و پرتاب NotFoundException
//
// 2. بررسی شرایط انتشار:
// - اگر قبلاً منتشر شده:
// if (message.IsActive && message.PublishedAt.HasValue)
// return موفقیت با پیام "این پیام قبلاً منتشر شده است"
// - اگر آرشیو شده:
// if (message.IsArchived)
// throw new InvalidOperationException("پیام آرشیو شده قابل انتشار نیست")
//
// 3. فعال‌سازی پیام:
// - message.IsActive = true
// - message.PublishedAt = DateTime.UtcNow
// - اگر StartDate خالی است، از الان شروع کن:
// if (!message.StartDate.HasValue)
// message.StartDate = DateTime.UtcNow
//
// 4. ذخیره و Log:
// - await _context.SaveChangesAsync(cancellationToken)
// - _logger.LogInformation("Public message {MessageId} published: {Title}", message.Id, message.Title)
//
// 5. برگشت Response:
// - return new PublishMessageResponseDto {
// Success = true,
// Message = "پیام با موفقیت منتشر شد",
// PublishedAt = message.PublishedAt
// }
//
// نکته: پس از publish، پیام برای کاربران قابل مشاهده می‌شود (GetActiveMessages)
throw new NotImplementedException("PublishMessage needs implementation");
}
}

View File

@@ -0,0 +1,11 @@
namespace CMSMicroservice.Application.PublicMessageCQ.Commands.PublishMessage;
public class PublishMessageCommandValidator : AbstractValidator<PublishMessageCommand>
{
public PublishMessageCommandValidator()
{
RuleFor(x => x.MessageId)
.GreaterThan(0)
.WithMessage("شناسه پیام باید بزرگتر از 0 باشد");
}
}

View File

@@ -14,8 +14,8 @@ public class PublicMessageDto
public string TypeName { get; set; } = string.Empty;
public MessagePriority Priority { get; set; }
public string PriorityName { get; set; } = string.Empty;
public DateTime StartsAt { get; set; }
public DateTime ExpiresAt { get; set; }
public DateTime? StartsAt { get; set; }
public DateTime? ExpiresAt { get; set; }
public string? LinkUrl { get; set; }
public string? LinkText { get; set; }
public DateTime Created { get; set; }

View File

@@ -15,9 +15,9 @@ public class AdminPublicMessageDto
public MessagePriority Priority { get; set; }
public string PriorityName { get; set; } = string.Empty;
public bool IsActive { get; set; }
public DateTime StartsAt { get; set; }
public DateTime ExpiresAt { get; set; }
public long CreatedByUserId { get; set; }
public DateTime? StartsAt { get; set; }
public DateTime? ExpiresAt { get; set; }
public long? CreatedByUserId { get; set; }
public int ViewCount { get; set; }
public string? LinkUrl { get; set; }
public string? LinkText { get; set; }

View File

@@ -0,0 +1,25 @@
namespace CMSMicroservice.Application.PublicMessageCQ.Queries.GetPublicMessage;
/// <summary>
/// دریافت یک پیام عمومی با شناسه
/// </summary>
public record GetPublicMessageQuery : IRequest<PublicMessageDto?>
{
public long MessageId { get; init; }
}
public class PublicMessageDto
{
public long Id { get; set; }
public string Title { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public MessageType MessageType { get; set; }
public bool IsActive { get; set; }
public bool IsArchived { get; set; }
public DateTime? StartDate { get; set; }
public DateTime? EndDate { get; set; }
public DateTime? PublishedAt { get; set; }
public DateTime? ArchivedAt { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? LastModifiedAt { get; set; }
}

View File

@@ -0,0 +1,46 @@
using CMSMicroservice.Application.Common.Interfaces;
namespace CMSMicroservice.Application.PublicMessageCQ.Queries.GetPublicMessage;
public class GetPublicMessageQueryHandler : IRequestHandler<GetPublicMessageQuery, PublicMessageDto?>
{
private readonly IApplicationDbContext _context;
public GetPublicMessageQueryHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<PublicMessageDto?> Handle(GetPublicMessageQuery request, CancellationToken cancellationToken)
{
// TODO: پیاده‌سازی دریافت پیام
// 1. پیدا کردن پیام:
// - var message = await _context.PublicMessages
// .AsNoTracking()
// .FirstOrDefaultAsync(m => m.Id == request.MessageId, cancellationToken)
//
// 2. چک null:
// - if (message == null) return null
//
// 3. Map به DTO:
// - return new PublicMessageDto {
// Id = message.Id,
// Title = message.Title,
// Content = message.Content,
// MessageType = message.MessageType,
// IsActive = message.IsActive,
// IsArchived = message.IsArchived,
// StartDate = message.StartDate,
// EndDate = message.EndDate,
// PublishedAt = message.PublishedAt,
// ArchivedAt = message.ArchivedAt,
// CreatedAt = message.CreatedAt,
// LastModifiedAt = message.LastModifiedAt
// }
//
// نکته: این query برای Admin است و همه پیام‌ها (حتی آرشیو شده) را برمی‌گرداند
// نکته: برای کاربران عادی از GetActiveMessages استفاده می‌شود
throw new NotImplementedException("GetPublicMessage needs implementation");
}
}

View File

@@ -0,0 +1,11 @@
namespace CMSMicroservice.Application.PublicMessageCQ.Queries.GetPublicMessage;
public class GetPublicMessageQueryValidator : AbstractValidator<GetPublicMessageQuery>
{
public GetPublicMessageQueryValidator()
{
RuleFor(x => x.MessageId)
.GreaterThan(0)
.WithMessage("شناسه پیام باید بزرگتر از 0 باشد");
}
}

View File

@@ -0,0 +1,39 @@
namespace CMSMicroservice.Application.UserOrderCQ.Commands.ApplyDiscountToOrder;
/// <summary>
/// اعمال تخفیف به سفارش
/// </summary>
public record ApplyDiscountToOrderCommand : IRequest<ApplyDiscountToOrderResponseDto>
{
/// <summary>
/// شناسه سفارش
/// </summary>
public long OrderId { get; init; }
/// <summary>
/// مبلغ تخفیف (ریال)
/// </summary>
public long DiscountAmount { get; init; }
/// <summary>
/// دلیل تخفیف
/// </summary>
public string Reason { get; init; } = string.Empty;
/// <summary>
/// کد تخفیف (اختیاری)
/// </summary>
public string? DiscountCode { get; init; }
}
/// <summary>
/// پاسخ اعمال تخفیف
/// </summary>
public class ApplyDiscountToOrderResponseDto
{
public bool Success { get; set; }
public string Message { get; set; } = string.Empty;
public long OriginalAmount { get; set; }
public long DiscountAmount { get; set; }
public long FinalAmount { get; set; }
}

View File

@@ -0,0 +1,74 @@
using CMSMicroservice.Application.Common.Interfaces;
namespace CMSMicroservice.Application.UserOrderCQ.Commands.ApplyDiscountToOrder;
public class ApplyDiscountToOrderCommandHandler : IRequestHandler<ApplyDiscountToOrderCommand, ApplyDiscountToOrderResponseDto>
{
private readonly IApplicationDbContext _context;
private readonly ILogger<ApplyDiscountToOrderCommandHandler> _logger;
public ApplyDiscountToOrderCommandHandler(
IApplicationDbContext context,
ILogger<ApplyDiscountToOrderCommandHandler> logger)
{
_context = context;
_logger = logger;
}
public async Task<ApplyDiscountToOrderResponseDto> Handle(ApplyDiscountToOrderCommand request, CancellationToken cancellationToken)
{
// TODO: پیاده‌سازی اعمال تخفیف به سفارش
// 1. پیدا کردن سفارش:
// - var order = await _context.UserOrders.FirstOrDefaultAsync(o => o.Id == request.OrderId, cancellationToken)
// - بررسی null و پرتاب NotFoundException
//
// 2. بررسی شرایط اعمال تخفیف:
// - سفارش نباید Delivered یا Cancelled باشد
// - مبلغ تخفیف نباید بیشتر از Amount باشد
// - if (order.DeliveryStatus == DeliveryStatus.Delivered || order.DeliveryStatus == DeliveryStatus.Cancelled)
// throw new InvalidOperationException("نمی‌توان به این سفارش تخفیف اعمال کرد")
// - if (request.DiscountAmount > order.Amount)
// throw new InvalidOperationException("مبلغ تخفیف نمی‌تواند بیشتر از مبلغ سفارش باشد")
//
// 3. محاسبه مبلغ نهایی:
// - var originalAmount = order.Amount
// - var newDiscountedPrice = order.Amount - request.DiscountAmount
// - مطمئن شوید که منفی نشود: newDiscountedPrice = Math.Max(0, newDiscountedPrice)
//
// 4. به‌روزرسانی سفارش:
// - order.DiscountedPrice = newDiscountedPrice
// - اگر فیلد OrderDiscountAmount وجود دارد، آن را هم به‌روز کنید
// - order.OrderDiscountAmount = request.DiscountAmount
// - اضافه کردن به توضیحات:
// order.DeliveryDescription = (order.DeliveryDescription ?? "") +
// $"\nتخفیف اعمال شده: {request.DiscountAmount} ریال - دلیل: {request.Reason}"
//
// 5. ذخیره Log تخفیف (اختیاری - اگر جدول OrderDiscountLog دارید):
// - var discountLog = new OrderDiscountLog {
// OrderId = order.Id,
// DiscountAmount = request.DiscountAmount,
// Reason = request.Reason,
// DiscountCode = request.DiscountCode,
// AppliedAt = DateTime.UtcNow
// }
// - await _context.OrderDiscountLogs.AddAsync(discountLog, cancellationToken)
//
// 6. ذخیره و Log:
// - await _context.SaveChangesAsync(cancellationToken)
// - _logger.LogInformation("Discount {Amount} applied to order {OrderId}: {Reason}",
// request.DiscountAmount, request.OrderId, request.Reason)
//
// 7. برگشت Response:
// - return new ApplyDiscountToOrderResponseDto {
// Success = true,
// Message = "تخفیف با موفقیت اعمال شد",
// OriginalAmount = originalAmount,
// DiscountAmount = request.DiscountAmount,
// FinalAmount = newDiscountedPrice
// }
//
// نکته: این تخفیف برای تخفیفات دستی Admin است و جدا از تخفیف‌های محصول
throw new NotImplementedException("ApplyDiscountToOrder needs implementation");
}
}

View File

@@ -0,0 +1,21 @@
namespace CMSMicroservice.Application.UserOrderCQ.Commands.ApplyDiscountToOrder;
public class ApplyDiscountToOrderCommandValidator : AbstractValidator<ApplyDiscountToOrderCommand>
{
public ApplyDiscountToOrderCommandValidator()
{
RuleFor(x => x.OrderId)
.GreaterThan(0)
.WithMessage("شناسه سفارش باید بزرگتر از 0 باشد");
RuleFor(x => x.DiscountAmount)
.GreaterThan(0)
.WithMessage("مبلغ تخفیف باید بزرگتر از 0 باشد");
RuleFor(x => x.Reason)
.NotEmpty()
.WithMessage("دلیل تخفیف الزامی است")
.MaximumLength(500)
.WithMessage("دلیل تخفیف نمی‌تواند بیشتر از 500 کاراکتر باشد");
}
}

View File

@@ -0,0 +1,34 @@
using CMSMicroservice.Domain.Enums;
namespace CMSMicroservice.Application.UserOrderCQ.Commands.UpdateOrderStatus;
/// <summary>
/// تغییر وضعیت سفارش
/// </summary>
public record UpdateOrderStatusCommand : IRequest<UpdateOrderStatusResponseDto>
{
/// <summary>
/// شناسه سفارش
/// </summary>
public long OrderId { get; init; }
/// <summary>
/// وضعیت تحویل جدید
/// </summary>
public DeliveryStatus NewStatus { get; init; }
/// <summary>
/// توضیحات (اختیاری)
/// </summary>
public string? Description { get; init; }
}
/// <summary>
/// پاسخ تغییر وضعیت سفارش
/// </summary>
public class UpdateOrderStatusResponseDto
{
public bool Success { get; set; }
public string Message { get; set; } = string.Empty;
public DeliveryStatus CurrentStatus { get; set; }
}

View File

@@ -0,0 +1,53 @@
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Enums;
namespace CMSMicroservice.Application.UserOrderCQ.Commands.UpdateOrderStatus;
public class UpdateOrderStatusCommandHandler : IRequestHandler<UpdateOrderStatusCommand, UpdateOrderStatusResponseDto>
{
private readonly IApplicationDbContext _context;
private readonly ILogger<UpdateOrderStatusCommandHandler> _logger;
public UpdateOrderStatusCommandHandler(
IApplicationDbContext context,
ILogger<UpdateOrderStatusCommandHandler> logger)
{
_context = context;
_logger = logger;
}
public async Task<UpdateOrderStatusResponseDto> Handle(UpdateOrderStatusCommand request, CancellationToken cancellationToken)
{
// TODO: پیاده‌سازی تغییر وضعیت سفارش
// 1. پیدا کردن سفارش:
// - await _context.UserOrders.FirstOrDefaultAsync(o => o.Id == request.OrderId)
// - بررسی null و پرتاب NotFoundException
//
// 2. بررسی‌های انتقال وضعیت (State Transition Validation):
// - نمی‌توان از Delivered به Cancelled رفت
// - نمی‌توان از Cancelled به سایر وضعیت‌ها رفت
// - الگوی معمول: Pending → Processing → Shipped → Delivered
// - Cancelled می‌تواند از Pending, Processing, Shipped باشد
//
// 3. تغییر وضعیت:
// - order.DeliveryStatus = request.NewStatus
// - اگر Description داریم: order.DeliveryDescription = request.Description
// - تنظیم تاریخ‌های مربوطه:
// * اگر NewStatus == Delivered → order.DeliveredAt = DateTime.UtcNow
// * اگر NewStatus == Shipped → order.ShippedAt = DateTime.UtcNow
// * اگر NewStatus == Processing → order.ProcessedAt = DateTime.UtcNow
//
// 4. ذخیره و Log:
// - await _context.SaveChangesAsync(cancellationToken)
// - _logger.LogInformation("Order {OrderId} status changed to {NewStatus}", request.OrderId, request.NewStatus)
//
// 5. برگشت Response:
// - Success = true
// - Message = "وضعیت سفارش با موفقیت تغییر کرد"
// - CurrentStatus = order.DeliveryStatus
//
// نکته: برای validation دقیق‌تر، می‌توان یک State Machine برای انتقال‌های مجاز تعریف کرد
throw new NotImplementedException("UpdateOrderStatus needs implementation");
}
}

View File

@@ -0,0 +1,17 @@
using CMSMicroservice.Domain.Enums;
namespace CMSMicroservice.Application.UserOrderCQ.Commands.UpdateOrderStatus;
public class UpdateOrderStatusCommandValidator : AbstractValidator<UpdateOrderStatusCommand>
{
public UpdateOrderStatusCommandValidator()
{
RuleFor(x => x.OrderId)
.GreaterThan(0)
.WithMessage("شناسه سفارش باید بزرگتر از 0 باشد");
RuleFor(x => x.NewStatus)
.IsInEnum()
.WithMessage("وضعیت تحویل نامعتبر است");
}
}

View File

@@ -0,0 +1,46 @@
namespace CMSMicroservice.Application.UserOrderCQ.Queries.CalculateOrderPV;
/// <summary>
/// محاسبه امتیاز PV سفارش
/// </summary>
public record CalculateOrderPVQuery : IRequest<CalculateOrderPVResponseDto>
{
/// <summary>
/// شناسه سفارش
/// </summary>
public long OrderId { get; init; }
}
/// <summary>
/// پاسخ محاسبه PV سفارش
/// </summary>
public class CalculateOrderPVResponseDto
{
/// <summary>
/// مجموع امتیاز PV سفارش
/// </summary>
public decimal TotalPV { get; set; }
/// <summary>
/// جزئیات PV هر محصول
/// </summary>
public List<ProductPVDto> ProductPVs { get; set; } = new();
/// <summary>
/// مبلغ قابل پرداخت
/// </summary>
public long PayableAmount { get; set; }
}
/// <summary>
/// جزئیات PV یک محصول در سفارش
/// </summary>
public class ProductPVDto
{
public long ProductId { get; set; }
public string ProductTitle { get; set; } = string.Empty;
public int Quantity { get; set; }
public decimal UnitPV { get; set; }
public decimal TotalPV { get; set; }
public long UnitPrice { get; set; }
}

View File

@@ -0,0 +1,62 @@
using CMSMicroservice.Application.Common.Interfaces;
namespace CMSMicroservice.Application.UserOrderCQ.Queries.CalculateOrderPV;
public class CalculateOrderPVQueryHandler : IRequestHandler<CalculateOrderPVQuery, CalculateOrderPVResponseDto>
{
private readonly IApplicationDbContext _context;
private readonly ILogger<CalculateOrderPVQueryHandler> _logger;
public CalculateOrderPVQueryHandler(
IApplicationDbContext context,
ILogger<CalculateOrderPVQueryHandler> logger)
{
_context = context;
_logger = logger;
}
public async Task<CalculateOrderPVResponseDto> Handle(CalculateOrderPVQuery request, CancellationToken cancellationToken)
{
// TODO: پیاده‌سازی محاسبه PV سفارش
// 1. پیدا کردن سفارش و جزئیات:
// - var order = await _context.UserOrders
// .Include(o => o.FactorDetails)
// .ThenInclude(fd => fd.Product)
// .FirstOrDefaultAsync(o => o.Id == request.OrderId, cancellationToken)
// - بررسی null و پرتاب NotFoundException
//
// 2. محاسبه PV هر محصول:
// - var productPVs = new List<ProductPVDto>()
// - decimal totalPV = 0
// - foreach (var detail in order.FactorDetails):
// * محاسبه PV واحد محصول (فرض: قیمت / 10000 یا از فیلد Product.PV اگر وجود دارد):
// decimal unitPV = detail.Product.PV ?? (detail.Product.Price / 10000m)
// * محاسبه PV کل این آیتم:
// decimal itemTotalPV = unitPV * detail.Count
// * اضافه به لیست:
// productPVs.Add(new ProductPVDto {
// ProductId = detail.ProductId,
// ProductTitle = detail.Product.Title,
// Quantity = detail.Count,
// UnitPV = unitPV,
// TotalPV = itemTotalPV,
// UnitPrice = detail.Product.Price
// })
// * اضافه به مجموع:
// totalPV += itemTotalPV
//
// 3. برگشت Response:
// - _logger.LogInformation("Calculated PV for order {OrderId}: {TotalPV}", request.OrderId, totalPV)
// - return new CalculateOrderPVResponseDto {
// TotalPV = totalPV,
// ProductPVs = productPVs,
// PayableAmount = order.DiscountedPrice
// }
//
// نکته: PV (Point Value) معمولاً برای سیستم‌های MLM و کمیسیون شبکه استفاده می‌شود
// نکته: فرمول محاسبه PV باید بر اساس business logic شما باشد (قیمت/10000 فقط مثال است)
// نکته: اگر entity Product فیلد PV ندارد، باید اضافه شود یا از Configuration استفاده کنید
throw new NotImplementedException("CalculateOrderPV needs implementation");
}
}

View File

@@ -0,0 +1,11 @@
namespace CMSMicroservice.Application.UserOrderCQ.Queries.CalculateOrderPV;
public class CalculateOrderPVQueryValidator : AbstractValidator<CalculateOrderPVQuery>
{
public CalculateOrderPVQueryValidator()
{
RuleFor(x => x.OrderId)
.GreaterThan(0)
.WithMessage("شناسه سفارش باید بزرگتر از 0 باشد");
}
}

View File

@@ -0,0 +1,66 @@
using CMSMicroservice.Application.Common.Models;
using CMSMicroservice.Domain.Enums;
namespace CMSMicroservice.Application.UserOrderCQ.Queries.GetOrdersByDateRange;
/// <summary>
/// دریافت سفارشات بر اساس بازه زمانی
/// </summary>
public record GetOrdersByDateRangeQuery : IRequest<GetOrdersByDateRangeResponseDto>
{
/// <summary>
/// تاریخ شروع (UTC)
/// </summary>
public DateTime StartDate { get; init; }
/// <summary>
/// تاریخ پایان (UTC)
/// </summary>
public DateTime EndDate { get; init; }
/// <summary>
/// فیلتر وضعیت تحویل (اختیاری)
/// </summary>
public DeliveryStatus? Status { get; init; }
/// <summary>
/// شناسه کاربر (اختیاری - برای فیلتر بر اساس کاربر)
/// </summary>
public long? UserId { get; init; }
/// <summary>
/// شماره صفحه
/// </summary>
public int PageIndex { get; init; } = 1;
/// <summary>
/// تعداد در صفحه
/// </summary>
public int PageSize { get; init; } = 20;
}
/// <summary>
/// پاسخ لیست سفارشات
/// </summary>
public class GetOrdersByDateRangeResponseDto
{
public MetaData MetaData { get; set; } = new();
public List<OrderSummaryDto> Orders { get; set; } = new();
}
/// <summary>
/// خلاصه اطلاعات سفارش
/// </summary>
public class OrderSummaryDto
{
public long Id { get; set; }
public long UserId { get; set; }
public string UserFullName { get; set; } = string.Empty;
public long Amount { get; set; }
public long DiscountedPrice { get; set; }
public DeliveryStatus Status { get; set; }
public DateTime Created { get; set; }
public DateTime? ShippedAt { get; set; }
public DateTime? DeliveredAt { get; set; }
public int ItemsCount { get; set; }
}

View File

@@ -0,0 +1,80 @@
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.Common.Models;
namespace CMSMicroservice.Application.UserOrderCQ.Queries.GetOrdersByDateRange;
public class GetOrdersByDateRangeQueryHandler : IRequestHandler<GetOrdersByDateRangeQuery, GetOrdersByDateRangeResponseDto>
{
private readonly IApplicationDbContext _context;
private readonly ILogger<GetOrdersByDateRangeQueryHandler> _logger;
public GetOrdersByDateRangeQueryHandler(
IApplicationDbContext context,
ILogger<GetOrdersByDateRangeQueryHandler> logger)
{
_context = context;
_logger = logger;
}
public async Task<GetOrdersByDateRangeResponseDto> Handle(GetOrdersByDateRangeQuery request, CancellationToken cancellationToken)
{
// TODO: پیاده‌سازی دریافت سفارشات بر اساس بازه زمانی
// 1. شروع Query:
// - var query = _context.UserOrders.AsQueryable()
// - Include User برای نام کاربر: .Include(o => o.User)
// - Include FactorDetails برای شمارش تعداد آیتم‌ها: .Include(o => o.FactorDetails)
//
// 2. اعمال فیلتر بازه زمانی:
// - query = query.Where(o => o.Created >= request.StartDate && o.Created <= request.EndDate)
//
// 3. اعمال فیلترهای اختیاری:
// - اگر request.Status.HasValue:
// query = query.Where(o => o.DeliveryStatus == request.Status.Value)
// - اگر request.UserId.HasValue:
// query = query.Where(o => o.UserId == request.UserId.Value)
//
// 4. محاسبه تعداد کل:
// - var totalCount = await query.CountAsync(cancellationToken)
//
// 5. مرتب‌سازی و Pagination:
// - query = query.OrderByDescending(o => o.Created)
// - query = query.Skip((request.PageIndex - 1) * request.PageSize)
// - query = query.Take(request.PageSize)
//
// 6. دریافت داده‌ها:
// - var orders = await query.ToListAsync(cancellationToken)
//
// 7. Mapping به DTO:
// - var orderDtos = orders.Select(o => new OrderSummaryDto {
// Id = o.Id,
// UserId = o.UserId,
// UserFullName = $"{o.User.Firstname} {o.User.Lastname}",
// Amount = o.Amount,
// DiscountedPrice = o.DiscountedPrice,
// Status = o.DeliveryStatus,
// Created = o.Created,
// ShippedAt = o.ShippedAt,
// DeliveredAt = o.DeliveredAt,
// ItemsCount = o.FactorDetails.Count
// }).ToList()
//
// 8. ساخت MetaData:
// - var totalPages = (int)Math.Ceiling(totalCount / (double)request.PageSize)
// - MetaData = new MetaData {
// CurrentPage = request.PageIndex,
// TotalPage = totalPages,
// PageSize = request.PageSize,
// TotalCount = totalCount,
// HasNext = request.PageIndex < totalPages,
// HasPrevious = request.PageIndex > 1
// }
//
// 9. Log و برگشت:
// - _logger.LogInformation("Retrieved {Count} orders for date range {Start} to {End}", orders.Count, request.StartDate, request.EndDate)
// - return new GetOrdersByDateRangeResponseDto { MetaData = metaData, Orders = orderDtos }
//
// نکته: برای performance بهتر، می‌توان از AsNoTracking() استفاده کرد
throw new NotImplementedException("GetOrdersByDateRange needs implementation");
}
}

View File

@@ -0,0 +1,28 @@
namespace CMSMicroservice.Application.UserOrderCQ.Queries.GetOrdersByDateRange;
public class GetOrdersByDateRangeQueryValidator : AbstractValidator<GetOrdersByDateRangeQuery>
{
public GetOrdersByDateRangeQueryValidator()
{
RuleFor(x => x.StartDate)
.LessThanOrEqualTo(x => x.EndDate)
.WithMessage("تاریخ شروع باید کوچکتر یا مساوی تاریخ پایان باشد");
RuleFor(x => x.EndDate)
.LessThanOrEqualTo(DateTime.UtcNow.AddDays(1))
.WithMessage("تاریخ پایان نمی‌تواند در آینده باشد");
RuleFor(x => x.PageIndex)
.GreaterThan(0)
.WithMessage("شماره صفحه باید بزرگتر از 0 باشد");
RuleFor(x => x.PageSize)
.InclusiveBetween(1, 100)
.WithMessage("تعداد در صفحه باید بین 1 تا 100 باشد");
// بازه زمانی نباید بیش از 1 سال باشد
RuleFor(x => x)
.Must(x => (x.EndDate - x.StartDate).TotalDays <= 365)
.WithMessage("بازه زمانی نمی‌تواند بیش از 1 سال باشد");
}
}