Browse Source

Application Layer Implementation of PayPal in PaymentService

enisn/payment-service-with-mock
enisn 5 years ago
parent
commit
66da3bb212
  1. 16
      services/payment/src/EShopOnAbp.PaymentService.Application.Contracts/PaymentRequests/IPaymentRequestAppService.cs
  2. 18
      services/payment/src/EShopOnAbp.PaymentService.Application.Contracts/PaymentRequests/PaymentRequestCreationDto.cs
  3. 23
      services/payment/src/EShopOnAbp.PaymentService.Application.Contracts/PaymentRequests/PaymentRequestDto.cs
  4. 22
      services/payment/src/EShopOnAbp.PaymentService.Application.Contracts/PaymentRequests/PaymentRequestProduct.cs
  5. 18
      services/payment/src/EShopOnAbp.PaymentService.Application.Contracts/PaymentRequests/PaymentRequestProductCreationDto.cs
  6. 16
      services/payment/src/EShopOnAbp.PaymentService.Application.Contracts/PaymentRequests/PaymentRequestStartDto.cs
  7. 10
      services/payment/src/EShopOnAbp.PaymentService.Application.Contracts/PaymentRequests/PaymentRequestStartResultDto.cs
  8. 1
      services/payment/src/EShopOnAbp.PaymentService.Application/EShopOnAbp.PaymentService.Application.csproj
  9. 47
      services/payment/src/EShopOnAbp.PaymentService.Application/PayPal/PayPalConsts.cs
  10. 22
      services/payment/src/EShopOnAbp.PaymentService.Application/PayPal/PayPalOptions.cs
  11. 156
      services/payment/src/EShopOnAbp.PaymentService.Application/PaymentRequests/PaymentRequestAppService.cs
  12. 16
      services/payment/src/EShopOnAbp.PaymentService.Application/PaymentServiceApplicationModule.cs
  13. 15
      services/payment/src/EShopOnAbp.PaymentService.Domain.Shared/PaymentRequests/PaymentRequestCompletedEto.cs
  14. 7
      services/payment/src/EShopOnAbp.PaymentService.Domain.Shared/PaymentRequests/PaymentRequestConsts.cs
  15. 16
      services/payment/src/EShopOnAbp.PaymentService.Domain.Shared/PaymentRequests/PaymentRequestFailedEto.cs
  16. 9
      services/payment/src/EShopOnAbp.PaymentService.Domain.Shared/PaymentRequests/PaymentRequestState.cs
  17. 9
      services/payment/src/EShopOnAbp.PaymentService.Domain/PaymentRequests/IPaymentRequestRepository.cs
  18. 73
      services/payment/src/EShopOnAbp.PaymentService.Domain/PaymentRequests/PaymentRequest.cs
  19. 37
      services/payment/src/EShopOnAbp.PaymentService.Domain/PaymentRequests/PaymentRequestProduct.cs
  20. 7
      services/payment/src/EShopOnAbp.PaymentService.EntityFrameworkCore/EntityFrameworkCore/PaymentServiceEntityFrameworkCoreModule.cs
  21. 16
      services/payment/src/EShopOnAbp.PaymentService.EntityFrameworkCore/PaymentRequests/PaymentRequestEfCoreRepository.cs
  22. 30
      services/payment/test/EShopOnAbp.PaymentService.EntityFrameworkCore.Tests/PaymentRequests/PaymentRequestRepository_Tests.cs

16
services/payment/src/EShopOnAbp.PaymentService.Application.Contracts/PaymentRequests/IPaymentRequestAppService.cs

@ -0,0 +1,16 @@
using System.Threading.Tasks;
using Volo.Abp.Application.Services;
namespace EShopOnAbp.PaymentService.PaymentRequests
{
public interface IPaymentRequestAppService : IApplicationService
{
Task<PaymentRequestDto> CreateAsync(PaymentRequestCreationDto input);
Task<PaymentRequestStartResultDto> StartAsync(PaymentRequestStartDto input);
Task<PaymentRequestDto> CompleteAsync(string token);
Task<bool> HandleWebhookAsync(string payload);
}
}

