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.Common.Interfaces; 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 IPaymentGatewayService _paymentGateway; private readonly IConfiguration _configuration; public PackageService( IDispatchRequestToCQRS dispatchRequestToCQRS, ISender sender, IApplicationDbContext context, ICurrentUserService currentUserService, IPaymentGatewayService paymentGateway, IConfiguration configuration) { _dispatchRequestToCQRS = dispatchRequestToCQRS; _sender = sender; _context = context; _currentUserService = currentUserService; _paymentGateway = paymentGateway; _configuration = configuration; } 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(); // Lookup package var package = await _context.Packages .AsNoTracking() .Where(p => p.Id == request.PackageId && !p.IsDeleted) .FirstOrDefaultAsync(context.CancellationToken); if (package == null) throw new RpcException(new Status(StatusCode.NotFound, "پکیج مورد نظر یافت نشد")); // Create transaction var transaction = new CMSMicroservice.Domain.Entities.Transaction { Amount = package.Price, Description = $"خرید پکیج {package.Title}", PaymentStatus = Domain.Enums.PaymentStatus.Pending, Type = Domain.Enums.TransactionType.Buy }; _context.Transactions.Add(transaction); await _context.SaveChangesAsync(context.CancellationToken); // Create purchase record 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(context.CancellationToken); // Initiate payment with gateway var user = await _context.Users .AsNoTracking() .Where(u => u.Id == userId) .Select(u => new { u.Mobile }) .FirstOrDefaultAsync(context.CancellationToken); // Embed orderId in callback URL so FrontOffice can pass it back for verification var separator = request.CallbackUrl.Contains('?') ? "&" : "?"; var callbackWithOrder = $"{request.CallbackUrl}{separator}orderId={purchase.Id}"; var paymentResult = await _paymentGateway.InitiatePaymentAsync(new PaymentRequest { Amount = package.Price, UserId = userId, Mobile = user?.Mobile ?? string.Empty, Description = $"خرید پکیج {package.Title}", CallbackUrl = callbackWithOrder }, context.CancellationToken); if (!paymentResult.IsSuccess) { transaction.PaymentStatus = Domain.Enums.PaymentStatus.Reject; await _context.SaveChangesAsync(context.CancellationToken); return new CustomerPurchasePackageResponse { Success = false, Message = paymentResult.ErrorMessage ?? "خطا در ارتباط با درگاه پرداخت" }; } // Save RefId transaction.RefId = paymentResult.RefId; // ثبت PaymentTransaction var paymentTx = new PaymentTransaction { GatewayProvider = _configuration["PaymentProvider"] ?? "zarinpal", MerchantId = _configuration["ZarinPal:MerchantId"] ?? "", Amount = package.Price, CallbackUrl = request.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(context.CancellationToken); return new CustomerPurchasePackageResponse { Success = true, Message = "درخواست خرید پکیج با موفقیت ثبت شد", OrderId = purchase.Id, PaymentGatewayUrl = paymentResult.GatewayUrl ?? string.Empty, Authority = paymentResult.RefId ?? string.Empty }; } public override async Task CustomerVerifyPackagePurchase(CustomerVerifyPackagePurchaseRequest request, ServerCallContext context) { // Find purchase record var purchase = await _context.UserPackagePurchases .Include(p => p.Package) .Where(p => p.Id == request.OrderId && !p.IsDeleted) .FirstOrDefaultAsync(context.CancellationToken); if (purchase == null) throw new RpcException(new Status(StatusCode.NotFound, "سفارش یافت نشد")); // Find the associated transaction var transaction = purchase.TransactionId.HasValue ? await _context.Transactions .Where(t => t.Id == purchase.TransactionId.Value) .FirstOrDefaultAsync(context.CancellationToken) : null; // If status from gateway callback is not OK if (request.Status != "OK") { if (transaction != null) { transaction.PaymentStatus = Domain.Enums.PaymentStatus.Reject; await _context.SaveChangesAsync(context.CancellationToken); } return new CustomerVerifyPackagePurchaseResponse { Success = false, Message = "پرداخت توسط کاربر لغو شد" }; } // Verify with payment gateway var verifyResult = await _paymentGateway.VerifyPaymentAsync( request.Authority, request.Status, context.CancellationToken); // آپدیت PaymentTransaction 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; } if (verifyResult.IsSuccess && transaction != null) { transaction.PaymentStatus = Domain.Enums.PaymentStatus.Success; transaction.PaymentDate = DateTime.UtcNow; transaction.RefId = verifyResult.RefId; // شارژ کیف پول کاربر var wallet = await _context.UserWallets .FirstOrDefaultAsync(w => w.UserId == purchase.UserId, context.CancellationToken); if (wallet == null) { wallet = new CMSMicroservice.Domain.Entities.UserWallet { UserId = purchase.UserId, Balance = 0, DiscountBalance = 0, NetworkBalance = 0 }; _context.UserWallets.Add(wallet); await _context.SaveChangesAsync(context.CancellationToken); } var discountMultiplier = purchase.Package?.DiscountMultiplier ?? throw new RpcException(new Status(StatusCode.Internal, "پکیج سفارش یافت نشد")); var discountAmount = (long)(purchase.Amount * (double)discountMultiplier); wallet.Balance += purchase.Amount; wallet.DiscountBalance += discountAmount; // ثبت لاگ کیف پول var walletLog = new CMSMicroservice.Domain.Entities.UserWalletHistory { WalletId = wallet.Id, CurrentBalance = wallet.Balance, ChangeValue = purchase.Amount, CurrentNetworkBalance = wallet.NetworkBalance, ChangeNerworkValue = 0, CurrentDiscountBalance = wallet.DiscountBalance, ChangeDiscountValue = discountAmount, IsIncrease = true, RefrenceId = transaction.Id, PackageId = purchase.PackageId }; _context.UserWalletHistories.Add(walletLog); // به‌روزرسانی کاربر var user = await _context.Users .FirstOrDefaultAsync(u => u.Id == purchase.UserId, context.CancellationToken); if (user != null) user.PackagePurchaseMethod = Domain.Enums.PackagePurchaseMethod.DirectPurchase; } else if (transaction != null) { transaction.PaymentStatus = Domain.Enums.PaymentStatus.Reject; } await _context.SaveChangesAsync(context.CancellationToken); return new CustomerVerifyPackagePurchaseResponse { Success = verifyResult.IsSuccess, Message = verifyResult.IsSuccess ? "خرید پکیج با موفقیت تایید شد" : (verifyResult.Message ?? "خرید پکیج ناموفق بود"), TransactionId = transaction?.Id ?? 0, ReferenceCode = verifyResult.RefId ?? string.Empty, PurchaseInfo = verifyResult.IsSuccess ? new PackagePurchaseInfo { PackageId = purchase.PackageId, PackageName = purchase.Package?.Title ?? string.Empty, AmountPaid = purchase.Amount, PurchaseDate = Timestamp.FromDateTime(DateTime.SpecifyKind(purchase.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; } }