diff --git a/src/FrontOffice.BFF.Application/CommissionCQ/Queries/GetMyCommissionPayouts/GetMyCommissionPayoutsQueryHandler.cs b/src/FrontOffice.BFF.Application/CommissionCQ/Queries/GetMyCommissionPayouts/GetMyCommissionPayoutsQueryHandler.cs index b7e9af7..0cd6819 100644 --- a/src/FrontOffice.BFF.Application/CommissionCQ/Queries/GetMyCommissionPayouts/GetMyCommissionPayoutsQueryHandler.cs +++ b/src/FrontOffice.BFF.Application/CommissionCQ/Queries/GetMyCommissionPayouts/GetMyCommissionPayoutsQueryHandler.cs @@ -41,7 +41,7 @@ public class GetMyCommissionPayoutsQueryHandler : IRequestHandler +/// درخواست پرداخت پکیج پایه 56 میلیون تومان +/// UserId از CurrentUserService گرفته می‌شود +/// +public record InitiateBasePackagePaymentCommand : IRequest +{ + /// + /// آدرس بازگشت از درگاه پرداخت + /// + public string CallbackUrl { get; init; } = string.Empty; +} + +public class InitiateBasePackagePaymentResponseDto +{ + /// + /// آیا عملیات موفق بود؟ + /// + public bool Success { get; set; } + + /// + /// پیام + /// + public string Message { get; set; } = string.Empty; + + /// + /// شناسه سفارش CMS + /// + public long OrderId { get; set; } + + /// + /// شناسه تراکنش CMS + /// + public long TransactionId { get; set; } + + /// + /// مبلغ پرداختی (ریال) + /// + public long Amount { get; set; } + + /// + /// آدرس درگاه پرداخت + /// + public string PaymentGatewayUrl { get; set; } = string.Empty; + + /// + /// کد پیگیری زرین‌پال + /// + public string Authority { get; set; } = string.Empty; +} diff --git a/src/FrontOffice.BFF.Application/PackageCQ/Commands/InitiateBasePackagePayment/InitiateBasePackagePaymentCommandHandler.cs b/src/FrontOffice.BFF.Application/PackageCQ/Commands/InitiateBasePackagePayment/InitiateBasePackagePaymentCommandHandler.cs new file mode 100644 index 0000000..d9fdf45 --- /dev/null +++ b/src/FrontOffice.BFF.Application/PackageCQ/Commands/InitiateBasePackagePayment/InitiateBasePackagePaymentCommandHandler.cs @@ -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; + +/// +/// Handler برای درخواست پرداخت پکیج پایه 56 میلیون تومان +/// 1. ابتدا CMS فراخوانی می‌شود برای ثبت Transaction و Order +/// 2. سپس PYMS فراخوانی می‌شود برای دریافت URL درگاه پرداخت +/// +public class InitiateBasePackagePaymentCommandHandler : IRequestHandler +{ + 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 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; + } +} diff --git a/src/FrontOffice.BFF.Application/PackageCQ/Commands/InitiateBasePackagePayment/InitiateBasePackagePaymentCommandValidator.cs b/src/FrontOffice.BFF.Application/PackageCQ/Commands/InitiateBasePackagePayment/InitiateBasePackagePaymentCommandValidator.cs new file mode 100644 index 0000000..130e9d8 --- /dev/null +++ b/src/FrontOffice.BFF.Application/PackageCQ/Commands/InitiateBasePackagePayment/InitiateBasePackagePaymentCommandValidator.cs @@ -0,0 +1,15 @@ +namespace FrontOffice.BFF.Application.PackageCQ.Commands.InitiateBasePackagePayment; + +public class InitiateBasePackagePaymentCommandValidator : AbstractValidator +{ + public InitiateBasePackagePaymentCommandValidator() + { + // RuleFor(x => x.UserId) + // .GreaterThan(0) + // .WithMessage("شناسه کاربر الزامی است"); + + RuleFor(x => x.CallbackUrl) + .NotEmpty() + .WithMessage("آدرس بازگشت الزامی است"); + } +} diff --git a/src/FrontOffice.BFF.Application/PackageCQ/Commands/VerifyBasePackagePayment/VerifyBasePackagePaymentCommand.cs b/src/FrontOffice.BFF.Application/PackageCQ/Commands/VerifyBasePackagePayment/VerifyBasePackagePaymentCommand.cs new file mode 100644 index 0000000..0a750fd --- /dev/null +++ b/src/FrontOffice.BFF.Application/PackageCQ/Commands/VerifyBasePackagePayment/VerifyBasePackagePaymentCommand.cs @@ -0,0 +1,65 @@ +namespace FrontOffice.BFF.Application.PackageCQ.Commands.VerifyBasePackagePayment; + +/// +/// تأیید پرداخت پکیج پایه (Callback از درگاه) +/// +public record VerifyBasePackagePaymentCommand : IRequest +{ + /// + /// شناسه سفارش CMS + /// + public long OrderId { get; init; } + + /// + /// شناسه تراکنش CMS + /// + public long TransactionId { get; init; } + + /// + /// کد پیگیری از زرین‌پال + /// + public string Authority { get; init; } = string.Empty; + + /// + /// وضعیت از زرین‌پال (OK یا NOK) + /// + public string Status { get; init; } = string.Empty; +} + +public class VerifyBasePackagePaymentResponseDto +{ + /// + /// آیا پرداخت موفق بود؟ + /// + public bool Success { get; set; } + + /// + /// پیام + /// + public string Message { get; set; } = string.Empty; + + /// + /// شناسه سفارش + /// + public long OrderId { get; set; } + + /// + /// شناسه تراکنش + /// + public long TransactionId { get; set; } + + /// + /// کد پیگیری بانکی + /// + public string? RefId { get; set; } + + /// + /// موجودی کیف پول بعد از شارژ + /// + public long WalletBalance { get; set; } + + /// + /// موجودی تخفیف بعد از شارژ + /// + public long DiscountBalance { get; set; } +} diff --git a/src/FrontOffice.BFF.Application/PackageCQ/Commands/VerifyBasePackagePayment/VerifyBasePackagePaymentCommandHandler.cs b/src/FrontOffice.BFF.Application/PackageCQ/Commands/VerifyBasePackagePayment/VerifyBasePackagePaymentCommandHandler.cs new file mode 100644 index 0000000..6e55751 --- /dev/null +++ b/src/FrontOffice.BFF.Application/PackageCQ/Commands/VerifyBasePackagePayment/VerifyBasePackagePaymentCommandHandler.cs @@ -0,0 +1,105 @@ +using CmsPackage = CMSMicroservice.Protobuf.Protos.Package; +using PYMSMicroservice.Protobuf.Protos.Transaction; + +namespace FrontOffice.BFF.Application.PackageCQ.Commands.VerifyBasePackagePayment; + +/// +/// Handler برای تأیید پرداخت پکیج پایه +/// 1. ابتدا PYMS فراخوانی می‌شود برای Verify پرداخت +/// 2. سپس CMS فراخوانی می‌شود برای تکمیل یا رد تراکنش +/// +public class VerifyBasePackagePaymentCommandHandler : IRequestHandler +{ + private readonly IApplicationContractContext _context; + + public VerifyBasePackagePaymentCommandHandler(IApplicationContractContext context) + { + _context = context; + } + + public async Task 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 + }; + } + + /// + /// اطلاع دادن به CMS که پرداخت ناموفق بود + /// + 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); + } +} diff --git a/src/FrontOffice.BFF.Application/PackageCQ/Commands/VerifyBasePackagePayment/VerifyBasePackagePaymentCommandValidator.cs b/src/FrontOffice.BFF.Application/PackageCQ/Commands/VerifyBasePackagePayment/VerifyBasePackagePaymentCommandValidator.cs new file mode 100644 index 0000000..61c9f4b --- /dev/null +++ b/src/FrontOffice.BFF.Application/PackageCQ/Commands/VerifyBasePackagePayment/VerifyBasePackagePaymentCommandValidator.cs @@ -0,0 +1,23 @@ +namespace FrontOffice.BFF.Application.PackageCQ.Commands.VerifyBasePackagePayment; + +public class VerifyBasePackagePaymentCommandValidator : AbstractValidator +{ + 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("وضعیت پرداخت الزامی است"); + } +} diff --git a/src/FrontOffice.BFF.Domain/FrontOffice.BFF.Domain.csproj b/src/FrontOffice.BFF.Domain/FrontOffice.BFF.Domain.csproj index 855385c..0bea543 100644 --- a/src/FrontOffice.BFF.Domain/FrontOffice.BFF.Domain.csproj +++ b/src/FrontOffice.BFF.Domain/FrontOffice.BFF.Domain.csproj @@ -7,7 +7,7 @@ - + @@ -16,4 +16,9 @@ + + + + + diff --git a/src/FrontOffice.BFF.WebApi/Common/Mappings/PackageProfile.cs b/src/FrontOffice.BFF.WebApi/Common/Mappings/PackageProfile.cs index c081cf7..346ab0a 100644 --- a/src/FrontOffice.BFF.WebApi/Common/Mappings/PackageProfile.cs +++ b/src/FrontOffice.BFF.WebApi/Common/Mappings/PackageProfile.cs @@ -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; public class PackageProfile : IRegister { void IRegister.Register(TypeAdapterConfig config) { - //config.NewConfig() - // .Map(dest => dest.FullName, src => $"{src.Firstname} {src.Lastname}"); + // InitiateBasePackagePayment + // UserId از CurrentUserService گرفته می‌شود، نه از Request + config.NewConfig() + .Map(dest => dest.CallbackUrl, src => src.CallbackUrl); + + config.NewConfig() + .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() + .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() + .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); } } diff --git a/src/FrontOffice.BFF.WebApi/Services/PackageService.cs b/src/FrontOffice.BFF.WebApi/Services/PackageService.cs index 5e08897..254e911 100644 --- a/src/FrontOffice.BFF.WebApi/Services/PackageService.cs +++ b/src/FrontOffice.BFF.WebApi/Services/PackageService.cs @@ -2,6 +2,9 @@ using FrontOffice.BFF.Package.Protobuf.Protos.Package; using FrontOffice.BFF.WebApi.Common.Services; using FrontOffice.BFF.Application.PackageCQ.Queries.GetPackage; 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; public class PackageService : PackageContract.PackageContractBase { @@ -19,4 +22,15 @@ public class PackageService : PackageContract.PackageContractBase { return await _dispatchRequestToCQRS.Handle(request, context); } + + // Base Package Payment (56M پکیج پایه) + public override async Task InitiateBasePackagePayment(InitiateBasePackagePaymentRequest request, ServerCallContext context) + { + return await _dispatchRequestToCQRS.Handle(request, context); + } + + public override async Task VerifyBasePackagePayment(VerifyBasePackagePaymentRequest request, ServerCallContext context) + { + return await _dispatchRequestToCQRS.Handle(request, context); + } } diff --git a/src/Protobufs/FrontOffice.BFF.Package.Protobuf/Protos/package.proto b/src/Protobufs/FrontOffice.BFF.Package.Protobuf/Protos/package.proto index 9d66e23..cc10ffd 100644 --- a/src/Protobufs/FrontOffice.BFF.Package.Protobuf/Protos/package.proto +++ b/src/Protobufs/FrontOffice.BFF.Package.Protobuf/Protos/package.proto @@ -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 { @@ -92,3 +107,43 @@ message DecimalValue 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; +}