From 5e3112d71f3044d60cb1cfb918f06562454d4c41 Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Thu, 4 Dec 2025 17:29:10 +0330 Subject: [PATCH] feat: Implement withdrawal reports query and service integration --- .../PurchaseGoldenPackageCommandHandler.cs | 195 +++++++++----- ...rifyGoldenPackagePurchaseCommandHandler.cs | 248 +++++++++++------- .../UpdateOrderStatusCommandHandler.cs | 63 ++--- .../CalculateOrderPVQueryHandler.cs | 98 ++++--- .../GetOrdersByDateRangeQueryHandler.cs | 130 +++++---- .../Protos/commission.proto | 51 ++++ .../Services/CommissionService.cs | 6 + 7 files changed, 515 insertions(+), 276 deletions(-) diff --git a/src/CMSMicroservice.Application/PackageCQ/Commands/PurchaseGoldenPackage/PurchaseGoldenPackageCommandHandler.cs b/src/CMSMicroservice.Application/PackageCQ/Commands/PurchaseGoldenPackage/PurchaseGoldenPackageCommandHandler.cs index 9e7c7bd..8d2aae5 100644 --- a/src/CMSMicroservice.Application/PackageCQ/Commands/PurchaseGoldenPackage/PurchaseGoldenPackageCommandHandler.cs +++ b/src/CMSMicroservice.Application/PackageCQ/Commands/PurchaseGoldenPackage/PurchaseGoldenPackageCommandHandler.cs @@ -1,6 +1,11 @@ +using CMSMicroservice.Application.Common.Exceptions; using CMSMicroservice.Application.Common.Interfaces; +using CMSMicroservice.Domain.Entities; using CMSMicroservice.Domain.Enums; using MediatR; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using ValidationException = FluentValidation.ValidationException; namespace CMSMicroservice.Application.PackageCQ.Commands.PurchaseGoldenPackage; @@ -22,67 +27,135 @@ public class PurchaseGoldenPackageCommandHandler : IRequestHandler 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 فراخوانی می‌شود + try + { + _logger.LogInformation( + "Starting golden package purchase for UserId: {UserId}, PackageId: {PackageId}", + request.UserId, + request.PackageId); - throw new NotImplementedException("PurchaseGoldenPackage needs implementation"); + // 1. پیدا کردن کاربر + var user = await _context.Users + .FirstOrDefaultAsync(u => u.Id == request.UserId, cancellationToken); + + if (user == null) + { + _logger.LogWarning("User not found for golden package purchase. UserId: {UserId}", request.UserId); + throw new NotFoundException(nameof(User), request.UserId); + } + + // 2. جلوگیری از خرید مجدد پکیج طلایی + if (user.PackagePurchaseMethod != PackagePurchaseMethod.None) + { + _logger.LogWarning( + "User {UserId} has already purchased golden package via {Method}", + request.UserId, + user.PackagePurchaseMethod); + + throw new ValidationException("شما قبلاً پکیج طلایی را خریداری کرده‌اید."); + } + + // 3. پیدا کردن پکیج + var package = await _context.Packages + .FirstOrDefaultAsync(p => p.Id == request.PackageId, cancellationToken); + + if (package == null) + { + _logger.LogWarning("Golden package not found. PackageId: {PackageId}", request.PackageId); + throw new NotFoundException(nameof(Package), request.PackageId); + } + + // اطمینان از اینکه این همان پکیج طلایی است + if (!package.Title.Contains("طلایی", StringComparison.OrdinalIgnoreCase) && + !package.Title.Contains("golden", StringComparison.OrdinalIgnoreCase)) + { + _logger.LogWarning( + "PackageId {PackageId} is not a golden package. Title: {Title}", + request.PackageId, + package.Title); + + throw new ValidationException("فقط پکیج طلایی قابل خرید است."); + } + + // 4. پیدا کردن آدرس پیش‌فرض کاربر (الزامی برای UserOrder) + var defaultAddress = await _context.UserAddresses + .Where(a => a.UserId == request.UserId) + .OrderByDescending(a => a.Created) + .FirstOrDefaultAsync(cancellationToken); + + if (defaultAddress == null) + { + _logger.LogWarning("No address found for user {UserId} in golden package purchase", request.UserId); + throw new ValidationException("لطفاً ابتدا یک آدرس برای خود ثبت کنید."); + } + + // 5. ایجاد سفارش + var order = new UserOrder + { + UserId = user.Id, + PackageId = package.Id, + Amount = package.Price, + PaymentStatus = PaymentStatus.Pending, + DeliveryStatus = DeliveryStatus.None, + UserAddressId = defaultAddress.Id, + PaymentMethod = PaymentMethod.IPG + }; + + _context.UserOrders.Add(order); + await _context.SaveChangesAsync(cancellationToken); + + _logger.LogInformation( + "Created golden package UserOrder {OrderId} for UserId {UserId}, Amount: {Amount}", + order.Id, + request.UserId, + order.Amount); + + // 6. شروع پرداخت با درگاه + var paymentRequest = new PaymentRequest + { + Amount = order.Amount, + UserId = user.Id, + Mobile = user.Mobile ?? string.Empty, + CallbackUrl = request.ReturnUrl, + Description = $"خرید پکیج طلایی - سفارش #{order.Id}" + }; + + var paymentResult = await _paymentGateway.InitiatePaymentAsync(paymentRequest, cancellationToken); + + if (!paymentResult.IsSuccess) + { + _logger.LogError( + "Payment gateway initiation failed for golden package. OrderId {OrderId}: {ErrorMessage}", + order.Id, + paymentResult.ErrorMessage); + + order.PaymentStatus = PaymentStatus.Reject; + await _context.SaveChangesAsync(cancellationToken); + + throw new Exception($"خطا در ارتباط با درگاه پرداخت: {paymentResult.ErrorMessage}"); + } + + _logger.LogInformation( + "Golden package payment initiated successfully. OrderId: {OrderId}, RefId: {RefId}", + order.Id, + paymentResult.RefId); + + return new PurchaseGoldenPackageResponseDto + { + Success = true, + Message = "لطفاً به درگاه پرداخت منتقل شوید.", + OrderId = order.Id, + PaymentGatewayUrl = paymentResult.GatewayUrl ?? string.Empty, + TrackingCode = paymentResult.RefId ?? string.Empty + }; + } + catch (Exception ex) + { + _logger.LogError( + ex, + "Error in PurchaseGoldenPackageCommand for UserId: {UserId}", + request.UserId); + throw; + } } } diff --git a/src/CMSMicroservice.Application/PackageCQ/Commands/VerifyGoldenPackagePurchase/VerifyGoldenPackagePurchaseCommandHandler.cs b/src/CMSMicroservice.Application/PackageCQ/Commands/VerifyGoldenPackagePurchase/VerifyGoldenPackagePurchaseCommandHandler.cs index df9fe85..c60c18a 100644 --- a/src/CMSMicroservice.Application/PackageCQ/Commands/VerifyGoldenPackagePurchase/VerifyGoldenPackagePurchaseCommandHandler.cs +++ b/src/CMSMicroservice.Application/PackageCQ/Commands/VerifyGoldenPackagePurchase/VerifyGoldenPackagePurchaseCommandHandler.cs @@ -1,6 +1,11 @@ +using CMSMicroservice.Application.Common.Exceptions; using CMSMicroservice.Application.Common.Interfaces; +using CMSMicroservice.Domain.Entities; using CMSMicroservice.Domain.Enums; using MediatR; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using ValidationException = FluentValidation.ValidationException; namespace CMSMicroservice.Application.PackageCQ.Commands.VerifyGoldenPackagePurchase; @@ -22,94 +27,161 @@ public class VerifyGoldenPackagePurchaseCommandHandler : IRequestHandler 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 باشد + try + { + _logger.LogInformation( + "Verifying golden package purchase. OrderId: {OrderId}, Authority: {Authority}, Status: {Status}", + request.OrderId, + request.Authority, + request.Status); - throw new NotImplementedException("VerifyGoldenPackagePurchase needs implementation"); + // 1. اگر پرداخت از سمت درگاه موفق گزارش نشده باشد + if (!string.Equals(request.Status, "OK", StringComparison.OrdinalIgnoreCase)) + { + var pendingOrder = await _context.UserOrders + .FirstOrDefaultAsync(o => o.Id == request.OrderId, cancellationToken); + + if (pendingOrder != null && pendingOrder.PaymentStatus == PaymentStatus.Pending) + { + pendingOrder.PaymentStatus = PaymentStatus.Reject; + await _context.SaveChangesAsync(cancellationToken); + } + + throw new ValidationException("پرداخت توسط کاربر لغو شد."); + } + + // 2. پیدا کردن سفارش به همراه کاربر + var order = await _context.UserOrders + .Include(o => o.User) + .FirstOrDefaultAsync(o => o.Id == request.OrderId, cancellationToken); + + if (order == null) + { + _logger.LogWarning("Golden package order not found. OrderId: {OrderId}", request.OrderId); + throw new NotFoundException(nameof(UserOrder), request.OrderId); + } + + // اگر قبلاً با موفقیت پرداخت شده، پاسخ idempotent برگردانیم + if (order.PaymentStatus == PaymentStatus.Success && order.TransactionId.HasValue) + { + var existingWallet = await _context.UserWallets + .FirstOrDefaultAsync(w => w.UserId == order.UserId, cancellationToken); + + var existingTransaction = await _context.Transactions + .FirstOrDefaultAsync(t => t.Id == order.TransactionId.Value, cancellationToken); + + return new VerifyGoldenPackagePurchaseResponseDto + { + Success = true, + Message = "پرداخت قبلاً با موفقیت تایید شده است.", + OrderId = order.Id, + TransactionId = existingTransaction?.Id ?? order.TransactionId.Value, + ReferenceCode = existingTransaction?.RefId ?? string.Empty, + WalletBalance = existingWallet?.Balance ?? 0 + }; + } + + // 3. Verify با درگاه پرداخت + var verifyResult = await _paymentGateway.VerifyPaymentAsync( + request.Authority, + request.Authority, + cancellationToken); + + if (!verifyResult.IsSuccess) + { + _logger.LogWarning( + "Golden package payment verification failed. OrderId: {OrderId}, Message: {Message}", + request.OrderId, + verifyResult.Message); + + order.PaymentStatus = PaymentStatus.Reject; + await _context.SaveChangesAsync(cancellationToken); + + throw new ValidationException($"تراکنش ناموفق: {verifyResult.Message}"); + } + + // 4. شارژ کیف پول (Balance فقط طبق سناریوی پکیج) + var wallet = await _context.UserWallets + .FirstOrDefaultAsync(w => w.UserId == order.UserId, cancellationToken); + + if (wallet == null) + { + _logger.LogError("Wallet not found for UserId: {UserId}", order.UserId); + throw new NotFoundException($"کیف پول کاربر با شناسه {order.UserId} یافت نشد"); + } + + var oldBalance = wallet.Balance; + wallet.Balance += order.Amount; + + _logger.LogInformation( + "Charging wallet Balance for user {UserId} from {OldBalance} to {NewBalance}", + order.UserId, + oldBalance, + wallet.Balance); + + // 5. ثبت Transaction + var transaction = new Transaction + { + Amount = order.Amount, + Description = $"خرید پکیج طلایی از درگاه - سفارش #{order.Id}", + PaymentStatus = PaymentStatus.Success, + PaymentDate = DateTime.UtcNow, + RefId = verifyResult.RefId, + Type = TransactionType.DepositIpg + }; + + _context.Transactions.Add(transaction); + await _context.SaveChangesAsync(cancellationToken); + + // 6. ثبت لاگ تغییر کیف پول + var changeLog = new UserWalletChangeLog + { + WalletId = wallet.Id, + CurrentBalance = wallet.Balance, + ChangeValue = order.Amount, + CurrentNetworkBalance = wallet.NetworkBalance, + ChangeNerworkValue = 0, + CurrentDiscountBalance = wallet.DiscountBalance, + ChangeDiscountValue = 0, + IsIncrease = true, + RefrenceId = transaction.Id + }; + + await _context.UserWalletChangeLogs.AddAsync(changeLog, cancellationToken); + + // 7. به‌روزرسانی سفارش و کاربر + order.TransactionId = transaction.Id; + order.PaymentStatus = PaymentStatus.Success; + order.PaymentDate = DateTime.UtcNow; + order.PaymentMethod = PaymentMethod.IPG; + order.User.PackagePurchaseMethod = PackagePurchaseMethod.DirectPurchase; + + await _context.SaveChangesAsync(cancellationToken); + + _logger.LogInformation( + "Golden package purchase verified successfully. OrderId: {OrderId}, UserId: {UserId}, TransactionId: {TransactionId}, RefId: {RefId}", + order.Id, + order.UserId, + transaction.Id, + verifyResult.RefId); + + return new VerifyGoldenPackagePurchaseResponseDto + { + Success = true, + Message = "پرداخت با موفقیت تایید شد. کیف پول شما شارژ گردید.", + OrderId = order.Id, + TransactionId = transaction.Id, + ReferenceCode = verifyResult.RefId, + WalletBalance = wallet.Balance + }; + } + catch (Exception ex) + { + _logger.LogError( + ex, + "Error in VerifyGoldenPackagePurchaseCommand. OrderId: {OrderId}", + request.OrderId); + throw; + } } } diff --git a/src/CMSMicroservice.Application/UserOrderCQ/Commands/UpdateOrderStatus/UpdateOrderStatusCommandHandler.cs b/src/CMSMicroservice.Application/UserOrderCQ/Commands/UpdateOrderStatus/UpdateOrderStatusCommandHandler.cs index 96279e7..5527fd1 100644 --- a/src/CMSMicroservice.Application/UserOrderCQ/Commands/UpdateOrderStatus/UpdateOrderStatusCommandHandler.cs +++ b/src/CMSMicroservice.Application/UserOrderCQ/Commands/UpdateOrderStatus/UpdateOrderStatusCommandHandler.cs @@ -1,5 +1,7 @@ using CMSMicroservice.Application.Common.Interfaces; using CMSMicroservice.Domain.Enums; +using Microsoft.EntityFrameworkCore; +using ValidationException = FluentValidation.ValidationException; namespace CMSMicroservice.Application.UserOrderCQ.Commands.UpdateOrderStatus; @@ -18,36 +20,37 @@ public class UpdateOrderStatusCommandHandler : IRequestHandler 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 برای انتقال‌های مجاز تعریف کرد + var order = await _context.UserOrders + .FirstOrDefaultAsync(o => o.Id == request.OrderId, cancellationToken); - throw new NotImplementedException("UpdateOrderStatus needs implementation"); + if (order == null) + { + throw new NotFoundException(nameof(order), request.OrderId); + } + + var oldStatus = order.DeliveryStatus; + + // قوانین ساده انتقال وضعیت: از Cancelled نمی‌توان خارج شد + if (oldStatus == DeliveryStatus.Cancelled) + { + throw new ValidationException("امکان تغییر وضعیت سفارش لغو شده وجود ندارد"); + } + + order.DeliveryStatus = request.NewStatus; + + await _context.SaveChangesAsync(cancellationToken); + + _logger.LogInformation( + "Order {OrderId} status changed from {OldStatus} to {NewStatus}", + request.OrderId, + oldStatus, + request.NewStatus); + + return new UpdateOrderStatusResponseDto + { + Success = true, + Message = "وضعیت سفارش با موفقیت تغییر کرد", + CurrentStatus = order.DeliveryStatus + }; } } diff --git a/src/CMSMicroservice.Application/UserOrderCQ/Queries/CalculateOrderPV/CalculateOrderPVQueryHandler.cs b/src/CMSMicroservice.Application/UserOrderCQ/Queries/CalculateOrderPV/CalculateOrderPVQueryHandler.cs index 54ffe48..8f3ea20 100644 --- a/src/CMSMicroservice.Application/UserOrderCQ/Queries/CalculateOrderPV/CalculateOrderPVQueryHandler.cs +++ b/src/CMSMicroservice.Application/UserOrderCQ/Queries/CalculateOrderPV/CalculateOrderPVQueryHandler.cs @@ -1,4 +1,6 @@ +using CMSMicroservice.Application.Common.Exceptions; using CMSMicroservice.Application.Common.Interfaces; +using Microsoft.EntityFrameworkCore; namespace CMSMicroservice.Application.UserOrderCQ.Queries.CalculateOrderPV; @@ -7,6 +9,12 @@ public class CalculateOrderPVQueryHandler : IRequestHandler _logger; + // نسبت PV به قیمت بر اساس مثال‌های بیزینسی: + // محصول ۱: قیمت 100,000 → PV = 50 + // محصول ۲: قیمت 200,000 → PV = 100 + // یعنی: PV = Price / 2000 + private const decimal PvPerRial = 1m / 2000m; + public CalculateOrderPVQueryHandler( IApplicationDbContext context, ILogger logger) @@ -17,46 +25,56 @@ public class CalculateOrderPVQueryHandler : IRequestHandler 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() - // - 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 استفاده کنید + var order = await _context.UserOrders + .Include(o => o.FactorDetails) + .ThenInclude(fd => fd.Product) + .FirstOrDefaultAsync(o => o.Id == request.OrderId, cancellationToken); - throw new NotImplementedException("CalculateOrderPV needs implementation"); + if (order == null) + { + throw new NotFoundException(nameof(order), request.OrderId); + } + + var productPVs = new List(); + decimal totalPV = 0; + + foreach (var detail in order.FactorDetails) + { + if (detail.Product == null) + { + continue; + } + + var unitPrice = detail.Product.Price; + var unitPV = Math.Round(unitPrice * PvPerRial, 2, MidpointRounding.AwayFromZero); + var itemTotalPV = unitPV * detail.Count; + + productPVs.Add(new ProductPVDto + { + ProductId = detail.ProductId, + ProductTitle = detail.Product.Title, + Quantity = detail.Count, + UnitPV = unitPV, + TotalPV = itemTotalPV, + UnitPrice = unitPrice + }); + + totalPV += itemTotalPV; + } + + var response = new CalculateOrderPVResponseDto + { + TotalPV = totalPV, + ProductPVs = productPVs, + // فعلاً مبلغ قابل پرداخت همان Amount است؛ در آینده می‌توان تخفیف را هم اعمال کرد + PayableAmount = order.Amount + }; + + _logger.LogInformation( + "Calculated PV for order {OrderId}: {TotalPV}", + request.OrderId, + totalPV); + + return response; } } diff --git a/src/CMSMicroservice.Application/UserOrderCQ/Queries/GetOrdersByDateRange/GetOrdersByDateRangeQueryHandler.cs b/src/CMSMicroservice.Application/UserOrderCQ/Queries/GetOrdersByDateRange/GetOrdersByDateRangeQueryHandler.cs index f7bcf95..4fb7d94 100644 --- a/src/CMSMicroservice.Application/UserOrderCQ/Queries/GetOrdersByDateRange/GetOrdersByDateRangeQueryHandler.cs +++ b/src/CMSMicroservice.Application/UserOrderCQ/Queries/GetOrdersByDateRange/GetOrdersByDateRangeQueryHandler.cs @@ -1,5 +1,6 @@ using CMSMicroservice.Application.Common.Interfaces; using CMSMicroservice.Application.Common.Models; +using Microsoft.EntityFrameworkCore; namespace CMSMicroservice.Application.UserOrderCQ.Queries.GetOrdersByDateRange; @@ -18,63 +19,78 @@ public class GetOrdersByDateRangeQueryHandler : IRequestHandler 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() استفاده کرد + var query = _context.UserOrders + .AsNoTracking() + .Include(o => o.User) + .Include(o => o.FactorDetails) + .AsQueryable(); - throw new NotImplementedException("GetOrdersByDateRange needs implementation"); + query = query.Where(o => o.Created >= request.StartDate && o.Created <= request.EndDate); + + if (request.Status.HasValue) + { + query = query.Where(o => o.DeliveryStatus == request.Status.Value); + } + + if (request.UserId.HasValue) + { + query = query.Where(o => o.UserId == request.UserId.Value); + } + + var totalCount = await query.CountAsync(cancellationToken); + + var response = new GetOrdersByDateRangeResponseDto + { + MetaData = new MetaData + { + CurrentPage = request.PageIndex, + TotalPage = totalCount == 0 ? 0 : (int)Math.Ceiling(totalCount / (double)request.PageSize), + PageSize = request.PageSize, + TotalCount = totalCount, + HasNext = totalCount > 0 && request.PageIndex * request.PageSize < totalCount, + HasPrevious = request.PageIndex > 1 + } + }; + + if (totalCount == 0) + { + return response; + } + + var orders = await query + .OrderByDescending(o => o.Created) + .Skip((request.PageIndex - 1) * request.PageSize) + .Take(request.PageSize) + .ToListAsync(cancellationToken); + + response.Orders = orders.Select(o => + { + var firstName = o.User?.FirstName ?? string.Empty; + var lastName = o.User?.LastName ?? string.Empty; + var fullName = $"{firstName} {lastName}".Trim(); + + return new OrderSummaryDto + { + Id = o.Id, + UserId = o.UserId, + UserFullName = fullName, + Amount = o.Amount, + // در حال حاضر فیلد DiscountedPrice در UserOrder وجود ندارد، پس همان Amount برگردانده می‌شود + DiscountedPrice = o.Amount, + Status = o.DeliveryStatus, + Created = o.Created, + ShippedAt = null, + DeliveredAt = null, + ItemsCount = o.FactorDetails?.Count ?? 0 + }; + }).ToList(); + + _logger.LogInformation( + "Retrieved {Count} orders for date range {Start} to {End}", + response.Orders.Count, + request.StartDate, + request.EndDate); + + return response; } } diff --git a/src/CMSMicroservice.Protobuf/Protos/commission.proto b/src/CMSMicroservice.Protobuf/Protos/commission.proto index 7e2ddbd..4379e77 100644 --- a/src/CMSMicroservice.Protobuf/Protos/commission.proto +++ b/src/CMSMicroservice.Protobuf/Protos/commission.proto @@ -105,6 +105,13 @@ service CommissionContract get: "/Commission/GetWorkerLogs" }; }; + + // Financial Reports + rpc GetWithdrawalReports(GetWithdrawalReportsRequest) returns (GetWithdrawalReportsResponse){ + option (google.api.http) = { + get: "/Commission/GetWithdrawalReports" + }; + }; } // ============ Commands ============ @@ -388,6 +395,50 @@ message WorkerExecutionLogModel google.protobuf.StringValue details = 10; // JSON or text details } +// GetWithdrawalReports Query +message GetWithdrawalReportsRequest +{ + google.protobuf.Timestamp start_date = 1; // Optional - default: 30 days ago + google.protobuf.Timestamp end_date = 2; // Optional - default: today + int32 period_type = 3; // ReportPeriodType: Daily=1, Weekly=2, Monthly=3 + google.protobuf.Int32Value status = 4; // CommissionPayoutStatus enum (optional) + google.protobuf.Int64Value user_id = 5; // Optional user filter +} + +message GetWithdrawalReportsResponse +{ + repeated PeriodReport period_reports = 1; + WithdrawalSummary summary = 2; +} + +message PeriodReport +{ + string period_label = 1; // e.g., "2025-01-15", "هفته 3", "فروردین 1404" + google.protobuf.Timestamp start_date = 2; + google.protobuf.Timestamp end_date = 3; + int32 total_requests = 4; + int32 pending_count = 5; + int32 approved_count = 6; + int32 rejected_count = 7; + int32 completed_count = 8; + int32 failed_count = 9; + int64 total_amount = 10; + int64 paid_amount = 11; + int64 pending_amount = 12; +} + +message WithdrawalSummary +{ + int32 total_requests = 1; + int64 total_amount = 2; + int64 total_paid = 3; + int64 total_pending = 4; + int64 total_rejected = 5; + int64 average_amount = 6; + int32 unique_users = 7; + float success_rate = 8; // Percentage (0-100) +} + message WithdrawalRequestModel { int64 id = 1; diff --git a/src/CMSMicroservice.WebApi/Services/CommissionService.cs b/src/CMSMicroservice.WebApi/Services/CommissionService.cs index 73b7a19..04d9b3f 100644 --- a/src/CMSMicroservice.WebApi/Services/CommissionService.cs +++ b/src/CMSMicroservice.WebApi/Services/CommissionService.cs @@ -16,6 +16,7 @@ using CMSMicroservice.Application.CommissionCQ.Queries.GetAllWeeklyPools; using CMSMicroservice.Application.CommissionCQ.Queries.GetWithdrawalRequests; using CMSMicroservice.Application.CommissionCQ.Queries.GetWorkerStatus; using CMSMicroservice.Application.CommissionCQ.Queries.GetWorkerExecutionLogs; +using CMSMicroservice.Application.CommissionCQ.Queries.GetWithdrawalReports; namespace CMSMicroservice.WebApi.Services; @@ -110,4 +111,9 @@ public class CommissionService : CommissionContract.CommissionContractBase { return await _dispatchRequestToCQRS.Handle(request, context); } + + public override async Task GetWithdrawalReports(GetWithdrawalReportsRequest request, ServerCallContext context) + { + return await _dispatchRequestToCQRS.Handle(request, context); + } }