committed by
GitHub
30 changed files with 942 additions and 80 deletions
@ -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); |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -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; } |
|||
|
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
using System; |
|||
|
|||
namespace EShopOnAbp.PaymentService.PaymentRequests |
|||
{ |
|||
[Serializable] |
|||
public class PaymentRequestStartResultDto |
|||
{ |
|||
public string CheckoutLink { get; set; } |
|||
} |
|||
} |
|||
@ -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"; |
|||
} |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
namespace EShopOnAbp.PaymentService.PaymentRequests |
|||
{ |
|||
public static class PaymentRequestConsts |
|||
{ |
|||
public const int MaxCurrencyLength = 3; |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
namespace EShopOnAbp.PaymentService.PaymentRequests |
|||
{ |
|||
public enum PaymentRequestState |
|||
{ |
|||
Waiting = 0, |
|||
Completed, |
|||
Failed |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
using System; |
|||
using Volo.Abp.Domain.Repositories; |
|||
|
|||
namespace EShopOnAbp.PaymentService.PaymentRequests |
|||
{ |
|||
public interface IPaymentRequestRepository : IBasicRepository<PaymentRequest, Guid> |
|||
{ |
|||
} |
|||
} |
|||
@ -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 |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -1,30 +0,0 @@ |
|||
// <auto-generated />
|
|||
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
|
|||
} |
|||
} |
|||
} |
|||
@ -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) |
|||
{ |
|||
|
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,123 @@ |
|||
// <auto-generated />
|
|||
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<Guid>("Id") |
|||
.HasColumnType("uuid"); |
|||
|
|||
b.Property<string>("BuyerId") |
|||
.HasColumnType("text"); |
|||
|
|||
b.Property<string>("ConcurrencyStamp") |
|||
.IsConcurrencyToken() |
|||
.HasMaxLength(40) |
|||
.HasColumnType("character varying(40)") |
|||
.HasColumnName("ConcurrencyStamp"); |
|||
|
|||
b.Property<DateTime>("CreationTime") |
|||
.HasColumnType("timestamp with time zone") |
|||
.HasColumnName("CreationTime"); |
|||
|
|||
b.Property<Guid?>("CreatorId") |
|||
.HasColumnType("uuid") |
|||
.HasColumnName("CreatorId"); |
|||
|
|||
b.Property<string>("Currency") |
|||
.IsRequired() |
|||
.HasMaxLength(3) |
|||
.HasColumnType("character varying(3)"); |
|||
|
|||
b.Property<string>("ExtraProperties") |
|||
.HasColumnType("text") |
|||
.HasColumnName("ExtraProperties"); |
|||
|
|||
b.Property<string>("FailReason") |
|||
.HasColumnType("text"); |
|||
|
|||
b.Property<bool>("IsDeleted") |
|||
.ValueGeneratedOnAdd() |
|||
.HasColumnType("boolean") |
|||
.HasDefaultValue(false) |
|||
.HasColumnName("IsDeleted"); |
|||
|
|||
b.Property<int>("State") |
|||
.HasColumnType("integer"); |
|||
|
|||
b.HasKey("Id"); |
|||
|
|||
b.ToTable("PaymentRequests"); |
|||
}); |
|||
|
|||
modelBuilder.Entity("EShopOnAbp.PaymentService.PaymentRequests.PaymentRequestProduct", b => |
|||
{ |
|||
b.Property<Guid>("Id") |
|||
.HasColumnType("uuid"); |
|||
|
|||
b.Property<string>("Name") |
|||
.HasColumnType("text"); |
|||
|
|||
b.Property<Guid>("PaymentRequestId") |
|||
.HasColumnType("uuid"); |
|||
|
|||
b.Property<int>("Quantity") |
|||
.HasColumnType("integer"); |
|||
|
|||
b.Property<string>("ReferenceId") |
|||
.HasColumnType("text"); |
|||
|
|||
b.Property<decimal>("TotalPrice") |
|||
.HasColumnType("numeric"); |
|||
|
|||
b.Property<decimal>("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
|
|||
} |
|||
} |
|||
} |
|||
@ -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<Guid>(type: "uuid", nullable: false), |
|||
Currency = table.Column<string>(type: "character varying(3)", maxLength: 3, nullable: false), |
|||
BuyerId = table.Column<string>(type: "text", nullable: true), |
|||
State = table.Column<int>(type: "integer", nullable: false), |
|||
FailReason = table.Column<string>(type: "text", nullable: true), |
|||
IsDeleted = table.Column<bool>(type: "boolean", nullable: false, defaultValue: false), |
|||
ExtraProperties = table.Column<string>(type: "text", nullable: true), |
|||
ConcurrencyStamp = table.Column<string>(type: "character varying(40)", maxLength: 40, nullable: true), |
|||
CreationTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false), |
|||
CreatorId = table.Column<Guid>(type: "uuid", nullable: true) |
|||
}, |
|||
constraints: table => |
|||
{ |
|||
table.PrimaryKey("PK_PaymentRequests", x => x.Id); |
|||
}); |
|||
|
|||
migrationBuilder.CreateTable( |
|||
name: "PaymentRequestProduct", |
|||
columns: table => new |
|||
{ |
|||
Id = table.Column<Guid>(type: "uuid", nullable: false), |
|||
PaymentRequestId = table.Column<Guid>(type: "uuid", nullable: false), |
|||
ReferenceId = table.Column<string>(type: "text", nullable: true), |
|||
Name = table.Column<string>(type: "text", nullable: true), |
|||
UnitPrice = table.Column<decimal>(type: "numeric", nullable: false), |
|||
Quantity = table.Column<int>(type: "integer", nullable: false), |
|||
TotalPrice = table.Column<decimal>(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"); |
|||
} |
|||
} |
|||
} |
|||
@ -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<IPaymentServiceDbContext> dbContextProvider) |
|||
: base(dbContextProvider) |
|||
{ |
|||
|
|||
} |
|||
} |
|||
} |
|||
@ -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<PaymentRequestDto> CompleteAsync(string token) |
|||
{ |
|||
return PaymentRequestAppService.CompleteAsync(token); |
|||
} |
|||
|
|||
[HttpPost] |
|||
public Task<PaymentRequestDto> CreateAsync(PaymentRequestCreationDto input) |
|||
{ |
|||
return PaymentRequestAppService.CreateAsync(input); |
|||
} |
|||
|
|||
[HttpPost] |
|||
[Route("webhook")] |
|||
public async Task<bool> HandleWebhookAsync(string payload) |
|||
{ |
|||
var bytes = await Request.Body.GetAllBytesAsync(); |
|||
payload = Encoding.UTF8.GetString(bytes); |
|||
|
|||
return await PaymentRequestAppService.HandleWebhookAsync(payload); |
|||
} |
|||
|
|||
[HttpPost("start")] |
|||
public Task<PaymentRequestStartResultDto> StartAsync(PaymentRequestStartDto input) |
|||
{ |
|||
return PaymentRequestAppService.StartAsync(input); |
|||
} |
|||
} |
|||
} |
|||
@ -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…
Reference in new issue