18
services/payment/src/EShopOnAbp.PaymentService.Application.Contracts/PaymentRequests/PaymentRequestCreationDto.cs

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace EShopOnAbp.PaymentService.PaymentRequests
{
[Serializable]
public class PaymentRequestCreationDto
{
[Required]
[MaxLength(PaymentRequestConsts.MaxCurrencyLength)]
public string Currency { get; set; }
public string BuyerId { get; set; }
public List<PaymentRequestProductCreationDto> Products { get; set; }
}
}

23
services/payment/src/EShopOnAbp.PaymentService.Application.Contracts/PaymentRequests/PaymentRequestDto.cs

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Volo.Abp.Application.Dtos;
namespace EShopOnAbp.PaymentService.PaymentRequests
{
[Serializable]
public class PaymentRequestDto : CreationAuditedEntityDto<Guid>
{
[Required]
[MaxLength(PaymentRequestConsts.MaxCurrencyLength)]
public string Currency { get; set; }
public string BuyerId { get; set; }
public bool IsDeleted { get; set; }
public PaymentRequestState State { get; set; }
public List<PaymentRequestProductDto> Products { get; set; }
}
}

22
services/payment/src/EShopOnAbp.PaymentService.Application.Contracts/PaymentRequests/PaymentRequestProduct.cs

@ -0,0 +1,22 @@
using System;
using Volo.Abp.Application.Dtos;
namespace EShopOnAbp.PaymentService.PaymentRequests
{
[Serializable]
public class PaymentRequestProductDto : EntityDto<Guid>
{
public Guid PaymentRequestId { get; private set; }
public string ReferenceId { get; set; }
public string Name { get; set; }
public decimal UnitPrice { get; set; }
public int Quantity { get; set; }
public decimal TotalPrice { get; set; }
}
}

18
services/payment/src/EShopOnAbp.PaymentService.Application.Contracts/PaymentRequests/PaymentRequestProductCreationDto.cs

@ -0,0 +1,18 @@
using System;
namespace EShopOnAbp.PaymentService
{
[Serializable]
public class PaymentRequestProductCreationDto
{
public string ReferenceId { get; set; }
public string Name { get; set; }
public decimal UnitPrice { get; set; }
public int Quantity { get; set; }
public decimal TotalPrice { get; set; }
}
}

16
services/payment/src/EShopOnAbp.PaymentService.Application.Contracts/PaymentRequests/PaymentRequestStartDto.cs

@ -0,0 +1,16 @@
using System;
using System.ComponentModel.DataAnnotations;
namespace EShopOnAbp.PaymentService.PaymentRequests
{
[Serializable]
public class PaymentRequestStartDto
{
public Guid PaymentRequestId { get; set; }
[Required]
public string ReturnUrl { get; set; }
public string CancelUrl { get; set; }
}
}

10
services/payment/src/EShopOnAbp.PaymentService.Application.Contracts/PaymentRequests/PaymentRequestStartResultDto.cs

@ -0,0 +1,10 @@
using System;
namespace EShopOnAbp.PaymentService.PaymentRequests
{
[Serializable]
public class PaymentRequestStartResultDto
{
public string CheckoutLink { get; set; }
}
}

1
services/payment/src/EShopOnAbp.PaymentService.Application/EShopOnAbp.PaymentService.Application.csproj

@ -8,6 +8,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="PayPalCheckoutSdk" Version="1.0.4" />
<PackageReference Include="Volo.Abp.AutoMapper" Version="5.0.0-beta.1" />
<PackageReference Include="Volo.Abp.Ddd.Application" Version="5.0.0-beta.1" />
<ProjectReference Include="..\EShopOnAbp.PaymentService.Application.Contracts\EShopOnAbp.PaymentService.Application.Contracts.csproj" />

