diff --git a/services/payment/src/EShopOnAbp.PaymentService.Application.Contracts/PaymentRequests/IPaymentRequestAppService.cs b/services/payment/src/EShopOnAbp.PaymentService.Application.Contracts/PaymentRequests/IPaymentRequestAppService.cs new file mode 100644 index 00000000..cf3e8f40 --- /dev/null +++ b/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 CreateAsync(PaymentRequestCreationDto input); + + Task StartAsync(PaymentRequestStartDto input); + + Task CompleteAsync(string token); + + Task HandleWebhookAsync(string payload); + } +} diff --git a/services/payment/src/EShopOnAbp.PaymentService.Application.Contracts/PaymentRequests/PaymentRequestCreationDto.cs b/services/payment/src/EShopOnAbp.PaymentService.Application.Contracts/PaymentRequests/PaymentRequestCreationDto.cs new file mode 100644 index 00000000..c5e0c328 --- /dev/null +++ b/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 Products { get; set; } + } +} \ No newline at end of file diff --git a/services/payment/src/EShopOnAbp.PaymentService.Application.Contracts/PaymentRequests/PaymentRequestDto.cs b/services/payment/src/EShopOnAbp.PaymentService.Application.Contracts/PaymentRequests/PaymentRequestDto.cs new file mode 100644 index 00000000..58c46b5a --- /dev/null +++ b/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 + { + [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 Products { get; set; } + } +} diff --git a/services/payment/src/EShopOnAbp.PaymentService.Application.Contracts/PaymentRequests/PaymentRequestProduct.cs b/services/payment/src/EShopOnAbp.PaymentService.Application.Contracts/PaymentRequests/PaymentRequestProduct.cs new file mode 100644 index 00000000..5af7d451 --- /dev/null +++ b/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 + { + 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; } + + } +} \ No newline at end of file diff --git a/services/payment/src/EShopOnAbp.PaymentService.Application.Contracts/PaymentRequests/PaymentRequestProductCreationDto.cs b/services/payment/src/EShopOnAbp.PaymentService.Application.Contracts/PaymentRequests/PaymentRequestProductCreationDto.cs new file mode 100644 index 00000000..6df979b4 --- /dev/null +++ b/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; } + } +} \ No newline at end of file diff --git a/services/payment/src/EShopOnAbp.PaymentService.Application.Contracts/PaymentRequests/PaymentRequestStartDto.cs b/services/payment/src/EShopOnAbp.PaymentService.Application.Contracts/PaymentRequests/PaymentRequestStartDto.cs new file mode 100644 index 00000000..44b39fdb --- /dev/null +++ b/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; } + } +} \ No newline at end of file diff --git a/services/payment/src/EShopOnAbp.PaymentService.Application.Contracts/PaymentRequests/PaymentRequestStartResultDto.cs b/services/payment/src/EShopOnAbp.PaymentService.Application.Contracts/PaymentRequests/PaymentRequestStartResultDto.cs new file mode 100644 index 00000000..faae6889 --- /dev/null +++ b/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; } + } +} \ No newline at end of file diff --git a/services/payment/src/EShopOnAbp.PaymentService.Application/PayPal/PayPalConsts.cs b/services/payment/src/EShopOnAbp.PaymentService.Application/PayPal/PayPalConsts.cs new file mode 100644 index 00000000..0e83292c --- /dev/null +++ b/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 + { + /// + /// 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/services/payment/src/EShopOnAbp.PaymentService.Application/PayPal/PayPalOptions.cs b/services/payment/src/EShopOnAbp.PaymentService.Application/PayPal/PayPalOptions.cs new file mode 100644 index 00000000..4c3d872b --- /dev/null +++ b/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; } + + /// + /// "Sandbox" or "Live". Default value is "Sandbox" + /// + public string Environment { get; set; } = PayPalConsts.Environment.Sandbox; + + public bool Recommended { get; set; } + + public List ExtraInfos { get; set; } = new(); + } +} diff --git a/services/payment/src/EShopOnAbp.PaymentService.Application/PaymentRequests/PaymentRequestAppService.cs b/services/payment/src/EShopOnAbp.PaymentService.Application/PaymentRequests/PaymentRequestAppService.cs new file mode 100644 index 00000000..4e4cfc9d --- /dev/null +++ b/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 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); + } + + public async Task 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 + { + 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(); + + return new PaymentRequestStartResultDto + { + 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 paymentRequest = await UpdatePaymentRequestStateAsync(order); + + return ObjectMapper.Map(paymentRequest); + } + + public async Task HandleWebhookAsync(string payload) + { + var jObject = JObject.Parse(payload); + + var order = jObject["resource"].ToObject(); + + var request = new OrdersGetRequest(order.Id); + + // Ensure order object comes from PayPal + var response = await PayPalHttpClient.Execute(request); + order = response.Result(); + + 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 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; + } + } +} diff --git a/services/payment/src/EShopOnAbp.PaymentService.Application/PaymentServiceApplicationModule.cs b/services/payment/src/EShopOnAbp.PaymentService.Application/PaymentServiceApplicationModule.cs index 6bbac237..024b3ba5 100644 --- a/services/payment/src/EShopOnAbp.PaymentService.Application/PaymentServiceApplicationModule.cs +++ b/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(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/services/payment/src/EShopOnAbp.PaymentService.Domain.Shared/PaymentRequests/PaymentRequestCompletedEto.cs b/services/payment/src/EShopOnAbp.PaymentService.Domain.Shared/PaymentRequests/PaymentRequestCompletedEto.cs new file mode 100644 index 00000000..7978cca4 --- /dev/null +++ b/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; } + } +} \ No newline at end of file diff --git a/services/payment/src/EShopOnAbp.PaymentService.Domain.Shared/PaymentRequests/PaymentRequestConsts.cs b/services/payment/src/EShopOnAbp.PaymentService.Domain.Shared/PaymentRequests/PaymentRequestConsts.cs new file mode 100644 index 00000000..f903f3a7 --- /dev/null +++ b/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; + } +} diff --git a/services/payment/src/EShopOnAbp.PaymentService.Domain.Shared/PaymentRequests/PaymentRequestFailedEto.cs b/services/payment/src/EShopOnAbp.PaymentService.Domain.Shared/PaymentRequests/PaymentRequestFailedEto.cs new file mode 100644 index 00000000..7723aff2 --- /dev/null +++ b/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; } + } +} \ No newline at end of file diff --git a/services/payment/src/EShopOnAbp.PaymentService.Domain.Shared/PaymentRequests/PaymentRequestState.cs b/services/payment/src/EShopOnAbp.PaymentService.Domain.Shared/PaymentRequests/PaymentRequestState.cs new file mode 100644 index 00000000..4709857d --- /dev/null +++ b/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 + } +} diff --git a/services/payment/src/EShopOnAbp.PaymentService.Domain/PaymentRequests/IPaymentRequestRepository.cs b/services/payment/src/EShopOnAbp.PaymentService.Domain/PaymentRequests/IPaymentRequestRepository.cs new file mode 100644 index 00000000..cbb7bee9 --- /dev/null +++ b/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 + { + } +} diff --git a/services/payment/src/EShopOnAbp.PaymentService.Domain/PaymentRequests/PaymentRequest.cs b/services/payment/src/EShopOnAbp.PaymentService.Domain/PaymentRequests/PaymentRequest.cs new file mode 100644 index 00000000..b06df627 --- /dev/null +++ b/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, 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 Products { get; } = new List(); + + 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 + }); + } + } +} diff --git a/services/payment/src/EShopOnAbp.PaymentService.Domain/PaymentRequests/PaymentRequestProduct.cs b/services/payment/src/EShopOnAbp.PaymentService.Domain/PaymentRequests/PaymentRequestProduct.cs new file mode 100644 index 00000000..024bfe7d --- /dev/null +++ b/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 + { + 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; + } + } +} diff --git a/services/payment/src/EShopOnAbp.PaymentService.EntityFrameworkCore/EntityFrameworkCore/IPaymentServiceDbContext.cs b/services/payment/src/EShopOnAbp.PaymentService.EntityFrameworkCore/EntityFrameworkCore/IPaymentServiceDbContext.cs index 44f56d19..d40bc42c 100644 --- a/services/payment/src/EShopOnAbp.PaymentService.EntityFrameworkCore/EntityFrameworkCore/IPaymentServiceDbContext.cs +++ b/services/payment/src/EShopOnAbp.PaymentService.EntityFrameworkCore/EntityFrameworkCore/IPaymentServiceDbContext.cs @@ -1,4 +1,6 @@ -using Volo.Abp.Data; +using EShopOnAbp.PaymentService.PaymentRequests; +using Microsoft.EntityFrameworkCore; +using Volo.Abp.Data; using Volo.Abp.EntityFrameworkCore; namespace EShopOnAbp.PaymentService.EntityFrameworkCore @@ -9,5 +11,7 @@ namespace EShopOnAbp.PaymentService.EntityFrameworkCore /* Add DbSet for each Aggregate Root here. Example: * DbSet Questions { get; } */ + + DbSet PaymentRequests { get; set; } } } \ No newline at end of file diff --git a/services/payment/src/EShopOnAbp.PaymentService.EntityFrameworkCore/EntityFrameworkCore/PaymentServiceDbContext.cs b/services/payment/src/EShopOnAbp.PaymentService.EntityFrameworkCore/EntityFrameworkCore/PaymentServiceDbContext.cs index cab4c878..9ab2ad65 100644 --- a/services/payment/src/EShopOnAbp.PaymentService.EntityFrameworkCore/EntityFrameworkCore/PaymentServiceDbContext.cs +++ b/services/payment/src/EShopOnAbp.PaymentService.EntityFrameworkCore/EntityFrameworkCore/PaymentServiceDbContext.cs @@ -1,4 +1,5 @@ -using Microsoft.EntityFrameworkCore; +using EShopOnAbp.PaymentService.PaymentRequests; +using Microsoft.EntityFrameworkCore; using Volo.Abp.Data; using Volo.Abp.EntityFrameworkCore; @@ -6,7 +7,8 @@ namespace EShopOnAbp.PaymentService.EntityFrameworkCore { [ConnectionStringName(PaymentServiceDbProperties.ConnectionStringName)] public class PaymentServiceDbContext : - AbpDbContext + AbpDbContext, + IPaymentServiceDbContext { /* Add DbSet properties for your Aggregate Roots / Entities here. */ @@ -27,6 +29,8 @@ namespace EShopOnAbp.PaymentService.EntityFrameworkCore } + public DbSet PaymentRequests { get; set; } + protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); @@ -36,13 +40,6 @@ namespace EShopOnAbp.PaymentService.EntityFrameworkCore builder.ConfigurePaymentService(); /* Configure your own tables/entities inside here */ - - //builder.Entity(b => - //{ - // b.ToTable(PaymentServiceConsts.DbTablePrefix + "YourEntities", PaymentServiceConsts.DbSchema); - // b.ConfigureByConvention(); //auto configure for the base class props - // //... - //}); } } } diff --git a/services/payment/src/EShopOnAbp.PaymentService.EntityFrameworkCore/EntityFrameworkCore/PaymentServiceDbContextModelCreatingExtensions.cs b/services/payment/src/EShopOnAbp.PaymentService.EntityFrameworkCore/EntityFrameworkCore/PaymentServiceDbContextModelCreatingExtensions.cs index 963dc838..bb7b7ed7 100644 --- a/services/payment/src/EShopOnAbp.PaymentService.EntityFrameworkCore/EntityFrameworkCore/PaymentServiceDbContextModelCreatingExtensions.cs +++ b/services/payment/src/EShopOnAbp.PaymentService.EntityFrameworkCore/EntityFrameworkCore/PaymentServiceDbContextModelCreatingExtensions.cs @@ -1,5 +1,7 @@ -using Microsoft.EntityFrameworkCore; +using EShopOnAbp.PaymentService.PaymentRequests; +using Microsoft.EntityFrameworkCore; using Volo.Abp; +using Volo.Abp.EntityFrameworkCore.Modeling; namespace EShopOnAbp.PaymentService.EntityFrameworkCore { @@ -10,25 +12,16 @@ namespace EShopOnAbp.PaymentService.EntityFrameworkCore { Check.NotNull(builder, nameof(builder)); - /* Configure all entities here. Example: - - builder.Entity(b => + builder.Entity(entity => { - //Configure table & schema name - b.ToTable(PaymentServiceDbProperties.DbTablePrefix + "Questions", PaymentServiceDbProperties.DbSchema); - - b.ConfigureByConvention(); - - //Properties - b.Property(q => q.Title).IsRequired().HasMaxLength(QuestionConsts.MaxTitleLength); + entity.ConfigureByConvention(); - //Relations - b.HasMany(question => question.Tags).WithOne().HasForeignKey(qt => qt.QuestionId); - - //Indexes - b.HasIndex(q => q.CreationTime); + entity + .Property(p => p.Currency) + .IsRequired() + .HasMaxLength(PaymentRequestConsts.MaxCurrencyLength); }); - */ + } } } diff --git a/services/payment/src/EShopOnAbp.PaymentService.EntityFrameworkCore/EntityFrameworkCore/PaymentServiceEntityFrameworkCoreModule.cs b/services/payment/src/EShopOnAbp.PaymentService.EntityFrameworkCore/EntityFrameworkCore/PaymentServiceEntityFrameworkCoreModule.cs index 3d0f2e05..b3b098fb 100644 --- a/services/payment/src/EShopOnAbp.PaymentService.EntityFrameworkCore/EntityFrameworkCore/PaymentServiceEntityFrameworkCoreModule.cs +++ b/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.PostgreSql; using Volo.Abp.Modularity; @@ -20,9 +21,7 @@ namespace EShopOnAbp.PaymentService.EntityFrameworkCore { context.Services.AddAbpDbContext(options => { - /* Remove "includeAllEntities: true" to create - * default repositories only for aggregate roots */ - options.AddDefaultRepositories(includeAllEntities: true); + options.AddRepository(); }); Configure(options => diff --git a/services/payment/src/EShopOnAbp.PaymentService.EntityFrameworkCore/Migrations/20211125131248_Initial.Designer.cs b/services/payment/src/EShopOnAbp.PaymentService.EntityFrameworkCore/Migrations/20211125131248_Initial.Designer.cs deleted file mode 100644 index 8d8560b6..00000000 --- a/services/payment/src/EShopOnAbp.PaymentService.EntityFrameworkCore/Migrations/20211125131248_Initial.Designer.cs +++ /dev/null @@ -1,30 +0,0 @@ -// -using EShopOnAbp.PaymentService.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using Volo.Abp.EntityFrameworkCore; - -#nullable disable - -namespace EShopOnAbp.PaymentService.Migrations -{ - [DbContext(typeof(PaymentServiceDbContext))] - [Migration("20211125131248_Initial")] - partial class Initial - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.PostgreSql) - .HasAnnotation("ProductVersion", "6.0.0") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); -#pragma warning restore 612, 618 - } - } -} diff --git a/services/payment/src/EShopOnAbp.PaymentService.EntityFrameworkCore/Migrations/20211125131248_Initial.cs b/services/payment/src/EShopOnAbp.PaymentService.EntityFrameworkCore/Migrations/20211125131248_Initial.cs deleted file mode 100644 index bfbb0f79..00000000 --- a/services/payment/src/EShopOnAbp.PaymentService.EntityFrameworkCore/Migrations/20211125131248_Initial.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace EShopOnAbp.PaymentService.Migrations -{ - public partial class Initial : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - - } - } -} diff --git a/services/payment/src/EShopOnAbp.PaymentService.EntityFrameworkCore/Migrations/20211129051351_Initial.Designer.cs b/services/payment/src/EShopOnAbp.PaymentService.EntityFrameworkCore/Migrations/20211129051351_Initial.Designer.cs new file mode 100644 index 00000000..48c21212 --- /dev/null +++ b/services/payment/src/EShopOnAbp.PaymentService.EntityFrameworkCore/Migrations/20211129051351_Initial.Designer.cs @@ -0,0 +1,123 @@ +// +using System; +using EShopOnAbp.PaymentService.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Volo.Abp.EntityFrameworkCore; + +#nullable disable + +namespace EShopOnAbp.PaymentService.Migrations +{ + [DbContext(typeof(PaymentServiceDbContext))] + [Migration("20211129051351_Initial")] + partial class Initial + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.PostgreSql) + .HasAnnotation("ProductVersion", "6.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("EShopOnAbp.PaymentService.PaymentRequests.PaymentRequest", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("BuyerId") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("character varying(3)"); + + b.Property("ExtraProperties") + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FailReason") + .HasColumnType("text"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("State") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("PaymentRequests"); + }); + + modelBuilder.Entity("EShopOnAbp.PaymentService.PaymentRequests.PaymentRequestProduct", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("PaymentRequestId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("ReferenceId") + .HasColumnType("text"); + + b.Property("TotalPrice") + .HasColumnType("numeric"); + + b.Property("UnitPrice") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("PaymentRequestId"); + + b.ToTable("PaymentRequestProduct"); + }); + + modelBuilder.Entity("EShopOnAbp.PaymentService.PaymentRequests.PaymentRequestProduct", b => + { + b.HasOne("EShopOnAbp.PaymentService.PaymentRequests.PaymentRequest", null) + .WithMany("Products") + .HasForeignKey("PaymentRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("EShopOnAbp.PaymentService.PaymentRequests.PaymentRequest", b => + { + b.Navigation("Products"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/services/payment/src/EShopOnAbp.PaymentService.EntityFrameworkCore/Migrations/20211129051351_Initial.cs b/services/payment/src/EShopOnAbp.PaymentService.EntityFrameworkCore/Migrations/20211129051351_Initial.cs new file mode 100644 index 00000000..81f10987 --- /dev/null +++ b/services/payment/src/EShopOnAbp.PaymentService.EntityFrameworkCore/Migrations/20211129051351_Initial.cs @@ -0,0 +1,70 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace EShopOnAbp.PaymentService.Migrations +{ + public partial class Initial : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "PaymentRequests", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Currency = table.Column(type: "character varying(3)", maxLength: 3, nullable: false), + BuyerId = table.Column(type: "text", nullable: true), + State = table.Column(type: "integer", nullable: false), + FailReason = table.Column(type: "text", nullable: true), + IsDeleted = table.Column(type: "boolean", nullable: false, defaultValue: false), + ExtraProperties = table.Column(type: "text", nullable: true), + ConcurrencyStamp = table.Column(type: "character varying(40)", maxLength: 40, nullable: true), + CreationTime = table.Column(type: "timestamp with time zone", nullable: false), + CreatorId = table.Column(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_PaymentRequests", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "PaymentRequestProduct", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + PaymentRequestId = table.Column(type: "uuid", nullable: false), + ReferenceId = table.Column(type: "text", nullable: true), + Name = table.Column(type: "text", nullable: true), + UnitPrice = table.Column(type: "numeric", nullable: false), + Quantity = table.Column(type: "integer", nullable: false), + TotalPrice = table.Column(type: "numeric", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_PaymentRequestProduct", x => x.Id); + table.ForeignKey( + name: "FK_PaymentRequestProduct_PaymentRequests_PaymentRequestId", + column: x => x.PaymentRequestId, + principalTable: "PaymentRequests", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_PaymentRequestProduct_PaymentRequestId", + table: "PaymentRequestProduct", + column: "PaymentRequestId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "PaymentRequestProduct"); + + migrationBuilder.DropTable( + name: "PaymentRequests"); + } + } +} diff --git a/services/payment/src/EShopOnAbp.PaymentService.EntityFrameworkCore/Migrations/PaymentServiceDbContextModelSnapshot.cs b/services/payment/src/EShopOnAbp.PaymentService.EntityFrameworkCore/Migrations/PaymentServiceDbContextModelSnapshot.cs index 6894351b..599c5fe9 100644 --- a/services/payment/src/EShopOnAbp.PaymentService.EntityFrameworkCore/Migrations/PaymentServiceDbContextModelSnapshot.cs +++ b/services/payment/src/EShopOnAbp.PaymentService.EntityFrameworkCore/Migrations/PaymentServiceDbContextModelSnapshot.cs @@ -1,4 +1,5 @@ // +using System; using EShopOnAbp.PaymentService.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -22,6 +23,98 @@ namespace EShopOnAbp.PaymentService.Migrations .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("EShopOnAbp.PaymentService.PaymentRequests.PaymentRequest", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("BuyerId") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("character varying(3)"); + + b.Property("ExtraProperties") + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FailReason") + .HasColumnType("text"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("State") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("PaymentRequests"); + }); + + modelBuilder.Entity("EShopOnAbp.PaymentService.PaymentRequests.PaymentRequestProduct", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("PaymentRequestId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("ReferenceId") + .HasColumnType("text"); + + b.Property("TotalPrice") + .HasColumnType("numeric"); + + b.Property("UnitPrice") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("PaymentRequestId"); + + b.ToTable("PaymentRequestProduct"); + }); + + modelBuilder.Entity("EShopOnAbp.PaymentService.PaymentRequests.PaymentRequestProduct", b => + { + b.HasOne("EShopOnAbp.PaymentService.PaymentRequests.PaymentRequest", null) + .WithMany("Products") + .HasForeignKey("PaymentRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("EShopOnAbp.PaymentService.PaymentRequests.PaymentRequest", b => + { + b.Navigation("Products"); + }); #pragma warning restore 612, 618 } } diff --git a/services/payment/src/EShopOnAbp.PaymentService.EntityFrameworkCore/PaymentRequests/EfCorePaymentRequestRepository.cs b/services/payment/src/EShopOnAbp.PaymentService.EntityFrameworkCore/PaymentRequests/EfCorePaymentRequestRepository.cs new file mode 100644 index 00000000..8c1acc3c --- /dev/null +++ b/services/payment/src/EShopOnAbp.PaymentService.EntityFrameworkCore/PaymentRequests/EfCorePaymentRequestRepository.cs @@ -0,0 +1,22 @@ +using EShopOnAbp.PaymentService.EntityFrameworkCore; +using System; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; + +namespace EShopOnAbp.PaymentService.PaymentRequests +{ + public class EfCorePaymentRequestRepository : + EfCoreRepository< + IPaymentServiceDbContext, + PaymentRequest, + Guid>, + IPaymentRequestRepository + { + public EfCorePaymentRequestRepository(IDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + + } + } +} diff --git a/services/payment/src/EShopOnAbp.PaymentService.HttpApi/Controllers/PaymentRequestController.cs b/services/payment/src/EShopOnAbp.PaymentService.HttpApi/Controllers/PaymentRequestController.cs new file mode 100644 index 00000000..7325ed28 --- /dev/null +++ b/services/payment/src/EShopOnAbp.PaymentService.HttpApi/Controllers/PaymentRequestController.cs @@ -0,0 +1,50 @@ +using EShopOnAbp.PaymentService.PaymentRequests; +using Microsoft.AspNetCore.Mvc; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Volo.Abp; + +namespace EShopOnAbp.PaymentService.Controllers +{ + [RemoteService(Name = PaymentServiceRemoteServiceConsts.RemoteServiceName)] + [Area("payment")] + [Route("api/payment/requests")] + public class PaymentRequestController : PaymentServiceController, IPaymentRequestAppService + { + protected IPaymentRequestAppService PaymentRequestAppService { get; } + + public PaymentRequestController(IPaymentRequestAppService paymentRequestAppService) + { + PaymentRequestAppService = paymentRequestAppService; + } + + [HttpPost("complete")] + public Task CompleteAsync(string token) + { + return PaymentRequestAppService.CompleteAsync(token); + } + + [HttpPost] + public Task CreateAsync(PaymentRequestCreationDto input) + { + return PaymentRequestAppService.CreateAsync(input); + } + + [HttpPost] + [Route("webhook")] + public async Task HandleWebhookAsync(string payload) + { + var bytes = await Request.Body.GetAllBytesAsync(); + payload = Encoding.UTF8.GetString(bytes); + + return await PaymentRequestAppService.HandleWebhookAsync(payload); + } + + [HttpPost("start")] + public Task StartAsync(PaymentRequestStartDto input) + { + return PaymentRequestAppService.StartAsync(input); + } + } +} diff --git a/services/payment/test/EShopOnAbp.PaymentService.EntityFrameworkCore.Tests/PaymentRequests/PaymentRequestRepository_Tests.cs b/services/payment/test/EShopOnAbp.PaymentService.EntityFrameworkCore.Tests/PaymentRequests/PaymentRequestRepository_Tests.cs new file mode 100644 index 00000000..406f5e10 --- /dev/null +++ b/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(); + } + + [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); + } + } +}