Files
CMS/src/CMSMicroservice.WebApi/Services/PackageService.cs
T
masoodafar-web 9d17f174af
Build and Deploy to Kubernetes / build-and-deploy (push) Has been cancelled
feat(config): add CallbackBaseUrl to configuration for payment processing
- Introduced CallbackBaseUrl in both cms-config.yaml and appsettings.Staging.json to support dynamic callback URLs for payment processing.
- Updated PackageService to utilize the new CallbackBaseUrl for enhanced security and flexibility in payment integration.
2026-09-21 21:35:42 +03:30

401 lines
17 KiB
C#

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<CreateNewPackageResponse> CreateNewPackage(CreateNewPackageRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<CreateNewPackageRequest, CreateNewPackageCommand, CreateNewPackageResponse>(request, context);
}
public override async Task<Empty> UpdatePackage(UpdatePackageRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<UpdatePackageRequest, UpdatePackageCommand>(request, context);
}
public override async Task<Empty> DeletePackage(DeletePackageRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<DeletePackageRequest, DeletePackageCommand>(request, context);
}
public override async Task<GetPackageResponse> GetPackage(GetPackageRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetPackageRequest, GetPackageQuery, GetPackageResponse>(request, context);
}
public override async Task<GetAllPackageByFilterResponse> GetAllPackageByFilter(GetAllPackageByFilterRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetAllPackageByFilterRequest, GetAllPackageByFilterQuery, GetAllPackageByFilterResponse>(request, context);
}
public override async Task<GetUserPackageStatusResponse> GetUserPackageStatus(GetUserPackageStatusRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetUserPackageStatusRequest, GetUserPackageStatusQuery, GetUserPackageStatusResponse>(request, context);
}
// ============= Customer-specific Method Implementations =============
public override async Task<GetCustomerPackagesResponse> 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<List<CustomerPackageModel>>());
return response;
}
public override async Task<GetCustomerPackageDetailsResponse> 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<CustomerPurchasePackageResponse> 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<CustomerPurchasePackageResponse> 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<long?>($"{packageKey}:GatewayAmountToman") ?? 0;
var walletCredit = _configuration.GetValue<long?>($"{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<CustomerVerifyPackagePurchaseResponse> 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<GetCustomerPurchaseHistoryResponse> GetCustomerPurchaseHistory(GetCustomerPurchaseHistoryRequest request, ServerCallContext context)
{
var query = new GetCustomerPurchaseHistoryQuery
{
UserId = request.UserId,
PaginationState = request.PaginationState?.Adapt<AppModels.PaginationState>(),
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<MetaData>()
};
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;
}
}