47
services/payment/src/EShopOnAbp.PaymentService.Application/PayPal/PayPalConsts.cs

@ -0,0 +1,47 @@
namespace EShopOnAbp.PaymentService.PayPal
{
public static class PayPalConsts
{
public const string OrderIdPropertyName = "OrderId";
public static class OrderStatus
{
/// <summary>
/// The order was created with the specified context.
/// </summary>
public const string Created = "CREATED";
/// <summary>
/// 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.
/// </summary>
public const string Saved = "SAVED";
/// <summary>
/// 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.
/// </summary>
public const string Approved = "APPROVED";
/// <summary>
/// All purchase units in the order are voided.
/// </summary>
public const string Voided = "VOIDED";
/// <summary>
/// The payment was authorized or the authorized payment was captured for the order.
/// </summary>
public const string Completed = "COMPLETED";
/// <summary>
/// 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.
/// </summary>
public const string PlayerActionRequired = "PAYER_ACTION_REQUIRED";
}
public static class Environment
{
public const string Sandbox = "Sandbox";
public const string Live = "Live";
}
}
}

22
services/payment/src/EShopOnAbp.PaymentService.Application/PayPal/PayPalOptions.cs

@ -0,0 +1,22 @@
using System.Collections.Generic;
namespace EShopOnAbp.PaymentService.PayPal
{
public class PayPalOptions
{
public string ClientId { get; set; }
public string Secret { get; set; }
public string Locale { get; set; }
/// <summary>
/// "Sandbox" or "Live". Default value is "Sandbox"
/// </summary>
public string Environment { get; set; } = PayPalConsts.Environment.Sandbox;
public bool Recommended { get; set; }
public List<string> ExtraInfos { get; set; } = new();
}
}

156
services/payment/src/EShopOnAbp.PaymentService.Application/PaymentRequests/PaymentRequestAppService.cs

