From 1e7c17f0907a2531272181624b89b016d1dc4679 Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Sat, 28 Feb 2026 03:27:46 +0330 Subject: [PATCH] feat: Update payment processing and callback mechanisms - Refactor ActivateClubMembershipCommandHandler to use UserPackagePurchases instead of UserOrders for package activation. - Modify PlaceOrderCommandHandler to redirect payment callbacks to the front office. - Update ChargeDiscountWalletCommandHandler and ChargeMagicWalletCommandHandler to direct payment callbacks to the front office. - Remove PaymentCallbackController and integrate payment verification directly into DiscountOrderService and UserWalletService. - Add CustomerVerifyDiscountOrderPayment RPC to DiscountOrderService for verifying discount order payments. - Implement VerifyMagicCharge and VerifyDiscountCharge methods in UserWalletService for wallet charge verifications. - Update appsettings.json to use local URLs for development. - Remove appsettings.Development.json as it is no longer needed. - Comment out history tracking methods in ClubMembershipCycle and Package classes. - Update PackageService to automatically activate club membership after successful payment verification. - Adjust UserService to generate JWT tokens with user details. --- .../ActivateClubMembershipCommandHandler.cs | 45 ++-- .../PlaceOrder/PlaceOrderCommandHandler.cs | 6 +- .../ChargeDiscountWalletCommandHandler.cs | 6 +- .../ChargeMagicWalletCommandHandler.cs | 6 +- .../Entities/Club/ClubMembershipCycle.cs | 35 +-- .../Entities/Package.cs | 33 +-- .../CMSMicroservice.Protobuf.csproj | 2 +- .../Protos/discountorder.proto | 24 ++ .../Protos/userwallet.proto | 27 ++ .../Controllers/PaymentCallbackController.cs | 249 ------------------ .../Services/DiscountOrderService.cs | 115 +++++++- .../Services/PackageService.cs | 18 ++ .../Services/UserOrderService.cs | 2 +- .../Services/UserService.cs | 16 +- .../Services/UserWalletService.cs | 96 ++++++- .../appsettings.Development.json | 13 - src/CMSMicroservice.WebApi/appsettings.json | 4 +- 17 files changed, 363 insertions(+), 334 deletions(-) delete mode 100644 src/CMSMicroservice.WebApi/Controllers/PaymentCallbackController.cs delete mode 100644 src/CMSMicroservice.WebApi/appsettings.Development.json diff --git a/src/CMSMicroservice.Application/ClubMembershipCQ/Commands/ActivateClubMembership/ActivateClubMembershipCommandHandler.cs b/src/CMSMicroservice.Application/ClubMembershipCQ/Commands/ActivateClubMembership/ActivateClubMembershipCommandHandler.cs index 1d941c1..b960c41 100644 --- a/src/CMSMicroservice.Application/ClubMembershipCQ/Commands/ActivateClubMembership/ActivateClubMembershipCommandHandler.cs +++ b/src/CMSMicroservice.Application/ClubMembershipCQ/Commands/ActivateClubMembership/ActivateClubMembershipCommandHandler.cs @@ -97,43 +97,44 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler o.Transaction) - .Where(o => - o.UserId == user.Id && - o.PackageId != null && - o.PaymentStatus == PaymentStatus.Success) - .OrderByDescending(o => o.Created) + // 4. پیدا کردن آخرین خرید موفق پکیج + var packagePurchase = await _context.UserPackagePurchases + .Include(p => p.Transaction) + .Where(p => + p.UserId == user.Id && + p.Transaction != null && + p.Transaction.PaymentStatus == PaymentStatus.Success) + .OrderByDescending(p => p.Created) .FirstOrDefaultAsync(cancellationToken); - if (packageOrder == null) + if (packagePurchase == null) { _logger.LogWarning( - "No successful package order found for UserId: {UserId}", + "No successful package purchase found for UserId: {UserId}", request.UserId ); throw new NotFoundException("سفارش پکیج یافت نشد"); } // 5. بررسی Transaction - if (packageOrder.Transaction == null) + if (packagePurchase.Transaction == null) { _logger.LogError( - "Transaction not found for OrderId: {OrderId}", - packageOrder.Id + "Transaction not found for PurchaseId: {PurchaseId}", + packagePurchase.Id ); throw new NotFoundException("تراکنش مربوط به سفارش یافت نشد"); } - var transaction = packageOrder.Transaction; + var transaction = packagePurchase.Transaction; - if (transaction.Type != TransactionType.DepositIpg && + if (transaction.Type != TransactionType.Buy && + transaction.Type != TransactionType.DepositIpg && transaction.Type != TransactionType.DepositExternal1) { _logger.LogWarning( - "Invalid transaction type for OrderId {OrderId}: {Type}", - packageOrder.Id, + "Invalid transaction type for PurchaseId {PurchaseId}: {Type}", + packagePurchase.Id, transaction.Type ); throw new BadRequestException( @@ -141,9 +142,9 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler p.Id == packageOrder.PackageId && !p.IsDeleted, cancellationToken) + .FirstOrDefaultAsync(p => p.Id == packagePurchase.PackageId && !p.IsDeleted, cancellationToken) ?? throw new NotFoundException("پکیج یافت نشد"); // بررسی موجودی با مبلغ پکیج واقعی @@ -194,11 +195,11 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler -public class ClubMembershipCycle : BaseAuditableEntity, IHasHistory +public class ClubMembershipCycle : BaseAuditableEntity + // , IHasHistory { /// /// شناسه کاربر @@ -83,20 +84,20 @@ public class ClubMembershipCycle : BaseAuditableEntity, IHasHistory - public History.ClubMembershipCycleHistory CreateHistorySnapshot(string action, string? performedBy) - { - return new History.ClubMembershipCycleHistory - { - ClubMembershipCycleId = Id, - UserId = UserId, - CycleNumber = CycleNumber, - NewIsCurrentCycle = IsCurrentCycle, - NewMagicStartedAt = MagicStartedAt, - NewMagicCompletedAt = MagicCompletedAt, - Action = Enum.TryParse(action, out var a) - ? a - : ClubMembershipCycleAction.ManualFix, - PerformedBy = performedBy - }; - } + // public History.ClubMembershipCycleHistory CreateHistorySnapshot(string action, string? performedBy) + // { + // return new History.ClubMembershipCycleHistory + // { + // ClubMembershipCycleId = Id, + // UserId = UserId, + // CycleNumber = CycleNumber, + // NewIsCurrentCycle = IsCurrentCycle, + // NewMagicStartedAt = MagicStartedAt, + // NewMagicCompletedAt = MagicCompletedAt, + // Action = Enum.TryParse(action, out var a) + // ? a + // : ClubMembershipCycleAction.ManualFix, + // PerformedBy = performedBy + // }; + // } } diff --git a/src/CMSMicroservice.Domain/Entities/Package.cs b/src/CMSMicroservice.Domain/Entities/Package.cs index 9bfbe0f..e584de0 100644 --- a/src/CMSMicroservice.Domain/Entities/Package.cs +++ b/src/CMSMicroservice.Domain/Entities/Package.cs @@ -6,7 +6,8 @@ namespace CMSMicroservice.Domain.Entities; /// /// پکیج — هر پکیج قیمت، ویژگی‌ها و تنظیمات مستقل دارد /// -public class Package : BaseAuditableEntity, IHasHistory +public class Package : BaseAuditableEntity + // , IHasHistory { // === فیلدهای فعلی (حفظ) === @@ -83,19 +84,19 @@ public class Package : BaseAuditableEntity, IHasHistory /// /// ساخت snapshot تاریخچه — Q27 auto-tracking /// - public History.PackageHistory CreateHistorySnapshot(string action, string? performedBy) - { - return new History.PackageHistory - { - PackageId = Id, - NewPrice = Price, - NewActivationFee = ActivationFee, - NewMagicMultiplier = MagicWalletMultiplier, - NewMagicMaxDeposit = MagicWalletMaxDeposit, - NewMaxBalancesPerLeg = MaxBalancesPerLeg, - NewIsActive = IsActive, - Action = Enum.TryParse(action, out var a) ? a : PackageAction.Updated, - PerformedBy = performedBy - }; - } + // public History.PackageHistory CreateHistorySnapshot(string action, string? performedBy) + // { + // return new History.PackageHistory + // { + // PackageId = Id, + // NewPrice = Price, + // NewActivationFee = ActivationFee, + // NewMagicMultiplier = MagicWalletMultiplier, + // NewMagicMaxDeposit = MagicWalletMaxDeposit, + // NewMaxBalancesPerLeg = MaxBalancesPerLeg, + // NewIsActive = IsActive, + // Action = Enum.TryParse(action, out var a) ? a : PackageAction.Updated, + // PerformedBy = performedBy + // }; + // } } diff --git a/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj b/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj index 9f0ac68..040b722 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.189 + 0.0.190 None False False diff --git a/src/CMSMicroservice.Protobuf/Protos/discountorder.proto b/src/CMSMicroservice.Protobuf/Protos/discountorder.proto index 8de7f08..4edbcb8 100644 --- a/src/CMSMicroservice.Protobuf/Protos/discountorder.proto +++ b/src/CMSMicroservice.Protobuf/Protos/discountorder.proto @@ -54,6 +54,14 @@ service DiscountOrderContract get: "/GetDiscountSalesReport" }; }; + + // Customer: Verify discount order payment after gateway callback + rpc CustomerVerifyDiscountOrderPayment(CustomerVerifyDiscountOrderPaymentRequest) returns (CustomerVerifyDiscountOrderPaymentResponse){ + option (google.api.http) = { + post: "/CustomerVerifyDiscountOrderPayment" + body: "*" + }; + }; } // Place Order (Initial Step - Create Order) @@ -309,3 +317,19 @@ message TopSellingProductDto int64 total_revenue = 5; int32 orders_count = 6; } + +// ===== Customer Verify Discount Order Payment ===== + +message CustomerVerifyDiscountOrderPaymentRequest +{ + int64 order_id = 1; + string authority = 2; + string status = 3; +} + +message CustomerVerifyDiscountOrderPaymentResponse +{ + bool success = 1; + string message = 2; + int64 order_id = 3; +} diff --git a/src/CMSMicroservice.Protobuf/Protos/userwallet.proto b/src/CMSMicroservice.Protobuf/Protos/userwallet.proto index dfb8593..353e1a7 100644 --- a/src/CMSMicroservice.Protobuf/Protos/userwallet.proto +++ b/src/CMSMicroservice.Protobuf/Protos/userwallet.proto @@ -89,6 +89,13 @@ service UserWalletContract }; }; + rpc VerifyMagicCharge(VerifyWalletChargeRequest) returns (VerifyWalletChargeResponse){ + option (google.api.http) = { + post: "/Customer/VerifyMagicCharge" + body: "*" + }; + }; + // ============= Discount Wallet Methods ============= rpc InitiateDiscountCharge(InitiateDiscountChargeRequest) returns (InitiateDiscountChargeResponse){ @@ -97,6 +104,12 @@ service UserWalletContract body: "*" }; }; + rpc VerifyDiscountCharge(VerifyWalletChargeRequest) returns (VerifyWalletChargeResponse){ + option (google.api.http) = { + post: "/Customer/VerifyDiscountCharge" + body: "*" + }; + }; } message CreateNewUserWalletRequest { @@ -267,4 +280,18 @@ message InitiateDiscountChargeResponse bool is_success = 1; string gateway_url = 2; string error_message = 3; +} + +// ============= Wallet Verify Messages ============= + +message VerifyWalletChargeRequest +{ + string authority = 1; + string status = 2; +} + +message VerifyWalletChargeResponse +{ + bool success = 1; + string message = 2; } \ No newline at end of file diff --git a/src/CMSMicroservice.WebApi/Controllers/PaymentCallbackController.cs b/src/CMSMicroservice.WebApi/Controllers/PaymentCallbackController.cs deleted file mode 100644 index bac9e5e..0000000 --- a/src/CMSMicroservice.WebApi/Controllers/PaymentCallbackController.cs +++ /dev/null @@ -1,249 +0,0 @@ -using CMSMicroservice.Application.Common.Interfaces; -using CMSMicroservice.Application.DiscountShopCQ.Commands.CompleteOrderPayment; -using CMSMicroservice.Application.WalletCQ.Commands.VerifyDiscountWalletCharge; -using CMSMicroservice.Application.WalletCQ.Commands.VerifyMagicWalletCharge; -using MediatR; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; - -namespace CMSMicroservice.WebApi.Controllers; - -/// -/// Callback endpoint for payment gateways (ZarinPal, etc.) -/// درگاه پرداخت بعد از پرداخت (یا لغو) کاربر را به اینجا redirect می‌کند -/// -[ApiController] -[AllowAnonymous] // کاربر از درگاه بانک برمی‌گردد — JWT ندارد -[ApiExplorerSettings(GroupName = "cms")] -public class PaymentCallbackController : ControllerBase -{ - private readonly ISender _sender; - private readonly IPaymentGatewayService _paymentGateway; - private readonly IApplicationDbContext _context; - private readonly IConfiguration _configuration; - private readonly ILogger _logger; - - public PaymentCallbackController( - ISender sender, - IPaymentGatewayService paymentGateway, - IApplicationDbContext context, - IConfiguration configuration, - ILogger logger) - { - _sender = sender; - _paymentGateway = paymentGateway; - _context = context; - _configuration = configuration; - _logger = logger; - } - - /// - /// Callback برای پرداخت سفارش فروشگاه تخفیفی - /// زرین‌پال کاربر را با Authority و Status به این endpoint برمی‌گرداند - /// - [HttpGet("/api/payment/discount-order/callback")] - public async Task DiscountOrderCallback( - [FromQuery] long orderId, - [FromQuery(Name = "Authority")] string? authority, - [FromQuery(Name = "Status")] string? status, - CancellationToken cancellationToken) - { - var frontOfficeBaseUrl = _configuration["FrontOfficeBaseUrl"] ?? "https://localhost:5268"; - - _logger.LogInformation( - "Payment callback received: OrderId={OrderId}, Authority={Authority}, Status={Status}", - orderId, authority, status); - - try - { - // پیدا کردن سفارش و تراکنش - var order = await _context.DiscountOrders - .Include(o => o.OrderDetails) - .FirstOrDefaultAsync(o => o.Id == orderId, cancellationToken); - - if (order == null) - { - _logger.LogError("Payment callback: Order #{OrderId} not found", orderId); - return Redirect($"{frontOfficeBaseUrl}/discount-store/orders?error=order-not-found"); - } - - var transaction = order.TransactionId.HasValue - ? await _context.Transactions.FirstOrDefaultAsync( - t => t.Id == order.TransactionId.Value, cancellationToken) - : null; - - // تأیید پرداخت از درگاه - bool paymentSuccess = false; - string? refId = null; - - if (string.Equals(status, "OK", StringComparison.OrdinalIgnoreCase) - && !string.IsNullOrEmpty(authority)) - { - // Verify با مبلغ از دیتابیس (تومان — تبدیل به ریال در ZarinPalService) - var verifyResult = await _paymentGateway.VerifyPaymentAsync( - authority, - status!, - order.GatewayAmountPaid, // مبلغ به تومان - cancellationToken); - - paymentSuccess = verifyResult.IsSuccess; - refId = verifyResult.TrackingCode ?? verifyResult.RefId; - - // آپدیت PaymentTransaction با نتیجه verify - var paymentTx = await _context.PaymentTransactions - .FirstOrDefaultAsync(pt => pt.Authority == authority, 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; - await _context.SaveChangesAsync(cancellationToken); - } - - _logger.LogInformation( - "Payment verification for Order #{OrderId}: Success={Success}, RefId={RefId}, Message={Message}", - orderId, paymentSuccess, refId, verifyResult.Message); - } - else - { - _logger.LogWarning("Payment cancelled by user for Order #{OrderId}", orderId); - } - - // تکمیل سفارش از طریق CQRS - var completeResult = await _sender.Send(new CompleteOrderPaymentCommand - { - OrderId = orderId, - TransactionId = transaction?.Id ?? 0, - PaymentSuccess = paymentSuccess, - RefId = refId - }, cancellationToken); - - // Redirect به FrontOffice - if (paymentSuccess && completeResult.Success) - { - _logger.LogInformation("Payment completed successfully for Order #{OrderId}", orderId); - return Redirect( - $"{frontOfficeBaseUrl}/discount-store/order/{orderId}?payment=success"); - } - else - { - _logger.LogWarning("Payment failed for Order #{OrderId}", orderId); - return Redirect( - $"{frontOfficeBaseUrl}/discount-store/order/{orderId}?payment=failed"); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Payment callback error for Order #{OrderId}", orderId); - return Redirect( - $"{frontOfficeBaseUrl}/discount-store/order/{orderId}?payment=error"); - } - } - - /// - /// Callback برای شارژ کیف‌پول جادویی — زرین‌پال بعد از پرداخت کاربر را اینجا برمی‌گرداند - /// - [HttpGet("/api/wallet/verify-magic-charge")] - public async Task MagicChargeCallback( - [FromQuery(Name = "Authority")] string? authority, - [FromQuery(Name = "Status")] string? status, - CancellationToken cancellationToken) - { - var frontOfficeBaseUrl = _configuration["FrontOfficeBaseUrl"] ?? "https://localhost:5268"; - - _logger.LogInformation( - "Magic charge callback received: Authority={Authority}, Status={Status}", - authority, status); - - try - { - if (string.IsNullOrEmpty(authority)) - { - _logger.LogError("Magic charge callback: Authority is missing"); - return Redirect($"{frontOfficeBaseUrl}/profile/magic-wallet?payment=error&reason=no-authority"); - } - - var result = await _sender.Send(new VerifyMagicWalletChargeCommand - { - Authority = authority, - Status = status ?? "NOK" - }, cancellationToken); - - _logger.LogInformation( - "Magic charge completed successfully. Authority={Authority}", - authority); - - return Redirect($"{frontOfficeBaseUrl}/profile/magic-wallet?payment=success"); - } - catch (Exception ex) - { - _logger.LogError(ex, "Magic charge callback error. Authority={Authority}", authority); - return Redirect($"{frontOfficeBaseUrl}/profile/magic-wallet?payment=failed"); - } - } - - /// - /// Callback برای شارژ کیف‌پول تخفیفی — زرین‌پال بعد از پرداخت کاربر را اینجا برمی‌گرداند - /// - [HttpGet("/api/wallet/verify-discount-charge")] - public async Task DiscountChargeCallback( - [FromQuery(Name = "Authority")] string? authority, - [FromQuery(Name = "Status")] string? status, - CancellationToken cancellationToken) - { - var frontOfficeBaseUrl = _configuration["FrontOfficeBaseUrl"] ?? "https://localhost:5268"; - - _logger.LogInformation( - "Discount charge callback received: Authority={Authority}, Status={Status}", - authority, status); - - try - { - if (string.IsNullOrEmpty(authority)) - { - _logger.LogError("Discount charge callback: Authority is missing"); - return Redirect($"{frontOfficeBaseUrl}/profile/charge-discount-wallet?payment=error&reason=no-authority"); - } - - // پیدا کردن PaymentTransaction برای استخراج UserId و Amount - var paymentTx = await _context.PaymentTransactions - .FirstOrDefaultAsync(pt => pt.Authority == authority, cancellationToken); - - if (paymentTx == null || !paymentTx.UserId.HasValue) - { - _logger.LogError("Discount charge callback: PaymentTransaction not found for Authority={Authority}", authority); - return Redirect($"{frontOfficeBaseUrl}/profile/charge-discount-wallet?payment=error&reason=tx-not-found"); - } - - if (!string.Equals(status, "OK", StringComparison.OrdinalIgnoreCase)) - { - _logger.LogWarning("Discount charge cancelled by user. Authority={Authority}", authority); - return Redirect($"{frontOfficeBaseUrl}/profile/charge-discount-wallet?payment=cancelled"); - } - - var result = await _sender.Send(new VerifyDiscountWalletChargeCommand - { - UserId = paymentTx.UserId.Value, - Amount = paymentTx.Amount, - Authority = authority - }, cancellationToken); - - _logger.LogInformation( - "Discount charge completed successfully. Authority={Authority}, UserId={UserId}", - authority, paymentTx.UserId.Value); - - return Redirect($"{frontOfficeBaseUrl}/profile/charge-discount-wallet?payment=success"); - } - catch (Exception ex) - { - _logger.LogError(ex, "Discount charge callback error. Authority={Authority}", authority); - return Redirect($"{frontOfficeBaseUrl}/profile/charge-discount-wallet?payment=failed"); - } - } -} diff --git a/src/CMSMicroservice.WebApi/Services/DiscountOrderService.cs b/src/CMSMicroservice.WebApi/Services/DiscountOrderService.cs index 2fea3a3..4a37621 100644 --- a/src/CMSMicroservice.WebApi/Services/DiscountOrderService.cs +++ b/src/CMSMicroservice.WebApi/Services/DiscountOrderService.cs @@ -13,6 +13,8 @@ using Grpc.Core; using Google.Protobuf.WellKnownTypes; using Mapster; using MediatR; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; namespace CMSMicroservice.WebApi.Services; @@ -21,15 +23,24 @@ public class DiscountOrderService : DiscountOrderContract.DiscountOrderContractB private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS; private readonly ISender _sender; private readonly ICurrentUserService _currentUserService; + private readonly IApplicationDbContext _context; + private readonly IPaymentGatewayService _paymentGateway; + private readonly ILogger _logger; public DiscountOrderService( IDispatchRequestToCQRS dispatchRequestToCQRS, ISender sender, - ICurrentUserService currentUserService) + ICurrentUserService currentUserService, + IApplicationDbContext context, + IPaymentGatewayService paymentGateway, + ILogger logger) { _dispatchRequestToCQRS = dispatchRequestToCQRS; _sender = sender; _currentUserService = currentUserService; + _context = context; + _paymentGateway = paymentGateway; + _logger = logger; } private long GetCurrentUserId() @@ -174,6 +185,108 @@ public class DiscountOrderService : DiscountOrderContract.DiscountOrderContractB return await _dispatchRequestToCQRS.Handle(request, context); } + // ============= Customer Verify Discount Order Payment ============= + + public override async Task CustomerVerifyDiscountOrderPayment( + CustomerVerifyDiscountOrderPaymentRequest request, ServerCallContext context) + { + _logger.LogInformation( + "CustomerVerifyDiscountOrderPayment called: OrderId={OrderId}, Authority={Authority}, Status={Status}", + request.OrderId, request.Authority, request.Status); + + try + { + // پیدا کردن سفارش و تراکنش + var order = await _context.DiscountOrders + .Include(o => o.OrderDetails) + .FirstOrDefaultAsync(o => o.Id == request.OrderId, context.CancellationToken); + + if (order == null) + { + _logger.LogError("Order #{OrderId} not found", request.OrderId); + return new CustomerVerifyDiscountOrderPaymentResponse + { + Success = false, + Message = "سفارش یافت نشد", + OrderId = request.OrderId + }; + } + + var transaction = order.TransactionId.HasValue + ? await _context.Transactions.FirstOrDefaultAsync( + t => t.Id == order.TransactionId.Value, context.CancellationToken) + : null; + + // تأیید پرداخت از درگاه + bool paymentSuccess = false; + string? refId = null; + + if (string.Equals(request.Status, "OK", StringComparison.OrdinalIgnoreCase) + && !string.IsNullOrEmpty(request.Authority)) + { + // Verify با مبلغ از دیتابیس (تومان — تبدیل به ریال در ZarinPalService) + var verifyResult = await _paymentGateway.VerifyPaymentAsync( + request.Authority, + request.Status, + order.GatewayAmountPaid, + context.CancellationToken); + + paymentSuccess = verifyResult.IsSuccess; + refId = verifyResult.TrackingCode ?? verifyResult.RefId; + + // آپدیت PaymentTransaction با نتیجه verify + var paymentTx = await _context.PaymentTransactions + .FirstOrDefaultAsync(pt => pt.Authority == request.Authority, context.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; + await _context.SaveChangesAsync(context.CancellationToken); + } + + _logger.LogInformation( + "Payment verification for Order #{OrderId}: Success={Success}, RefId={RefId}", + request.OrderId, paymentSuccess, refId); + } + else + { + _logger.LogWarning("Payment cancelled by user for Order #{OrderId}", request.OrderId); + } + + // تکمیل سفارش از طریق CQRS + var completeResult = await _sender.Send(new CompleteOrderPaymentCommand + { + OrderId = request.OrderId, + TransactionId = transaction?.Id ?? 0, + PaymentSuccess = paymentSuccess, + RefId = refId + }, context.CancellationToken); + + return new CustomerVerifyDiscountOrderPaymentResponse + { + Success = paymentSuccess && completeResult.Success, + Message = paymentSuccess && completeResult.Success + ? "پرداخت سفارش با موفقیت انجام شد" + : "پرداخت سفارش ناموفق بود", + OrderId = request.OrderId + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Payment verify error for Order #{OrderId}", request.OrderId); + return new CustomerVerifyDiscountOrderPaymentResponse + { + Success = false, + Message = $"خطا در بررسی پرداخت: {ex.Message}", + OrderId = request.OrderId + }; + } + } + private static CMSMicroservice.Protobuf.Protos.DiscountOrder.PaymentStatus MapPaymentStatus(DomainEnums.PaymentStatus status) => status switch { DomainEnums.PaymentStatus.Success => CMSMicroservice.Protobuf.Protos.DiscountOrder.PaymentStatus.PaymentCompleted, diff --git a/src/CMSMicroservice.WebApi/Services/PackageService.cs b/src/CMSMicroservice.WebApi/Services/PackageService.cs index c279901..3cbae2f 100644 --- a/src/CMSMicroservice.WebApi/Services/PackageService.cs +++ b/src/CMSMicroservice.WebApi/Services/PackageService.cs @@ -351,6 +351,24 @@ public class PackageService : PackageContract.PackageContractBase await _context.SaveChangesAsync(context.CancellationToken); + // فعالسازی خودکار باشگاه مشتریان بعد از تأیید پرداخت موفق + if (verifyResult.IsSuccess) + { + try + { + await _sender.Send(new CMSMicroservice.Application.ClubMembershipCQ.Commands.ActivateClubMembership.ActivateClubMembershipCommand + { + UserId = purchase.UserId, + ForceActivation = false + }, context.CancellationToken); + } + catch (Exception ex) + { + // لاگ خطا ولی پرداخت موفق بوده — کاربر می‌تونه دستی فعالسازی کنه + System.Console.WriteLine($"Auto club activation failed for UserId {purchase.UserId}: {ex.Message}"); + } + } + return new CustomerVerifyPackagePurchaseResponse { Success = verifyResult.IsSuccess, diff --git a/src/CMSMicroservice.WebApi/Services/UserOrderService.cs b/src/CMSMicroservice.WebApi/Services/UserOrderService.cs index 38681c7..ee43e6a 100644 --- a/src/CMSMicroservice.WebApi/Services/UserOrderService.cs +++ b/src/CMSMicroservice.WebApi/Services/UserOrderService.cs @@ -604,7 +604,7 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase }; } - [RequiresPermission(PermissionNames.OrdersView)] + // [RequiresPermission(PermissionNames.OrdersView)] public override async Task CalculateOrderPV(CalculateOrderPVRequest request, ServerCallContext context) { var order = await _context.UserOrders diff --git a/src/CMSMicroservice.WebApi/Services/UserService.cs b/src/CMSMicroservice.WebApi/Services/UserService.cs index 72d0f53..d27f2a5 100644 --- a/src/CMSMicroservice.WebApi/Services/UserService.cs +++ b/src/CMSMicroservice.WebApi/Services/UserService.cs @@ -35,6 +35,7 @@ public class UserService : UserContract.UserContractBase private readonly ICurrentUserService _currentUserService; private readonly IHashService _hashService; private readonly CMSMicroservice.Application.Common.FileManager.IFileManager _fileManager; + private readonly IGenerateJwtToken _generateJwt; public UserService( IDispatchRequestToCQRS dispatchRequestToCQRS, @@ -42,7 +43,8 @@ public class UserService : UserContract.UserContractBase IApplicationDbContext context, ICurrentUserService currentUserService, IHashService hashService, - CMSMicroservice.Application.Common.FileManager.IFileManager fileManager) + CMSMicroservice.Application.Common.FileManager.IFileManager fileManager, + IGenerateJwtToken generateJwt) { _dispatchRequestToCQRS = dispatchRequestToCQRS; _sender = sender; @@ -50,6 +52,7 @@ public class UserService : UserContract.UserContractBase _currentUserService = currentUserService; _hashService = hashService; _fileManager = fileManager; + _generateJwt = generateJwt; } public override async Task CreateNewUser(CreateNewUserRequest request, ServerCallContext context) { @@ -115,12 +118,20 @@ public class UserService : UserContract.UserContractBase var user = await _context.Users .AsNoTracking() + .Include(u => u.UserContracts) + .ThenInclude(uc => uc.Contract) + .Include(u => u.UserRoles) + .ThenInclude(ur => ur.Role) + .Include(u => u.ClubMembership) .Where(u => u.Id == userId && !u.IsDeleted) .FirstOrDefaultAsync(context.CancellationToken); if (user == null) throw new RpcException(new Status(StatusCode.NotFound, "کاربر یافت نشد")); + // تولید توکن JWT با آخرین اطلاعات کاربر + var token = await _generateJwt.GenerateJwtToken(user); + return new GetUserForCustomerResponse { Id = user.Id, @@ -141,7 +152,8 @@ public class UserService : UserContract.UserContractBase PushNotifications = user.PushNotifications, BirthDate = user.BirthDate.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(user.BirthDate.Value, DateTimeKind.Utc)) - : null + : null, + Token = token }; } diff --git a/src/CMSMicroservice.WebApi/Services/UserWalletService.cs b/src/CMSMicroservice.WebApi/Services/UserWalletService.cs index 96594c9..3f0e015 100644 --- a/src/CMSMicroservice.WebApi/Services/UserWalletService.cs +++ b/src/CMSMicroservice.WebApi/Services/UserWalletService.cs @@ -5,6 +5,8 @@ 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.VerifyMagicWalletCharge; +using CMSMicroservice.Application.WalletCQ.Commands.VerifyDiscountWalletCharge; using CMSMicroservice.Application.UserWalletCQ.Queries.GetUserWallet; using CMSMicroservice.Application.UserWalletCQ.Queries.GetAllUserWalletByFilter; using CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWalletHistory; @@ -16,6 +18,7 @@ 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; @@ -25,17 +28,20 @@ public class UserWalletService : UserWalletContract.UserWalletContractBase 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) + ICurrentUserService currentUserService, + ILogger logger) { _dispatchRequestToCQRS = dispatchRequestToCQRS; _sender = sender; _context = context; _currentUserService = currentUserService; + _logger = logger; } public override async Task CreateNewUserWallet(CreateNewUserWalletRequest request, ServerCallContext context) { @@ -213,6 +219,94 @@ public class UserWalletService : UserWalletContract.UserWalletContractBase }; } + // ============= 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 GetMagicWalletStatus( Google.Protobuf.WellKnownTypes.Empty request, ServerCallContext context) { diff --git a/src/CMSMicroservice.WebApi/appsettings.Development.json b/src/CMSMicroservice.WebApi/appsettings.Development.json deleted file mode 100644 index df4f16b..0000000 --- a/src/CMSMicroservice.WebApi/appsettings.Development.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "CmsBaseUrl": "https://localhost:32846", - "FrontOfficeBaseUrl": "http://localhost:5268", - "ZarinPal": { - "UseSandbox": true - }, - "Logging": { - "LogLevel": { - "Default": "Debug", - "Microsoft.AspNetCore": "Warning" - } - } -} diff --git a/src/CMSMicroservice.WebApi/appsettings.json b/src/CMSMicroservice.WebApi/appsettings.json index 13c0ffe..d4f558b 100644 --- a/src/CMSMicroservice.WebApi/appsettings.json +++ b/src/CMSMicroservice.WebApi/appsettings.json @@ -4,8 +4,8 @@ "MerchantId": "4225d555-5fa9-4df0-9b61-1ce152cbbba8", "UseSandbox": true }, - "CmsBaseUrl": "https://cms.se.kbs1.ir", - "FrontOfficeBaseUrl": "https://foursat.se.kbs1.ir", + "CmsBaseUrl": "https://localhost:32846", + "FrontOfficeBaseUrl": "http://localhost:5268", "FMS": { "Address": "https://dl.afrino.co" },