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)
{