@ -0,0 +1,156 @@
using EShopOnAbp.PaymentService.PayPal;
using Newtonsoft.Json.Linq;
using PayPalCheckoutSdk.Core;
using PayPalCheckoutSdk.Orders;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace EShopOnAbp.PaymentService.PaymentRequests
{
public class PaymentRequestAppService : PaymentServiceAppService, IPaymentRequestAppService
{
protected IPaymentRequestRepository PaymentRequestRepository { get; }
protected PayPalHttpClient PayPalHttpClient { get; }
public PaymentRequestAppService(
IPaymentRequestRepository paymentRequestRepository,
PayPalHttpClient payPalHttpClient)
{
PaymentRequestRepository = paymentRequestRepository;
PayPalHttpClient = payPalHttpClient;
}
public async Task<PaymentRequestDto> CreateAsync(PaymentRequestCreationDto input)
{
var paymentRequest = new PaymentRequest(GuidGenerator.Create(), input.Currency, input.BuyerId);
foreach (var paymentRequestProduct in input.Products
.Select(s => new PaymentRequestProduct(
paymentRequest.Id,
s.Name,
s.UnitPrice,
s.Quantity,
s.TotalPrice,
s.ReferenceId)))
{
paymentRequest.Products.Add(paymentRequestProduct);
}
return ObjectMapper.Map<PaymentRequest, PaymentRequestDto>(paymentRequest);
}
public async Task<PaymentRequestStartResultDto> StartAsync(PaymentRequestStartDto input)
{
var paymentRequest = await PaymentRequestRepository.GetAsync(input.PaymentRequestId);
var totalCheckoutPrice = paymentRequest.Products.Sum(s => s.TotalPrice);
var order = new OrderRequest
{
CheckoutPaymentIntent = "CAPTURE",
ApplicationContext = new ApplicationContext
{
ReturnUrl = input.ReturnUrl,
CancelUrl = input.CancelUrl,
},
PurchaseUnits = new List<PurchaseUnitRequest>
{
new PurchaseUnitRequest
{
AmountWithBreakdown = new AmountWithBreakdown
{
AmountBreakdown = new AmountBreakdown
{
ItemTotal = new Money
{
CurrencyCode = paymentRequest.Currency,
Value = totalCheckoutPrice.ToString(".00")
}
},
CurrencyCode = paymentRequest.Currency,
Value = totalCheckoutPrice.ToString(".00"),
},
Items = paymentRequest.Products.Select(p => new Item
{
Quantity = p.Quantity.ToString(),
Name = p.Name,
UnitAmount = new Money
{
CurrencyCode = paymentRequest.Currency,
Value = p.UnitPrice.ToString(".00")
}
}).ToList(),
ReferenceId = paymentRequest.Id.ToString()
}
}
};
var request = new OrdersCreateRequest();
request.Prefer("return=representation");
request.RequestBody(order);
var result = (await PayPalHttpClient.Execute(request)).Result<Order>();
return new PaymentRequestStartResultDto
{
CheckoutLink = result.Links.First(x => x.Rel == "approve").Href
};
}
public async Task<PaymentRequestDto> CompleteAsync(string token)
{
var request = new OrdersCaptureRequest(token);
request.RequestBody(new OrderActionRequest());
var order = (await PayPalHttpClient.Execute(request)).Result<Order>();
var paymentRequest = await UpdatePaymentRequestStateAsync(order);
return ObjectMapper.Map<PaymentRequest, PaymentRequestDto>(paymentRequest);
}
public async Task<bool> HandleWebhookAsync(string payload)
{
var jObject = JObject.Parse(payload);
var order = jObject["resource"].ToObject<Order>();
var request = new OrdersGetRequest(order.Id);
// Ensure order object comes from PayPal
var response = await PayPalHttpClient.Execute(request);
order = response.Result<Order>();
await UpdatePaymentRequestStateAsync(order);
// PayPal doesn't accept Http 204 (NoContent) result and tries to execute webhook again.
// So with following value, API returns Http 200 (OK) result.
return true;
}
private async Task<PaymentRequest> UpdatePaymentRequestStateAsync(Order order)
{
var paymentRequestId = Guid.Parse(order.PurchaseUnits.First().ReferenceId);
var paymentRequest = await PaymentRequestRepository.GetAsync(paymentRequestId);
if (order.Status == PayPalConsts.OrderStatus.Completed || order.Status == PayPalConsts.OrderStatus.Approved)
{
paymentRequest.SetAsCompleted();
}
else
{
paymentRequest.SetAsFailed(order.Status);
}
paymentRequest.ExtraProperties[PayPalConsts.OrderIdPropertyName] = order.Id;
paymentRequest.ExtraProperties[nameof(order.Status)] = order.Status;
await PaymentRequestRepository.UpdateAsync(paymentRequest);
return paymentRequest;
}
}
}

16
services/payment/src/EShopOnAbp.PaymentService.Application/PaymentServiceApplicationModule.cs

@ -2,6 +2,10 @@
using Volo.Abp.AutoMapper;
using Volo.Abp.Modularity;
using Volo.Abp.Application;
using Microsoft.Extensions.Options;
using EShopOnAbp.PaymentService.PayPal;
using PayPalCheckoutSdk.Core;
using System;
namespace EShopOnAbp.PaymentService
{
@ -20,6 +24,18 @@ namespace EShopOnAbp.PaymentService
{
options.AddMaps<PaymentServiceApplicationModule>(validate: true);
});
context.Services.AddTransient(provider =>
{
var options = provider.GetService<IOptions<PayPalOptions>>().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));
});
}
}
}

15
services/payment/src/EShopOnAbp.PaymentService.Domain.Shared/PaymentRequests/PaymentRequestCompletedEto.cs

