feat: add base package payment initiation and verification

This commit is contained in:
masoodafar-web
2025-12-16 02:34:22 +03:30
parent b4e41163ec
commit 63f05e0883
12 changed files with 469 additions and 5 deletions
@@ -41,7 +41,7 @@ public class GetMyCommissionPayoutsQueryHandler : IRequestHandler<GetMyCommissio
Id = p.Id, Id = p.Id,
WeekNumber = p.WeekNumber, WeekNumber = p.WeekNumber,
WeekLabel = $"هفته {p.WeekNumber}", WeekLabel = $"هفته {p.WeekNumber}",
BalancesEarned = p.BalancesEarned, BalancesEarned = (int)p.BalancesEarned,
TotalAmount = p.TotalAmount, TotalAmount = p.TotalAmount,
AmountFormatted = FormatCurrency(p.TotalAmount), AmountFormatted = FormatCurrency(p.TotalAmount),
Status = MapStatus(p.Status), Status = MapStatus(p.Status),
@@ -21,7 +21,7 @@ public class GetMyNetworkTreeQueryHandler : IRequestHandler<GetMyNetworkTreeQuer
var cmsRequest = new GetNetworkTreeRequest var cmsRequest = new GetNetworkTreeRequest
{ {
RootUserId = userId, UserId = userId,
MaxDepth = Math.Clamp(request.MaxDepth, 1, 10) // محدود کردن بین 1-10 MaxDepth = Math.Clamp(request.MaxDepth, 1, 10) // محدود کردن بین 1-10
}; };
@@ -0,0 +1,51 @@
namespace FrontOffice.BFF.Application.PackageCQ.Commands.InitiateBasePackagePayment;
/// <summary>
/// درخواست پرداخت پکیج پایه 56 میلیون تومان
/// UserId از CurrentUserService گرفته می‌شود
/// </summary>
public record InitiateBasePackagePaymentCommand : IRequest<InitiateBasePackagePaymentResponseDto>
{
/// <summary>
/// آدرس بازگشت از درگاه پرداخت
/// </summary>
public string CallbackUrl { get; init; } = string.Empty;
}
public class InitiateBasePackagePaymentResponseDto
{
/// <summary>
/// آیا عملیات موفق بود؟
/// </summary>
public bool Success { get; set; }
/// <summary>
/// پیام
/// </summary>
public string Message { get; set; } = string.Empty;
/// <summary>
/// شناسه سفارش CMS
/// </summary>
public long OrderId { get; set; }
/// <summary>
/// شناسه تراکنش CMS
/// </summary>
public long TransactionId { get; set; }
/// <summary>
/// مبلغ پرداختی (ریال)
/// </summary>
public long Amount { get; set; }
/// <summary>
/// آدرس درگاه پرداخت
/// </summary>
public string PaymentGatewayUrl { get; set; } = string.Empty;
/// <summary>
/// کد پیگیری زرین‌پال
/// </summary>
public string Authority { get; set; } = string.Empty;
}
@@ -0,0 +1,100 @@
using CmsPackage = CMSMicroservice.Protobuf.Protos.Package;
using PYMSMicroservice.Protobuf.Protos;
using PYMSMicroservice.Protobuf.Protos.Transaction;
namespace FrontOffice.BFF.Application.PackageCQ.Commands.InitiateBasePackagePayment;
/// <summary>
/// Handler برای درخواست پرداخت پکیج پایه 56 میلیون تومان
/// 1. ابتدا CMS فراخوانی می‌شود برای ثبت Transaction و Order
/// 2. سپس PYMS فراخوانی می‌شود برای دریافت URL درگاه پرداخت
/// </summary>
public class InitiateBasePackagePaymentCommandHandler : IRequestHandler<InitiateBasePackagePaymentCommand, InitiateBasePackagePaymentResponseDto>
{
private readonly IApplicationContractContext _context;
private readonly ICurrentUserService _currentUserService;
private const string ZarinpalMerchantId = "6b098fc8-f490-47a1-aac3-1de1a1b84404";
public InitiateBasePackagePaymentCommandHandler(IApplicationContractContext context, ICurrentUserService currentUserService)
{
_context = context;
_currentUserService = currentUserService;
}
public async Task<InitiateBasePackagePaymentResponseDto> Handle(InitiateBasePackagePaymentCommand request, CancellationToken cancellationToken)
{
// 1. ابتدا CMS را فراخوانی کن برای ثبت تراکنش و سفارش
var cmsRequest = new CmsPackage.InitiateBasePackagePaymentRequest
{
UserId = _currentUserService.UserId.Value
};
var cmsResponse = await _context.Package.InitiateBasePackagePaymentAsync(
cmsRequest,
cancellationToken: cancellationToken);
// اگر CMS خطا داد
if (!cmsResponse.Success)
{
return new InitiateBasePackagePaymentResponseDto
{
Success = false,
Message = cmsResponse.Message
};
}
// 2. حالا PYMS را برای دریافت URL درگاه فراخوانی کن
// مبلغ به ریال (56 میلیون تومان = 560,000,000 ریال)
var paymentRequest = new PaymentRequestRequest
{
MerchantId = ZarinpalMerchantId,
Amount = cmsResponse.Amount * 10, // تبدیل تومان به ریال
CallbackUrl = $"{request.CallbackUrl}?orderId={cmsResponse.OrderId}&transactionId={cmsResponse.TransactionId}",
Description = "پرداخت پکیج پایه",
Currency = CurrencyEnum.Irr,
Type = TransactionTypeEnum.Sandbox,
OrderId = cmsResponse.OrderId.ToString()
};
var paymentResponse = await _context.ZarinTransactions.PaymentRequestAsync(
paymentRequest,
cancellationToken: cancellationToken);
// اگر درگاه URL برنگردانده
if (string.IsNullOrEmpty(paymentResponse.PaymentGWUrl))
{
return new InitiateBasePackagePaymentResponseDto
{
Success = false,
Message = "خطا در دریافت آدرس درگاه پرداخت",
OrderId = cmsResponse.OrderId,
TransactionId = cmsResponse.TransactionId
};
}
// استخراج Authority از URL
// URL معمولا به شکل: https://www.zarinpal.com/pg/StartPay/{Authority}
var authority = ExtractAuthorityFromUrl(paymentResponse.PaymentGWUrl);
return new InitiateBasePackagePaymentResponseDto
{
Success = true,
Message = "لطفا برای تکمیل پرداخت به درگاه بانکی مراجعه کنید",
OrderId = cmsResponse.OrderId,
TransactionId = cmsResponse.TransactionId,
Amount = cmsResponse.Amount,
PaymentGatewayUrl = paymentResponse.PaymentGWUrl,
Authority = authority
};
}
private static string ExtractAuthorityFromUrl(string url)
{
// URL: https://www.zarinpal.com/pg/StartPay/{Authority}
if (string.IsNullOrEmpty(url))
return string.Empty;
var parts = url.TrimEnd('/').Split('/');
return parts.Length > 0 ? parts[^1] : string.Empty;
}
}
@@ -0,0 +1,15 @@
namespace FrontOffice.BFF.Application.PackageCQ.Commands.InitiateBasePackagePayment;
public class InitiateBasePackagePaymentCommandValidator : AbstractValidator<InitiateBasePackagePaymentCommand>
{
public InitiateBasePackagePaymentCommandValidator()
{
// RuleFor(x => x.UserId)
// .GreaterThan(0)
// .WithMessage("شناسه کاربر الزامی است");
RuleFor(x => x.CallbackUrl)
.NotEmpty()
.WithMessage("آدرس بازگشت الزامی است");
}
}
@@ -0,0 +1,65 @@
namespace FrontOffice.BFF.Application.PackageCQ.Commands.VerifyBasePackagePayment;
/// <summary>
/// تأیید پرداخت پکیج پایه (Callback از درگاه)
/// </summary>
public record VerifyBasePackagePaymentCommand : IRequest<VerifyBasePackagePaymentResponseDto>
{
/// <summary>
/// شناسه سفارش CMS
/// </summary>
public long OrderId { get; init; }
/// <summary>
/// شناسه تراکنش CMS
/// </summary>
public long TransactionId { get; init; }
/// <summary>
/// کد پیگیری از زرین‌پال
/// </summary>
public string Authority { get; init; } = string.Empty;
/// <summary>
/// وضعیت از زرین‌پال (OK یا NOK)
/// </summary>
public string Status { get; init; } = string.Empty;
}
public class VerifyBasePackagePaymentResponseDto
{
/// <summary>
/// آیا پرداخت موفق بود؟
/// </summary>
public bool Success { get; set; }
/// <summary>
/// پیام
/// </summary>
public string Message { get; set; } = string.Empty;
/// <summary>
/// شناسه سفارش
/// </summary>
public long OrderId { get; set; }
/// <summary>
/// شناسه تراکنش
/// </summary>
public long TransactionId { get; set; }
/// <summary>
/// کد پیگیری بانکی
/// </summary>
public string? RefId { get; set; }
/// <summary>
/// موجودی کیف پول بعد از شارژ
/// </summary>
public long WalletBalance { get; set; }
/// <summary>
/// موجودی تخفیف بعد از شارژ
/// </summary>
public long DiscountBalance { get; set; }
}
@@ -0,0 +1,105 @@
using CmsPackage = CMSMicroservice.Protobuf.Protos.Package;
using PYMSMicroservice.Protobuf.Protos.Transaction;
namespace FrontOffice.BFF.Application.PackageCQ.Commands.VerifyBasePackagePayment;
/// <summary>
/// Handler برای تأیید پرداخت پکیج پایه
/// 1. ابتدا PYMS فراخوانی می‌شود برای Verify پرداخت
/// 2. سپس CMS فراخوانی می‌شود برای تکمیل یا رد تراکنش
/// </summary>
public class VerifyBasePackagePaymentCommandHandler : IRequestHandler<VerifyBasePackagePaymentCommand, VerifyBasePackagePaymentResponseDto>
{
private readonly IApplicationContractContext _context;
public VerifyBasePackagePaymentCommandHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<VerifyBasePackagePaymentResponseDto> Handle(VerifyBasePackagePaymentCommand request, CancellationToken cancellationToken)
{
// اگر وضعیت از زرین‌پال NOK باشد، کاربر پرداخت را لغو کرده
if (request.Status != "OK")
{
// به CMS اطلاع بده که پرداخت ناموفق بود
await NotifyCmsPaymentFailed(request.OrderId, request.TransactionId, "پرداخت توسط کاربر لغو شد", cancellationToken);
return new VerifyBasePackagePaymentResponseDto
{
Success = false,
Message = "پرداخت لغو شد",
OrderId = request.OrderId,
TransactionId = request.TransactionId
};
}
// 1. تأیید پرداخت از زرین‌پال
var verifyRequest = new PaymentVerificationRequest
{
Authority = request.Authority,
Status = request.Status,
};
var verifyResponse = await _context.ZarinTransactions.PaymentVerificationAsync(
verifyRequest,
cancellationToken: cancellationToken);
// 2. بر اساس نتیجه Verify، CMS را به‌روزرسانی کن
if (!verifyResponse.PaymentStatus)
{
// پرداخت ناموفق بود
await NotifyCmsPaymentFailed(request.OrderId, request.TransactionId, verifyResponse.Message, cancellationToken);
return new VerifyBasePackagePaymentResponseDto
{
Success = false,
Message = verifyResponse.Message ?? "پرداخت ناموفق بود",
OrderId = request.OrderId,
TransactionId = request.TransactionId
};
}
// پرداخت موفق بود - CMS را برای شارژ کیف پول فراخوانی کن
var cmsRequest = new CmsPackage.VerifyBasePackagePaymentRequest
{
OrderId = request.OrderId,
TransactionId = request.TransactionId,
PaymentSuccess = true,
RefId = verifyResponse.RefId,
Message = verifyResponse.Message
};
var cmsResponse = await _context.Package.VerifyBasePackagePaymentAsync(
cmsRequest,
cancellationToken: cancellationToken);
return new VerifyBasePackagePaymentResponseDto
{
Success = cmsResponse.Success,
Message = cmsResponse.Message,
OrderId = cmsResponse.OrderId,
TransactionId = cmsResponse.TransactionId,
RefId = cmsResponse.ReferenceCode,
WalletBalance = cmsResponse.WalletBalance,
DiscountBalance = cmsResponse.DiscountBalance
};
}
/// <summary>
/// اطلاع دادن به CMS که پرداخت ناموفق بود
/// </summary>
private async Task NotifyCmsPaymentFailed(long orderId, long transactionId, string reason, CancellationToken cancellationToken)
{
var cmsRequest = new CmsPackage.VerifyBasePackagePaymentRequest
{
OrderId = orderId,
TransactionId = transactionId,
PaymentSuccess = false,
Message = reason
};
await _context.Package.VerifyBasePackagePaymentAsync(cmsRequest, cancellationToken: cancellationToken);
}
}
@@ -0,0 +1,23 @@
namespace FrontOffice.BFF.Application.PackageCQ.Commands.VerifyBasePackagePayment;
public class VerifyBasePackagePaymentCommandValidator : AbstractValidator<VerifyBasePackagePaymentCommand>
{
public VerifyBasePackagePaymentCommandValidator()
{
RuleFor(x => x.OrderId)
.GreaterThan(0)
.WithMessage("شناسه سفارش الزامی است");
RuleFor(x => x.TransactionId)
.GreaterThan(0)
.WithMessage("شناسه تراکنش الزامی است");
RuleFor(x => x.Authority)
.NotEmpty()
.WithMessage("کد پیگیری الزامی است");
RuleFor(x => x.Status)
.NotEmpty()
.WithMessage("وضعیت پرداخت الزامی است");
}
}
@@ -7,7 +7,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Afrino.PYMSMicroservice.Protobuf" Version="0.0.11" /> <PackageReference Include="Afrino.PYMSMicroservice.Protobuf" Version="0.0.11" />
<PackageReference Include="Foursat.CMSMicroservice.Protobuf" Version="0.0.144" /> <!-- <PackageReference Include="Foursat.CMSMicroservice.Protobuf" Version="0.0.155" /> -->
<PackageReference Include="Google.Protobuf" Version="3.33.0" /> <PackageReference Include="Google.Protobuf" Version="3.33.0" />
<PackageReference Include="Grpc.Net.ClientFactory" Version="2.54.0" /> <PackageReference Include="Grpc.Net.ClientFactory" Version="2.54.0" />
<PackageReference Include="Grpc.Tools" Version="2.76.0"> <PackageReference Include="Grpc.Tools" Version="2.76.0">
@@ -16,4 +16,9 @@
</PackageReference> </PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup>
<!-- Direct project reference to local CMS Protobuf for development -->
<ProjectReference Include="../../../CMS/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj" />
</ItemGroup>
</Project> </Project>
@@ -1,10 +1,41 @@
using FrontOffice.BFF.Package.Protobuf.Protos.Package;
using FrontOffice.BFF.Application.PackageCQ.Commands.InitiateBasePackagePayment;
using FrontOffice.BFF.Application.PackageCQ.Commands.VerifyBasePackagePayment;
namespace FrontOffice.BFF.WebApi.Common.Mappings; namespace FrontOffice.BFF.WebApi.Common.Mappings;
public class PackageProfile : IRegister public class PackageProfile : IRegister
{ {
void IRegister.Register(TypeAdapterConfig config) void IRegister.Register(TypeAdapterConfig config)
{ {
//config.NewConfig<Source,Destination>() // InitiateBasePackagePayment
// .Map(dest => dest.FullName, src => $"{src.Firstname} {src.Lastname}"); // UserId از CurrentUserService گرفته می‌شود، نه از Request
config.NewConfig<InitiateBasePackagePaymentRequest, InitiateBasePackagePaymentCommand>()
.Map(dest => dest.CallbackUrl, src => src.CallbackUrl);
config.NewConfig<InitiateBasePackagePaymentResponseDto, InitiateBasePackagePaymentResponse>()
.Map(dest => dest.Success, src => src.Success)
.Map(dest => dest.Message, src => src.Message)
.Map(dest => dest.OrderId, src => src.OrderId)
.Map(dest => dest.TransactionId, src => src.TransactionId)
.Map(dest => dest.Amount, src => src.Amount)
.Map(dest => dest.PaymentGatewayUrl, src => src.PaymentGatewayUrl)
.Map(dest => dest.Authority, src => src.Authority);
// VerifyBasePackagePayment
config.NewConfig<VerifyBasePackagePaymentRequest, VerifyBasePackagePaymentCommand>()
.Map(dest => dest.OrderId, src => src.OrderId)
.Map(dest => dest.TransactionId, src => src.TransactionId)
.Map(dest => dest.Authority, src => src.Authority)
.Map(dest => dest.Status, src => src.Status);
config.NewConfig<VerifyBasePackagePaymentResponseDto, VerifyBasePackagePaymentResponse>()
.Map(dest => dest.Success, src => src.Success)
.Map(dest => dest.Message, src => src.Message)
.Map(dest => dest.OrderId, src => src.OrderId)
.Map(dest => dest.TransactionId, src => src.TransactionId)
.Map(dest => dest.RefId, src => src.RefId)
.Map(dest => dest.WalletBalance, src => src.WalletBalance)
.Map(dest => dest.DiscountBalance, src => src.DiscountBalance);
} }
} }
@@ -2,6 +2,9 @@ using FrontOffice.BFF.Package.Protobuf.Protos.Package;
using FrontOffice.BFF.WebApi.Common.Services; using FrontOffice.BFF.WebApi.Common.Services;
using FrontOffice.BFF.Application.PackageCQ.Queries.GetPackage; using FrontOffice.BFF.Application.PackageCQ.Queries.GetPackage;
using FrontOffice.BFF.Application.PackageCQ.Queries.GetAllPackageByFilter; using FrontOffice.BFF.Application.PackageCQ.Queries.GetAllPackageByFilter;
using FrontOffice.BFF.Application.PackageCQ.Commands.InitiateBasePackagePayment;
using FrontOffice.BFF.Application.PackageCQ.Commands.VerifyBasePackagePayment;
namespace FrontOffice.BFF.WebApi.Services; namespace FrontOffice.BFF.WebApi.Services;
public class PackageService : PackageContract.PackageContractBase public class PackageService : PackageContract.PackageContractBase
{ {
@@ -19,4 +22,15 @@ public class PackageService : PackageContract.PackageContractBase
{ {
return await _dispatchRequestToCQRS.Handle<GetAllPackageByFilterRequest, GetAllPackageByFilterQuery, GetAllPackageByFilterResponse>(request, context); return await _dispatchRequestToCQRS.Handle<GetAllPackageByFilterRequest, GetAllPackageByFilterQuery, GetAllPackageByFilterResponse>(request, context);
} }
// Base Package Payment (56M پکیج پایه)
public override async Task<InitiateBasePackagePaymentResponse> InitiateBasePackagePayment(InitiateBasePackagePaymentRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<InitiateBasePackagePaymentRequest, InitiateBasePackagePaymentCommand, InitiateBasePackagePaymentResponse>(request, context);
}
public override async Task<VerifyBasePackagePaymentResponse> VerifyBasePackagePayment(VerifyBasePackagePaymentRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<VerifyBasePackagePaymentRequest, VerifyBasePackagePaymentCommand, VerifyBasePackagePaymentResponse>(request, context);
}
} }
@@ -24,6 +24,21 @@ service PackageContract
}; };
}; };
// Base Package Payment (56M پکیج پایه)
rpc InitiateBasePackagePayment(InitiateBasePackagePaymentRequest) returns (InitiateBasePackagePaymentResponse){
option (google.api.http) = {
post: "/InitiateBasePackagePayment"
body: "*"
};
};
rpc VerifyBasePackagePayment(VerifyBasePackagePaymentRequest) returns (VerifyBasePackagePaymentResponse){
option (google.api.http) = {
post: "/VerifyBasePackagePayment"
body: "*"
};
};
} }
message GetPackageRequest message GetPackageRequest
{ {
@@ -92,3 +107,43 @@ message DecimalValue
sfixed32 nanos = 2; sfixed32 nanos = 2;
} }
// Base Package Payment Messages
// درخواست پرداخت پکیج پایه
// UserId از CurrentUserService گرفته می‌شود
message InitiateBasePackagePaymentRequest
{
string callback_url = 1;
}
message InitiateBasePackagePaymentResponse
{
bool success = 1;
string message = 2;
int64 order_id = 3;
int64 transaction_id = 4;
int64 amount = 5;
string payment_gateway_url = 6;
string authority = 7;
}
// تأیید پرداخت پکیج پایه (بعد از بازگشت از درگاه)
message VerifyBasePackagePaymentRequest
{
int64 order_id = 1;
int64 transaction_id = 2;
string authority = 3;
string status = 4;
}
message VerifyBasePackagePaymentResponse
{
bool success = 1;
string message = 2;
int64 order_id = 3;
int64 transaction_id = 4;
google.protobuf.StringValue ref_id = 5;
int64 wallet_balance = 6;
int64 discount_balance = 7;
}