9d17f174af
Build and Deploy to Kubernetes / build-and-deploy (push) Has been cancelled
- 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.
401 lines
17 KiB
C#
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;
|
|
}
|
|
}
|