@ -0,0 +1,15 @@
using System;
using Volo.Abp.Data;
using Volo.Abp.Domain.Entities.Events.Distributed;
using Volo.Abp.EventBus;
namespace EShopOnAbp.PaymentService
{
[Serializable]
[EventName("Payment.Completed")]
public class PaymentRequestCompletedEto: EtoBase, IHasExtraProperties
{
public Guid PaymentRequestId { get; set; }
public ExtraPropertyDictionary ExtraProperties { get; set; }
}
}

7
services/payment/src/EShopOnAbp.PaymentService.Domain.Shared/PaymentRequests/PaymentRequestConsts.cs

@ -0,0 +1,7 @@
namespace EShopOnAbp.PaymentService.PaymentRequests
{
public static class PaymentRequestConsts
{
public const int MaxCurrencyLength = 3;
}
}

16
services/payment/src/EShopOnAbp.PaymentService.Domain.Shared/PaymentRequests/PaymentRequestFailedEto.cs

@ -0,0 +1,16 @@
using System;
using Volo.Abp.Data;
using Volo.Abp.Domain.Entities.Events.Distributed;
using Volo.Abp.EventBus;
namespace EShopOnAbp.PaymentService
{
[Serializable]
[EventName("Payment.Completed")]
public class PaymentRequestFailedEto : EtoBase, IHasExtraProperties
{
public Guid PaymentRequestId { get; set; }
public string FailReason { get; set; }
public ExtraPropertyDictionary ExtraProperties { get; set; }
}
}

9
services/payment/src/EShopOnAbp.PaymentService.Domain.Shared/PaymentRequests/PaymentRequestState.cs

@ -0,0 +1,9 @@
namespace EShopOnAbp.PaymentService.PaymentRequests
{
public enum PaymentRequestState
{
Waiting = 0,
Completed,
Failed
}
}

9
services/payment/src/EShopOnAbp.PaymentService.Domain/PaymentRequests/IPaymentRequestRepository.cs

@ -0,0 +1,9 @@
using System;
using Volo.Abp.Domain.Repositories;
namespace EShopOnAbp.PaymentService.PaymentRequests
{
public interface IPaymentRequestRepository : IBasicRepository<PaymentRequest, Guid>
{
}
}

73
services/payment/src/EShopOnAbp.PaymentService.Domain/PaymentRequests/PaymentRequest.cs

@ -0,0 +1,73 @@
using JetBrains.Annotations;
using System;
using System.Collections.Generic;
using Volo.Abp;
using Volo.Abp.Domain.Entities.Auditing;
namespace EShopOnAbp.PaymentService.PaymentRequests
{
public class PaymentRequest : CreationAuditedAggregateRoot<Guid>, ISoftDelete
{
[NotNull]
public string Currency { get; protected set; }
[CanBeNull]
public string BuyerId { get; protected set; }
public PaymentRequestState State { get; protected set; }
[CanBeNull]
public string FailReason { get; protected set; }
public bool IsDeleted { get; set; }
public ICollection<PaymentRequestProduct> Products { get; } = new List<PaymentRequestProduct>();
private PaymentRequest()
{
}
public PaymentRequest(Guid id, [NotNull] string currency, [CanBeNull] string buyerId = null)
{
Id = id;
Currency = Check.NotNullOrWhiteSpace(currency, nameof(currency), maxLength: PaymentRequestConsts.MaxCurrencyLength);
BuyerId = buyerId;
}
public virtual void SetAsCompleted()
{
if (State == PaymentRequestState.Completed)
{
return;
}
State = PaymentRequestState.Completed;
FailReason = null;
AddDistributedEvent(new PaymentRequestCompletedEto
{
PaymentRequestId = Id,
ExtraProperties = ExtraProperties
});
}
public virtual void SetAsFailed(string failReason)
{
if (State != PaymentRequestState.Failed)
{
return;
}
State = PaymentRequestState.Failed;
FailReason = failReason;
AddDistributedEvent(new PaymentRequestFailedEto
{
PaymentRequestId = Id,
FailReason = failReason,
ExtraProperties = ExtraProperties
});
}
}
}

