using CMSMicroservice.Protobuf.Protos.Package; using CMSMicroservice.WebApi.Common.Services; using CMSMicroservice.Application.PackageCQ.Commands.CreateNewPackage; using CMSMicroservice.Application.PackageCQ.Commands.UpdatePackage; using CMSMicroservice.Application.PackageCQ.Commands.DeletePackage; using CMSMicroservice.Application.PackageCQ.Queries.GetPackage; using CMSMicroservice.Application.PackageCQ.Queries.GetAllPackageByFilter; using CMSMicroservice.Application.PackageCQ.Queries.GetUserPackageStatus; using CMSMicroservice.Application.PackageCQ.Queries.GetCustomerPackages; using CMSMicroservice.Application.PackageCQ.Queries.GetCustomerPackageDetails; using CMSMicroservice.Application.PackageCQ.Queries.GetCustomerPurchaseHistory; using CMSMicroservice.Application.PackageCQ.Commands.VerifyUserPackagePurchasePayment; using CMSMicroservice.Application.Common.Interfaces; using CMSMicroservice.Application.Common; using CMSMicroservice.Application.Common.Exceptions; using CMSMicroservice.Domain.Entities.Payment; using AppModels = CMSMicroservice.Application.Common.Models; using Grpc.Core; using Google.Protobuf.WellKnownTypes; using System.Collections.Generic; using System.Linq; using CMSMicroservice.Protobuf.Protos; using Microsoft.EntityFrameworkCore; using MediatR; using Mapster; using Microsoft.Extensions.Configuration; namespace CMSMicroservice.WebApi.Services; public class PackageService : PackageContract.PackageContractBase { private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS; private readonly ISender _sender; private readonly IApplicationDbContext _context; private readonly ICurrentUserService _currentUserService; private readonly IPaymentGatewayResolver _paymentGatewayResolver; private readonly IConfiguration _configuration; private readonly IUserPaymentLock _paymentLock; public PackageService( IDispatchRequestToCQRS dispatchRequestToCQRS, ISender sender, IApplicationDbContext context, ICurrentUserService currentUserService, IPaymentGatewayResolver paymentGatewayResolver, IConfiguration configuration, IUserPaymentLock paymentLock) { _dispatchRequestToCQRS = dispatchRequestToCQRS; _sender = sender; _context = context; _currentUserService = currentUserService; _paymentGatewayResolver = paymentGatewayResolver; _configuration = configuration; _paymentLock = paymentLock; } public override async Task CreateNewPackage(CreateNewPackageRequest request, ServerCallContext context) { return await _dispatchRequestToCQRS.Handle(request, context); } public override async Task UpdatePackage(UpdatePackageRequest request, ServerCallContext context) { return await _dispatchRequestToCQRS.Handle(request, context); } public override async Task DeletePackage(DeletePackageRequest request, ServerCallContext context) { return await _dispatchRequestToCQRS.Handle(request, context); } public override async Task GetPackage(GetPackageRequest request, ServerCallContext context) { return await _dispatchRequestToCQRS.Handle(request, context); } public override async Task GetAllPackageByFilter(GetAllPackageByFilterRequest request, ServerCallContext context) { return await _dispatchRequestToCQRS.Handle(request, context); } public override async Task GetUserPackageStatus(GetUserPackageStatusRequest request, ServerCallContext context) { return await _dispatchRequestToCQRS.Handle(request, context); } // ============= Customer-specific Method Implementations ============= public override async Task GetCustomerPackages(GetCustomerPackagesRequest request, ServerCallContext context) { var query = new GetCustomerPackagesQuery { IncludeInactive = request.IncludeInactive, PackageTypeFilter = request.PackageTypeFilter != PackageTypeEnum.PackageTypeBasic ? (int?)request.PackageTypeFilter : null }; var result = await _sender.Send(query, context.CancellationToken); var response = new GetCustomerPackagesResponse(); response.Models.AddRange(result.Adapt>()); return response; } public override async Task GetCustomerPackageDetails(GetCustomerPackageDetailsRequest request, ServerCallContext context) { var query = new GetCustomerPackageDetailsQuery { PackageId = request.PackageId }; var result = await _sender.Send(query, context.CancellationToken); var response = new GetCustomerPackageDetailsResponse { Id = result.Id, Title = result.Title, Description = result.Description, Price = result.Price, ImagePath = result.ImagePath }; // Map Features foreach (var feature in result.Features) { response.Features.Add(new PackageFeature { Title = feature.Title, Description = feature.Description, Icon = feature.Icon, IsHighlighted = feature.IsHighlighted }); } // Map Requirements response.Requirements = new PurchaseRequirements { RequiresMembership = result.Requirements.RequiresMembership, MinimumWalletBalance = result.Requirements.MinimumWalletBalance }; response.Requirements.Restrictions.AddRange(result.Requirements.Restrictions); return response; } public override async Task CustomerPurchasePackage(CustomerPurchasePackageRequest request, ServerCallContext context) { var userId = GetCurrentUserId(); try { return await _paymentLock.ExecuteAsync( PaymentLockScopes.Initiate(userId), PaymentLockStrategy.FailFast, ct => CustomerPurchasePackageCore(request, userId, ct), context.CancellationToken); } catch (PaymentInProgressException ex) { throw new RpcException(new Status(StatusCode.FailedPrecondition, ex.Message)); } } private async Task CustomerPurchasePackageCore( CustomerPurchasePackageRequest request, long userId, CancellationToken cancellationToken) { // Lookup package var package = await _context.Packages .AsNoTracking() .Where(p => p.Id == request.PackageId && !p.IsDeleted) .FirstOrDefaultAsync(cancellationToken); if (package == null) throw new RpcException(new Status(StatusCode.NotFound, "پکیج مورد نظر یافت نشد")); var gatewayProvider = NormalizeGatewayProvider(request.GatewayProvider); var isTwentyCard = gatewayProvider == "20card"; long gatewayAmount = package.Price; if (isTwentyCard) { var packageKey = $"TwentyCard:Packages:{package.Id}"; gatewayAmount = _configuration.GetValue($"{packageKey}:GatewayAmountToman") ?? 0; var walletCredit = _configuration.GetValue($"{packageKey}:WalletCreditToman") ?? 0; if (gatewayAmount <= 0 || walletCredit <= 0) { throw new RpcException(new Status( StatusCode.FailedPrecondition, "پرداخت ۲۰کارت برای این پکیج پیکربندی نشده است")); } } // Create transaction — 20card: Amount = gateway charge; zarinpal: Package.Price var transaction = new CMSMicroservice.Domain.Entities.Transaction { Amount = gatewayAmount, Description = isTwentyCard ? $"خرید پکیج {package.Title} (۲۰کارت)" : $"خرید پکیج {package.Title}", PaymentStatus = Domain.Enums.PaymentStatus.Pending, Type = Domain.Enums.TransactionType.Buy }; _context.Transactions.Add(transaction); await _context.SaveChangesAsync(cancellationToken); // Create purchase record — UPP.Amount always Package.Price (catalog) var purchaseMethod = request.PurchaseMethod == PurchaseMethodEnum.PurchaseMethodGateway ? Domain.Enums.PackagePurchaseMethod.DirectPurchase : Domain.Enums.PackagePurchaseMethod.DayaLoan; var purchase = new CMSMicroservice.Domain.Entities.UserPackagePurchase { UserId = userId, PackageId = package.Id, PurchaseMethod = purchaseMethod, PurchasedAt = DateTime.UtcNow, Amount = package.Price, TransactionId = transaction.Id }; _context.UserPackagePurchases.Add(purchase); await _context.SaveChangesAsync(cancellationToken); // Initiate payment with gateway var user = await _context.Users .AsNoTracking() .Where(u => u.Id == userId) .Select(u => new { u.Mobile, u.FirstName, u.LastName, u.Email, u.NationalCode }) .FirstOrDefaultAsync(cancellationToken); // Callback URL از config — نه از ورودی کاربر (امنیت) // ۲۰کارت: CallbackBaseUrl اختیاری (مثلاً استیج → kbs2.ir چون دامنه استیج در ترمینال ثبت نیست) var frontOfficeBaseUrl = isTwentyCard ? (_configuration["TwentyCard:CallbackBaseUrl"] ?? _configuration["FrontOfficeBaseUrl"] ?? "https://localhost:5268") : (_configuration["FrontOfficeBaseUrl"] ?? "https://localhost:5268"); var callbackUrl = $"{frontOfficeBaseUrl.TrimEnd('/')}/profile/payment-callback?orderId={purchase.Id}"; var paymentGateway = _paymentGatewayResolver.Get(gatewayProvider); var payerName = $"{user?.FirstName} {user?.LastName}".Trim(); var paymentResult = await paymentGateway.InitiatePaymentAsync(new PaymentRequest { Amount = gatewayAmount, UserId = userId, Mobile = user?.Mobile ?? string.Empty, Name = payerName, Invoice = purchase.Id.ToString(), Description = $"خرید پکیج {package.Title}", CallbackUrl = callbackUrl, Email = user?.Email, NationalId = user?.NationalCode }, cancellationToken); if (!paymentResult.IsSuccess) { transaction.PaymentStatus = Domain.Enums.PaymentStatus.Reject; await _context.SaveChangesAsync(cancellationToken); return new CustomerPurchasePackageResponse { Success = false, Message = paymentResult.ErrorMessage ?? "خطا در ارتباط با درگاه پرداخت" }; } // Save RefId transaction.RefId = paymentResult.RefId; // ثبت PaymentTransaction — Amount = gateway amount var paymentTx = new PaymentTransaction { GatewayProvider = gatewayProvider, MerchantId = isTwentyCard ? (_configuration["TwentyCard:TerminalId"] ?? "") : (_configuration["ZarinPal:MerchantId"] ?? ""), Amount = gatewayAmount, CallbackUrl = callbackUrl, Description = $"خرید پکیج {package.Title}", Mobile = user?.Mobile, UserId = userId, RequestStatusCode = 100, RequestStatusMessage = "Success", Authority = paymentResult.RefId, PaymentStatus = false, TransactionId = transaction.Id, OrderId = purchase.Id.ToString() }; _context.PaymentTransactions.Add(paymentTx); await _context.SaveChangesAsync(cancellationToken); return new CustomerPurchasePackageResponse { Success = true, Message = "درخواست خرید پکیج با موفقیت ثبت شد", OrderId = purchase.Id, PaymentGatewayUrl = paymentResult.GatewayUrl ?? string.Empty, Authority = paymentResult.RefId ?? string.Empty }; } private static string NormalizeGatewayProvider(string? provider) { if (string.IsNullOrWhiteSpace(provider)) return "zarinpal"; var key = provider.Trim().ToLowerInvariant(); return key is "20card" or "twentycard" ? "20card" : "zarinpal"; } public override async Task CustomerVerifyPackagePurchase( CustomerVerifyPackagePurchaseRequest request, ServerCallContext context) { try { var result = await _sender.Send(new VerifyUserPackagePurchasePaymentCommand { PurchaseId = request.OrderId, Authority = request.Authority ?? string.Empty, Status = request.Status ?? "NOK" }, context.CancellationToken); return MapVerifyPackagePurchaseResponse(result); } catch (PaymentInProgressException ex) { throw new RpcException(new Status(StatusCode.FailedPrecondition, ex.Message)); } } private static CustomerVerifyPackagePurchaseResponse MapVerifyPackagePurchaseResponse( VerifyUserPackagePurchasePaymentResult result) { return new CustomerVerifyPackagePurchaseResponse { Success = result.Success, Message = result.Message, TransactionId = result.TransactionId, ReferenceCode = result.ReferenceCode, PurchaseInfo = result.Success ? new PackagePurchaseInfo { PackageId = result.PackageId, PackageName = result.PackageName, AmountPaid = result.AmountPaid, PurchaseDate = Timestamp.FromDateTime( DateTime.SpecifyKind(result.PurchasedAt, DateTimeKind.Utc)) } : null }; } 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 GetCustomerPurchaseHistory(GetCustomerPurchaseHistoryRequest request, ServerCallContext context) { var query = new GetCustomerPurchaseHistoryQuery { UserId = request.UserId, PaginationState = request.PaginationState?.Adapt(), PackageTypeFilter = request.PackageTypeFilter != PackageTypeEnum.PackageTypeBasic ? (int?)request.PackageTypeFilter : null, FromDate = request.FromDate?.ToDateTime(), ToDate = request.ToDate?.ToDateTime() }; var result = await _sender.Send(query, context.CancellationToken); var response = new GetCustomerPurchaseHistoryResponse { MetaData = result.MetaData.Adapt() }; foreach (var purchase in result.Purchases) { response.Purchases.Add(new PackagePurchaseHistory { Id = purchase.Id, PackageId = purchase.PackageId, PackageName = purchase.PackageName, Amount = purchase.Amount, PackageType = (PackageTypeEnum)purchase.PackageType, PurchaseDate = Timestamp.FromDateTime(DateTime.SpecifyKind(purchase.PurchaseDate, DateTimeKind.Utc)), ExpiryDate = purchase.ExpiryDate.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(purchase.ExpiryDate.Value, DateTimeKind.Utc)) : null, Status = (PaymentStatusEnum)purchase.Status, StatusMessage = purchase.StatusMessage, ReferenceCode = purchase.ReferenceCode }); } return response; } }