From 6c3fbe43b18f5162cb80ad311e1cfd1b199d03d6 Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Fri, 26 Jun 2026 16:11:18 +0330 Subject: [PATCH] feat(wallet): add credit wallet functionality and update Protobuf definitions - Introduced new CreditWalletCharge enum value to TransactionType for clarity. - Implemented InitiateCreditCharge and VerifyCreditCharge methods in UserWalletService for handling credit wallet transactions. - Updated ZarinpalReconciliationJob to support credit wallet payment verification. - Enhanced Protobuf definitions with new messages and RPC methods for credit wallet operations. - Bumped Protobuf project version to reflect the addition of new features. --- .../ChargeCreditWalletCommand.cs | 13 ++ .../ChargeCreditWalletCommandHandler.cs | 123 ++++++++++++++ .../ChargeCreditWalletCommandValidator.cs | 20 +++ .../VerifyCreditWalletChargeCommand.cs | 13 ++ .../VerifyCreditWalletChargeCommandHandler.cs | 157 ++++++++++++++++++ .../Enums/TransactionType.cs | 5 + .../ZarinpalReconciliationJob.cs | 16 ++ .../CMSMicroservice.Protobuf.csproj | 2 +- .../Protos/userwallet.proto | 29 ++++ .../Services/UserWalletService.cs | 85 ++++++++++ 10 files changed, 462 insertions(+), 1 deletion(-) create mode 100644 src/CMSMicroservice.Application/WalletCQ/Commands/ChargeCreditWallet/ChargeCreditWalletCommand.cs create mode 100644 src/CMSMicroservice.Application/WalletCQ/Commands/ChargeCreditWallet/ChargeCreditWalletCommandHandler.cs create mode 100644 src/CMSMicroservice.Application/WalletCQ/Commands/ChargeCreditWallet/ChargeCreditWalletCommandValidator.cs create mode 100644 src/CMSMicroservice.Application/WalletCQ/Commands/VerifyCreditWalletCharge/VerifyCreditWalletChargeCommand.cs create mode 100644 src/CMSMicroservice.Application/WalletCQ/Commands/VerifyCreditWalletCharge/VerifyCreditWalletChargeCommandHandler.cs diff --git a/src/CMSMicroservice.Application/WalletCQ/Commands/ChargeCreditWallet/ChargeCreditWalletCommand.cs b/src/CMSMicroservice.Application/WalletCQ/Commands/ChargeCreditWallet/ChargeCreditWalletCommand.cs new file mode 100644 index 0000000..fc6dab9 --- /dev/null +++ b/src/CMSMicroservice.Application/WalletCQ/Commands/ChargeCreditWallet/ChargeCreditWalletCommand.cs @@ -0,0 +1,13 @@ +using CMSMicroservice.Application.Common.Models; +using MediatR; + +namespace CMSMicroservice.Application.WalletCQ.Commands.ChargeCreditWallet; + +/// +/// دستور شارژ کیف پول اصلی از طریق درگاه +/// +public class ChargeCreditWalletCommand : IRequest +{ + public long UserId { get; set; } + public long Amount { get; set; } +} diff --git a/src/CMSMicroservice.Application/WalletCQ/Commands/ChargeCreditWallet/ChargeCreditWalletCommandHandler.cs b/src/CMSMicroservice.Application/WalletCQ/Commands/ChargeCreditWallet/ChargeCreditWalletCommandHandler.cs new file mode 100644 index 0000000..d983cb7 --- /dev/null +++ b/src/CMSMicroservice.Application/WalletCQ/Commands/ChargeCreditWallet/ChargeCreditWalletCommandHandler.cs @@ -0,0 +1,123 @@ +using CMSMicroservice.Application.Common; +using CMSMicroservice.Application.Common.Exceptions; +using CMSMicroservice.Application.Common.Interfaces; +using CMSMicroservice.Application.Common.Models; +using CMSMicroservice.Domain.Entities; +using CMSMicroservice.Domain.Entities.Payment; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace CMSMicroservice.Application.WalletCQ.Commands.ChargeCreditWallet; + +public class ChargeCreditWalletCommandHandler + : IRequestHandler +{ + private readonly IApplicationDbContext _context; + private readonly IPaymentGatewayService _paymentGateway; + private readonly IConfiguration _configuration; + private readonly ILogger _logger; + private readonly IUserPaymentLock _paymentLock; + + public ChargeCreditWalletCommandHandler( + IApplicationDbContext context, + IPaymentGatewayService paymentGateway, + IConfiguration configuration, + ILogger logger, + IUserPaymentLock paymentLock) + { + _context = context; + _paymentGateway = paymentGateway; + _configuration = configuration; + _logger = logger; + _paymentLock = paymentLock; + } + + public Task Handle( + ChargeCreditWalletCommand request, + CancellationToken cancellationToken) => + _paymentLock.ExecuteAsync( + PaymentLockScopes.Initiate(request.UserId), + PaymentLockStrategy.FailFast, + ct => HandleCore(request, ct), + cancellationToken); + + private async Task HandleCore( + ChargeCreditWalletCommand request, + CancellationToken cancellationToken) + { + try + { + _logger.LogInformation( + "Charging credit wallet for UserId: {UserId}, Amount: {Amount}", + request.UserId, + request.Amount); + + var user = await _context.Users + .FirstOrDefaultAsync(u => u.Id == request.UserId, cancellationToken) + ?? throw new NotFoundException(nameof(User), request.UserId); + + var wallet = await _context.UserWallets + .FirstOrDefaultAsync(w => w.UserId == request.UserId, cancellationToken) + ?? throw new NotFoundException("کیف پول کاربر یافت نشد"); + + var frontOfficeBaseUrl = _configuration["FrontOfficeBaseUrl"] ?? "https://localhost:5268"; + var callbackUrl = $"{frontOfficeBaseUrl}/profile/payment-callback?type=credit-wallet"; + + var paymentRequest = new PaymentRequest + { + Amount = request.Amount, + UserId = user.Id, + Mobile = user.Mobile ?? "", + CallbackUrl = callbackUrl, + Description = $"شارژ کیف پول اصلی - کاربر {user.Id}" + }; + + var paymentResult = await _paymentGateway.InitiatePaymentAsync(paymentRequest); + + if (!paymentResult.IsSuccess) + { + _logger.LogError( + "Payment gateway failed for UserId {UserId}: {ErrorMessage}", + user.Id, + paymentResult.ErrorMessage); + + throw new Exception($"خطا در ارتباط با درگاه پرداخت: {paymentResult.ErrorMessage}"); + } + + var paymentTx = new PaymentTransaction + { + GatewayProvider = _configuration["PaymentProvider"] ?? "zarinpal", + MerchantId = _configuration["ZarinPal:MerchantId"] ?? "", + Amount = request.Amount, + CallbackUrl = callbackUrl, + Description = $"شارژ کیف پول اصلی - کاربر {user.Id}", + Mobile = user.Mobile, + UserId = user.Id, + RequestStatusCode = 100, + RequestStatusMessage = "Success", + Authority = paymentResult.RefId, + PaymentStatus = false + }; + _context.PaymentTransactions.Add(paymentTx); + await _context.SaveChangesAsync(cancellationToken); + + _logger.LogInformation( + "Credit wallet charge initiated. UserId: {UserId}, RefId: {RefId}, PaymentTxId: {PaymentTxId}", + user.Id, + paymentResult.RefId, + paymentTx.Id); + + return paymentResult; + } + catch (Exception ex) + { + _logger.LogError( + ex, + "Error in ChargeCreditWalletCommand for UserId: {UserId}", + request.UserId); + throw; + } + } +} diff --git a/src/CMSMicroservice.Application/WalletCQ/Commands/ChargeCreditWallet/ChargeCreditWalletCommandValidator.cs b/src/CMSMicroservice.Application/WalletCQ/Commands/ChargeCreditWallet/ChargeCreditWalletCommandValidator.cs new file mode 100644 index 0000000..8e106b7 --- /dev/null +++ b/src/CMSMicroservice.Application/WalletCQ/Commands/ChargeCreditWallet/ChargeCreditWalletCommandValidator.cs @@ -0,0 +1,20 @@ +using CMSMicroservice.Domain.Common; +using FluentValidation; + +namespace CMSMicroservice.Application.WalletCQ.Commands.ChargeCreditWallet; + +public class ChargeCreditWalletCommandValidator : AbstractValidator +{ + public ChargeCreditWalletCommandValidator() + { + RuleFor(x => x.UserId) + .GreaterThan(0) + .WithMessage("شناسه کاربر باید بزرگتر از صفر باشد"); + + RuleFor(x => x.Amount) + .GreaterThanOrEqualTo(SystemConstants.DiscountWalletMinCharge) + .WithMessage("حداقل مبلغ شارژ ۱۰,۰۰۰ تومان است") + .LessThanOrEqualTo(SystemConstants.WalletMaxSafeAmount) + .WithMessage("مبلغ وارد شده بیش از حد مجاز است"); + } +} diff --git a/src/CMSMicroservice.Application/WalletCQ/Commands/VerifyCreditWalletCharge/VerifyCreditWalletChargeCommand.cs b/src/CMSMicroservice.Application/WalletCQ/Commands/VerifyCreditWalletCharge/VerifyCreditWalletChargeCommand.cs new file mode 100644 index 0000000..4ec5aad --- /dev/null +++ b/src/CMSMicroservice.Application/WalletCQ/Commands/VerifyCreditWalletCharge/VerifyCreditWalletChargeCommand.cs @@ -0,0 +1,13 @@ +using MediatR; + +namespace CMSMicroservice.Application.WalletCQ.Commands.VerifyCreditWalletCharge; + +/// +/// دستور تأیید شارژ کیف پول اصلی +/// +public class VerifyCreditWalletChargeCommand : IRequest +{ + public long UserId { get; set; } + public long Amount { get; set; } + public string Authority { get; set; } = string.Empty; +} diff --git a/src/CMSMicroservice.Application/WalletCQ/Commands/VerifyCreditWalletCharge/VerifyCreditWalletChargeCommandHandler.cs b/src/CMSMicroservice.Application/WalletCQ/Commands/VerifyCreditWalletCharge/VerifyCreditWalletChargeCommandHandler.cs new file mode 100644 index 0000000..c3985a5 --- /dev/null +++ b/src/CMSMicroservice.Application/WalletCQ/Commands/VerifyCreditWalletCharge/VerifyCreditWalletChargeCommandHandler.cs @@ -0,0 +1,157 @@ +using CMSMicroservice.Application.Common; +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; + +namespace CMSMicroservice.Application.WalletCQ.Commands.VerifyCreditWalletCharge; + +public class VerifyCreditWalletChargeCommandHandler + : IRequestHandler +{ + private readonly IApplicationDbContext _context; + private readonly IPaymentGatewayService _paymentGateway; + private readonly ILogger _logger; + private readonly IUserPaymentLock _paymentLock; + + public VerifyCreditWalletChargeCommandHandler( + IApplicationDbContext context, + IPaymentGatewayService paymentGateway, + ILogger logger, + IUserPaymentLock paymentLock) + { + _context = context; + _paymentGateway = paymentGateway; + _logger = logger; + _paymentLock = paymentLock; + } + + public Task Handle( + VerifyCreditWalletChargeCommand request, + CancellationToken cancellationToken) => + _paymentLock.ExecuteAsync( + PaymentLockScopes.Verify(request.UserId, request.Authority), + PaymentLockStrategy.WaitForRelease, + ct => HandleCore(request, ct), + cancellationToken); + + private async Task HandleCore( + VerifyCreditWalletChargeCommand request, + CancellationToken cancellationToken) + { + try + { + _logger.LogInformation( + "Verifying credit wallet charge. UserId: {UserId}, Amount: {Amount}, Authority: {Authority}", + request.UserId, + request.Amount, + request.Authority); + + var user = await _context.Users + .FirstOrDefaultAsync(u => u.Id == request.UserId, cancellationToken) + ?? throw new NotFoundException(nameof(User), request.UserId); + + var paymentTx = await _context.PaymentTransactions + .FirstOrDefaultAsync(pt => pt.Authority == request.Authority, cancellationToken); + + if (paymentTx?.PaymentStatus == true) + { + _logger.LogWarning("PaymentTransaction already verified: {Authority}", request.Authority); + return true; + } + + var amountInToman = (decimal)(paymentTx?.Amount ?? 0); + + var verifyResult = await _paymentGateway.VerifyPaymentAsync( + request.Authority, + "OK", + amountInToman, + cancellationToken); + + if (paymentTx != null) + { + paymentTx.PaymentStatus = verifyResult.IsSuccess; + paymentTx.VerificationStatusCode = verifyResult.VerificationCode; + paymentTx.VerificationStatusMessage = verifyResult.Message; + paymentTx.CardPan = verifyResult.CardPan; + paymentTx.CardHash = verifyResult.CardHash; + paymentTx.RefId = verifyResult.TrackingCode; + } + + if (!verifyResult.IsSuccess) + { + _logger.LogWarning( + "Credit wallet charge verification failed for UserId {UserId}: {Message}", + request.UserId, + verifyResult.Message); + + throw new Exception($"تراکنش ناموفق: {verifyResult.Message}"); + } + + var wallet = await _context.UserWallets + .FirstOrDefaultAsync(w => w.UserId == user.Id, cancellationToken) + ?? throw new NotFoundException($"کیف پول کاربر با شناسه {request.UserId} یافت نشد"); + + var oldBalance = wallet.Balance; + wallet.Balance += request.Amount; + + _logger.LogInformation( + "Charging credit balance for UserId {UserId}: {OldBalance} -> {NewBalance}", + request.UserId, + oldBalance, + wallet.Balance); + + var transaction = new Transaction + { + Amount = request.Amount, + Description = $"شارژ کیف پول اصلی - کاربر {user.Id}", + PaymentStatus = PaymentStatus.Success, + PaymentDate = DateTime.Now, + RefId = verifyResult.RefId, + Type = TransactionType.CreditWalletCharge + }; + + _context.Transactions.Add(transaction); + await _context.SaveChangesAsync(cancellationToken); + + _context.UserWalletHistories.Add(new Domain.Entities.UserWalletHistory + { + WalletId = wallet.Id, + CurrentBalance = wallet.Balance, + ChangeValue = request.Amount, + CurrentNetworkBalance = wallet.NetworkBalance, + ChangeNerworkValue = 0, + CurrentDiscountBalance = wallet.DiscountBalance, + ChangeDiscountValue = 0, + IsIncrease = true, + RefrenceId = transaction.Id + }); + await _context.SaveChangesAsync(cancellationToken); + + if (paymentTx != null) + { + paymentTx.TransactionId = transaction.Id; + await _context.SaveChangesAsync(cancellationToken); + } + + _logger.LogInformation( + "Credit wallet charged successfully. UserId: {UserId}, TransactionId: {TransactionId}, RefId: {RefId}", + user.Id, + transaction.Id, + verifyResult.RefId); + + return true; + } + catch (Exception ex) + { + _logger.LogError( + ex, + "Error in VerifyCreditWalletChargeCommand for UserId: {UserId}", + request.UserId); + throw; + } + } +} diff --git a/src/CMSMicroservice.Domain/Enums/TransactionType.cs b/src/CMSMicroservice.Domain/Enums/TransactionType.cs index f93f9b6..7b32d15 100644 --- a/src/CMSMicroservice.Domain/Enums/TransactionType.cs +++ b/src/CMSMicroservice.Domain/Enums/TransactionType.cs @@ -36,4 +36,9 @@ public enum TransactionType /// بونوس داخلی کیف‌پول جادویی (مبلغ × 1.5) /// MagicWalletBonus = 15, + + /// + /// شارژ کیف پول اصلی از درگاه + /// + CreditWalletCharge = 16, } diff --git a/src/CMSMicroservice.Infrastructure/BackgroundJobs/ZarinpalReconciliationJob.cs b/src/CMSMicroservice.Infrastructure/BackgroundJobs/ZarinpalReconciliationJob.cs index 4e402f7..0e39c69 100644 --- a/src/CMSMicroservice.Infrastructure/BackgroundJobs/ZarinpalReconciliationJob.cs +++ b/src/CMSMicroservice.Infrastructure/BackgroundJobs/ZarinpalReconciliationJob.cs @@ -3,6 +3,7 @@ using CMSMicroservice.Application.PackageCQ.Commands.VerifyUserPackagePurchasePa using CMSMicroservice.Application.Common.Interfaces; using CMSMicroservice.Application.DiscountShopCQ.Commands.CompleteOrderPayment; using CMSMicroservice.Application.WalletCQ.Commands.VerifyDiscountWalletCharge; +using CMSMicroservice.Application.WalletCQ.Commands.VerifyCreditWalletCharge; using CMSMicroservice.Application.WalletCQ.Commands.VerifyMagicWalletCharge; using CMSMicroservice.Domain.Entities; using CMSMicroservice.Domain.Entities.Payment; @@ -174,6 +175,19 @@ public class ZarinpalReconciliationJob }, ct); break; + case "credit-wallet": + if (!paymentTx.UserId.HasValue) + throw new InvalidOperationException( + $"UserId is null for credit-wallet authority {authority}"); + + await _sender.Send(new VerifyCreditWalletChargeCommand + { + Authority = authority, + UserId = paymentTx.UserId.Value, + Amount = paymentTx.Amount + }, ct); + break; + case "discount-order": await ReconcileDiscountOrderAsync(paymentTx, ct); break; @@ -194,6 +208,8 @@ public class ZarinpalReconciliationJob return "magic-wallet"; if (callbackUrl.Contains("type=discount-wallet", StringComparison.OrdinalIgnoreCase)) return "discount-wallet"; + if (callbackUrl.Contains("type=credit-wallet", StringComparison.OrdinalIgnoreCase)) + return "credit-wallet"; if (callbackUrl.Contains("type=discount-order", StringComparison.OrdinalIgnoreCase)) return "discount-order"; // Package callbacks have orderId= but no type= parameter diff --git a/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj b/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj index 02bd97b..b403094 100644 --- a/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj +++ b/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj @@ -3,7 +3,7 @@ net9.0 enable enable - 0.0.201 + 0.0.202 None False False diff --git a/src/CMSMicroservice.Protobuf/Protos/userwallet.proto b/src/CMSMicroservice.Protobuf/Protos/userwallet.proto index c574685..ab9a1a8 100644 --- a/src/CMSMicroservice.Protobuf/Protos/userwallet.proto +++ b/src/CMSMicroservice.Protobuf/Protos/userwallet.proto @@ -110,6 +110,21 @@ service UserWalletContract body: "*" }; }; + + // ============= Credit (Main) Wallet Methods ============= + + rpc InitiateCreditCharge(InitiateCreditChargeRequest) returns (InitiateCreditChargeResponse){ + option (google.api.http) = { + post: "/Customer/InitiateCreditCharge" + body: "*" + }; + }; + rpc VerifyCreditCharge(VerifyWalletChargeRequest) returns (VerifyWalletChargeResponse){ + option (google.api.http) = { + post: "/Customer/VerifyCreditCharge" + body: "*" + }; + }; } message CreateNewUserWalletRequest { @@ -283,6 +298,20 @@ message InitiateDiscountChargeResponse string error_message = 3; } +// ============= Credit (Main) Wallet Messages ============= + +message InitiateCreditChargeRequest +{ + int64 amount = 1; // مبلغ واریزی (تومان) +} + +message InitiateCreditChargeResponse +{ + bool is_success = 1; + string gateway_url = 2; + string error_message = 3; +} + // ============= Wallet Verify Messages ============= message VerifyWalletChargeRequest diff --git a/src/CMSMicroservice.WebApi/Services/UserWalletService.cs b/src/CMSMicroservice.WebApi/Services/UserWalletService.cs index 3863aa5..72338f0 100644 --- a/src/CMSMicroservice.WebApi/Services/UserWalletService.cs +++ b/src/CMSMicroservice.WebApi/Services/UserWalletService.cs @@ -5,8 +5,10 @@ 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.UserWalletCQ.Queries.GetUserWallet; using CMSMicroservice.Application.UserWalletCQ.Queries.GetAllUserWalletByFilter; using CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWalletHistory; @@ -242,6 +244,38 @@ public class UserWalletService : UserWalletContract.UserWalletContractBase } } + // ============= 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 + }; + } + } + // ============= Wallet Verify Methods ============= public override async Task VerifyMagicCharge( @@ -330,6 +364,57 @@ public class UserWalletService : UserWalletContract.UserWalletContractBase } } + 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) {