diff --git a/modules/payment/src/Payment.Application.Contracts/PaymentRequests/IPaymentRequestAppService.cs b/modules/payment/src/Payment.Application.Contracts/PaymentRequests/IPaymentRequestAppService.cs index 85fc4a0..4cf5295 100644 --- a/modules/payment/src/Payment.Application.Contracts/PaymentRequests/IPaymentRequestAppService.cs +++ b/modules/payment/src/Payment.Application.Contracts/PaymentRequests/IPaymentRequestAppService.cs @@ -9,5 +9,9 @@ namespace Payment.PaymentRequests Task GetAsync(Guid id); Task CreateAsync(PaymentRequestCreationDto input); + + Task StartPaymentAsync(StartPaymentDto input); + + Task CompleteAsync(string token); } } \ No newline at end of file diff --git a/modules/payment/src/Payment.Application.Contracts/PaymentRequests/StartPaymentDto.cs b/modules/payment/src/Payment.Application.Contracts/PaymentRequests/StartPaymentDto.cs new file mode 100644 index 0000000..00b1cc3 --- /dev/null +++ b/modules/payment/src/Payment.Application.Contracts/PaymentRequests/StartPaymentDto.cs @@ -0,0 +1,17 @@ +using JetBrains.Annotations; +using System; +using System.ComponentModel.DataAnnotations; + +namespace Payment.PaymentRequests +{ + public class StartPaymentDto + { + public Guid PaymentRequestId { get; set; } + + [NotNull] + [Required] + public string ReturnUrl { get; set; } + + public string CancelUrl { get; set; } + } +} diff --git a/modules/payment/src/Payment.Application.Contracts/PaymentRequests/StartPaymentResultDto.cs b/modules/payment/src/Payment.Application.Contracts/PaymentRequests/StartPaymentResultDto.cs new file mode 100644 index 0000000..da2be6b --- /dev/null +++ b/modules/payment/src/Payment.Application.Contracts/PaymentRequests/StartPaymentResultDto.cs @@ -0,0 +1,7 @@ +namespace Payment.PaymentRequests +{ + public class StartPaymentResultDto + { + public string CheckoutLink { get; set; } + } +} diff --git a/modules/payment/src/Payment.Application/Payment.Application.csproj b/modules/payment/src/Payment.Application/Payment.Application.csproj index dc0b6f5..cef131a 100644 --- a/modules/payment/src/Payment.Application/Payment.Application.csproj +++ b/modules/payment/src/Payment.Application/Payment.Application.csproj @@ -14,4 +14,7 @@ + + + diff --git a/modules/payment/src/Payment.Application/PaymentApplicationModule.cs b/modules/payment/src/Payment.Application/PaymentApplicationModule.cs index 309ee80..b700bc9 100644 --- a/modules/payment/src/Payment.Application/PaymentApplicationModule.cs +++ b/modules/payment/src/Payment.Application/PaymentApplicationModule.cs @@ -2,6 +2,10 @@ using Volo.Abp.AutoMapper; using Volo.Abp.Modularity; using Volo.Abp.Application; +using Microsoft.Extensions.Options; +using Payment.PayPal; +using System; +using PayPalCheckoutSdk.Core; namespace Payment { @@ -20,6 +24,18 @@ namespace Payment { options.AddMaps(validate: true); }); + + context.Services.AddTransient(provider => + { + var options = provider.GetService>().Value; + + if (options.Environment.IsNullOrWhiteSpace() || options.Environment == PayPalConsts.Environment.Sandbox) + { + return new PayPalHttpClient(new SandboxEnvironment(options.ClientId, options.Secret)); + } + + return new PayPalHttpClient(new LiveEnvironment(options.ClientId, options.Secret)); + }); } } } diff --git a/modules/payment/src/Payment.Application/PaymentRequests/PaymentRequestAppService.cs b/modules/payment/src/Payment.Application/PaymentRequests/PaymentRequestAppService.cs index 8bf1759..bd5de42 100644 --- a/modules/payment/src/Payment.Application/PaymentRequests/PaymentRequestAppService.cs +++ b/modules/payment/src/Payment.Application/PaymentRequests/PaymentRequestAppService.cs @@ -1,5 +1,10 @@ using Microsoft.Extensions.Options; +using Payment.PayPal; +using PayPalCheckoutSdk.Core; +using PayPalCheckoutSdk.Orders; using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; namespace Payment.PaymentRequests @@ -8,13 +13,16 @@ namespace Payment.PaymentRequests { private readonly IPaymentRequestRepository _paymentRequestRepository; private readonly PaymentOptions _paymentOptions; + private readonly PayPalHttpClient _payPalHttpClient; public PaymentRequestAppService( IPaymentRequestRepository paymentRequestRepository, - IOptions paymentOptions) + IOptions paymentOptions, + PayPalHttpClient payPalHttpClient) { _paymentRequestRepository = paymentRequestRepository; _paymentOptions = paymentOptions.Value; + _payPalHttpClient = payPalHttpClient; } public async Task GetAsync(Guid id) @@ -39,5 +47,87 @@ namespace Payment.PaymentRequests return ObjectMapper.Map(paymentRequest); } + + public async Task StartPaymentAsync(StartPaymentDto input) + { + var paymentRequest = await _paymentRequestRepository.GetAsync(input.PaymentRequestId); + + var order = new OrderRequest + { + CheckoutPaymentIntent = "CAPTURE", + ApplicationContext = new ApplicationContext + { + //ReturnUrl = _paymentUrlBuilder.BuildReturnUrl(PaymentRequest.Id).AbsoluteUri, + //CancelUrl = _paymentUrlBuilder.BuildCheckoutUrl(PaymentRequest.Id).AbsoluteUri, + ReturnUrl = input.ReturnUrl, + CancelUrl = input.CancelUrl, + }, + PurchaseUnits = new List + { + new PurchaseUnitRequest + { + AmountWithBreakdown = new AmountWithBreakdown + { + AmountBreakdown = new AmountBreakdown + { + ItemTotal = new Money + { + CurrencyCode = paymentRequest.Currency, + Value = paymentRequest.Price.ToString(".00") + } + }, + CurrencyCode = paymentRequest.Currency, + Value = paymentRequest.Price.ToString(".00"), + }, + Items = new List + { + new Item + { + Quantity = 1.ToString(), + Name = paymentRequest.ProductName, + UnitAmount = new Money + { + CurrencyCode = paymentRequest.Currency, + Value = paymentRequest.Price.ToString(".00") + } + } + }, + ReferenceId = paymentRequest.Id.ToString() + } + } + }; + + var request = new OrdersCreateRequest(); + request.Prefer("return=representation"); + request.RequestBody(order); + + var result = (await _payPalHttpClient.Execute(request)).Result(); + + return new StartPaymentResultDto + { + CheckoutLink = result.Links.First(x => x.Rel == "approve").Href + }; + } + + public async Task CompleteAsync(string token) + { + var request = new OrdersCaptureRequest(token); + request.RequestBody(new OrderActionRequest()); + + var order = (await _payPalHttpClient.Execute(request)).Result(); + + var paymentRequestId = Guid.Parse(order.PurchaseUnits.First().ReferenceId); + + var paymentRequest = await _paymentRequestRepository.GetAsync(paymentRequestId); + + paymentRequest.State = order.Status == PayPalConsts.OrderStatus.Approved || order.Status == PayPalConsts.OrderStatus.Completed ? + PaymentRequestState.Completed : PaymentRequestState.Failed; + + paymentRequest.ExtraProperties.Add(PayPalConsts.OrderIdPropertyName, order.Id); + + await _paymentRequestRepository.UpdateAsync(paymentRequest); + + return ObjectMapper.Map(paymentRequest); + } } } \ No newline at end of file diff --git a/modules/payment/src/Payment.Domain.Shared/Localization/Payment/en.json b/modules/payment/src/Payment.Domain.Shared/Localization/Payment/en.json index 0838f51..5cf8ce3 100644 --- a/modules/payment/src/Payment.Domain.Shared/Localization/Payment/en.json +++ b/modules/payment/src/Payment.Domain.Shared/Localization/Payment/en.json @@ -1,7 +1,9 @@ { "culture": "en", "texts": { - "MyAccount": "My account", - "SamplePageMessage": "A sample page for the Payment module" + "MyAccount": "My account", + "SamplePageMessage": "A sample page for the Payment module", + "YourPaymentCompletedSuccessfully": "Your payment has been completed successfully!", + "YourPaymentFailed": "Your payment has been failed unfortunetely!" } } \ No newline at end of file diff --git a/modules/payment/src/Payment.Domain.Shared/PayPal/PayPalConsts.cs b/modules/payment/src/Payment.Domain.Shared/PayPal/PayPalConsts.cs new file mode 100644 index 0000000..51f4374 --- /dev/null +++ b/modules/payment/src/Payment.Domain.Shared/PayPal/PayPalConsts.cs @@ -0,0 +1,47 @@ +namespace Payment.PayPal +{ + public static class PayPalConsts + { + public const string OrderIdPropertyName = "OrderId"; + + public static class OrderStatus + { + /// + /// The order was created with the specified context. + /// + public const string Created = "CREATED"; + + /// + /// The order was saved and persisted. The order status continues to be in progress until a capture is made with final_capture = true for all purchase units within the order. + /// + public const string Saved = "SAVED"; + + /// + /// The customer approved the payment through the PayPal wallet or another form of guest or unbranded payment. For example, a card, bank account, or so on. + /// + public const string Approved = "APPROVED"; + + /// + /// All purchase units in the order are voided. + /// + public const string Voided = "VOIDED"; + + /// + /// The payment was authorized or the authorized payment was captured for the order. + /// + public const string Completed = "COMPLETED"; + + /// + /// The order requires an action from the payer (e.g. 3DS authentication). Redirect the payer to the "rel":"payer-action" HATEOAS link returned as part of the response prior to authorizing or capturing the order. + /// + public const string PlayerActionRequired = "PAYER_ACTION_REQUIRED"; + } + + public static class Environment + { + public const string Sandbox = "Sandbox"; + + public const string Live = "Live"; + } + } +} diff --git a/modules/payment/src/Payment.Domain/PayPal/PayPalConsts.cs b/modules/payment/src/Payment.Domain/PayPal/PayPalConsts.cs deleted file mode 100644 index 0544336..0000000 --- a/modules/payment/src/Payment.Domain/PayPal/PayPalConsts.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Payment.PayPal -{ - public static class PayPalConsts - { - public static class Environment - { - public const string Sandbox = "Sandbox"; - - public const string Live = "Live"; - } - } -} diff --git a/modules/payment/src/Payment.Web/Pages/Payment/Checkout.cshtml.cs b/modules/payment/src/Payment.Web/Pages/Payment/Checkout.cshtml.cs index 0468b07..4f9cb23 100644 --- a/modules/payment/src/Payment.Web/Pages/Payment/Checkout.cshtml.cs +++ b/modules/payment/src/Payment.Web/Pages/Payment/Checkout.cshtml.cs @@ -1,14 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; -using NUglify.Helpers; using Payment.PaymentRequests; using Payment.Web.PaymentRequest; -using PayPalCheckoutSdk.Core; -using PayPalCheckoutSdk.Orders; +using System; +using System.Threading.Tasks; namespace Payment.Web.Pages.Payment { @@ -21,16 +15,13 @@ namespace Payment.Web.Pages.Payment private readonly IPaymentRequestAppService _paymentRequestAppService; private readonly IPaymentUrlBuilder _paymentUrlBuilder; - private readonly PayPalHttpClient _payPalHttpClient; public CheckoutPageModel( IPaymentRequestAppService paymentRequestAppService, - IPaymentUrlBuilder paymentUrlBuilder, - PayPalHttpClient payPalHttpClient) + IPaymentUrlBuilder paymentUrlBuilder) { _paymentRequestAppService = paymentRequestAppService; this._paymentUrlBuilder = paymentUrlBuilder; - this._payPalHttpClient = payPalHttpClient; } public async Task OnGetAsync() @@ -40,58 +31,14 @@ namespace Payment.Web.Pages.Payment public async Task OnPostAsync() { - PaymentRequest = await _paymentRequestAppService.GetAsync(PaymentRequestId); - - var order = new OrderRequest + var result = await _paymentRequestAppService.StartPaymentAsync(new StartPaymentDto { - CheckoutPaymentIntent = "CAPTURE", - ApplicationContext = new ApplicationContext - { - ReturnUrl = _paymentUrlBuilder.BuildReturnUrl(PaymentRequest.Id).AbsoluteUri, - CancelUrl = _paymentUrlBuilder.BuildCheckoutUrl(PaymentRequest.Id).AbsoluteUri, - }, - PurchaseUnits = new List - { - new PurchaseUnitRequest - { - AmountWithBreakdown = new AmountWithBreakdown - { - AmountBreakdown = new AmountBreakdown - { - ItemTotal = new Money - { - CurrencyCode = PaymentRequest.Currency, - Value = PaymentRequest.Price.ToString(".00") - } - }, - CurrencyCode = PaymentRequest.Currency, - Value = PaymentRequest.Price.ToString(".00"), - }, - Items = new List - { - new Item - { - Quantity = 1.ToString(), - Name = PaymentRequest.ProductName, - UnitAmount = new Money - { - CurrencyCode = PaymentRequest.Currency, - Value = PaymentRequest.Price.ToString(".00") - } - } - }, - ReferenceId = PaymentRequest.Id.ToString() - } - } - }; - - var request = new OrdersCreateRequest(); - request.Prefer("return=representation"); - request.RequestBody(order); - - var result = (await _payPalHttpClient.Execute(request)).Result(); + PaymentRequestId = PaymentRequestId, + ReturnUrl = _paymentUrlBuilder.BuildReturnUrl(PaymentRequestId).AbsoluteUri, + CancelUrl = _paymentUrlBuilder.BuildCheckoutUrl(PaymentRequestId).AbsoluteUri + }); - Response.Redirect(result.Links.First(x => x.Rel == "approve").Href); + Response.Redirect(result.CheckoutLink); } } } \ No newline at end of file diff --git a/modules/payment/src/Payment.Web/Pages/Payment/CheckoutCallback.cshtml b/modules/payment/src/Payment.Web/Pages/Payment/CheckoutCallback.cshtml index cec29af..bf85e61 100644 --- a/modules/payment/src/Payment.Web/Pages/Payment/CheckoutCallback.cshtml +++ b/modules/payment/src/Payment.Web/Pages/Payment/CheckoutCallback.cshtml @@ -1,8 +1,35 @@ @page @using Payment.Web.Pages.Payment +@using Payment.PaymentRequests @inherits Payment.Web.Pages.PaymentPageBase @model Payment.Web.Pages.Payment.CheckoutCallbackModel @{ - PageLayout.Content.Title = L["Redirecting"]; + PageLayout.Content.Title = L["PaymentResult"] + L["Enum:PaymentRequestState:" + Model.PaymentRequest.State]; } -

@L["Redirecting"]

\ No newline at end of file + + + +

@L["PaymentResult"]

+
+ + + + @if (Model.PaymentRequest.State == PaymentRequestState.Completed) + { + +

+ @L["YourPaymentCompletedSuccessfully"] +
+ } + else + { + +

+ @L["YourPaymentFailed"] +
+ + + } + +
+
\ No newline at end of file diff --git a/modules/payment/src/Payment.Web/Pages/Payment/CheckoutCallback.cshtml.cs b/modules/payment/src/Payment.Web/Pages/Payment/CheckoutCallback.cshtml.cs index a5d2b66..0a1315c 100644 --- a/modules/payment/src/Payment.Web/Pages/Payment/CheckoutCallback.cshtml.cs +++ b/modules/payment/src/Payment.Web/Pages/Payment/CheckoutCallback.cshtml.cs @@ -1,6 +1,53 @@ -namespace Payment.Web.Pages.Payment +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Payment.PaymentRequests; +using System; +using System.Threading.Tasks; + +namespace Payment.Web.Pages.Payment { public class CheckoutCallbackModel : PaymentPageModel { + [BindProperty(SupportsGet = true)] + public string Token { get; set; } + + public PaymentRequestDto PaymentRequest { get; protected set; } + + private readonly IPaymentRequestAppService _paymentRequestAppService; + private readonly PaymentWebOptions _paymentWebOptions; + + public CheckoutCallbackModel( + IPaymentRequestAppService appService, + IOptions paymentWebOptions) + { + _paymentRequestAppService = appService; + _paymentWebOptions = paymentWebOptions.Value; + } + + public async Task OnGetAsync() + { + if (Token.IsNullOrWhiteSpace()) + { + return BadRequest(); + } + + PaymentRequest = await _paymentRequestAppService.CompleteAsync(Token); + + if (PaymentRequest.State == PaymentRequestState.Completed + && !_paymentWebOptions.PaymentSuccessfulCallbackUrl.IsNullOrWhiteSpace()) + { + var callbackUrl = _paymentWebOptions.PaymentSuccessfulCallbackUrl + "?paymentRequestId=" + PaymentRequest.Id; + Response.Redirect(callbackUrl); + } + + if (PaymentRequest.State == PaymentRequestState.Failed + && !_paymentWebOptions.PaymentFailureCallbackUrl.IsNullOrWhiteSpace()) + { + var callbackUrl = _paymentWebOptions.PaymentFailureCallbackUrl + "?paymentRequestId=" + PaymentRequest.Id; + Response.Redirect(callbackUrl); + } + + return Page(); + } } } diff --git a/modules/payment/src/Payment.Web/Payment.Web.csproj b/modules/payment/src/Payment.Web/Payment.Web.csproj index e136494..8989689 100644 --- a/modules/payment/src/Payment.Web/Payment.Web.csproj +++ b/modules/payment/src/Payment.Web/Payment.Web.csproj @@ -12,7 +12,6 @@ - diff --git a/modules/payment/src/Payment.Web/PaymentWebOptions.cs b/modules/payment/src/Payment.Web/PaymentWebOptions.cs new file mode 100644 index 0000000..4e6017e --- /dev/null +++ b/modules/payment/src/Payment.Web/PaymentWebOptions.cs @@ -0,0 +1,8 @@ +namespace Payment.Web +{ + public class PaymentWebOptions + { + public string PaymentSuccessfulCallbackUrl { get; set; } + public string PaymentFailureCallbackUrl { get; set; } + } +}