From 9864302bab828ce268f427e7ac5a6cb771f1b5e4 Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Fri, 19 Dec 2025 23:14:42 +0330 Subject: [PATCH] feat: implement user wallet charging and transaction logging for Daya loan processing --- ...ulateWeeklyCommissionPoolCommandHandler.cs | 140 +++++++++++++++++- .../CheckDayaLoanStatusCommandHandler.cs | 133 ++++++++++++++++- .../GetNetworkTree/GetNetworkTreeQuery.cs | 2 +- .../GetNetworkTreeQueryHandler.cs | 39 ++--- .../Queries/GetNetworkTree/NetworkTreeDto.cs | 2 +- .../SubmitShopBuyOrderCommandHandler.cs | 77 +++++++--- .../Protos/commission.proto | 2 + .../Protos/networkmembership.proto | 4 +- .../Mappings/NetworkMembershipProfile.cs | 4 +- 9 files changed, 352 insertions(+), 51 deletions(-) diff --git a/src/CMSMicroservice.Application/CommissionCQ/Commands/CalculateWeeklyCommissionPool/CalculateWeeklyCommissionPoolCommandHandler.cs b/src/CMSMicroservice.Application/CommissionCQ/Commands/CalculateWeeklyCommissionPool/CalculateWeeklyCommissionPoolCommandHandler.cs index 3c28c7d..3435261 100644 --- a/src/CMSMicroservice.Application/CommissionCQ/Commands/CalculateWeeklyCommissionPool/CalculateWeeklyCommissionPoolCommandHandler.cs +++ b/src/CMSMicroservice.Application/CommissionCQ/Commands/CalculateWeeklyCommissionPool/CalculateWeeklyCommissionPoolCommandHandler.cs @@ -84,7 +84,10 @@ public class CalculateWeeklyCommissionPoolCommandHandler : IRequestHandler p.Id).ToList(); - // ⭐ اول باید تاریخچه‌ها حذف بشن (به خاطر FK constraint) + // ⭐ ۱. برگرداندن NetworkBalance به حالت قبل (Revert) + await RevertUserWalletsAsync(oldPayoutIds, cancellationToken); + + // ⭐ ۲. حذف تاریخچه‌ها (به خاطر FK constraint) var oldHistories = await _context.CommissionPayoutHistories .Where(h => oldPayoutIds.Contains(h.UserCommissionPayoutId)) .ToListAsync(cancellationToken); @@ -94,7 +97,7 @@ public class CalculateWeeklyCommissionPoolCommandHandler : IRequestHandler + /// شارژ کیف پول کاربران با مبلغ کمیسیون (به NetworkBalance اضافه می‌شود) + /// و ثبت لاگ تغییرات کیف پول + /// + private async Task ChargeUserWalletsAsync( + List payouts, + long weekDefinitionId, + CancellationToken cancellationToken) + { + var userIds = payouts.Select(p => p.UserId).Distinct().ToList(); + + // دریافت کیف پول‌های موجود + var existingWallets = await _context.UserWallets + .Where(w => userIds.Contains(w.UserId)) + .ToDictionaryAsync(w => w.UserId, cancellationToken); + + var newWallets = new List(); + var walletLogs = new List(); + + foreach (var payout in payouts) + { + if (payout.TotalAmount <= 0) continue; + + UserWallet wallet; + + // پیدا کردن یا ایجاد کیف پول + if (existingWallets.TryGetValue(payout.UserId, out var existingWallet)) + { + wallet = existingWallet; + } + else + { + // ایجاد کیف پول جدید + wallet = new UserWallet + { + UserId = payout.UserId, + Balance = 0, + NetworkBalance = 0, + DiscountBalance = 0 + }; + newWallets.Add(wallet); + existingWallets[payout.UserId] = wallet; // برای جلوگیری از تکرار + } + } + + // ذخیره کیف پول‌های جدید + if (newWallets.Any()) + { + await _context.UserWallets.AddRangeAsync(newWallets, cancellationToken); + await _context.SaveChangesAsync(cancellationToken); + } + + // شارژ کیف پول‌ها و ثبت لاگ + foreach (var payout in payouts) + { + if (payout.TotalAmount <= 0) continue; + + var wallet = existingWallets[payout.UserId]; + + // ذخیره موجودی قبلی برای لاگ + var previousNetworkBalance = wallet.NetworkBalance; + + // شارژ NetworkBalance + wallet.NetworkBalance += payout.TotalAmount; + + // ایجاد لاگ تغییر کیف پول + var walletLog = new UserWalletChangeLog + { + WalletId = wallet.Id, + CurrentBalance = wallet.Balance, + ChangeValue = 0, // Balance اصلی تغییر نمی‌کند + CurrentNetworkBalance = wallet.NetworkBalance, + ChangeNerworkValue = payout.TotalAmount, + CurrentDiscountBalance = wallet.DiscountBalance, + ChangeDiscountValue = 0, + IsIncrease = true, + RefrenceId = payout.Id // ارجاع به UserCommissionPayout + }; + + walletLogs.Add(walletLog); + } + + // ذخیره تغییرات کیف پول و لاگ‌ها + await _context.UserWalletChangeLogs.AddRangeAsync(walletLogs, cancellationToken); + await _context.SaveChangesAsync(cancellationToken); + } + + /// + /// برگرداندن NetworkBalance به حالت قبل از شارژ کمیسیون + /// این متد قبل از محاسبه مجدد (ForceRecalculate) اجرا می‌شود + /// + private async Task RevertUserWalletsAsync( + List oldPayoutIds, + CancellationToken cancellationToken) + { + // پیدا کردن لاگ‌های کیف پول مرتبط با پرداخت‌های قبلی + var oldWalletLogs = await _context.UserWalletChangeLogs + .Where(l => l.RefrenceId.HasValue && oldPayoutIds.Contains(l.RefrenceId.Value)) + .ToListAsync(cancellationToken); + + if (!oldWalletLogs.Any()) return; + + // پیدا کردن کیف پول‌های مرتبط + var walletIds = oldWalletLogs.Select(l => l.WalletId).Distinct().ToList(); + var wallets = await _context.UserWallets + .Where(w => walletIds.Contains(w.Id)) + .ToDictionaryAsync(w => w.Id, cancellationToken); + + // برگرداندن NetworkBalance + foreach (var log in oldWalletLogs) + { + if (wallets.TryGetValue(log.WalletId, out var wallet)) + { + // کم کردن مبلغ شارژ شده قبلی از NetworkBalance + wallet.NetworkBalance -= log.ChangeNerworkValue; + + // اطمینان از منفی نشدن + if (wallet.NetworkBalance < 0) + { + wallet.NetworkBalance = 0; + } + } + } + + // حذف لاگ‌های قبلی + _context.UserWalletChangeLogs.RemoveRange(oldWalletLogs); + + await _context.SaveChangesAsync(cancellationToken); + } } diff --git a/src/CMSMicroservice.Application/DayaLoanCQ/Commands/CheckDayaLoanStatus/CheckDayaLoanStatusCommandHandler.cs b/src/CMSMicroservice.Application/DayaLoanCQ/Commands/CheckDayaLoanStatus/CheckDayaLoanStatusCommandHandler.cs index 3261853..6fb89c6 100644 --- a/src/CMSMicroservice.Application/DayaLoanCQ/Commands/CheckDayaLoanStatus/CheckDayaLoanStatusCommandHandler.cs +++ b/src/CMSMicroservice.Application/DayaLoanCQ/Commands/CheckDayaLoanStatus/CheckDayaLoanStatusCommandHandler.cs @@ -10,6 +10,11 @@ public class CheckDayaLoanStatusCommandHandler : IRequestHandler _logger; + + /// + /// مبلغ وام دایا - 56 میلیون ریال + /// + private const long DAYA_LOAN_AMOUNT = 56_000_000; public CheckDayaLoanStatusCommandHandler( IApplicationDbContext context, @@ -48,9 +53,24 @@ public class CheckDayaLoanStatusCommandHandler : IRequestHandler r.ContractNumber != null) }; } + + /// + /// پردازش دریافت وام دایا برای کاربر + /// 1. شارژ کیف پول (Balance + DiscountBalance) + /// 2. ثبت تراکنش + /// 3. ثبت لاگ کیف پول + /// 4. به‌روزرسانی وضعیت کاربر + /// + private async Task ProcessDayaLoanReceivedAsync(long userId, string contractNumber, CancellationToken cancellationToken) + { + // پیدا کردن کاربر + var user = await _context.Users + .FirstOrDefaultAsync(u => u.Id == userId, cancellationToken); + + if (user == null) + { + _logger.LogWarning("User {UserId} not found for Daya loan processing", userId); + return; + } + + // بررسی اینکه قبلاً دریافت نکرده باشد + if (user.HasReceivedDayaCredit) + { + _logger.LogWarning("User {UserId} has already received Daya credit", userId); + return; + } + + // پیدا کردن یا ایجاد کیف پول کاربر + var wallet = await _context.UserWallets + .FirstOrDefaultAsync(w => w.UserId == userId, cancellationToken); + + if (wallet == null) + { + wallet = new UserWallet + { + UserId = userId, + Balance = 0, + NetworkBalance = 0, + DiscountBalance = 0 + }; + await _context.UserWallets.AddAsync(wallet, cancellationToken); + await _context.SaveChangesAsync(cancellationToken); + } + + // 1. شارژ کیف پول - هم موجودی اصلی و هم موجودی تخفیف + var previousBalance = wallet.Balance; + var previousDiscountBalance = wallet.DiscountBalance; + + wallet.Balance += DAYA_LOAN_AMOUNT; + wallet.DiscountBalance += DAYA_LOAN_AMOUNT; + + // 2. ثبت تراکنش - RefId = شماره قرارداد، Status = 0 (Success)، Type = 2 (DepositExternal1) + var transaction = new Transaction + { + Amount = DAYA_LOAN_AMOUNT, + Description = $"شارژ کیف پول از وام دایا - قرارداد {contractNumber}", + PaymentStatus = PaymentStatus.Success, // 0 + PaymentDate = DateTime.Now, + RefId = contractNumber, + Type = TransactionType.DepositExternal1 // 2 + }; + + await _context.Transactions.AddAsync(transaction, cancellationToken); + await _context.SaveChangesAsync(cancellationToken); + + // 3. ثبت لاگ کیف پول با شناسه تراکنش + var walletLog = new UserWalletChangeLog + { + WalletId = wallet.Id, + CurrentBalance = wallet.Balance, + ChangeValue = DAYA_LOAN_AMOUNT, + CurrentNetworkBalance = wallet.NetworkBalance, + ChangeNerworkValue = 0, + CurrentDiscountBalance = wallet.DiscountBalance, + ChangeDiscountValue = DAYA_LOAN_AMOUNT, + IsIncrease = true, + RefrenceId = transaction.Id + }; + + await _context.UserWalletChangeLogs.AddAsync(walletLog, cancellationToken); + + // 4. به‌روزرسانی وضعیت کاربر + user.HasReceivedDayaCredit = true; + user.DayaCreditReceivedAt = DateTime.Now; + user.PackagePurchaseMethod = PackagePurchaseMethod.DayaLoan; // 1 + + await _context.SaveChangesAsync(cancellationToken); + + _logger.LogInformation( + "Daya loan fully processed for User {UserId}: " + + "Wallet Balance {PreviousBalance} -> {NewBalance}, " + + "DiscountBalance {PreviousDiscountBalance} -> {NewDiscountBalance}, " + + "Transaction Id: {TransactionId}", + userId, previousBalance, wallet.Balance, + previousDiscountBalance, wallet.DiscountBalance, + transaction.Id); + } } diff --git a/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkTree/GetNetworkTreeQuery.cs b/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkTree/GetNetworkTreeQuery.cs index 4c81777..1b84717 100644 --- a/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkTree/GetNetworkTreeQuery.cs +++ b/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkTree/GetNetworkTreeQuery.cs @@ -23,5 +23,5 @@ public record GetNetworkTreeQuery : IRequest /// /// فیلتر براساس شماره هفته فعالسازی (مثال: 2025-W05) /// - public string? ActivationWeekNumber { get; init; } + public long? ActivationWeekDefinitionId { get; init; } } diff --git a/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkTree/GetNetworkTreeQueryHandler.cs b/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkTree/GetNetworkTreeQueryHandler.cs index d502b2d..a349938 100644 --- a/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkTree/GetNetworkTreeQueryHandler.cs +++ b/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkTree/GetNetworkTreeQueryHandler.cs @@ -42,19 +42,21 @@ public class GetNetworkTreeQueryHandler : IRequestHandler /// محاسبه شماره هفته از تاریخ /// - private string CalculateWeekNumber(DateTime date) - { - // First try to get from repository cache - var weekDef = _weekDefinitionRepository.GetWeekByDate(date); - if (weekDef != null) - { - return $"{weekDef.GregorianYear}-W{weekDef.GregorianWeekNumber:D2}"; - } - - // Fallback: use repository's calculation method - return _weekDefinitionRepository.CalculateGregorianWeekNumber(date); - } + // private long CalculateWeekNumber(DateTime date) + // { + // // First try to get from repository cache + // var weekDef = _weekDefinitionRepository.GetWeekByDate(date); + // if (weekDef != null) + // { + // return weekDef.Id; + // } + // + // // Fallback: use repository's calculation method + // // return _weekDefinitionRepository.CalculateGregorianWeekNumber(date); + // return 0; + // } diff --git a/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkTree/NetworkTreeDto.cs b/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkTree/NetworkTreeDto.cs index f0c14ac..99233f6 100644 --- a/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkTree/NetworkTreeDto.cs +++ b/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkTree/NetworkTreeDto.cs @@ -25,7 +25,7 @@ public class NetworkTreeDto /// /// شماره هفته فعال‌سازی /// - public string? ActivationWeekNumber { get; set; } + public long? ActivationWeekDefinitionId { get; set; } /// /// نام نمایشی هفته فعال‌سازی (مثل: هفته یکم) diff --git a/src/CMSMicroservice.Application/UserOrderCQ/Commands/SubmitShopBuyOrder/SubmitShopBuyOrderCommandHandler.cs b/src/CMSMicroservice.Application/UserOrderCQ/Commands/SubmitShopBuyOrder/SubmitShopBuyOrderCommandHandler.cs index 4b50cda..9a5e768 100644 --- a/src/CMSMicroservice.Application/UserOrderCQ/Commands/SubmitShopBuyOrder/SubmitShopBuyOrderCommandHandler.cs +++ b/src/CMSMicroservice.Application/UserOrderCQ/Commands/SubmitShopBuyOrder/SubmitShopBuyOrderCommandHandler.cs @@ -10,6 +10,7 @@ public class { private readonly IApplicationDbContext _context; private readonly ILogger _logger; + private float _vatRate; public SubmitShopBuyOrderCommandHandler( IApplicationDbContext context, @@ -35,7 +36,7 @@ public class // چک موجودی محصولات var outOfStockProducts = new List(); var insufficientStockProducts = new List<(string Title, int Requested, int Available)>(); - + foreach (var cartItem in user.UserCarts) { if (cartItem.Product.RemainingCount <= 0) @@ -44,33 +45,55 @@ public class } else if (cartItem.Product.RemainingCount < cartItem.Count) { - insufficientStockProducts.Add((cartItem.Product.Title, cartItem.Count, cartItem.Product.RemainingCount)); + insufficientStockProducts.Add((cartItem.Product.Title, cartItem.Count, + cartItem.Product.RemainingCount)); } } - + if (outOfStockProducts.Any()) { throw new Exception($"محصولات زیر ناموجود شده‌اند: {string.Join("، ", outOfStockProducts)}"); } - + if (insufficientStockProducts.Any()) { - var messages = insufficientStockProducts.Select(p => + var messages = insufficientStockProducts.Select(p => $"«{p.Title}»: درخواست {p.Requested} عدد، موجودی {p.Available} عدد"); throw new Exception($"موجودی محصولات زیر کافی نیست: {string.Join(" | ", messages)}"); } - if (user.UserCarts.Sum(s => s.Count * s.Product.Price) != request.TotalAmount) + long finalAmount = 0; + var vatIsEnable = + _context.SystemConfigurations + .FirstOrDefault(f => f.Scope == ConfigurationScope.VAT && f.Key == "IsEnabled")?.Value ?? + "0"; + if (vatIsEnable=="1") + { + _vatRate = float.Parse( + _context.SystemConfigurations + .FirstOrDefault(f => f.Scope == ConfigurationScope.VAT && f.Key == "Shop.VAT")?.Value ?? + throw new InvalidOperationException()); + finalAmount = AddVAT(user.UserCarts.Sum(s => s.Count * s.Product.Price)); + _logger.LogInformation( + "Calculating final amount with VAT. Base Amount: {BaseAmount}, VAT Rate: {VATRate}, Final Amount: {FinalAmount}", + user.UserCarts.Sum(s => s.Count * s.Product.Price), _vatRate, finalAmount); + } + else + { + finalAmount = user.UserCarts.Sum(s => s.Count * s.Product.Price); + } + + if (finalAmount != request.TotalAmount) throw new Exception("مبلغ سفارش با مجموع سبد خرید مطابقت ندارد."); var userWallet = user.UserWallets.FirstOrDefault(); if (userWallet == null) throw new Exception("کیف پول کاربر یافت نشد."); - - if (userWallet.Balance<=0 || userWallet.Balance new FactorDetails() { ProductId = s.ProductId, Count = s.Count, - UnitPrice = s.Product.Price, + UnitPrice =vatIsEnable=="1" ?AddVAT(s.Product.Price): s.Product.Price, OrderId = newOrder.Id }); await _context.FactorDetails.AddRangeAsync(factorDetailsList, cancellationToken); - + // کاهش موجودی محصولات و افزایش تعداد فروش foreach (var cartItem in user.UserCarts) { cartItem.Product.RemainingCount -= cartItem.Count; cartItem.Product.SaleCount += cartItem.Count; } - + user.UserCarts.Clear(); await _context.SaveChangesAsync(cancellationToken); - - _logger.LogInformation("Order {OrderId} completed. Stock updated for {ProductCount} products.", + + _logger.LogInformation("Order {OrderId} completed. Stock updated for {ProductCount} products.", newOrder.Id, factorDetailsList.Count()); - + var finalResult = new SubmitShopBuyOrderResponseDto() { Id = newOrder.Id, - }; return finalResult; } + public long CalculateVAT(long amount) => (long)(amount * _vatRate); + + /// + /// محاسبه قیمت با احتساب مالیات + /// + public long AddVAT(long amount) => amount + CalculateVAT(amount); + private async Task CalculateAndSaveVAT(long orderId, long orderAmount, CancellationToken cancellationToken) { try @@ -166,7 +195,7 @@ public class // دریافت نرخ VAT var vatRateConfig = await _context.SystemConfigurations - .FirstOrDefaultAsync(x => x.Scope == ConfigurationScope.VAT && x.Key == "Rate", cancellationToken); + .FirstOrDefaultAsync(x => x.Scope == ConfigurationScope.VAT && x.Key == "Shop.VAT", cancellationToken); if (vatRateConfig == null || !decimal.TryParse(vatRateConfig.Value, out var vatRate)) { @@ -201,7 +230,7 @@ public class vatAmount, totalAmount ); - + return true; } catch (Exception ex) @@ -211,4 +240,4 @@ public class return false; } } -} +} \ No newline at end of file diff --git a/src/CMSMicroservice.Protobuf/Protos/commission.proto b/src/CMSMicroservice.Protobuf/Protos/commission.proto index c6040e4..324bad7 100644 --- a/src/CMSMicroservice.Protobuf/Protos/commission.proto +++ b/src/CMSMicroservice.Protobuf/Protos/commission.proto @@ -347,6 +347,7 @@ message WeeklyCommissionPoolModel { int64 id = 1; int64 week_definition_id = 2; + string week_display_name = 3; int64 total_pool_amount = 4; int32 total_balances = 5; int64 value_per_balance = 6; @@ -433,6 +434,7 @@ message WorkerExecutionLogModel { string execution_id = 1; int64 week_definition_id = 2; + string week_display_name = 3; string step = 4; // "Balances" | "Pool" | "Payouts" | "Full" bool success = 5; google.protobuf.StringValue error_message = 6; diff --git a/src/CMSMicroservice.Protobuf/Protos/networkmembership.proto b/src/CMSMicroservice.Protobuf/Protos/networkmembership.proto index 2386b31..326f083 100644 --- a/src/CMSMicroservice.Protobuf/Protos/networkmembership.proto +++ b/src/CMSMicroservice.Protobuf/Protos/networkmembership.proto @@ -156,7 +156,7 @@ message GetNetworkTreeRequest google.protobuf.Int32Value max_depth = 2; google.protobuf.BoolValue only_active = 3; google.protobuf.BoolValue is_club_active = 4; - google.protobuf.StringValue activation_week_number = 5; + google.protobuf.StringValue activation_week_definition_id = 5; } message GetNetworkTreeResponse @@ -175,7 +175,7 @@ message NetworkTreeNodeModel google.protobuf.Timestamp joined_at = 7; google.protobuf.Timestamp club_activated_at = 8; // تاریخ فعال‌سازی در باشگاه bool is_club_active = 9; // فعال بودن در باشگاه - string activation_week_number = 10; // شماره هفته فعال‌سازی + google.protobuf.Int64Value activation_week_definition_id = 10; // شماره هفته فعال‌سازی bool is_activated_in_target_week = 11; // آیا در هفته هدف فعال شده google.protobuf.Timestamp user_created = 12; // تاریخ ایجاد کاربر } diff --git a/src/CMSMicroservice.WebApi/Common/Mappings/NetworkMembershipProfile.cs b/src/CMSMicroservice.WebApi/Common/Mappings/NetworkMembershipProfile.cs index 6badf7a..edeeb6d 100644 --- a/src/CMSMicroservice.WebApi/Common/Mappings/NetworkMembershipProfile.cs +++ b/src/CMSMicroservice.WebApi/Common/Mappings/NetworkMembershipProfile.cs @@ -14,7 +14,7 @@ public class NetworkMembershipProfile : IRegister .Map(dest => dest.UserId, src => src.UserId) .Map(dest => dest.MaxDepth, src => src.MaxDepth != null && src.MaxDepth.Value > 0 ? src.MaxDepth.Value : 3) .Map(dest => dest.IsClubActive, src => src.IsClubActive) - .Map(dest => dest.ActivationWeekNumber, src => src.ActivationWeekNumber); + .Map(dest => dest.ActivationWeekDefinitionId, src => src.ActivationWeekDefinitionId); // Response mapping: تبدیل درخت به لیست مسطح config.NewConfig() @@ -85,7 +85,7 @@ public class NetworkMembershipProfile : IRegister NetworkLevel = node.CurrentDepth, IsActive = true, IsClubActive = node.IsClubActive, - ActivationWeekNumber = node.ActivationWeekNumber ?? string.Empty, + ActivationWeekDefinitionId = node.ActivationWeekDefinitionId ?? 0, IsActivatedInTargetWeek = node.IsActivatedInTargetWeek };