37
services/payment/src/EShopOnAbp.PaymentService.Domain/PaymentRequests/PaymentRequestProduct.cs

@ -0,0 +1,37 @@
using JetBrains.Annotations;
using System;
using Volo.Abp.Domain.Entities;
namespace EShopOnAbp.PaymentService.PaymentRequests
{
public class PaymentRequestProduct : Entity<Guid>
{
public Guid PaymentRequestId { get; private set; }
public string ReferenceId { get; set; }
public string Name { get; set; }
public decimal UnitPrice { get; set; }
public int Quantity { get; set; }
public decimal TotalPrice { get; set; }
public PaymentRequestProduct(
Guid paymentRequestId,
[NotNull] string name,
decimal unitPrice,
int quantity,
decimal totalPrice,
[CanBeNull] string referenceId = null)
{
PaymentRequestId = paymentRequestId;
Name = name;
UnitPrice = unitPrice;
Quantity = quantity;
TotalPrice = totalPrice;
ReferenceId = referenceId;
}
}
}

7
services/payment/src/EShopOnAbp.PaymentService.EntityFrameworkCore/EntityFrameworkCore/PaymentServiceEntityFrameworkCoreModule.cs

@ -1,4 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
using EShopOnAbp.PaymentService.PaymentRequests;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore.SqlServer;
using Volo.Abp.Modularity;
@ -20,9 +21,7 @@ namespace EShopOnAbp.PaymentService.EntityFrameworkCore
{
context.Services.AddAbpDbContext<PaymentServiceDbContext>(options =>
{
/* Remove "includeAllEntities: true" to create
* default repositories only for aggregate roots */
options.AddDefaultRepositories(includeAllEntities: true);
options.AddRepository<PaymentRequest, PaymentRequestEfCoreRepository>();
});
Configure<AbpDbContextOptions>(options =>

16
services/payment/src/EShopOnAbp.PaymentService.EntityFrameworkCore/PaymentRequests/PaymentRequestEfCoreRepository.cs

@ -0,0 +1,16 @@
using EShopOnAbp.PaymentService.EntityFrameworkCore;
using System;
using Volo.Abp.Domain.Repositories.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore;
namespace EShopOnAbp.PaymentService.PaymentRequests
{
public class PaymentRequestEfCoreRepository : EfCoreRepository<IPaymentServiceDbContext, PaymentRequest, Guid>, IPaymentRequestRepository
{
public PaymentRequestEfCoreRepository(IDbContextProvider<IPaymentServiceDbContext> dbContextProvider)
: base(dbContextProvider)
{
}
}
}

30
services/payment/test/EShopOnAbp.PaymentService.EntityFrameworkCore.Tests/PaymentRequests/PaymentRequestRepository_Tests.cs

@ -0,0 +1,30 @@
using EShopOnAbp.PaymentService.EntityFrameworkCore;
using Shouldly;
using System;
using System.Threading.Tasks;
using Xunit;
namespace EShopOnAbp.PaymentService.PaymentRequests
{
public class PaymentRequestRepository_Tests : PaymentServiceEntityFrameworkCoreTestBase
{
private readonly IPaymentRequestRepository _paymentRequestRepository;
public PaymentRequestRepository_Tests()
{
_paymentRequestRepository = GetRequiredService<IPaymentRequestRepository>();
}
[Fact]
public async Task Should_Insert_Payment_Request()
{
var id = Guid.NewGuid();
var paymentRequest = new PaymentRequest(id, "USD");
await _paymentRequestRepository.InsertAsync(paymentRequest, autoSave: true);
var inserted = await _paymentRequestRepository.GetAsync(id);
inserted.Id.ShouldNotBe(Guid.Empty);
}
}
}
Loading…
Cancel
Save