using CMSMicroservice.Protobuf.Protos.UserWallet; using CMSMicroservice.WebApi.Common.Services; using CMSMicroservice.Application.UserWalletCQ.Commands.CreateNewUserWallet; using CMSMicroservice.Application.UserWalletCQ.Commands.UpdateUserWallet; using CMSMicroservice.Application.UserWalletCQ.Commands.DeleteUserWallet; using CMSMicroservice.Application.WalletCQ.Commands.ChargeMagicWallet; using CMSMicroservice.Application.WalletCQ.Commands.ChargeDiscountWallet; using CMSMicroservice.Application.WalletCQ.Commands.ChargeCreditWallet; using CMSMicroservice.Application.WalletCQ.Commands.VerifyMagicWalletCharge; using CMSMicroservice.Application.WalletCQ.Commands.VerifyDiscountWalletCharge; using CMSMicroservice.Application.WalletCQ.Commands.VerifyCreditWalletCharge; using CMSMicroservice.Application.WalletCQ.Commands.AdminManualCreditCharge; using CMSMicroservice.Application.WalletCQ.Queries.GetManualCreditCharges; using AppManualChargeFilter = CMSMicroservice.Application.WalletCQ.Queries.GetManualCreditCharges.GetManualCreditChargesFilter; using CMSMicroservice.Application.UserWalletCQ.Queries.GetUserWallet; using CMSMicroservice.Application.UserWalletCQ.Queries.GetAllUserWalletByFilter; using CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWalletHistory; using CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWithdrawals; using CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWithdrawalSettings; using CMSMicroservice.Application.Common.Interfaces; using CMSMicroservice.Application.Common; using CMSMicroservice.Application.Common.Exceptions; using CMSMicroservice.Domain.Common; using CMSMicroservice.Domain.Enums; using Grpc.Core; using Google.Protobuf.WellKnownTypes; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using System.Linq; namespace CMSMicroservice.WebApi.Services; public class UserWalletService : UserWalletContract.UserWalletContractBase { private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS; private readonly ISender _sender; private readonly IApplicationDbContext _context; private readonly ICurrentUserService _currentUserService; private readonly ILogger _logger; public UserWalletService( IDispatchRequestToCQRS dispatchRequestToCQRS, ISender sender, IApplicationDbContext context, ICurrentUserService currentUserService, ILogger logger) { _dispatchRequestToCQRS = dispatchRequestToCQRS; _sender = sender; _context = context; _currentUserService = currentUserService; _logger = logger; } public override async Task CreateNewUserWallet(CreateNewUserWalletRequest request, ServerCallContext context) { return await _dispatchRequestToCQRS.Handle(request, context); } public override async Task UpdateUserWallet(UpdateUserWalletRequest request, ServerCallContext context) { return await _dispatchRequestToCQRS.Handle(request, context); } public override async Task DeleteUserWallet(DeleteUserWalletRequest request, ServerCallContext context) { return await _dispatchRequestToCQRS.Handle(request, context); } public override async Task GetUserWallet(GetUserWalletRequest request, ServerCallContext context) { return await _dispatchRequestToCQRS.Handle(request, context); } public override async Task GetAllUserWalletByFilter(GetAllUserWalletByFilterRequest request, ServerCallContext context) { var response = await _dispatchRequestToCQRS.Handle(request, context); // Enrich response with user names if (response?.Models != null && response.Models.Any()) { var userIds = response.Models.Select(m => m.UserId).Distinct().ToList(); var users = await _context.Users .AsNoTracking() .Where(u => userIds.Contains(u.Id)) .Select(u => new { u.Id, u.FirstName, u.LastName }) .ToDictionaryAsync(u => u.Id, context.CancellationToken); foreach (var model in response.Models) { if (users.TryGetValue(model.UserId, out var user)) { model.UserName = $"{user.FirstName} {user.LastName}".Trim(); } } } return response; } // ============= Customer-specific Methods ============= public override async Task GetCustomerWallet(Google.Protobuf.WellKnownTypes.Empty request, ServerCallContext context) { // Customer endpoint: resolve userId from JWT if (!long.TryParse(_currentUserService.UserId, out var userId) || userId <= 0) throw new RpcException(new Status(StatusCode.Unauthenticated, "کاربر احراز هویت نشده است")); var walletQuery = new GetUserWalletQuery { Id = userId }; var wallet = await _sender.Send(walletQuery, context.CancellationToken); var walletEntity = await _context.UserWallets .FirstOrDefaultAsync(w => w.UserId == userId, context.CancellationToken); return new GetCustomerWalletResponse { Balance = wallet.Balance, NetworkBalance = wallet.NetworkBalance, DiscountBalance = wallet.DiscountBalance, WalletMode = (int)(walletEntity?.WalletMode ?? WalletMode.Normal) }; } public override async Task GetCustomerWalletHistory(GetCustomerWalletHistoryRequest request, ServerCallContext context) { var query = new GetCustomerWalletHistoryQuery { ReferenceId = request.ReferenceId, IsIncrease = request.IsIncrease }; var changeLogs = await _sender.Send(query, context.CancellationToken); var response = new GetCustomerWalletHistoryResponse { MetaData = new CMSMicroservice.Protobuf.Protos.MetaData { CurrentPage = 1, TotalPage = 1, PageSize = changeLogs.Count, TotalCount = changeLogs.Count, HasPrevious = false, HasNext = false } }; foreach (var log in changeLogs) { response.Models.Add(new CustomerWalletHistoryModel { CurrentBalance = log.CurrentBalance, ChangeValue = log.ChangeValue, CurrentNetworkBalance = log.CurrentNetworkBalance, ChangeNerworkValue = log.ChangeNerworkValue, CurrentDiscountBalance = log.CurrentDiscountBalance, ChangeDiscountValue = log.ChangeDiscountValue, IsIncrease = log.IsIncrease, RefrenceId = log.RefrenceId, CreatedAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(DateTime.SpecifyKind(log.Created, DateTimeKind.Utc)) }); } return response; } public override async Task CustomerWithdrawBalance(CustomerWithdrawBalanceRequest request, ServerCallContext context) { var userId = GetCurrentUserId(); // Find the commission payout record var payout = await _context.UserCommissionPayouts .Where(p => p.Id == request.PayoutId && p.UserId == userId && !p.IsDeleted) .FirstOrDefaultAsync(context.CancellationToken); if (payout == null) throw new RpcException(new Status(StatusCode.NotFound, "رکورد پرداخت کمیسیون یافت نشد")); // Validate status - can only withdraw if already paid to wallet if (payout.Status != Domain.Enums.CommissionPayoutStatus.Paid) throw new RpcException(new Status(StatusCode.FailedPrecondition, "فقط کمیسیون‌های واریز شده به کیف پول قابل برداشت هستند")); // Update payout with withdrawal info payout.WithdrawalMethod = (Domain.Enums.WithdrawalMethod)request.WithdrawalMethod; if (payout.WithdrawalMethod == Domain.Enums.WithdrawalMethod.Cash) { var normalizedIban = IbanNormalizer.TryNormalize(request.IbanNumber); if (normalizedIban is null) throw new RpcException(new Status(StatusCode.InvalidArgument, "فرمت شماره شبا معتبر نیست. باید IR و ۲۴ رقم باشد.")); payout.IbanNumber = normalizedIban; } else { payout.IbanNumber = null; } payout.Status = Domain.Enums.CommissionPayoutStatus.WithdrawRequested; await _context.SaveChangesAsync(context.CancellationToken); return new Google.Protobuf.WellKnownTypes.Empty(); } // ============= Magic Wallet Methods ============= public override async Task InitiateMagicCharge( InitiateMagicChargeRequest request, ServerCallContext context) { var userId = GetCurrentUserId(); try { var result = await _sender.Send(new ChargeMagicWalletCommand { UserId = userId, Amount = request.Amount }, context.CancellationToken); return new InitiateMagicChargeResponse { IsSuccess = result.IsSuccess, GatewayUrl = result.GatewayUrl ?? "", ErrorMessage = result.ErrorMessage ?? "" }; } catch (PaymentInProgressException ex) { return new InitiateMagicChargeResponse { IsSuccess = false, ErrorMessage = ex.Message }; } } // ============= Discount Wallet Methods ============= public override async Task InitiateDiscountCharge( InitiateDiscountChargeRequest request, ServerCallContext context) { var userId = GetCurrentUserId(); try { var result = await _sender.Send(new ChargeDiscountWalletCommand { UserId = userId, Amount = request.Amount }, context.CancellationToken); return new InitiateDiscountChargeResponse { IsSuccess = result.IsSuccess, GatewayUrl = result.GatewayUrl ?? "", ErrorMessage = result.ErrorMessage ?? "" }; } catch (PaymentInProgressException ex) { return new InitiateDiscountChargeResponse { IsSuccess = false, ErrorMessage = ex.Message }; } } // ============= Credit (Main) Wallet Methods ============= public override async Task InitiateCreditCharge( InitiateCreditChargeRequest request, ServerCallContext context) { var userId = GetCurrentUserId(); try { var result = await _sender.Send(new ChargeCreditWalletCommand { UserId = userId, Amount = request.Amount }, context.CancellationToken); return new InitiateCreditChargeResponse { IsSuccess = result.IsSuccess, GatewayUrl = result.GatewayUrl ?? "", ErrorMessage = result.ErrorMessage ?? "" }; } catch (PaymentInProgressException ex) { return new InitiateCreditChargeResponse { IsSuccess = false, ErrorMessage = ex.Message }; } catch (BadRequestException ex) { return new InitiateCreditChargeResponse { IsSuccess = false, ErrorMessage = ex.Message }; } } // ============= Wallet Verify Methods ============= public override async Task VerifyMagicCharge( VerifyWalletChargeRequest request, ServerCallContext context) { _logger.LogInformation("VerifyMagicCharge called: Authority={Authority}, Status={Status}", request.Authority, request.Status); try { if (string.IsNullOrEmpty(request.Authority)) return new VerifyWalletChargeResponse { Success = false, Message = "کد Authority نامعتبر است" }; var result = await _sender.Send(new VerifyMagicWalletChargeCommand { Authority = request.Authority, Status = request.Status ?? "NOK" }, context.CancellationToken); _logger.LogInformation("VerifyMagicCharge result: {Result}, Authority={Authority}", result, request.Authority); return new VerifyWalletChargeResponse { Success = result, Message = result ? "شارژ کیف‌پول جادویی با موفقیت انجام شد" : "شارژ کیف‌پول جادویی ناموفق بود" }; } catch (Exception ex) { _logger.LogError(ex, "VerifyMagicCharge error: Authority={Authority}", request.Authority); return new VerifyWalletChargeResponse { Success = false, Message = ex.Message }; } } public override async Task VerifyDiscountCharge( VerifyWalletChargeRequest request, ServerCallContext context) { _logger.LogInformation("VerifyDiscountCharge called: Authority={Authority}, Status={Status}", request.Authority, request.Status); try { if (string.IsNullOrEmpty(request.Authority)) return new VerifyWalletChargeResponse { Success = false, Message = "کد Authority نامعتبر است" }; // پیدا کردن PaymentTransaction برای استخراج UserId و Amount var paymentTx = await _context.PaymentTransactions .FirstOrDefaultAsync(pt => pt.Authority == request.Authority, context.CancellationToken); if (paymentTx == null || !paymentTx.UserId.HasValue) { _logger.LogError("VerifyDiscountCharge: PaymentTransaction not found for Authority={Authority}", request.Authority); return new VerifyWalletChargeResponse { Success = false, Message = "تراکنش یافت نشد" }; } if (!string.Equals(request.Status, "OK", StringComparison.OrdinalIgnoreCase)) { _logger.LogWarning("VerifyDiscountCharge: Payment cancelled by user. Authority={Authority}", request.Authority); return new VerifyWalletChargeResponse { Success = false, Message = "پرداخت توسط کاربر لغو شد" }; } var result = await _sender.Send(new VerifyDiscountWalletChargeCommand { UserId = paymentTx.UserId.Value, Amount = paymentTx.Amount, Authority = request.Authority }, context.CancellationToken); _logger.LogInformation("VerifyDiscountCharge result: {Result}, Authority={Authority}, UserId={UserId}", result, request.Authority, paymentTx.UserId.Value); return new VerifyWalletChargeResponse { Success = result, Message = result ? "شارژ کیف پول تخفیفی با موفقیت انجام شد" : "شارژ کیف پول تخفیفی ناموفق بود" }; } catch (Exception ex) { _logger.LogError(ex, "VerifyDiscountCharge error: Authority={Authority}", request.Authority); return new VerifyWalletChargeResponse { Success = false, Message = ex.Message }; } } public override async Task VerifyCreditCharge( VerifyWalletChargeRequest request, ServerCallContext context) { _logger.LogInformation("VerifyCreditCharge called: Authority={Authority}, Status={Status}", request.Authority, request.Status); try { if (string.IsNullOrEmpty(request.Authority)) return new VerifyWalletChargeResponse { Success = false, Message = "کد Authority نامعتبر است" }; var paymentTx = await _context.PaymentTransactions .FirstOrDefaultAsync(pt => pt.Authority == request.Authority, context.CancellationToken); if (paymentTx == null || !paymentTx.UserId.HasValue) { _logger.LogError("VerifyCreditCharge: PaymentTransaction not found for Authority={Authority}", request.Authority); return new VerifyWalletChargeResponse { Success = false, Message = "تراکنش یافت نشد" }; } if (!string.Equals(request.Status, "OK", StringComparison.OrdinalIgnoreCase)) { _logger.LogWarning("VerifyCreditCharge: Payment cancelled by user. Authority={Authority}", request.Authority); return new VerifyWalletChargeResponse { Success = false, Message = "پرداخت توسط کاربر لغو شد" }; } var result = await _sender.Send(new VerifyCreditWalletChargeCommand { UserId = paymentTx.UserId.Value, Amount = paymentTx.Amount, Authority = request.Authority }, context.CancellationToken); _logger.LogInformation("VerifyCreditCharge result: {Result}, Authority={Authority}, UserId={UserId}", result, request.Authority, paymentTx.UserId.Value); return new VerifyWalletChargeResponse { Success = result, Message = result ? "شارژ کیف پول اصلی با موفقیت انجام شد" : "شارژ کیف پول اصلی ناموفق بود" }; } catch (Exception ex) { _logger.LogError(ex, "VerifyCreditCharge error: Authority={Authority}", request.Authority); return new VerifyWalletChargeResponse { Success = false, Message = ex.Message }; } } public override async Task GetMagicWalletStatus( Google.Protobuf.WellKnownTypes.Empty request, ServerCallContext context) { var userId = GetCurrentUserId(); var wallet = await _context.UserWallets .FirstOrDefaultAsync(w => w.UserId == userId, context.CancellationToken); if (wallet == null) throw new RpcException(new Status(StatusCode.NotFound, "کیف پول یافت نشد")); var cycleCount = await _context.ClubMembershipCycles .CountAsync(c => c.UserId == userId, context.CancellationToken); // بارگذاری پکیج کاربر برای سقف کیف‌پول جادویی var currentCycle = await _context.ClubMembershipCycles .FirstOrDefaultAsync(c => c.UserId == userId && c.IsCurrentCycle, context.CancellationToken); var package = currentCycle != null ? await _context.Packages.FirstOrDefaultAsync(p => p.Id == currentCycle.PackageId, context.CancellationToken) : await _context.Packages.FirstOrDefaultAsync(p => p.IsBasePackage && !p.IsDeleted, context.CancellationToken); var magicMaxDeposit = package?.MagicWalletMaxDeposit ?? SystemConstants.MagicWalletDefaultMaxDeposit; var magicMultiplier = package != null ? (double)package.MagicWalletMultiplier : SystemConstants.MagicWalletDefaultMultiplier; var magicMaxCredit = package?.MagicWalletMaxCredit ?? SystemConstants.MagicWalletDefaultMaxCredit; var response = new GetMagicWalletStatusResponse { WalletMode = (int)wallet.WalletMode, MagicTotalDeposited = wallet.MagicTotalDeposited, MagicTotalCredited = wallet.MagicTotalCredited, MagicMaxDeposit = magicMaxDeposit, MagicRemainingDeposit = Math.Max(0, magicMaxDeposit - wallet.MagicTotalDeposited), Balance = wallet.Balance, PurchaseCycleCount = cycleCount, MagicMultiplier = magicMultiplier, MagicMaxCredit = magicMaxCredit }; if (wallet.MagicActivatedAt.HasValue) { response.MagicActivatedAt = Timestamp.FromDateTime( DateTime.SpecifyKind(wallet.MagicActivatedAt.Value, DateTimeKind.Utc)); } return response; } private long GetCurrentUserId() { if (long.TryParse(_currentUserService.UserId, out var userId) && userId > 0) return userId; throw new RpcException(new Status(StatusCode.Unauthenticated, "لطفاً وارد حساب کاربری خود شوید")); } public override async Task GetCustomerWithdrawals(GetCustomerWithdrawalsRequest request, ServerCallContext context) { var query = new GetCustomerWithdrawalsQuery { Status = request.Status }; var withdrawals = await _sender.Send(query, context.CancellationToken); var response = new GetCustomerWithdrawalsResponse { MetaData = new CMSMicroservice.Protobuf.Protos.MetaData { CurrentPage = 1, TotalPage = 1, PageSize = withdrawals.Count, TotalCount = withdrawals.Count, HasPrevious = false, HasNext = false } }; foreach (var withdrawal in withdrawals) { response.Models.Add(new CustomerWithdrawalModel { Id = withdrawal.Id, WeekDefinitionId = withdrawal.WeekDefinitionId, WeekDisplayName = withdrawal.WeekDisplayName, TotalAmount = withdrawal.TotalAmount, Status = withdrawal.Status, WithdrawalMethod = withdrawal.WithdrawalMethod, IbanNumber = withdrawal.IbanNumber, Created = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(DateTime.SpecifyKind(withdrawal.Created, DateTimeKind.Utc)) }); } return response; } public override async Task GetCustomerWithdrawalSettings(Google.Protobuf.WellKnownTypes.Empty request, ServerCallContext context) { var query = new GetCustomerWithdrawalSettingsQuery(); var settings = await _sender.Send(query, context.CancellationToken); return new GetCustomerWithdrawalSettingsResponse { MinWithdrawalAmount = settings.MinWithdrawalAmount }; } // ============= Admin Manual Credit Charge ============= public override async Task AdminManualCreditCharge( AdminManualCreditChargeRequest request, ServerCallContext context) { try { var result = await _sender.Send(new AdminManualCreditChargeCommand { UserId = request.UserId, Amount = request.Amount, Note = request.Note, PerformedByAdminId = _currentUserService.GetPerformedBy() }, context.CancellationToken); return new AdminManualCreditChargeResponse { Success = result.Success, Message = result.Message ?? string.Empty, TransactionId = result.TransactionId, NewBalance = result.NewBalance }; } catch (PaymentInProgressException ex) { return new AdminManualCreditChargeResponse { Success = false, Message = ex.Message }; } catch (NotFoundException ex) { return new AdminManualCreditChargeResponse { Success = false, Message = ex.Message }; } catch (Exception ex) { _logger.LogError(ex, "AdminManualCreditCharge failed for UserId={UserId}", request.UserId); return new AdminManualCreditChargeResponse { Success = false, Message = ex.Message }; } } public override async Task GetManualCreditCharges( GetManualCreditChargesRequest request, ServerCallContext context) { var result = await _sender.Send(new GetManualCreditChargesQuery { SortBy = request.SortBy, PaginationState = request.PaginationState != null ? new Application.Common.Models.PaginationState { PageNumber = request.PaginationState.PageNumber, PageSize = request.PaginationState.PageSize } : new Application.Common.Models.PaginationState { PageNumber = 1, PageSize = 20 }, Filter = new AppManualChargeFilter { UserId = request.Filter?.UserId, TransactionId = request.Filter?.TransactionId, RefId = request.Filter?.RefId, CreatedFrom = request.Filter?.CreatedFrom?.ToDateTime(), CreatedTo = request.Filter?.CreatedTo?.ToDateTime() } }, context.CancellationToken); var response = new GetManualCreditChargesResponse { MetaData = new CMSMicroservice.Protobuf.Protos.MetaData { CurrentPage = result.MetaData.CurrentPage, TotalPage = result.MetaData.TotalPage, PageSize = result.MetaData.PageSize, TotalCount = result.MetaData.TotalCount, HasPrevious = result.MetaData.HasPrevious, HasNext = result.MetaData.HasNext } }; foreach (var m in result.Models) { var model = new ManualCreditChargeModel { TransactionId = m.TransactionId, UserId = m.UserId, UserName = m.UserName ?? string.Empty, Amount = m.Amount, Description = m.Description ?? string.Empty, RefId = m.RefId ?? string.Empty, NewBalance = m.NewBalance, Created = Timestamp.FromDateTime(DateTime.SpecifyKind(m.Created, DateTimeKind.Utc)) }; if (m.PaymentDate.HasValue) { model.PaymentDate = Timestamp.FromDateTime( DateTime.SpecifyKind(m.PaymentDate.Value, DateTimeKind.Utc)); } response.Models.Add(model); } return response; } }