01073084ab
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 9m28s
- PackageService: build callback URL from FrontOfficeBaseUrl config instead of request.CallbackUrl - TransactionsService: same fix — callback URL from config - Prevents potential open redirect vulnerability (user could send arbitrary URL) All callback URLs now come from appsettings FrontOfficeBaseUrl or CmsBaseUrl.
419 lines
18 KiB
C#
419 lines
18 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.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<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();
|
|
|
|
// 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);
|
|
|
|
// Callback URL از config — نه از ورودی کاربر (امنیت)
|
|
var frontOfficeBaseUrl = _configuration["FrontOfficeBaseUrl"] ?? "https://localhost:5268";
|
|
var callbackUrl = $"{frontOfficeBaseUrl}/profile/payment-callback?orderId={purchase.Id}";
|
|
|
|
var paymentResult = await _paymentGateway.InitiatePaymentAsync(new PaymentRequest
|
|
{
|
|
Amount = package.Price,
|
|
UserId = userId,
|
|
Mobile = user?.Mobile ?? string.Empty,
|
|
Description = $"خرید پکیج {package.Title}",
|
|
CallbackUrl = callbackUrl
|
|
}, 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 = 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<CustomerVerifyPackagePurchaseResponse> 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 = "پرداخت توسط کاربر لغو شد"
|
|
};
|
|
}
|
|
|
|
// واکشی PaymentTransaction برای گرفتن مبلغ (تومان) جهت verify
|
|
var paymentTx = await _context.PaymentTransactions
|
|
.FirstOrDefaultAsync(pt => pt.Authority == request.Authority, context.CancellationToken);
|
|
|
|
var amountInToman = paymentTx?.Amount ?? purchase.Amount;
|
|
|
|
// Verify with payment gateway (مبلغ به تومان — سرویس زرینپال خودش ×۱۰ میکنه)
|
|
var verifyResult = await _paymentGateway.VerifyPaymentAsync(
|
|
request.Authority, request.Status, (decimal)amountInToman, 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<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;
|
|
}
|
|
}
|