Browse Source

Introduce `PaymentAmount` to validate refund amounts

pull/268/head
gdlcf88 3 years ago
parent
commit
a21b3fdfa2
  1. 2
      common.props
  2. 2
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Application.Contracts/EasyAbp/EShop/Orders/Orders/Dtos/OrderDto.cs
  3. 2
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Application.Contracts/EasyAbp/EShop/Orders/Orders/Dtos/OrderExtraFeeDto.cs
  4. 2
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Application.Contracts/EasyAbp/EShop/Orders/Orders/Dtos/OrderLineDto.cs
  5. 5
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Localization/Orders/en.json
  6. 5
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Localization/Orders/zh-Hans.json
  7. 5
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Localization/Orders/zh-Hant.json
  8. 2
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Orders/IOrder.cs
  9. 2
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Orders/IOrderExtraFee.cs
  10. 2
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Orders/IOrderLine.cs
  11. 28
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Orders/OrderEto.cs
  12. 2
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Orders/OrderExtraFeeEto.cs
  13. 28
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Orders/OrderLineEto.cs
  14. 6
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp.EShop.Orders.Domain.csproj
  15. 10
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/IMoneyDistributor.cs
  16. 25
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/MoneyDistributionResult.cs
  17. 57
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/MoneyDistributor.cs
  18. 135
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/Order.cs
  19. 14
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/OrderDiscountDistributionModel.cs
  20. 67
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/OrderDiscountDistributor.cs
  21. 3
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/OrderDiscountResolver.cs
  22. 28
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/OrderExtraFee.cs
  23. 52
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/OrderLine.cs
  24. 10
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/OrderManager.cs
  25. 8
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/PaymentCanceledEventHandler.cs
  26. 3
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/PaymentCompletedEventHandler.cs
  27. 17
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/PaymentCreatedEventHandler.cs
  28. 4
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/ProductInventoryReductionEventHandler.cs
  29. 51
      modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Application.Tests/Orders/OrderAppServiceTests.cs
  30. 36
      modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Domain.Tests/Orders/InventoryReductionResultTests.cs
  31. 166
      modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Domain.Tests/Orders/OrderDomainTests.cs
  32. 2
      modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Application.Contracts/EasyAbp/EShop/Payments/Refunds/Dtos/CreateEShopRefundInput.cs
  33. 17
      modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Application/EasyAbp/EShop/Payments/Refunds/InvalidOrderExtraFeeRefundAmountException.cs
  34. 17
      modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Application/EasyAbp/EShop/Payments/Refunds/InvalidOrderLineRefundAmountException.cs
  35. 16
      modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Application/EasyAbp/EShop/Payments/Refunds/InvalidOrderRefundAmountException.cs
  36. 16
      modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Application/EasyAbp/EShop/Payments/Refunds/InvalidRefundAmountException.cs
  37. 26
      modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Application/EasyAbp/EShop/Payments/Refunds/RefundAppService.cs
  38. 4
      modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Domain.Shared/EasyAbp/EShop/Payments/Localization/Payments/en.json
  39. 4
      modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Domain.Shared/EasyAbp/EShop/Payments/Localization/Payments/zh-Hans.json
  40. 4
      modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Domain.Shared/EasyAbp/EShop/Payments/Localization/Payments/zh-Hant.json
  41. 4
      modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Domain.Shared/EasyAbp/EShop/Payments/PaymentsErrorCodes.cs
  42. 223
      modules/EasyAbp.EShop.Payments/test/EasyAbp.EShop.Payments.Application.Tests/Refunds/RefundAppServiceTests.cs
  43. 6553
      samples/EShopSample/aspnet-core/src/EShopSample.EntityFrameworkCore/Migrations/20230729082400_AddedPaymentAmount.Designer.cs
  44. 48
      samples/EShopSample/aspnet-core/src/EShopSample.EntityFrameworkCore/Migrations/20230729082400_AddedPaymentAmount.cs
  45. 15
      samples/EShopSample/aspnet-core/src/EShopSample.EntityFrameworkCore/Migrations/EShopSampleDbContextModelSnapshot.cs

2
common.props

@ -1,7 +1,7 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
<Version>4.3.0</Version> <Version>5.0.0-preview.1</Version>
<NoWarn>$(NoWarn);CS1591</NoWarn> <NoWarn>$(NoWarn);CS1591</NoWarn>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild> <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Authors>EasyAbp Team</Authors> <Authors>EasyAbp Team</Authors>

2
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Application.Contracts/EasyAbp/EShop/Orders/Orders/Dtos/OrderDto.cs

@ -35,6 +35,8 @@ namespace EasyAbp.EShop.Orders.Orders.Dtos
public DateTime? PaidTime { get; set; } public DateTime? PaidTime { get; set; }
public decimal? PaymentAmount { get; set; }
public DateTime? CompletionTime { get; set; } public DateTime? CompletionTime { get; set; }
public DateTime? CanceledTime { get; set; } public DateTime? CanceledTime { get; set; }

2
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Application.Contracts/EasyAbp/EShop/Orders/Orders/Dtos/OrderExtraFeeDto.cs

@ -15,5 +15,7 @@ namespace EasyAbp.EShop.Orders.Orders.Dtos
public decimal Fee { get; set; } public decimal Fee { get; set; }
public decimal RefundAmount { get; set; } public decimal RefundAmount { get; set; }
public decimal? PaymentAmount { get; set; }
} }
} }

2
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Application.Contracts/EasyAbp/EShop/Orders/Orders/Dtos/OrderLineDto.cs

@ -48,5 +48,7 @@ namespace EasyAbp.EShop.Orders.Orders.Dtos
public int RefundedQuantity { get; set; } public int RefundedQuantity { get; set; }
public decimal RefundAmount { get; set; } public decimal RefundAmount { get; set; }
public decimal? PaymentAmount { get; set; }
} }
} }

5
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Localization/Orders/en.json

@ -22,6 +22,7 @@
"OrderCustomerRemark": "Customer remark", "OrderCustomerRemark": "Customer remark",
"OrderStaffRemark": "Staff remark", "OrderStaffRemark": "Staff remark",
"OrderPaidTime": "Paid time", "OrderPaidTime": "Paid time",
"OrderPaymentAmount": "Payment amount",
"OrderCompletionTime": "Completion time", "OrderCompletionTime": "Completion time",
"OrderCanceledTime": "Canceled time", "OrderCanceledTime": "Canceled time",
"OrderReducedInventoryAfterPlacingTime": "Time of inventory reduced after placing", "OrderReducedInventoryAfterPlacingTime": "Time of inventory reduced after placing",
@ -45,6 +46,9 @@
"OrderLineTotalDiscount": "Total discount", "OrderLineTotalDiscount": "Total discount",
"OrderLineActualTotalPrice": "Actual total price", "OrderLineActualTotalPrice": "Actual total price",
"OrderLineQuantity": "Quantity", "OrderLineQuantity": "Quantity",
"OrderLineRefundedQuantity": "Refunded quantity",
"OrderLineRefundAmount": "Refund amount",
"OrderLinePaymentAmount": "Payment amount",
"OrderDiscount": "Order discount", "OrderDiscount": "Order discount",
"OrderDiscountOrderId": "Order ID", "OrderDiscountOrderId": "Order ID",
"OrderDiscountOrderLineId": "Order line ID", "OrderDiscountOrderLineId": "Order line ID",
@ -60,6 +64,7 @@
"OrderExtraFeeDisplayName": "Display name", "OrderExtraFeeDisplayName": "Display name",
"OrderExtraFeeFee": "Fee", "OrderExtraFeeFee": "Fee",
"OrderExtraFeeRefundAmount": "Refund amount", "OrderExtraFeeRefundAmount": "Refund amount",
"OrderExtraFeePaymentAmount": "Payment amount",
"EasyAbp.EShop.Orders:UnexpectedCurrency": "Only the specified currency {expectedCurrency} is allowed.", "EasyAbp.EShop.Orders:UnexpectedCurrency": "Only the specified currency {expectedCurrency} is allowed.",
"EasyAbp.EShop.Orders:OrderLineInvalidQuantity": "Invalid quantity {quantity} for product {productId} (SKU: {productSkuId}).", "EasyAbp.EShop.Orders:OrderLineInvalidQuantity": "Invalid quantity {quantity} for product {productId} (SKU: {productSkuId}).",
"EasyAbp.EShop.Orders:DiscountAmountOverflow": "The discount amount overflows.", "EasyAbp.EShop.Orders:DiscountAmountOverflow": "The discount amount overflows.",

5
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Localization/Orders/zh-Hans.json

@ -22,6 +22,7 @@
"OrderCustomerRemark": "客户备注", "OrderCustomerRemark": "客户备注",
"OrderStaffRemark": "员工备注", "OrderStaffRemark": "员工备注",
"OrderPaidTime": "支付时间", "OrderPaidTime": "支付时间",
"OrderPaymentAmount": "支付金额",
"OrderCompletionTime": "完成时间", "OrderCompletionTime": "完成时间",
"OrderCanceledTime": "取消时间", "OrderCanceledTime": "取消时间",
"OrderReducedInventoryAfterPlacingTime": "下单后减库存时间", "OrderReducedInventoryAfterPlacingTime": "下单后减库存时间",
@ -45,6 +46,9 @@
"OrderLineTotalDiscount": "总折扣", "OrderLineTotalDiscount": "总折扣",
"OrderLineActualTotalPrice": "折后总价", "OrderLineActualTotalPrice": "折后总价",
"OrderLineQuantity": "数量", "OrderLineQuantity": "数量",
"OrderLineRefundedQuantity": "已退款的数量",
"OrderLineRefundAmount": "已退款金额",
"OrderLinePaymentAmount": "支付金额",
"OrderDiscount": "订单折扣项", "OrderDiscount": "订单折扣项",
"OrderDiscountOrderId": "订单 ID", "OrderDiscountOrderId": "订单 ID",
"OrderDiscountOrderLineId": "订单项 ID", "OrderDiscountOrderLineId": "订单项 ID",
@ -60,6 +64,7 @@
"OrderExtraFeeDisplayName": "显示名称", "OrderExtraFeeDisplayName": "显示名称",
"OrderExtraFeeFee": "金额", "OrderExtraFeeFee": "金额",
"OrderExtraFeeRefundAmount": "已退款金额", "OrderExtraFeeRefundAmount": "已退款金额",
"OrderExtraFeePaymentAmount": "支付金额",
"EasyAbp.EShop.Orders:UnexpectedCurrency": "只允许指定的{expectedCurrency}货币", "EasyAbp.EShop.Orders:UnexpectedCurrency": "只允许指定的{expectedCurrency}货币",
"EasyAbp.EShop.Orders:OrderLineInvalidQuantity": "产品{productId}(SKU: {productSkuId})的{quantity}数量无效", "EasyAbp.EShop.Orders:OrderLineInvalidQuantity": "产品{productId}(SKU: {productSkuId})的{quantity}数量无效",
"EasyAbp.EShop.Orders:DiscountAmountOverflow": "折扣金额溢出", "EasyAbp.EShop.Orders:DiscountAmountOverflow": "折扣金额溢出",

5
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Localization/Orders/zh-Hant.json

@ -22,6 +22,7 @@
"OrderCustomerRemark": "客戶備註", "OrderCustomerRemark": "客戶備註",
"OrderStaffRemark": "員工備註", "OrderStaffRemark": "員工備註",
"OrderPaidTime": "支付時間", "OrderPaidTime": "支付時間",
"OrderPaymentAmount": "支付金額",
"OrderCompletionTime": "完成時間", "OrderCompletionTime": "完成時間",
"OrderCanceledTime": "取消時間", "OrderCanceledTime": "取消時間",
"OrderReducedInventoryAfterPlacingTime": "下單後減庫存時間", "OrderReducedInventoryAfterPlacingTime": "下單後減庫存時間",
@ -45,6 +46,9 @@
"OrderLineTotalDiscount": "總折扣", "OrderLineTotalDiscount": "總折扣",
"OrderLineActualTotalPrice": "折後總價", "OrderLineActualTotalPrice": "折後總價",
"OrderLineQuantity": "數量", "OrderLineQuantity": "數量",
"OrderLineRefundedQuantity": "已退款的數量",
"OrderLineRefundAmount": "已退款金額",
"OrderLinePaymentAmount": "支付金額",
"OrderDiscount": "訂單折扣項", "OrderDiscount": "訂單折扣項",
"OrderDiscountOrderId": "訂單 ID", "OrderDiscountOrderId": "訂單 ID",
"OrderDiscountOrderLineId": "訂單項 ID", "OrderDiscountOrderLineId": "訂單項 ID",
@ -60,6 +64,7 @@
"OrderExtraFeeDisplayName": "顯示名稱", "OrderExtraFeeDisplayName": "顯示名稱",
"OrderExtraFeeFee": "金額", "OrderExtraFeeFee": "金額",
"OrderExtraFeeRefundAmount": "已退款金額", "OrderExtraFeeRefundAmount": "已退款金額",
"OrderExtraFeePaymentAmount": "支付金額",
"EasyAbp.EShop.Orders:UnexpectedCurrency": "只允許指定的{expectedCurrency}貨幣", "EasyAbp.EShop.Orders:UnexpectedCurrency": "只允許指定的{expectedCurrency}貨幣",
"EasyAbp.EShop.Orders:OrderLineInvalidQuantity": "產品{productId}(SKU: {productSkuId})的{quantity}數量無效", "EasyAbp.EShop.Orders:OrderLineInvalidQuantity": "產品{productId}(SKU: {productSkuId})的{quantity}數量無效",
"EasyAbp.EShop.Orders:DiscountAmountOverflow": "折扣金額溢出", "EasyAbp.EShop.Orders:DiscountAmountOverflow": "折扣金額溢出",

2
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Orders/IOrder.cs

@ -43,6 +43,8 @@ namespace EasyAbp.EShop.Orders.Orders
DateTime? PaidTime { get; } DateTime? PaidTime { get; }
decimal? PaymentAmount { get; }
DateTime? CompletionTime { get; } DateTime? CompletionTime { get; }
DateTime? CanceledTime { get; } DateTime? CanceledTime { get; }

2
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Orders/IOrderExtraFee.cs

@ -19,5 +19,7 @@ namespace EasyAbp.EShop.Orders.Orders
decimal Fee { get; } decimal Fee { get; }
decimal RefundAmount { get; } decimal RefundAmount { get; }
decimal? PaymentAmount { get; }
} }
} }

2
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Orders/IOrderLine.cs

@ -62,5 +62,7 @@ namespace EasyAbp.EShop.Orders.Orders
int RefundedQuantity { get; } int RefundedQuantity { get; }
decimal RefundAmount { get; } decimal RefundAmount { get; }
decimal? PaymentAmount { get; }
} }
} }

28
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Orders/OrderEto.cs

@ -11,13 +11,13 @@ namespace EasyAbp.EShop.Orders.Orders
public class OrderEto : ExtensibleObject, IOrder, IFullAuditedObject, IMultiTenant public class OrderEto : ExtensibleObject, IOrder, IFullAuditedObject, IMultiTenant
{ {
public Guid Id { get; set; } public Guid Id { get; set; }
public Guid? TenantId { get; set; } public Guid? TenantId { get; set; }
public Guid StoreId { get; set; } public Guid StoreId { get; set; }
public string OrderNumber { get; set; } public string OrderNumber { get; set; }
public Guid CustomerUserId { get; set; } public Guid CustomerUserId { get; set; }
public OrderStatus OrderStatus { get; set; } public OrderStatus OrderStatus { get; set; }
@ -29,7 +29,7 @@ namespace EasyAbp.EShop.Orders.Orders
public decimal TotalDiscount { get; set; } public decimal TotalDiscount { get; set; }
public decimal TotalPrice { get; set; } public decimal TotalPrice { get; set; }
public decimal ActualTotalPrice { get; set; } public decimal ActualTotalPrice { get; set; }
public decimal RefundAmount { get; set; } public decimal RefundAmount { get; set; }
@ -37,21 +37,23 @@ namespace EasyAbp.EShop.Orders.Orders
public string CustomerRemark { get; set; } public string CustomerRemark { get; set; }
public string StaffRemark { get; set; } public string StaffRemark { get; set; }
public Guid? PaymentId { get; set; } public Guid? PaymentId { get; set; }
public DateTime? PaidTime { get; set; } public DateTime? PaidTime { get; set; }
public decimal? PaymentAmount { get; set; }
public DateTime? CompletionTime { get; set; } public DateTime? CompletionTime { get; set; }
public DateTime? CanceledTime { get; set; } public DateTime? CanceledTime { get; set; }
public string CancellationReason { get; set; } public string CancellationReason { get; set; }
public DateTime? ReducedInventoryAfterPlacingTime { get; set; } public DateTime? ReducedInventoryAfterPlacingTime { get; set; }
public DateTime? ReducedInventoryAfterPaymentTime { get; set; } public DateTime? ReducedInventoryAfterPaymentTime { get; set; }
public DateTime? PaymentExpiration { get; set; } public DateTime? PaymentExpiration { get; set; }
IEnumerable<IOrderLine> IOrder.OrderLines => OrderLines; IEnumerable<IOrderLine> IOrder.OrderLines => OrderLines;
@ -64,17 +66,17 @@ namespace EasyAbp.EShop.Orders.Orders
public List<OrderExtraFeeEto> OrderExtraFees { get; set; } public List<OrderExtraFeeEto> OrderExtraFees { get; set; }
public DateTime CreationTime { get; set; } public DateTime CreationTime { get; set; }
public Guid? CreatorId { get; set; } public Guid? CreatorId { get; set; }
public DateTime? LastModificationTime { get; set; } public DateTime? LastModificationTime { get; set; }
public Guid? LastModifierId { get; set; } public Guid? LastModifierId { get; set; }
public bool IsDeleted { get; set; } public bool IsDeleted { get; set; }
public DateTime? DeletionTime { get; set; } public DateTime? DeletionTime { get; set; }
public Guid? DeleterId { get; set; } public Guid? DeleterId { get; set; }
} }
} }

2
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Orders/OrderExtraFeeEto.cs

@ -16,5 +16,7 @@ namespace EasyAbp.EShop.Orders.Orders
public decimal Fee { get; set; } public decimal Fee { get; set; }
public decimal RefundAmount { get; set; } public decimal RefundAmount { get; set; }
public decimal? PaymentAmount { get; set; }
} }
} }

28
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Orders/OrderLineEto.cs

@ -10,17 +10,17 @@ namespace EasyAbp.EShop.Orders.Orders
public Guid Id { get; set; } public Guid Id { get; set; }
public Guid ProductId { get; set; } public Guid ProductId { get; set; }
public Guid ProductSkuId { get; set; } public Guid ProductSkuId { get; set; }
public Guid? ProductDetailId { get; set; } public Guid? ProductDetailId { get; set; }
public DateTime ProductModificationTime { get; set; } public DateTime ProductModificationTime { get; set; }
public DateTime? ProductDetailModificationTime { get; set; } public DateTime? ProductDetailModificationTime { get; set; }
public string ProductGroupName { get; set; } public string ProductGroupName { get; set; }
public string ProductGroupDisplayName { get; set; } public string ProductGroupDisplayName { get; set; }
public string ProductUniqueName { get; set; } public string ProductUniqueName { get; set; }
@ -32,23 +32,25 @@ namespace EasyAbp.EShop.Orders.Orders
public string SkuName { get; set; } public string SkuName { get; set; }
public string SkuDescription { get; set; } public string SkuDescription { get; set; }
public string MediaResources { get; set; } public string MediaResources { get; set; }
public string Currency { get; set; } public string Currency { get; set; }
public decimal UnitPrice { get; set; } public decimal UnitPrice { get; set; }
public decimal TotalPrice { get; set; } public decimal TotalPrice { get; set; }
public decimal TotalDiscount { get; set; } public decimal TotalDiscount { get; set; }
public decimal ActualTotalPrice { get; set; } public decimal ActualTotalPrice { get; set; }
public int Quantity { get; set; } public int Quantity { get; set; }
public int RefundedQuantity { get; set; } public int RefundedQuantity { get; set; }
public decimal RefundAmount { get; set; } public decimal RefundAmount { get; set; }
public decimal? PaymentAmount { get; set; }
} }
} }

6
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp.EShop.Orders.Domain.csproj

@ -7,6 +7,12 @@
<RootNamespace /> <RootNamespace />
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<InternalsVisibleTo Include="EasyAbp.EShop.Orders.Application.Tests" />
<InternalsVisibleTo Include="EasyAbp.EShop.Orders.Domain.Tests" />
<InternalsVisibleTo Include="EasyAbp.EShop.Plugins.Promotions.Application.Tests" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="NodaMoney" Version="$(NodaMoneyVersion)" /> <PackageReference Include="NodaMoney" Version="$(NodaMoneyVersion)" />
<PackageReference Include="Volo.Abp.AutoMapper" Version="$(AbpVersion)" /> <PackageReference Include="Volo.Abp.AutoMapper" Version="$(AbpVersion)" />

10
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/IMoneyDistributor.cs

@ -0,0 +1,10 @@
using System.Collections.Generic;
using System.Threading.Tasks;
namespace EasyAbp.EShop.Orders.Orders;
public interface IMoneyDistributor
{
Task<MoneyDistributionResult<TKey>> DistributeAsync<TKey>(string currency, Dictionary<TKey, decimal> currentAmounts,
decimal totalDistributionAmount);
}

25
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/MoneyDistributionResult.cs

@ -0,0 +1,25 @@
using System.Collections.Generic;
using JetBrains.Annotations;
using Volo.Abp;
namespace EasyAbp.EShop.Orders.Orders;
public class MoneyDistributionResult<TKey>
{
[NotNull]
public string Currency { get; }
public Dictionary<TKey, decimal> AmountsAfterDistribution { get; }
public Dictionary<TKey, decimal> Distributions { get; }
public MoneyDistributionResult(
[NotNull] string currency,
Dictionary<TKey, decimal> amountsAfterDistribution,
Dictionary<TKey, decimal> distributions)
{
Currency = Check.NotNullOrWhiteSpace(currency, nameof(currency));
AmountsAfterDistribution = Check.NotNull(amountsAfterDistribution, nameof(amountsAfterDistribution));
Distributions = Check.NotNull(distributions, nameof(distributions));
}
}

57
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/MoneyDistributor.cs

@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NodaMoney;
using Volo.Abp.DependencyInjection;
namespace EasyAbp.EShop.Orders.Orders;
public class MoneyDistributor : IMoneyDistributor, ITransientDependency
{
public virtual Task<MoneyDistributionResult<TKey>> DistributeAsync<TKey>(string currency,
Dictionary<TKey, decimal> currentAmounts, decimal totalDistributionAmount)
{
var distributions = new Dictionary<TKey, decimal>();
var originalAmountSum = currentAmounts.Sum(x => x.Value);
var remainingDistributionAmount = totalDistributionAmount;
foreach (var key in currentAmounts.Keys)
{
var calculatedDistributionAmount = new Money(
currentAmounts[key] / originalAmountSum *
totalDistributionAmount, currency, MidpointRounding.ToZero);
var distributionAmount = currentAmounts[key] + calculatedDistributionAmount.Amount < 0
? currentAmounts[key]
: calculatedDistributionAmount.Amount;
distributions[key] = distributionAmount;
currentAmounts[key] += distributionAmount;
remainingDistributionAmount -= distributionAmount;
}
foreach (var key in currentAmounts.OrderByDescending(x => x.Value).Select(x => x.Key))
{
if (remainingDistributionAmount == decimal.Zero)
{
break;
}
var distributionAmount = currentAmounts[key] + remainingDistributionAmount < 0
? currentAmounts[key]
: remainingDistributionAmount;
distributions[key] += distributionAmount;
currentAmounts[key] += distributionAmount;
remainingDistributionAmount -= distributionAmount;
}
if (remainingDistributionAmount != decimal.Zero)
{
throw new ApplicationException("The MoneyDistributor failed to distribute the remaining");
}
return Task.FromResult(new MoneyDistributionResult<TKey>(currency, currentAmounts, distributions));
}
}

135
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/Order.cs

@ -1,9 +1,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using EasyAbp.EShop.Products.Products; using System.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;
using NodaMoney;
using Volo.Abp.Domain.Entities.Auditing; using Volo.Abp.Domain.Entities.Auditing;
using Volo.Abp.MultiTenancy; using Volo.Abp.MultiTenancy;
@ -41,6 +40,8 @@ namespace EasyAbp.EShop.Orders.Orders
public virtual DateTime? PaidTime { get; protected set; } public virtual DateTime? PaidTime { get; protected set; }
public virtual decimal? PaymentAmount { get; protected set; }
public virtual DateTime? CompletionTime { get; protected set; } public virtual DateTime? CompletionTime { get; protected set; }
public virtual DateTime? CanceledTime { get; protected set; } public virtual DateTime? CanceledTime { get; protected set; }
@ -66,7 +67,7 @@ namespace EasyAbp.EShop.Orders.Orders
{ {
} }
public Order( internal Order(
Guid id, Guid id,
Guid? tenantId, Guid? tenantId,
Guid storeId, Guid storeId,
@ -99,55 +100,103 @@ namespace EasyAbp.EShop.Orders.Orders
OrderExtraFees = new List<OrderExtraFee>(); OrderExtraFees = new List<OrderExtraFee>();
} }
public void SetOrderNumber([NotNull] string orderNumber) internal void SetOrderNumber([NotNull] string orderNumber)
{ {
OrderNumber = orderNumber; OrderNumber = orderNumber;
} }
public void SetOrderLines(List<OrderLine> orderLines) internal void SetOrderLines(List<OrderLine> orderLines)
{ {
OrderLines = orderLines; OrderLines = orderLines;
} }
public void SetReducedInventoryAfterPlacingTime(DateTime? time) internal void SetReducedInventoryAfterPlacingTime(DateTime time)
{ {
if (ReducedInventoryAfterPlacingTime.HasValue)
{
throw new OrderIsInWrongStageException(Id);
}
ReducedInventoryAfterPlacingTime = time; ReducedInventoryAfterPlacingTime = time;
} }
public void SetReducedInventoryAfterPaymentTime(DateTime? time) internal void SetReducedInventoryAfterPaymentTime(DateTime time)
{ {
ReducedInventoryAfterPaymentTime = time; if (ReducedInventoryAfterPaymentTime.HasValue)
} {
throw new OrderIsInWrongStageException(Id);
}
public void SetPaymentExpiration(DateTime? paymentExpiration) ReducedInventoryAfterPaymentTime = time;
{
PaymentExpiration = paymentExpiration;
} }
public void SetPaymentId(Guid? paymentId) internal async Task StartPaymentAsync(Guid paymentId, decimal? paymentAmount, IMoneyDistributor distributor)
{ {
PaymentId = paymentId; PaymentId = paymentId;
PaymentAmount = paymentAmount;
if (paymentAmount is null)
{
// PaymentAmount is always null before EShop v5
return;
}
var currentAmounts = OrderLines.ToDictionary(x => (object)x, x => x.ActualTotalPrice)
.Union(OrderExtraFees.ToDictionary(x => (object)x, x => x.Fee))
.ToDictionary(x => x.Key, x => x.Value);
var distributionResult = await distributor.DistributeAsync(
Currency,
currentAmounts,
paymentAmount.Value);
foreach (var (key, amount) in distributionResult.Distributions)
{
switch (key)
{
case OrderLine orderLine:
orderLine.SetPaymentAmount(amount);
break;
case OrderExtraFee orderExtraFee:
orderExtraFee.SetPaymentAmount(amount);
break;
}
}
} }
public void SetPaidTime(DateTime? paidTime) internal void CancelPayment()
{ {
PaidTime = paidTime; PaymentId = null;
PaymentAmount = null;
foreach (var orderLine in OrderLines)
{
orderLine.SetPaymentAmount(null);
}
} }
public void SetOrderStatus(OrderStatus orderStatus) internal void SetPaid(DateTime paidTime)
{ {
OrderStatus = orderStatus; PaidTime = paidTime;
OrderStatus = OrderStatus.Processing;
} }
public void SetCompletionTime(DateTime? completionTime) internal void Complete(DateTime completionTime)
{ {
CompletionTime = completionTime; CompletionTime = completionTime;
OrderStatus = OrderStatus.Completed;
} }
public void SetCanceled(DateTime canceledTime, [CanBeNull] string cancellationReason) internal void SetCanceled(DateTime canceledTime, [CanBeNull] string cancellationReason, bool forceCancel)
{ {
CanceledTime = canceledTime; CanceledTime = canceledTime;
CancellationReason = cancellationReason; CancellationReason = cancellationReason;
OrderStatus = OrderStatus.Canceled;
}
public bool IsCanceled()
{
return CanceledTime.HasValue;
} }
public bool IsPaid() public bool IsPaid()
@ -155,9 +204,16 @@ namespace EasyAbp.EShop.Orders.Orders
return PaidTime.HasValue; return PaidTime.HasValue;
} }
public void RefundOrderLine(Guid orderLineId, int quantity, decimal amount) internal void RefundOrderLine(Guid orderLineId, int quantity, decimal amount)
{ {
if (amount <= decimal.Zero) if (!IsPaid())
{
throw new OrderIsInWrongStageException(Id);
}
// PaymentAmount is always null before EShop v5
var paymentAmount = PaymentAmount ?? ActualTotalPrice;
if (amount <= decimal.Zero || RefundAmount + amount > paymentAmount)
{ {
throw new InvalidRefundAmountException(amount); throw new InvalidRefundAmountException(amount);
} }
@ -169,9 +225,16 @@ namespace EasyAbp.EShop.Orders.Orders
RefundAmount += amount; RefundAmount += amount;
} }
public void RefundOrderExtraFee([NotNull] string extraFeeName, [CanBeNull] string extraFeeKey, decimal amount) internal void RefundOrderExtraFee([NotNull] string extraFeeName, [CanBeNull] string extraFeeKey, decimal amount)
{ {
if (amount <= decimal.Zero) if (!IsPaid())
{
throw new OrderIsInWrongStageException(Id);
}
// PaymentAmount is always null before EShop v5
var paymentAmount = PaymentAmount ?? ActualTotalPrice;
if (amount <= decimal.Zero || RefundAmount + amount > paymentAmount)
{ {
throw new InvalidRefundAmountException(amount); throw new InvalidRefundAmountException(amount);
} }
@ -185,14 +248,19 @@ namespace EasyAbp.EShop.Orders.Orders
public bool IsInPayment() public bool IsInPayment()
{ {
return !(!PaymentId.HasValue || PaidTime.HasValue); return PaymentId.HasValue && !PaidTime.HasValue;
} }
public void AddDiscounts(OrderDiscountDistributionModel model) internal void AddDiscounts(OrderDiscountDistributionModel model)
{ {
foreach (var (orderLineId, discountAmount) in model.Distributions) if (OrderStatus != OrderStatus.Pending)
{
throw new OrderIsInWrongStageException(Id);
}
foreach (var (o, discountAmount) in model.Distributions)
{ {
var orderLine = OrderLines.Single(x => x.Id == orderLineId); var orderLine = OrderLines.Single(x => x.Id == o.Id);
orderLine.AddDiscount(discountAmount); orderLine.AddDiscount(discountAmount);
@ -205,14 +273,14 @@ namespace EasyAbp.EShop.Orders.Orders
} }
if (OrderDiscounts.Any(x => if (OrderDiscounts.Any(x =>
x.OrderLineId == orderLineId && x.Name == model.DiscountInfoModel.Name && x.OrderLineId == orderLine.Id && x.Name == model.DiscountInfoModel.Name &&
x.Key == model.DiscountInfoModel.Key)) x.Key == model.DiscountInfoModel.Key))
{ {
throw new DuplicateOrderDiscountException(orderLineId, model.DiscountInfoModel.Name, throw new DuplicateOrderDiscountException(orderLine.Id, model.DiscountInfoModel.Name,
model.DiscountInfoModel.Key); model.DiscountInfoModel.Key);
} }
var orderDiscount = new OrderDiscount(Id, orderLineId, model.DiscountInfoModel.EffectGroup, var orderDiscount = new OrderDiscount(Id, orderLine.Id, model.DiscountInfoModel.EffectGroup,
model.DiscountInfoModel.Name, model.DiscountInfoModel.Key, model.DiscountInfoModel.DisplayName, model.DiscountInfoModel.Name, model.DiscountInfoModel.Key, model.DiscountInfoModel.DisplayName,
discountAmount); discountAmount);
@ -220,9 +288,14 @@ namespace EasyAbp.EShop.Orders.Orders
} }
} }
public void AddOrderExtraFee(decimal extraFee, [NotNull] string extraFeeName, [CanBeNull] string extraFeeKey, internal void AddOrderExtraFee(decimal extraFee, [NotNull] string extraFeeName, [CanBeNull] string extraFeeKey,
[CanBeNull] string extraFeeDisplayName) [CanBeNull] string extraFeeDisplayName)
{ {
if (OrderStatus != OrderStatus.Pending)
{
throw new OrderIsInWrongStageException(Id);
}
if (extraFee <= decimal.Zero) if (extraFee <= decimal.Zero)
{ {
throw new InvalidOrderExtraFeeException(extraFee); throw new InvalidOrderExtraFeeException(extraFee);

14
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/OrderDiscountDistributionModel.cs

@ -1,4 +1,3 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Volo.Abp; using Volo.Abp;
@ -9,18 +8,23 @@ public class OrderDiscountDistributionModel
{ {
public OrderDiscountInfoModel DiscountInfoModel { get; set; } public OrderDiscountInfoModel DiscountInfoModel { get; set; }
public Dictionary<IOrderLine, decimal> CurrentTotalPrices { get; set; }
/// <summary> /// <summary>
/// OrderLine to discount amount mapping. /// OrderLine to discount amount mapping.
/// </summary> /// </summary>
public Dictionary<Guid, decimal> Distributions { get; set; } public Dictionary<IOrderLine, decimal> Distributions { get; set; }
public OrderDiscountDistributionModel(OrderDiscountInfoModel discountInfoModel, public OrderDiscountDistributionModel(
Dictionary<Guid, decimal> distributions) OrderDiscountInfoModel discountInfoModel,
Dictionary<IOrderLine, decimal> currentTotalPrices,
Dictionary<IOrderLine, decimal> distributions)
{ {
DiscountInfoModel = Check.NotNull(discountInfoModel, nameof(discountInfoModel)); DiscountInfoModel = Check.NotNull(discountInfoModel, nameof(discountInfoModel));
CurrentTotalPrices = Check.NotNull(currentTotalPrices, nameof(currentTotalPrices));
Distributions = Check.NotNull(distributions, nameof(distributions)); Distributions = Check.NotNull(distributions, nameof(distributions));
if (DiscountInfoModel.AffectedOrderLineIds.Any(x => !Distributions.ContainsKey(x))) if (DiscountInfoModel.AffectedOrderLineIds.Any(x => Distributions.Keys.All(y => y.Id != x)))
{ {
throw new AbpException("The OrderDiscountDistributionModel got incorrect distributions."); throw new AbpException("The OrderDiscountDistributionModel got incorrect distributions.");
} }

67
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/OrderDiscountDistributor.cs

@ -1,8 +1,6 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using EasyAbp.EShop.Products.Products;
using NodaMoney; using NodaMoney;
using Volo.Abp.DependencyInjection; using Volo.Abp.DependencyInjection;
@ -10,58 +8,37 @@ namespace EasyAbp.EShop.Orders.Orders;
public class OrderDiscountDistributor : IOrderDiscountDistributor, ITransientDependency public class OrderDiscountDistributor : IOrderDiscountDistributor, ITransientDependency
{ {
public virtual Task<OrderDiscountDistributionModel> DistributeAsync(IOrder order, protected IMoneyDistributor MoneyDistributor { get; }
public OrderDiscountDistributor(IMoneyDistributor moneyDistributor)
{
MoneyDistributor = moneyDistributor;
}
public virtual async Task<OrderDiscountDistributionModel> DistributeAsync(IOrder order,
Dictionary<IOrderLine, decimal> currentTotalPrices, OrderDiscountInfoModel discount) Dictionary<IOrderLine, decimal> currentTotalPrices, OrderDiscountInfoModel discount)
{ {
var affectedOrderLines = discount.AffectedOrderLineIds var affectedCurrentTotalPrices = discount.AffectedOrderLineIds
.Select(orderLineId => order.OrderLines.Single(x => x.Id == orderLineId)) .Select(orderLineId => order.OrderLines.Single(x => x.Id == orderLineId))
.ToList(); .ToDictionary(x => x, x => currentTotalPrices[x]);
var affectedOrderLinesCurrentTotalPrice =
new Money(affectedOrderLines.Sum(x => currentTotalPrices[x]), order.Currency);
var totalDiscountAmount =
discount.CalculateDiscountAmount(affectedOrderLinesCurrentTotalPrice.Amount, order.Currency);
var distributions = new Dictionary<Guid, decimal>();
var remainingDiscountAmount = totalDiscountAmount;
foreach (var orderLine in affectedOrderLines) var affectedPriceSum = new Money(affectedCurrentTotalPrices.Sum(x => x.Value), order.Currency);
{
var calculatedDiscountAmount = new Money(
currentTotalPrices[orderLine] / affectedOrderLinesCurrentTotalPrice.Amount *
totalDiscountAmount, order.Currency, MidpointRounding.ToZero);
var discountAmount = calculatedDiscountAmount.Amount > currentTotalPrices[orderLine]
? currentTotalPrices[orderLine]
: calculatedDiscountAmount.Amount;
distributions[orderLine.Id] = discountAmount; var totalDiscountAmount = discount.CalculateDiscountAmount(affectedPriceSum.Amount, order.Currency);
currentTotalPrices[orderLine] -= discountAmount;
remainingDiscountAmount -= discountAmount;
}
foreach (var orderLine in affectedOrderLines.OrderByDescending(x => currentTotalPrices[x])) var result = await MoneyDistributor.DistributeAsync(
{ order.Currency, affectedCurrentTotalPrices, -totalDiscountAmount);
if (remainingDiscountAmount == decimal.Zero)
{
break;
}
var discountAmount = remainingDiscountAmount > currentTotalPrices[orderLine]
? currentTotalPrices[orderLine]
: remainingDiscountAmount;
distributions[orderLine.Id] += discountAmount;
currentTotalPrices[orderLine] -= discountAmount;
remainingDiscountAmount -= discountAmount;
}
if (remainingDiscountAmount != decimal.Zero) foreach (var affectedCurrentTotalPrice in affectedCurrentTotalPrices)
{ {
throw new ApplicationException("The OrderDiscountDistributor failed to distribute the remaining"); currentTotalPrices[affectedCurrentTotalPrice.Key] = affectedCurrentTotalPrice.Value;
} }
return Task.FromResult(new OrderDiscountDistributionModel(discount, distributions)); // revert to positive amount
return new OrderDiscountDistributionModel(
discount,
currentTotalPrices,
result.Distributions.ToDictionary(x => x.Key, x => -x.Value)
);
} }
} }

3
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/OrderDiscountResolver.cs

@ -91,7 +91,8 @@ public class OrderDiscountResolver : IOrderDiscountResolver, ITransientDependenc
var distributionResult = var distributionResult =
await OrderDiscountDistributor.DistributeAsync(order, currentTotalPrices, discount); await OrderDiscountDistributor.DistributeAsync(order, currentTotalPrices, discount);
distributionModels.Add(new OrderDiscountDistributionModel(discount, distributionResult.Distributions)); distributionModels.Add(new OrderDiscountDistributionModel(discount, currentTotalPrices,
distributionResult.Distributions));
} }
electionModel.Schemes.Add(new OrderDiscountsSchemeModel(distributionModels)); electionModel.Schemes.Add(new OrderDiscountsSchemeModel(distributionModels));

28
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/OrderExtraFee.cs

@ -7,20 +7,22 @@ namespace EasyAbp.EShop.Orders.Orders
public class OrderExtraFee : Entity, IOrderExtraFee public class OrderExtraFee : Entity, IOrderExtraFee
{ {
public virtual Guid OrderId { get; protected set; } public virtual Guid OrderId { get; protected set; }
[NotNull] [NotNull]
public virtual string Name { get; protected set; } public virtual string Name { get; protected set; }
[CanBeNull] [CanBeNull]
public virtual string Key { get; protected set; } public virtual string Key { get; protected set; }
[CanBeNull] [CanBeNull]
public virtual string DisplayName { get; protected set; } public virtual string DisplayName { get; protected set; }
public virtual decimal Fee { get; protected set; } public virtual decimal Fee { get; protected set; }
public virtual decimal RefundAmount { get; protected set; } public virtual decimal RefundAmount { get; protected set; }
public virtual decimal? PaymentAmount { get; protected set; }
protected OrderExtraFee() protected OrderExtraFee()
{ {
} }
@ -38,15 +40,27 @@ namespace EasyAbp.EShop.Orders.Orders
DisplayName = displayName; DisplayName = displayName;
Fee = fee; Fee = fee;
} }
internal void Refund(decimal amount) internal void Refund(decimal amount)
{ {
// PaymentAmount is always null before EShop v5
var paymentAmount = PaymentAmount ?? Fee;
if (amount <= decimal.Zero || RefundAmount + amount > paymentAmount)
{
throw new InvalidRefundAmountException(amount);
}
RefundAmount += amount; RefundAmount += amount;
} }
internal void SetPaymentAmount(decimal? paymentAmount)
{
PaymentAmount = paymentAmount;
}
public override object[] GetKeys() public override object[] GetKeys()
{ {
return new object[] {OrderId, Name, Key}; return new object[] { OrderId, Name, Key };
} }
} }
} }

52
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/OrderLine.cs

@ -10,47 +10,49 @@ namespace EasyAbp.EShop.Orders.Orders
public class OrderLine : FullAuditedEntity<Guid>, IOrderLine public class OrderLine : FullAuditedEntity<Guid>, IOrderLine
{ {
public virtual Guid ProductId { get; protected set; } public virtual Guid ProductId { get; protected set; }
public virtual Guid ProductSkuId { get; protected set; } public virtual Guid ProductSkuId { get; protected set; }
public virtual Guid? ProductDetailId { get; protected set; } public virtual Guid? ProductDetailId { get; protected set; }
public virtual DateTime ProductModificationTime { get; protected set; } public virtual DateTime ProductModificationTime { get; protected set; }
public virtual DateTime? ProductDetailModificationTime { get; protected set; } public virtual DateTime? ProductDetailModificationTime { get; protected set; }
public virtual string ProductGroupName { get; protected set; } public virtual string ProductGroupName { get; protected set; }
public virtual string ProductGroupDisplayName { get; protected set; } public virtual string ProductGroupDisplayName { get; protected set; }
public virtual string ProductUniqueName { get; protected set; } public virtual string ProductUniqueName { get; protected set; }
public virtual string ProductDisplayName { get; protected set; } public virtual string ProductDisplayName { get; protected set; }
public virtual InventoryStrategy? ProductInventoryStrategy { get; protected set; } public virtual InventoryStrategy? ProductInventoryStrategy { get; protected set; }
public virtual string SkuName { get; protected set; } public virtual string SkuName { get; protected set; }
public virtual string SkuDescription { get; protected set; } public virtual string SkuDescription { get; protected set; }
public virtual string MediaResources { get; protected set; } public virtual string MediaResources { get; protected set; }
public virtual string Currency { get; protected set; } public virtual string Currency { get; protected set; }
public virtual decimal UnitPrice { get; protected set; } public virtual decimal UnitPrice { get; protected set; }
public virtual decimal TotalPrice { get; protected set; } public virtual decimal TotalPrice { get; protected set; }
public virtual decimal TotalDiscount { get; protected set; } public virtual decimal TotalDiscount { get; protected set; }
public virtual decimal ActualTotalPrice { get; protected set; } public virtual decimal ActualTotalPrice { get; protected set; }
public virtual int Quantity { get; protected set; } public virtual int Quantity { get; protected set; }
public virtual int RefundedQuantity { get; protected set; } public virtual int RefundedQuantity { get; protected set; }
public virtual decimal RefundAmount { get; protected set; } public virtual decimal RefundAmount { get; protected set; }
public virtual decimal? PaymentAmount { get; protected set; }
public ExtraPropertyDictionary ExtraProperties { get; protected set; } public ExtraPropertyDictionary ExtraProperties { get; protected set; }
protected OrderLine() protected OrderLine()
@ -103,7 +105,7 @@ namespace EasyAbp.EShop.Orders.Orders
RefundedQuantity = 0; RefundedQuantity = 0;
RefundAmount = 0; RefundAmount = 0;
ExtraProperties = new ExtraPropertyDictionary(); ExtraProperties = new ExtraPropertyDictionary();
this.SetDefaultsForExtraProperties(ProxyHelper.UnProxy(this).GetType()); this.SetDefaultsForExtraProperties(ProxyHelper.UnProxy(this).GetType());
} }
@ -114,11 +116,18 @@ namespace EasyAbp.EShop.Orders.Orders
{ {
throw new InvalidRefundQuantityException(quantity); throw new InvalidRefundQuantityException(quantity);
} }
// PaymentAmount is always null before EShop v5
var paymentAmount = PaymentAmount ?? ActualTotalPrice;
if (amount <= decimal.Zero || RefundAmount + amount > paymentAmount)
{
throw new InvalidRefundAmountException(amount);
}
RefundedQuantity += quantity; RefundedQuantity += quantity;
RefundAmount += amount; RefundAmount += amount;
} }
internal void AddDiscount(decimal expectedDiscountAmount) internal void AddDiscount(decimal expectedDiscountAmount)
{ {
TotalDiscount += expectedDiscountAmount; TotalDiscount += expectedDiscountAmount;
@ -129,5 +138,10 @@ namespace EasyAbp.EShop.Orders.Orders
throw new DiscountAmountOverflowException(); throw new DiscountAmountOverflowException();
} }
} }
internal void SetPaymentAmount(decimal? paymentAmount)
{
PaymentAmount = paymentAmount;
}
} }
} }

10
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/OrderManager.cs

@ -34,7 +34,7 @@ namespace EasyAbp.EShop.Orders.Orders
[UnitOfWork] [UnitOfWork]
public virtual async Task<Order> CompleteAsync(Order order) public virtual async Task<Order> CompleteAsync(Order order)
{ {
if (order.CompletionTime.HasValue || !order.ReducedInventoryAfterPaymentTime.HasValue) if (order.CompletionTime.HasValue)
{ {
throw new OrderIsInWrongStageException(order.Id); throw new OrderIsInWrongStageException(order.Id);
} }
@ -46,8 +46,7 @@ namespace EasyAbp.EShop.Orders.Orders
await provider.CheckAsync(order); await provider.CheckAsync(order);
} }
order.SetCompletionTime(_clock.Now); order.Complete(_clock.Now);
order.SetOrderStatus(OrderStatus.Completed);
await _orderRepository.UpdateAsync(order, true); await _orderRepository.UpdateAsync(order, true);
@ -59,7 +58,7 @@ namespace EasyAbp.EShop.Orders.Orders
[UnitOfWork] [UnitOfWork]
public virtual async Task<Order> CancelAsync(Order order, string cancellationReason, bool forceCancel = false) public virtual async Task<Order> CancelAsync(Order order, string cancellationReason, bool forceCancel = false)
{ {
if (order.CanceledTime.HasValue) if (order.IsCanceled())
{ {
throw new OrderIsInWrongStageException(order.Id); throw new OrderIsInWrongStageException(order.Id);
} }
@ -69,8 +68,7 @@ namespace EasyAbp.EShop.Orders.Orders
throw new OrderIsInWrongStageException(order.Id); throw new OrderIsInWrongStageException(order.Id);
} }
order.SetCanceled(_clock.Now, cancellationReason); order.SetCanceled(_clock.Now, cancellationReason, forceCancel);
order.SetOrderStatus(OrderStatus.Canceled);
await _orderRepository.UpdateAsync(order, true); await _orderRepository.UpdateAsync(order, true);

8
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/PaymentCanceledEventHandler.cs

@ -29,16 +29,16 @@ namespace EasyAbp.EShop.Orders.Orders
using var currentTenant = _currentTenant.Change(eventData.TenantId); using var currentTenant = _currentTenant.Change(eventData.TenantId);
foreach (var paymentItem in eventData.Payment.PaymentItems.Where(item => foreach (var paymentItem in eventData.Payment.PaymentItems.Where(item =>
item.ItemType == PaymentsConsts.PaymentItemType)) item.ItemType == PaymentsConsts.PaymentItemType))
{ {
var order = await _orderRepository.GetAsync(Guid.Parse(paymentItem.ItemKey)); var order = await _orderRepository.GetAsync(Guid.Parse(paymentItem.ItemKey));
if (order.PaymentId != eventData.Payment.Id) if (order.PaymentId != eventData.Payment.Id || !order.IsInPayment() || order.IsCanceled())
{ {
continue; continue;
} }
order.SetPaymentId(null); order.CancelPayment();
// OrderAutoCancelOnUpdatedHandler may auto cancel the unpaid order. // OrderAutoCancelOnUpdatedHandler may auto cancel the unpaid order.
await _orderRepository.UpdateAsync(order, true); await _orderRepository.UpdateAsync(order, true);

3
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/PaymentCompletedEventHandler.cs

@ -66,8 +66,7 @@ namespace EasyAbp.EShop.Orders.Orders
throw new InvalidPaymentException(payment.Id, orderId); throw new InvalidPaymentException(payment.Id, orderId);
} }
order.SetPaidTime(_clock.Now); order.SetPaid(_clock.Now);
order.SetOrderStatus(OrderStatus.Processing);
await _orderRepository.UpdateAsync(order, true); await _orderRepository.UpdateAsync(order, true);

17
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/PaymentCreatedEventHandler.cs

@ -15,26 +15,30 @@ namespace EasyAbp.EShop.Orders.Orders
private readonly ICurrentTenant _currentTenant; private readonly ICurrentTenant _currentTenant;
private readonly IOrderPaymentChecker _orderPaymentChecker; private readonly IOrderPaymentChecker _orderPaymentChecker;
private readonly IOrderRepository _orderRepository; private readonly IOrderRepository _orderRepository;
private readonly IMoneyDistributor _moneyDistributor;
public PaymentCreatedEventHandler( public PaymentCreatedEventHandler(
ICurrentTenant currentTenant, ICurrentTenant currentTenant,
IOrderPaymentChecker orderPaymentChecker, IOrderPaymentChecker orderPaymentChecker,
IOrderRepository orderRepository) IOrderRepository orderRepository,
IMoneyDistributor moneyDistributor)
{ {
_currentTenant = currentTenant; _currentTenant = currentTenant;
_orderPaymentChecker = orderPaymentChecker; _orderPaymentChecker = orderPaymentChecker;
_orderRepository = orderRepository; _orderRepository = orderRepository;
_moneyDistributor = moneyDistributor;
} }
[UnitOfWork(true)] [UnitOfWork(true)]
public virtual async Task HandleEventAsync(EntityCreatedEto<EShopPaymentEto> eventData) public virtual async Task HandleEventAsync(EntityCreatedEto<EShopPaymentEto> eventData)
{ {
using var currentTenant = _currentTenant.Change(eventData.Entity.TenantId); using var currentTenant = _currentTenant.Change(eventData.Entity.TenantId);
foreach (var item in eventData.Entity.PaymentItems.Where(item => item.ItemType == PaymentsConsts.PaymentItemType)) foreach (var item in eventData.Entity.PaymentItems.Where(item =>
item.ItemType == PaymentsConsts.PaymentItemType))
{ {
var orderId = Guid.Parse(item.ItemKey); var orderId = Guid.Parse(item.ItemKey);
var order = await _orderRepository.GetAsync(orderId); var order = await _orderRepository.GetAsync(orderId);
if (order.PaymentId.HasValue || order.OrderStatus != OrderStatus.Pending) if (order.PaymentId.HasValue || order.OrderStatus != OrderStatus.Pending)
@ -42,14 +46,15 @@ namespace EasyAbp.EShop.Orders.Orders
// Todo: should cancel the payment? // Todo: should cancel the payment?
throw new OrderIsInWrongStageException(order.Id); throw new OrderIsInWrongStageException(order.Id);
} }
if (!await _orderPaymentChecker.IsValidPaymentAsync(order, eventData.Entity, item)) if (!await _orderPaymentChecker.IsValidPaymentAsync(order, eventData.Entity, item))
{ {
// Todo: should cancel the payment? // Todo: should cancel the payment?
throw new InvalidPaymentException(eventData.Entity.Id, orderId); throw new InvalidPaymentException(eventData.Entity.Id, orderId);
} }
order.SetPaymentId(eventData.Entity.Id); await order.StartPaymentAsync(
eventData.Entity.Id, eventData.Entity.ActualPaymentAmount, _moneyDistributor);
await _orderRepository.UpdateAsync(order, true); await _orderRepository.UpdateAsync(order, true);
} }

4
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/ProductInventoryReductionEventHandler.cs

@ -109,14 +109,14 @@ namespace EasyAbp.EShop.Orders.Orders
OrdersConsts.InventoryReductionFailedAutoCancellationReason, OrdersConsts.InventoryReductionFailedAutoCancellationReason,
OrdersConsts.InventoryReductionFailedAutoCancellationReason, OrdersConsts.InventoryReductionFailedAutoCancellationReason,
OrdersConsts.InventoryReductionFailedAutoCancellationReason); OrdersConsts.InventoryReductionFailedAutoCancellationReason);
eto.OrderLines.AddRange(order.OrderLines.Select(x => new OrderLineRefundInfoModel eto.OrderLines.AddRange(order.OrderLines.Select(x => new OrderLineRefundInfoModel
{ {
OrderLineId = x.Id, OrderLineId = x.Id,
Quantity = x.Quantity - x.RefundedQuantity, Quantity = x.Quantity - x.RefundedQuantity,
TotalAmount = x.ActualTotalPrice - x.RefundAmount TotalAmount = x.ActualTotalPrice - x.RefundAmount
})); }));
eto.OrderExtraFees.AddRange(order.OrderExtraFees.Select(x => new OrderExtraFeeRefundInfoModel eto.OrderExtraFees.AddRange(order.OrderExtraFees.Select(x => new OrderExtraFeeRefundInfoModel
{ {
Name = x.Name, Name = x.Name,

51
modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Application.Tests/Orders/OrderAppServiceTests.cs

@ -14,7 +14,6 @@ using NSubstitute;
using Shouldly; using Shouldly;
using Volo.Abp; using Volo.Abp;
using Volo.Abp.Domain.Repositories; using Volo.Abp.Domain.Repositories;
using Volo.Abp.Settings;
using Volo.Abp.Timing; using Volo.Abp.Timing;
using Xunit; using Xunit;
@ -24,6 +23,7 @@ namespace EasyAbp.EShop.Orders.Orders
{ {
private readonly IClock _clock; private readonly IClock _clock;
private readonly IOrderAppService _orderAppService; private readonly IOrderAppService _orderAppService;
private readonly IMoneyDistributor _moneyDistributor;
private ProductDto Product1 { get; set; } private ProductDto Product1 { get; set; }
@ -31,6 +31,7 @@ namespace EasyAbp.EShop.Orders.Orders
{ {
_clock = GetRequiredService<IClock>(); _clock = GetRequiredService<IClock>();
_orderAppService = GetRequiredService<IOrderAppService>(); _orderAppService = GetRequiredService<IOrderAppService>();
_moneyDistributor = GetRequiredService<IMoneyDistributor>();
} }
protected override void AfterAddApplication(IServiceCollection services) protected override void AfterAddApplication(IServiceCollection services)
@ -243,10 +244,10 @@ namespace EasyAbp.EShop.Orders.Orders
order.StaffRemark.ShouldBeNullOrEmpty(); order.StaffRemark.ShouldBeNullOrEmpty();
order.StoreId.ShouldBe(OrderTestData.Store1Id); order.StoreId.ShouldBe(OrderTestData.Store1Id);
order.TotalDiscount.ShouldBe(0m); order.TotalDiscount.ShouldBe(0m);
order.TotalPrice.ShouldBe(12m); order.TotalPrice.ShouldBe(order.ActualTotalPrice);
order.ActualTotalPrice.ShouldBe(12m); order.ActualTotalPrice.ShouldBe(order.ActualTotalPrice);
order.CustomerUserId.ShouldBe(Guid.Parse("2e701e62-0953-4dd3-910b-dc6cc93ccb0d")); order.CustomerUserId.ShouldBe(Guid.Parse("2e701e62-0953-4dd3-910b-dc6cc93ccb0d"));
order.ProductTotalPrice.ShouldBe(12m); order.ProductTotalPrice.ShouldBe(order.ActualTotalPrice);
order.ReducedInventoryAfterPaymentTime.ShouldBeNull(); order.ReducedInventoryAfterPaymentTime.ShouldBeNull();
order.ReducedInventoryAfterPlacingTime.ShouldNotBeNull(); order.ReducedInventoryAfterPlacingTime.ShouldNotBeNull();
order.OrderLines.Count.ShouldBe(2); order.OrderLines.Count.ShouldBe(2);
@ -288,7 +289,7 @@ namespace EasyAbp.EShop.Orders.Orders
order.OrderStatus.ShouldNotBe(OrderStatus.Completed); order.OrderStatus.ShouldNotBe(OrderStatus.Completed);
order.CompletionTime.ShouldBeNull(); order.CompletionTime.ShouldBeNull();
orderId = order.Id; orderId = order.Id;
order.SetPaidTime(DateTime.Now); order.SetPaid(DateTime.Now);
order.SetReducedInventoryAfterPaymentTime(DateTime.Now); order.SetReducedInventoryAfterPaymentTime(DateTime.Now);
db.SaveChanges(); db.SaveChanges();
}); });
@ -345,12 +346,12 @@ namespace EasyAbp.EShop.Orders.Orders
// Arrange // Arrange
await Order_Should_Be_Created(); await Order_Should_Be_Created();
Guid orderId = Guid.Empty; Guid orderId = Guid.Empty;
UsingDbContext(db => UsingDbContext(async db =>
{ {
var order = db.Orders.First(); var order = db.Orders.First();
orderId = order.Id; orderId = order.Id;
order.SetPaymentId(Guid.NewGuid()); await order.StartPaymentAsync(Guid.NewGuid(), order.ActualTotalPrice, _moneyDistributor);
db.SaveChanges(); await db.SaveChangesAsync();
}); });
// Act // Act
@ -360,8 +361,8 @@ namespace EasyAbp.EShop.Orders.Orders
{ {
var orderRepository = ServiceProvider.GetRequiredService<IOrderRepository>(); var orderRepository = ServiceProvider.GetRequiredService<IOrderRepository>();
var order = await orderRepository.GetAsync(orderId); var order = await orderRepository.GetAsync(orderId);
order.SetPaymentExpiration(now); typeof(Order).GetProperty(nameof(Order.PaymentExpiration))!.SetValue(order, now);
order.SetPaymentId(null); order.CancelPayment();
await orderRepository.UpdateAsync(order, true); await orderRepository.UpdateAsync(order, true);
}); });
@ -393,28 +394,28 @@ namespace EasyAbp.EShop.Orders.Orders
// Arrange // Arrange
await Order_Should_Be_Created(); await Order_Should_Be_Created();
Guid orderId = Guid.Empty; Guid orderId = Guid.Empty;
UsingDbContext(db => await WithUnitOfWorkAsync(async () =>
{ {
var order = db.Orders.First(); var orderRepository = ServiceProvider.GetRequiredService<IOrderRepository>();
orderId = order.Id; orderId = await (await orderRepository.GetQueryableAsync()).Select(x => x.Id).FirstAsync();
order.SetPaymentId(Guid.NewGuid()); var order = await orderRepository.GetAsync(orderId);
order.SetPaidTime(_clock.Now); await order.StartPaymentAsync(Guid.NewGuid(), order.ActualTotalPrice, _moneyDistributor);
order.SetOrderStatus(OrderStatus.Processing); order.SetPaid(_clock.Now);
db.SaveChanges(); await orderRepository.UpdateAsync(order, true);
}); });
// Act // Act
var now = _clock.Now; var now = _clock.Now;
UsingDbContext(async db => await WithUnitOfWorkAsync(async () =>
{ {
var orderRepository = ServiceProvider.GetRequiredService<IOrderRepository>(); var orderRepository = ServiceProvider.GetRequiredService<IOrderRepository>();
var order = await orderRepository.GetAsync(orderId); var order = await orderRepository.GetAsync(orderId);
order.SetPaymentExpiration(now); typeof(Order).GetProperty(nameof(Order.PaymentExpiration))!.SetValue(order, now);
await orderRepository.UpdateAsync(order, true); await orderRepository.UpdateAsync(order, true);
}); });
UsingDbContext(async db => await WithUnitOfWorkAsync(async () =>
{ {
var backgroundJob = ServiceProvider.GetRequiredService<UnpaidOrderAutoCancelJob>(); var backgroundJob = ServiceProvider.GetRequiredService<UnpaidOrderAutoCancelJob>();
await backgroundJob.ExecuteAsync(new UnpaidOrderAutoCancelArgs await backgroundJob.ExecuteAsync(new UnpaidOrderAutoCancelArgs
@ -465,7 +466,7 @@ namespace EasyAbp.EShop.Orders.Orders
{ {
var orderRepository = ServiceProvider.GetRequiredService<IOrderRepository>(); var orderRepository = ServiceProvider.GetRequiredService<IOrderRepository>();
var order = await orderRepository.GetAsync(orderId); var order = await orderRepository.GetAsync(orderId);
order.SetPaymentExpiration(now); typeof(Order).GetProperty(nameof(Order.PaymentExpiration))!.SetValue(order, now);
await orderRepository.UpdateAsync(order, true); await orderRepository.UpdateAsync(order, true);
}); });
@ -507,12 +508,12 @@ namespace EasyAbp.EShop.Orders.Orders
// Arrange // Arrange
await Order_Should_Be_Created(); await Order_Should_Be_Created();
Guid orderId = Guid.Empty; Guid orderId = Guid.Empty;
UsingDbContext(db => UsingDbContext(async db =>
{ {
var order = db.Orders.First(); var order = db.Orders.First();
orderId = order.Id; orderId = order.Id;
order.SetPaymentId(Guid.NewGuid()); await order.StartPaymentAsync(Guid.NewGuid(), order.ActualTotalPrice, _moneyDistributor);
db.SaveChanges(); await db.SaveChangesAsync();
}); });
// Act // Act
@ -522,7 +523,7 @@ namespace EasyAbp.EShop.Orders.Orders
{ {
var orderRepository = ServiceProvider.GetRequiredService<IOrderRepository>(); var orderRepository = ServiceProvider.GetRequiredService<IOrderRepository>();
var order = await orderRepository.GetAsync(orderId); var order = await orderRepository.GetAsync(orderId);
order.SetPaymentExpiration(now); typeof(Order).GetProperty(nameof(Order.PaymentExpiration))!.SetValue(order, now);
await orderRepository.UpdateAsync(order, true); await orderRepository.UpdateAsync(order, true);
}); });

36
modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Domain.Tests/Orders/InventoryReductionResultTests.cs

@ -67,11 +67,11 @@ public class InventoryReductionResultTests : OrdersDomainTestBase
{ {
typeof(Order).GetProperty(nameof(Order.CanceledTime))!.SetValue(Order1, null); typeof(Order).GetProperty(nameof(Order.CanceledTime))!.SetValue(Order1, null);
typeof(Order).GetProperty(nameof(Order.CancellationReason))!.SetValue(Order1, null); typeof(Order).GetProperty(nameof(Order.CancellationReason))!.SetValue(Order1, null);
Order1.SetReducedInventoryAfterPlacingTime(null); typeof(Order).GetProperty(nameof(Order.ReducedInventoryAfterPlacingTime))!.SetValue(Order1, null);
Order1.SetReducedInventoryAfterPaymentTime(null); typeof(Order).GetProperty(nameof(Order.ReducedInventoryAfterPaymentTime))!.SetValue(Order1, null);
Order1.SetOrderStatus(OrderStatus.Pending); typeof(Order).GetProperty(nameof(Order.OrderStatus))!.SetValue(Order1, OrderStatus.Pending);
Order1.SetPaymentId(null); typeof(Order).GetProperty(nameof(Order.PaidTime))!.SetValue(Order1, null);
Order1.SetPaidTime(null); Order1.CancelPayment();
var handler = ServiceProvider.GetRequiredService<ProductInventoryReductionEventHandler>(); var handler = ServiceProvider.GetRequiredService<ProductInventoryReductionEventHandler>();
@ -93,11 +93,11 @@ public class InventoryReductionResultTests : OrdersDomainTestBase
{ {
typeof(Order).GetProperty(nameof(Order.CanceledTime))!.SetValue(Order1, null); typeof(Order).GetProperty(nameof(Order.CanceledTime))!.SetValue(Order1, null);
typeof(Order).GetProperty(nameof(Order.CancellationReason))!.SetValue(Order1, null); typeof(Order).GetProperty(nameof(Order.CancellationReason))!.SetValue(Order1, null);
Order1.SetReducedInventoryAfterPlacingTime(null); typeof(Order).GetProperty(nameof(Order.ReducedInventoryAfterPlacingTime))!.SetValue(Order1, null);
Order1.SetReducedInventoryAfterPaymentTime(null); typeof(Order).GetProperty(nameof(Order.ReducedInventoryAfterPaymentTime))!.SetValue(Order1, null);
Order1.SetOrderStatus(OrderStatus.Pending); typeof(Order).GetProperty(nameof(Order.OrderStatus))!.SetValue(Order1, OrderStatus.Pending);
Order1.SetPaymentId(null); typeof(Order).GetProperty(nameof(Order.PaidTime))!.SetValue(Order1, null);
Order1.SetPaidTime(null); Order1.CancelPayment();
var handler = ServiceProvider.GetRequiredService<ProductInventoryReductionEventHandler>(); var handler = ServiceProvider.GetRequiredService<ProductInventoryReductionEventHandler>();
@ -120,10 +120,10 @@ public class InventoryReductionResultTests : OrdersDomainTestBase
typeof(Order).GetProperty(nameof(Order.CanceledTime))!.SetValue(Order1, null); typeof(Order).GetProperty(nameof(Order.CanceledTime))!.SetValue(Order1, null);
typeof(Order).GetProperty(nameof(Order.CancellationReason))!.SetValue(Order1, null); typeof(Order).GetProperty(nameof(Order.CancellationReason))!.SetValue(Order1, null);
Order1.SetReducedInventoryAfterPlacingTime(DateTime.Now); Order1.SetReducedInventoryAfterPlacingTime(DateTime.Now);
Order1.SetReducedInventoryAfterPaymentTime(null); typeof(Order).GetProperty(nameof(Order.ReducedInventoryAfterPaymentTime))!.SetValue(Order1, null);
Order1.SetOrderStatus(OrderStatus.Processing); await Order1.StartPaymentAsync(OrderTestData.Payment1Id, Order1.ActualTotalPrice,
Order1.SetPaymentId(OrderTestData.Payment1Id); GetRequiredService<IMoneyDistributor>());
Order1.SetPaidTime(DateTime.Now); Order1.SetPaid(DateTime.Now);
var handler = ServiceProvider.GetRequiredService<ProductInventoryReductionEventHandler>(); var handler = ServiceProvider.GetRequiredService<ProductInventoryReductionEventHandler>();
@ -169,10 +169,10 @@ public class InventoryReductionResultTests : OrdersDomainTestBase
typeof(Order).GetProperty(nameof(Order.CanceledTime))!.SetValue(Order1, null); typeof(Order).GetProperty(nameof(Order.CanceledTime))!.SetValue(Order1, null);
typeof(Order).GetProperty(nameof(Order.CancellationReason))!.SetValue(Order1, null); typeof(Order).GetProperty(nameof(Order.CancellationReason))!.SetValue(Order1, null);
Order1.SetReducedInventoryAfterPlacingTime(DateTime.Now); Order1.SetReducedInventoryAfterPlacingTime(DateTime.Now);
Order1.SetReducedInventoryAfterPaymentTime(null); typeof(Order).GetProperty(nameof(Order.ReducedInventoryAfterPaymentTime))!.SetValue(Order1, null);
Order1.SetOrderStatus(OrderStatus.Processing); await Order1.StartPaymentAsync(OrderTestData.Payment1Id, Order1.ActualTotalPrice,
Order1.SetPaymentId(OrderTestData.Payment1Id); GetRequiredService<IMoneyDistributor>());
Order1.SetPaidTime(DateTime.Now); Order1.SetPaid(DateTime.Now);
var handler = ServiceProvider.GetRequiredService<ProductInventoryReductionEventHandler>(); var handler = ServiceProvider.GetRequiredService<ProductInventoryReductionEventHandler>();

166
modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Domain.Tests/Orders/OrderDomainTests.cs

@ -16,10 +16,12 @@ namespace EasyAbp.EShop.Orders.Orders
private Order Order1 { get; set; } private Order Order1 { get; set; }
private readonly IOrderRepository _orderRepository; private readonly IOrderRepository _orderRepository;
private readonly IMoneyDistributor _moneyDistributor;
public OrderDomainTests() public OrderDomainTests()
{ {
_orderRepository = ServiceProvider.GetRequiredService<IOrderRepository>(); _orderRepository = ServiceProvider.GetRequiredService<IOrderRepository>();
_moneyDistributor = ServiceProvider.GetRequiredService<IMoneyDistributor>();
} }
protected override void AfterAddApplication(IServiceCollection services) protected override void AfterAddApplication(IServiceCollection services)
@ -33,8 +35,8 @@ namespace EasyAbp.EShop.Orders.Orders
"USD", "USD",
1m, 1m,
0m, 0m,
1.5m, 1.36m,
1.5m, 1.36m,
null, null,
null); null);
Order1.OrderLines.Add(new OrderLine( Order1.OrderLines.Add(new OrderLine(
@ -53,10 +55,10 @@ namespace EasyAbp.EShop.Orders.Orders
null, null,
null, null,
"USD", "USD",
0.5m, 0.53m,
1m, 1.06m,
0m, 0m,
1m, 1.06m,
2 2
)); ));
Order1.OrderExtraFees.Add(new OrderExtraFee( Order1.OrderExtraFees.Add(new OrderExtraFee(
@ -77,6 +79,10 @@ namespace EasyAbp.EShop.Orders.Orders
{ {
var handler = ServiceProvider.GetRequiredService<RefundCompletedEventHandler>(); var handler = ServiceProvider.GetRequiredService<RefundCompletedEventHandler>();
await Order1.StartPaymentAsync(OrderTestData.Payment1Id, Order1.ActualTotalPrice,
_moneyDistributor);
Order1.SetPaid(DateTime.Now);
await handler.HandleEventAsync(new EShopRefundCompletedEto await handler.HandleEventAsync(new EShopRefundCompletedEto
{ {
Refund = new EShopRefundEto Refund = new EShopRefundEto
@ -110,6 +116,7 @@ namespace EasyAbp.EShop.Orders.Orders
{ {
Name = "Name", Name = "Name",
Key = "Key", Key = "Key",
DisplayName = "DisplayName",
RefundAmount = 0.1m RefundAmount = 0.1m
} }
} }
@ -118,16 +125,14 @@ namespace EasyAbp.EShop.Orders.Orders
} }
}); });
Order1.SetPaymentId(OrderTestData.Payment1Id);
Order1.SetPaidTime(DateTime.Now);
Order1.RefundAmount.ShouldBe(0.3m); Order1.RefundAmount.ShouldBe(0.3m);
var orderLine1 = Order1.OrderLines.Single(x => x.Id == OrderTestData.OrderLine1Id); var orderLine1 = Order1.OrderLines.Single(x => x.Id == OrderTestData.OrderLine1Id);
orderLine1.RefundAmount.ShouldBe(0.2m); orderLine1.RefundAmount.ShouldBe(0.2m);
orderLine1.RefundedQuantity.ShouldBe(1); orderLine1.RefundedQuantity.ShouldBe(1);
var extraFee = Order1.OrderExtraFees.Single(x => x.Name == "Name" && x.Key == "Key"); var extraFee = Order1.OrderExtraFees.Single(
x => x.Name == "Name" && x.Key == "Key" && x.DisplayName == "DisplayName");
extraFee.RefundAmount.ShouldBe(0.1m); extraFee.RefundAmount.ShouldBe(0.1m);
} }
@ -136,8 +141,9 @@ namespace EasyAbp.EShop.Orders.Orders
{ {
var handler = ServiceProvider.GetRequiredService<RefundCompletedEventHandler>(); var handler = ServiceProvider.GetRequiredService<RefundCompletedEventHandler>();
Order1.SetPaymentId(OrderTestData.Payment1Id); await Order1.StartPaymentAsync(OrderTestData.Payment1Id, Order1.ActualTotalPrice,
Order1.SetPaidTime(DateTime.Now); _moneyDistributor);
Order1.SetPaid(DateTime.Now);
await Should.ThrowAsync<InvalidRefundAmountException>(async () => await Should.ThrowAsync<InvalidRefundAmountException>(async () =>
{ {
@ -175,13 +181,49 @@ namespace EasyAbp.EShop.Orders.Orders
}); });
} }
[Fact]
public async Task Should_Support_Different_PaymentAmounts()
{
// paymentAmount < actualTotalPrice
await Order1.StartPaymentAsync(OrderTestData.Payment1Id, 1.2m,
_moneyDistributor);
Order1.SetPaid(DateTime.Now);
Order1.ActualTotalPrice.ShouldBe(1.36m);
Order1.PaymentAmount.ShouldBe(1.2m);
Order1.OrderLines[0].PaymentAmount.ShouldBe(0.93m + 0.01m);
Order1.OrderExtraFees[0].PaymentAmount.ShouldBe(0.26m);
// paymentAmount == actualTotalPrice
await Order1.StartPaymentAsync(OrderTestData.Payment1Id, 1.36m,
_moneyDistributor);
Order1.SetPaid(DateTime.Now);
Order1.ActualTotalPrice.ShouldBe(1.36m);
Order1.PaymentAmount.ShouldBe(1.36m);
Order1.OrderLines[0].PaymentAmount.ShouldBe(1.06m);
Order1.OrderExtraFees[0].PaymentAmount.ShouldBe(0.3m);
// paymentAmount > actualTotalPrice
await Order1.StartPaymentAsync(OrderTestData.Payment1Id, 1.5m,
_moneyDistributor);
Order1.SetPaid(DateTime.Now);
Order1.ActualTotalPrice.ShouldBe(1.36m);
Order1.PaymentAmount.ShouldBe(1.5m);
Order1.OrderLines[0].PaymentAmount.ShouldBe(1.16m + 0.01m);
Order1.OrderExtraFees[0].PaymentAmount.ShouldBe(0.33m);
}
[Fact] [Fact]
public async Task Should_Avoid_Over_Quantity_Refund() public async Task Should_Avoid_Over_Quantity_Refund()
{ {
var handler = ServiceProvider.GetRequiredService<RefundCompletedEventHandler>(); var handler = ServiceProvider.GetRequiredService<RefundCompletedEventHandler>();
Order1.SetPaymentId(OrderTestData.Payment1Id); await Order1.StartPaymentAsync(OrderTestData.Payment1Id, Order1.ActualTotalPrice,
Order1.SetPaidTime(DateTime.Now); _moneyDistributor);
Order1.SetPaid(DateTime.Now);
await Should.ThrowAsync<InvalidRefundQuantityException>(async () => await Should.ThrowAsync<InvalidRefundQuantityException>(async () =>
{ {
@ -193,14 +235,14 @@ namespace EasyAbp.EShop.Orders.Orders
TenantId = null, TenantId = null,
PaymentId = OrderTestData.Payment1Id, PaymentId = OrderTestData.Payment1Id,
Currency = "USD", Currency = "USD",
RefundAmount = 0.3m, RefundAmount = 0.2m,
RefundItems = new List<EShopRefundItemEto> RefundItems = new List<EShopRefundItemEto>
{ {
new() new()
{ {
Id = Guid.NewGuid(), Id = Guid.NewGuid(),
PaymentItemId = Guid.NewGuid(), PaymentItemId = Guid.NewGuid(),
RefundAmount = 0.3m, RefundAmount = 0.2m,
StoreId = OrderTestData.Store1Id, StoreId = OrderTestData.Store1Id,
OrderId = OrderTestData.Order1Id, OrderId = OrderTestData.Order1Id,
OrderLines = new List<RefundItemOrderLineEto> OrderLines = new List<RefundItemOrderLineEto>
@ -219,14 +261,95 @@ namespace EasyAbp.EShop.Orders.Orders
}); });
} }
[Fact]
public async Task Should_Avoid_Over_Amount_Refund()
{
var handler = ServiceProvider.GetRequiredService<RefundCompletedEventHandler>();
await Order1.StartPaymentAsync(OrderTestData.Payment1Id, Order1.ActualTotalPrice,
_moneyDistributor);
Order1.SetPaid(DateTime.Now);
await Should.ThrowAsync<InvalidRefundAmountException>(async () =>
{
await handler.HandleEventAsync(new EShopRefundCompletedEto
{
Refund = new EShopRefundEto
{
Id = Guid.NewGuid(),
TenantId = null,
PaymentId = OrderTestData.Payment1Id,
Currency = "USD",
RefundAmount = 1.04m,
RefundItems = new List<EShopRefundItemEto>
{
new()
{
Id = Guid.NewGuid(),
PaymentItemId = Guid.NewGuid(),
RefundAmount = 0.3m,
StoreId = OrderTestData.Store1Id,
OrderId = OrderTestData.Order1Id,
OrderLines = new List<RefundItemOrderLineEto>
{
new()
{
OrderLineId = OrderTestData.OrderLine1Id,
RefundedQuantity = 1,
RefundAmount = 1.07m // 1.07m > 1.06m
}
}
}
}
}
});
});
await Should.ThrowAsync<InvalidRefundAmountException>(async () =>
{
await handler.HandleEventAsync(new EShopRefundCompletedEto
{
Refund = new EShopRefundEto
{
Id = Guid.NewGuid(),
TenantId = null,
PaymentId = OrderTestData.Payment1Id,
Currency = "USD",
RefundAmount = 0.3m,
RefundItems = new List<EShopRefundItemEto>
{
new()
{
Id = Guid.NewGuid(),
PaymentItemId = Guid.NewGuid(),
RefundAmount = 0.31m,
StoreId = OrderTestData.Store1Id,
OrderId = OrderTestData.Order1Id,
OrderExtraFees = new List<RefundItemOrderExtraFeeEto>
{
new()
{
Name = "Name",
Key = "Key",
DisplayName = "DisplayName",
RefundAmount = 0.31m // 0.31m > 0.3m
}
}
}
}
}
});
});
}
[Fact] [Fact]
public async Task Should_Forbid_Canceling_Order_During_Payment_State() public async Task Should_Forbid_Canceling_Order_During_Payment_State()
{ {
var orderManager = ServiceProvider.GetRequiredService<IOrderManager>(); var orderManager = ServiceProvider.GetRequiredService<IOrderManager>();
var order = await _orderRepository.GetAsync(OrderTestData.Order1Id); var order = await _orderRepository.GetAsync(OrderTestData.Order1Id);
order.SetPaymentId(Guid.NewGuid()); await Order1.StartPaymentAsync(Guid.NewGuid(), Order1.ActualTotalPrice, _moneyDistributor);
order.SetPaidTime(null); order.PaidTime.ShouldBeNull();
await Should.ThrowAsync<OrderIsInWrongStageException>(() => orderManager.CancelAsync(order, "my-reason")); await Should.ThrowAsync<OrderIsInWrongStageException>(() => orderManager.CancelAsync(order, "my-reason"));
} }
@ -236,13 +359,14 @@ namespace EasyAbp.EShop.Orders.Orders
var orderManager = ServiceProvider.GetRequiredService<IOrderManager>(); var orderManager = ServiceProvider.GetRequiredService<IOrderManager>();
var order = await _orderRepository.GetAsync(OrderTestData.Order1Id); var order = await _orderRepository.GetAsync(OrderTestData.Order1Id);
order.SetReducedInventoryAfterPlacingTime(null); order.ReducedInventoryAfterPlacingTime.ShouldBeNull();
await Should.ThrowAsync<OrderIsInWrongStageException>(() => orderManager.CancelAsync(order, "my-reason")); await Should.ThrowAsync<OrderIsInWrongStageException>(() => orderManager.CancelAsync(order, "my-reason"));
order.SetReducedInventoryAfterPlacingTime(DateTime.Now); order.SetReducedInventoryAfterPlacingTime(DateTime.Now);
order.SetPaymentId(Guid.NewGuid()); await order.StartPaymentAsync(Guid.NewGuid(), order.ActualTotalPrice, _moneyDistributor);
order.SetPaidTime(DateTime.Now); order.SetPaid(DateTime.Now);
order.SetReducedInventoryAfterPlacingTime(null); typeof(Order).GetProperty(nameof(Order.ReducedInventoryAfterPlacingTime))!.SetValue(Order1, null);
typeof(Order).GetProperty(nameof(Order.OrderStatus))!.SetValue(Order1, OrderStatus.Pending);
await Should.ThrowAsync<OrderIsInWrongStageException>(() => orderManager.CancelAsync(order, "my-reason")); await Should.ThrowAsync<OrderIsInWrongStageException>(() => orderManager.CancelAsync(order, "my-reason"));
} }
} }

2
modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Application.Contracts/EasyAbp/EShop/Payments/Refunds/Dtos/CreateEShopRefundInput.cs

@ -21,7 +21,7 @@ namespace EasyAbp.EShop.Payments.Refunds.Dtos
[CanBeNull] [CanBeNull]
public string StaffRemark { get; set; } public string StaffRemark { get; set; }
public List<CreateEShopRefundItemInput> RefundItems { get; set; } = new List<CreateEShopRefundItemInput>(); public List<CreateEShopRefundItemInput> RefundItems { get; set; } = new();
public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext) public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{ {

17
modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Application/EasyAbp/EShop/Payments/Refunds/InvalidOrderExtraFeeRefundAmountException.cs

@ -0,0 +1,17 @@
using System;
using Volo.Abp;
namespace EasyAbp.EShop.Payments.Refunds
{
public class InvalidOrderExtraFeeRefundAmountException : BusinessException
{
public InvalidOrderExtraFeeRefundAmountException(Guid paymentId, Guid orderId, string extraFeeDisplayName,
decimal refundAmount) : base(PaymentsErrorCodes.InvalidOrderExtraFeeRefundAmount)
{
WithData(nameof(paymentId), paymentId);
WithData(nameof(orderId), orderId);
WithData(nameof(extraFeeDisplayName), extraFeeDisplayName);
WithData(nameof(refundAmount), refundAmount);
}
}
}

17
modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Application/EasyAbp/EShop/Payments/Refunds/InvalidOrderLineRefundAmountException.cs

@ -0,0 +1,17 @@
using System;
using Volo.Abp;
namespace EasyAbp.EShop.Payments.Refunds
{
public class InvalidOrderLineRefundAmountException : BusinessException
{
public InvalidOrderLineRefundAmountException(Guid paymentId, Guid orderId, Guid orderLineId,
decimal refundAmount) : base(PaymentsErrorCodes.InvalidOrderLineRefundAmount)
{
WithData(nameof(paymentId), paymentId);
WithData(nameof(orderId), orderId);
WithData(nameof(orderLineId), orderLineId);
WithData(nameof(refundAmount), refundAmount);
}
}
}

16
modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Application/EasyAbp/EShop/Payments/Refunds/InvalidOrderRefundAmountException.cs

@ -0,0 +1,16 @@
using System;
using Volo.Abp;
namespace EasyAbp.EShop.Payments.Refunds
{
public class InvalidOrderRefundAmountException : BusinessException
{
public InvalidOrderRefundAmountException(Guid paymentId, Guid orderId, decimal refundAmount) : base(
PaymentsErrorCodes.InvalidOrderRefundAmount)
{
WithData(nameof(paymentId), paymentId);
WithData(nameof(orderId), orderId);
WithData(nameof(refundAmount), refundAmount);
}
}
}

16
modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Application/EasyAbp/EShop/Payments/Refunds/InvalidRefundAmountException.cs

@ -1,16 +0,0 @@
using System;
using Volo.Abp;
namespace EasyAbp.EShop.Payments.Refunds
{
public class InvalidRefundAmountException : BusinessException
{
public InvalidRefundAmountException(Guid paymentId, Guid paymentItemId, decimal refundAmount) : base(
PaymentsErrorCodes.InvalidRefundAmount)
{
WithData(nameof(paymentId), paymentId);
WithData(nameof(paymentItemId), paymentItemId);
WithData(nameof(refundAmount), refundAmount);
}
}
}

26
modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Application/EasyAbp/EShop/Payments/Refunds/RefundAppService.cs

@ -83,6 +83,7 @@ namespace EasyAbp.EShop.Payments.Refunds
{ {
await AuthorizationService.CheckAsync(PaymentsPermissions.Refunds.Manage); await AuthorizationService.CheckAsync(PaymentsPermissions.Refunds.Manage);
// todo: needs a lock.
var payment = await _paymentRepository.GetAsync(input.PaymentId); var payment = await _paymentRepository.GetAsync(input.PaymentId);
if (payment.PendingRefundAmount != decimal.Zero) if (payment.PendingRefundAmount != decimal.Zero)
@ -108,7 +109,7 @@ namespace EasyAbp.EShop.Payments.Refunds
{ {
throw new OrderIsNotInSpecifiedPaymentException(order.Id, payment.Id); throw new OrderIsNotInSpecifiedPaymentException(order.Id, payment.Id);
} }
await AuthorizationService.CheckMultiStorePolicyAsync(paymentItem.StoreId, await AuthorizationService.CheckMultiStorePolicyAsync(paymentItem.StoreId,
PaymentsPermissions.Refunds.Manage, PaymentsPermissions.Refunds.CrossStore); PaymentsPermissions.Refunds.Manage, PaymentsPermissions.Refunds.CrossStore);
@ -117,7 +118,7 @@ namespace EasyAbp.EShop.Payments.Refunds
if (refundAmount + paymentItem.RefundAmount > paymentItem.ActualPaymentAmount) if (refundAmount + paymentItem.RefundAmount > paymentItem.ActualPaymentAmount)
{ {
throw new InvalidRefundAmountException(payment.Id, paymentItem.Id, refundAmount); throw new InvalidOrderRefundAmountException(payment.Id, paymentItem.Id, refundAmount);
} }
foreach (var model in refundItem.OrderLines) foreach (var model in refundItem.OrderLines)
@ -128,7 +129,15 @@ namespace EasyAbp.EShop.Payments.Refunds
{ {
throw new OrderLineNotFoundException(order.Id, model.OrderLineId); throw new OrderLineNotFoundException(order.Id, model.OrderLineId);
} }
// PaymentAmount is always null before EShop v5
var paymentAmount = orderLine.PaymentAmount ?? orderLine.ActualTotalPrice;
if (orderLine.RefundAmount + model.TotalAmount > paymentAmount)
{
throw new InvalidOrderLineRefundAmountException(
payment.Id, paymentItem.Id, orderLine.Id, refundAmount);
}
if (orderLine.RefundedQuantity + model.Quantity > orderLine.Quantity) if (orderLine.RefundedQuantity + model.Quantity > orderLine.Quantity)
{ {
throw new InvalidRefundQuantityException(model.Quantity); throw new InvalidRefundQuantityException(model.Quantity);
@ -143,6 +152,14 @@ namespace EasyAbp.EShop.Payments.Refunds
{ {
throw new OrderExtraFeeNotFoundException(order.Id, model.Name, model.Key); throw new OrderExtraFeeNotFoundException(order.Id, model.Name, model.Key);
} }
// PaymentAmount is always null before EShop v5
var paymentAmount = orderExtraFee.PaymentAmount ?? orderExtraFee.Fee;
if (orderExtraFee.RefundAmount + model.TotalAmount > paymentAmount)
{
throw new InvalidOrderExtraFeeRefundAmountException(
payment.Id, paymentItem.Id, orderExtraFee.DisplayName, refundAmount);
}
} }
var eto = new CreateRefundItemInput var eto = new CreateRefundItemInput
@ -156,7 +173,8 @@ namespace EasyAbp.EShop.Payments.Refunds
eto.SetProperty(nameof(RefundItem.StoreId), order.StoreId); eto.SetProperty(nameof(RefundItem.StoreId), order.StoreId);
eto.SetProperty(nameof(RefundItem.OrderId), order.Id); eto.SetProperty(nameof(RefundItem.OrderId), order.Id);
eto.SetProperty(nameof(RefundItem.OrderLines), _jsonSerializer.Serialize(refundItem.OrderLines)); eto.SetProperty(nameof(RefundItem.OrderLines), _jsonSerializer.Serialize(refundItem.OrderLines));
eto.SetProperty(nameof(RefundItem.OrderExtraFees), _jsonSerializer.Serialize(refundItem.OrderExtraFees)); eto.SetProperty(nameof(RefundItem.OrderExtraFees),
_jsonSerializer.Serialize(refundItem.OrderExtraFees));
createRefundInput.RefundItems.Add(eto); createRefundInput.RefundItems.Add(eto);
} }

4
modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Domain.Shared/EasyAbp/EShop/Payments/Localization/Payments/en.json

@ -11,7 +11,9 @@
"EasyAbp.EShop.Payments:InvalidRefundQuantity": "The refund quantity ({quantity}) is invalid.", "EasyAbp.EShop.Payments:InvalidRefundQuantity": "The refund quantity ({quantity}) is invalid.",
"EasyAbp.EShop.Payments:OrderIsNotInSpecifiedPayment": "The order ({orderId}) is not in the specified payment ({paymentId}).", "EasyAbp.EShop.Payments:OrderIsNotInSpecifiedPayment": "The order ({orderId}) is not in the specified payment ({paymentId}).",
"EasyAbp.EShop.Payments:AnotherRefundTaskIsOnGoing": "Payment ({id}) has another ongoing refund task.", "EasyAbp.EShop.Payments:AnotherRefundTaskIsOnGoing": "Payment ({id}) has another ongoing refund task.",
"EasyAbp.EShop.Payments:InvalidRefundAmount": "Refund amount ({refundAmount}) is invalid for the payment (id: {paymentId}, item id: {paymentItemId}).", "EasyAbp.EShop.Payments:InvalidOrderRefundAmount": "Refund amount ({refundAmount}) is invalid for the payment (PaymentId: {paymentId}, OrderId: {orderId}).",
"EasyAbp.EShop.Payments:InvalidOrderLineRefundAmount": "Refund amount ({refundAmount}) is invalid for the payment (PaymentId: {paymentId}, OrderId: {orderId}, OrderLineId: {orderLineId}).",
"EasyAbp.EShop.Payments:InvalidOrderExtraFeeRefundAmount": "Refund amount ({refundAmount}) is invalid for the payment (PaymentId: {paymentId}, OrderId: {orderId}, ExtraFee: {extraFeeDisplayName}).",
"EasyAbp.EShop.Payments:OrderIdNotFound": "Cannot get valid OrderId from ExtraProperties.", "EasyAbp.EShop.Payments:OrderIdNotFound": "Cannot get valid OrderId from ExtraProperties.",
"EasyAbp.EShop.Payments:StoreIdNotFound": "Cannot get valid StoreId from ExtraProperties.", "EasyAbp.EShop.Payments:StoreIdNotFound": "Cannot get valid StoreId from ExtraProperties.",
"EasyAbp.EShop.Payments:OrderLineNotFound": "There is no such an order line. (order ID: {orderId}, order line ID: {orderLineId})", "EasyAbp.EShop.Payments:OrderLineNotFound": "There is no such an order line. (order ID: {orderId}, order line ID: {orderLineId})",

4
modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Domain.Shared/EasyAbp/EShop/Payments/Localization/Payments/zh-Hans.json

@ -11,7 +11,9 @@
"EasyAbp.EShop.Payments:InvalidRefundQuantity": "退款数量({quantity})无效", "EasyAbp.EShop.Payments:InvalidRefundQuantity": "退款数量({quantity})无效",
"EasyAbp.EShop.Payments:OrderIsNotInSpecifiedPayment": "订单({orderId})不在指定的支付({paymentId})中", "EasyAbp.EShop.Payments:OrderIsNotInSpecifiedPayment": "订单({orderId})不在指定的支付({paymentId})中",
"EasyAbp.EShop.Payments:AnotherRefundTaskIsOnGoing": "支付({id})存在进行中的退款任务", "EasyAbp.EShop.Payments:AnotherRefundTaskIsOnGoing": "支付({id})存在进行中的退款任务",
"EasyAbp.EShop.Payments:InvalidRefundAmount": "退款金额({refundAmount})对于支付(id: {paymentId}, item id: {paymentItemId})不正确", "EasyAbp.EShop.Payments:InvalidOrderRefundAmount": "退款金额({refundAmount})对于支付(PaymentId: {paymentId}, OrderId: {orderId})不正确",
"EasyAbp.EShop.Payments:InvalidOrderLineRefundAmount": "退款金额({refundAmount})对于支付(PaymentId: {paymentId}, OrderId: {orderId}, OrderLineId: {orderLineId})不正确",
"EasyAbp.EShop.Payments:InvalidOrderExtraFeeRefundAmount": "退款金额({refundAmount})对于支付(PaymentId: {paymentId}, OrderId: {orderId}, ExtraFee: {extraFeeDisplayName})不正确",
"EasyAbp.EShop.Payments:OrderIdNotFound": "无法从ExtraProperties获得有效的OrderId", "EasyAbp.EShop.Payments:OrderIdNotFound": "无法从ExtraProperties获得有效的OrderId",
"EasyAbp.EShop.Payments:StoreIdNotFound": "无法从ExtraProperties获得有效的StoreId", "EasyAbp.EShop.Payments:StoreIdNotFound": "无法从ExtraProperties获得有效的StoreId",
"EasyAbp.EShop.Payments:OrderLineNotFound": "不存在的订单项 (订单ID: {orderId}, 订单项ID: {orderLineId})", "EasyAbp.EShop.Payments:OrderLineNotFound": "不存在的订单项 (订单ID: {orderId}, 订单项ID: {orderLineId})",

4
modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Domain.Shared/EasyAbp/EShop/Payments/Localization/Payments/zh-Hant.json

@ -11,7 +11,9 @@
"EasyAbp.EShop.Payments:InvalidRefundQuantity": "退款數量({quantity})無效", "EasyAbp.EShop.Payments:InvalidRefundQuantity": "退款數量({quantity})無效",
"EasyAbp.EShop.Payments:OrderIsNotInSpecifiedPayment": "訂單({orderId})不在指定的支付({paymentId})中", "EasyAbp.EShop.Payments:OrderIsNotInSpecifiedPayment": "訂單({orderId})不在指定的支付({paymentId})中",
"EasyAbp.EShop.Payments:AnotherRefundTaskIsOnGoing": "支付({id})存在進行中的退款任務", "EasyAbp.EShop.Payments:AnotherRefundTaskIsOnGoing": "支付({id})存在進行中的退款任務",
"EasyAbp.EShop.Payments:InvalidRefundAmount": "退款金額({refundAmount})對於支付(id: {paymentId}, item id: {paymentItemId})不正確", "EasyAbp.EShop.Payments:InvalidOrderRefundAmount": "退款金額({refundAmount})對於支付(PaymentId: {paymentId}, OrderId: {orderId})不正確",
"EasyAbp.EShop.Payments:InvalidOrderLineRefundAmount": "退款金額({refundAmount})對於支付(PaymentId: {paymentId}, OrderId: {orderId}, OrderLineId: {orderLineId})不正確",
"EasyAbp.EShop.Payments:InvalidOrderExtraFeeRefundAmount": "退款金額({refundAmount})對於支付(PaymentId: {paymentId}, OrderId: {orderId}, ExtraFee: {extraFeeDisplayName})不正確",
"EasyAbp.EShop.Payments:OrderIdNotFound": "無法從ExtraProperties獲得有效的OrderId", "EasyAbp.EShop.Payments:OrderIdNotFound": "無法從ExtraProperties獲得有效的OrderId",
"EasyAbp.EShop.Payments:StoreIdNotFound": "無法從ExtraProperties獲得有效的StoreId", "EasyAbp.EShop.Payments:StoreIdNotFound": "無法從ExtraProperties獲得有效的StoreId",
"EasyAbp.EShop.Payments:OrderLineNotFound": "不存在的訂單項 (訂單ID: {orderId}, 訂單項ID: {orderLineId})", "EasyAbp.EShop.Payments:OrderLineNotFound": "不存在的訂單項 (訂單ID: {orderId}, 訂單項ID: {orderLineId})",

4
modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Domain.Shared/EasyAbp/EShop/Payments/PaymentsErrorCodes.cs

@ -6,7 +6,9 @@
public const string InvalidRefundQuantity = "EasyAbp.EShop.Payments:InvalidRefundQuantity"; public const string InvalidRefundQuantity = "EasyAbp.EShop.Payments:InvalidRefundQuantity";
public const string OrderIsNotInSpecifiedPayment = "EasyAbp.EShop.Payments:OrderIsNotInSpecifiedPayment"; public const string OrderIsNotInSpecifiedPayment = "EasyAbp.EShop.Payments:OrderIsNotInSpecifiedPayment";
public const string AnotherRefundTaskIsOnGoing = "EasyAbp.EShop.Payments:AnotherRefundTaskIsOnGoing"; public const string AnotherRefundTaskIsOnGoing = "EasyAbp.EShop.Payments:AnotherRefundTaskIsOnGoing";
public const string InvalidRefundAmount = "EasyAbp.EShop.Payments:InvalidRefundAmount"; public const string InvalidOrderRefundAmount = "EasyAbp.EShop.Payments:InvalidOrderRefundAmount";
public const string InvalidOrderLineRefundAmount = "EasyAbp.EShop.Payments:InvalidOrderLineRefundAmount";
public const string InvalidOrderExtraFeeRefundAmount = "EasyAbp.EShop.Payments:InvalidOrderExtraFeeRefundAmount";
public const string OrderIdNotFound = "EasyAbp.EShop.Payments:OrderIdNotFound"; public const string OrderIdNotFound = "EasyAbp.EShop.Payments:OrderIdNotFound";
public const string StoreIdNotFound = "EasyAbp.EShop.Payments:StoreIdNotFound"; public const string StoreIdNotFound = "EasyAbp.EShop.Payments:StoreIdNotFound";
public const string OrderLineNotFound = "EasyAbp.EShop.Payments:OrderLineNotFound"; public const string OrderLineNotFound = "EasyAbp.EShop.Payments:OrderLineNotFound";

223
modules/EasyAbp.EShop.Payments/test/EasyAbp.EShop.Payments.Application.Tests/Refunds/RefundAppServiceTests.cs

@ -40,22 +40,27 @@ namespace EasyAbp.EShop.Payments.Refunds
{ {
var paymentType = typeof(Payment); var paymentType = typeof(Payment);
var paymentItemType = typeof(PaymentItem); var paymentItemType = typeof(PaymentItem);
var paymentItem = Activator.CreateInstance(paymentItemType, true) as PaymentItem; var paymentItem = Activator.CreateInstance(paymentItemType, true) as PaymentItem;
paymentItem.ShouldNotBeNull(); paymentItem.ShouldNotBeNull();
paymentItemType.GetProperty(nameof(PaymentItem.Id))?.SetValue(paymentItem, PaymentsTestData.PaymentItem1); paymentItemType.GetProperty(nameof(PaymentItem.Id))
paymentItemType.GetProperty(nameof(PaymentItem.ActualPaymentAmount))?.SetValue(paymentItem, 1m); ?.SetValue(paymentItem, PaymentsTestData.PaymentItem1);
paymentItemType.GetProperty(nameof(PaymentItem.ItemType))?.SetValue(paymentItem, PaymentsConsts.PaymentItemType); paymentItemType.GetProperty(nameof(PaymentItem.ActualPaymentAmount))?.SetValue(paymentItem, 5m);
paymentItemType.GetProperty(nameof(PaymentItem.ItemKey))?.SetValue(paymentItem, PaymentsTestData.Order1.ToString()); paymentItemType.GetProperty(nameof(PaymentItem.ItemType))
paymentItemType.GetProperty(nameof(PaymentItem.StoreId))?.SetValue(paymentItem, PaymentsTestData.Store1); ?.SetValue(paymentItem, PaymentsConsts.PaymentItemType);
paymentItemType.GetProperty(nameof(PaymentItem.ItemKey))
?.SetValue(paymentItem, PaymentsTestData.Order1.ToString());
paymentItemType.GetProperty(nameof(PaymentItem.StoreId))
?.SetValue(paymentItem, PaymentsTestData.Store1);
// paymentItem.ExtraProperties.Add(nameof(paymentItem.StoreId), PaymentsTestData.Store1); // paymentItem.ExtraProperties.Add(nameof(paymentItem.StoreId), PaymentsTestData.Store1);
var payment = Activator.CreateInstance(paymentType, true) as Payment; var payment = Activator.CreateInstance(paymentType, true) as Payment;
payment.ShouldNotBeNull(); payment.ShouldNotBeNull();
paymentType.GetProperty(nameof(Payment.Id))?.SetValue(payment, PaymentsTestData.Payment1); paymentType.GetProperty(nameof(Payment.Id))?.SetValue(payment, PaymentsTestData.Payment1);
paymentType.GetProperty(nameof(Payment.Currency))?.SetValue(payment, "USD"); paymentType.GetProperty(nameof(Payment.Currency))?.SetValue(payment, "USD");
paymentType.GetProperty(nameof(Payment.ActualPaymentAmount))?.SetValue(payment, 1m); paymentType.GetProperty(nameof(Payment.ActualPaymentAmount))?.SetValue(payment, 5m);
paymentType.GetProperty(nameof(Payment.PaymentItems))?.SetValue(payment, new List<PaymentItem> {paymentItem}); paymentType.GetProperty(nameof(Payment.PaymentItems))
?.SetValue(payment, new List<PaymentItem> { paymentItem });
return payment; return payment;
}; };
@ -95,13 +100,13 @@ namespace EasyAbp.EShop.Payments.Refunds
paymentRepository.GetAsync(PaymentsTestData.Payment1).Returns(payment1Returns); paymentRepository.GetAsync(PaymentsTestData.Payment1).Returns(payment1Returns);
paymentRepository.FindAsync(PaymentsTestData.Payment1).Returns(payment1Returns); paymentRepository.FindAsync(PaymentsTestData.Payment1).Returns(payment1Returns);
paymentRepository.GetAsync(PaymentsTestData.Payment2).Returns(payment2Returns); paymentRepository.GetAsync(PaymentsTestData.Payment2).Returns(payment2Returns);
paymentRepository.FindAsync(PaymentsTestData.Payment2).Returns(payment2Returns); paymentRepository.FindAsync(PaymentsTestData.Payment2).Returns(payment2Returns);
services.AddTransient(_ => paymentRepository); services.AddTransient(_ => paymentRepository);
} }
private void MockOrderService(IServiceCollection services) private void MockOrderService(IServiceCollection services)
{ {
var orderService = Substitute.For<IOrderAppService>(); var orderService = Substitute.For<IOrderAppService>();
@ -109,7 +114,8 @@ namespace EasyAbp.EShop.Payments.Refunds
{ {
Id = PaymentsTestData.Order1, Id = PaymentsTestData.Order1,
Currency = "USD", Currency = "USD",
ActualTotalPrice = 0, ActualTotalPrice = 6m,
PaymentAmount = 5m,
StoreId = PaymentsTestData.Store1, StoreId = PaymentsTestData.Store1,
OrderLines = new List<OrderLineDto> OrderLines = new List<OrderLineDto>
{ {
@ -118,6 +124,7 @@ namespace EasyAbp.EShop.Payments.Refunds
Id = PaymentsTestData.OrderLine1, Id = PaymentsTestData.OrderLine1,
Currency = "USD", Currency = "USD",
ActualTotalPrice = 1m, ActualTotalPrice = 1m,
PaymentAmount = 0.83m,
Quantity = 1 Quantity = 1
} }
}, },
@ -127,15 +134,17 @@ namespace EasyAbp.EShop.Payments.Refunds
{ {
Name = "Name", Name = "Name",
Key = "Key", Key = "Key",
Fee = 5m DisplayName = "DisplayName",
Fee = 5m,
PaymentAmount = 4.17m
} }
}, },
PaymentId = PaymentsTestData.Payment1 PaymentId = PaymentsTestData.Payment1
})); }));
services.AddTransient(_ => orderService); services.AddTransient(_ => orderService);
} }
public RefundAppServiceTests() public RefundAppServiceTests()
{ {
_jsonSerializer = GetRequiredService<IJsonSerializer>(); _jsonSerializer = GetRequiredService<IJsonSerializer>();
@ -176,6 +185,7 @@ namespace EasyAbp.EShop.Payments.Refunds
{ {
Name = "Name", Name = "Name",
Key = "Key", Key = "Key",
DisplayName = "DisplayName",
TotalAmount = 0.6m TotalAmount = 0.6m
} }
} }
@ -190,7 +200,7 @@ namespace EasyAbp.EShop.Payments.Refunds
TestRefundPaymentEventHandler.LastEto = null; TestRefundPaymentEventHandler.LastEto = null;
eventData.ShouldNotBeNull(); eventData.ShouldNotBeNull();
eventData.CreateRefundInput.RefundItems.Count.ShouldBe(1); eventData.CreateRefundInput.RefundItems.Count.ShouldBe(1);
var refundItem = eventData.CreateRefundInput.RefundItems[0]; var refundItem = eventData.CreateRefundInput.RefundItems[0];
refundItem.GetProperty<Guid?>(nameof(RefundItem.OrderId)).ShouldBe(PaymentsTestData.Order1); refundItem.GetProperty<Guid?>(nameof(RefundItem.OrderId)).ShouldBe(PaymentsTestData.Order1);
@ -202,14 +212,15 @@ namespace EasyAbp.EShop.Payments.Refunds
orderLines[0].OrderLineId.ShouldBe(PaymentsTestData.OrderLine1); orderLines[0].OrderLineId.ShouldBe(PaymentsTestData.OrderLine1);
orderLines[0].Quantity.ShouldBe(1); orderLines[0].Quantity.ShouldBe(1);
orderLines[0].TotalAmount.ShouldBe(0.4m); orderLines[0].TotalAmount.ShouldBe(0.4m);
var orderExtraFees = var orderExtraFees =
_jsonSerializer.Deserialize<List<OrderExtraFeeRefundInfoModel>>( _jsonSerializer.Deserialize<List<OrderExtraFeeRefundInfoModel>>(
refundItem.GetProperty<string>(nameof(RefundItem.OrderExtraFees))); refundItem.GetProperty<string>(nameof(RefundItem.OrderExtraFees)));
orderExtraFees.Count.ShouldBe(1); orderExtraFees.Count.ShouldBe(1);
orderExtraFees[0].Name.ShouldBe("Name"); orderExtraFees[0].Name.ShouldBe("Name");
orderExtraFees[0].Key.ShouldBe("Key"); orderExtraFees[0].Key.ShouldBe("Key");
orderExtraFees[0].DisplayName.ShouldBe("DisplayName");
orderExtraFees[0].TotalAmount.ShouldBe(0.6m); orderExtraFees[0].TotalAmount.ShouldBe(0.6m);
} }
@ -230,24 +241,25 @@ namespace EasyAbp.EShop.Payments.Refunds
CustomerRemark = "CustomerRemark", CustomerRemark = "CustomerRemark",
OrderId = PaymentsTestData.Order1, OrderId = PaymentsTestData.Order1,
StaffRemark = "StaffRemark", StaffRemark = "StaffRemark",
OrderLines = new List<OrderLineRefundInfoModel>(), // empty OrderLines = new List<OrderLineRefundInfoModel>(), // empty
OrderExtraFees = new List<OrderExtraFeeRefundInfoModel>() // empty OrderExtraFees = new List<OrderExtraFeeRefundInfoModel>() // empty
} }
} }
}; };
// Act & Assert // Act & Assert
await Should.ThrowAsync<AbpValidationException>(async () => (await Should.ThrowAsync<AbpValidationException>(async () =>
{ {
await _refundAppService.CreateAsync(request); await _refundAppService.CreateAsync(request);
}, "RefundItem.OrderLines and RefundItem.OrderExtraFees should not both be empty!"); })).ValidationErrors[0].ErrorMessage
.ShouldBe("RefundItem.OrderLines and RefundItem.OrderExtraFees should not both be empty!");
} }
[Fact] [Fact]
public async Task Should_Avoid_Over_Refund() public async Task Should_Avoid_Over_Amount_Refund()
{ {
// Arrange // Arrange
var request = new CreateEShopRefundInput var request1 = new CreateEShopRefundInput
{ {
DisplayReason = "Reason", DisplayReason = "Reason",
CustomerRemark = "Customer Remark", CustomerRemark = "Customer Remark",
@ -266,16 +278,108 @@ namespace EasyAbp.EShop.Payments.Refunds
{ {
OrderLineId = PaymentsTestData.OrderLine1, OrderLineId = PaymentsTestData.OrderLine1,
Quantity = 1, Quantity = 1,
TotalAmount = 1m TotalAmount = 0.84m // 0.84m > 0.83m
} }
}, }
}
}
};
// Arrange
var request2 = new CreateEShopRefundInput
{
DisplayReason = "Reason",
CustomerRemark = "Customer Remark",
PaymentId = PaymentsTestData.Payment1,
StaffRemark = "StaffRemark",
RefundItems = new List<CreateEShopRefundItemInput>
{
new()
{
CustomerRemark = "CustomerRemark",
OrderId = PaymentsTestData.Order1,
StaffRemark = "StaffRemark",
OrderExtraFees = new List<OrderExtraFeeRefundInfoModel> OrderExtraFees = new List<OrderExtraFeeRefundInfoModel>
{ {
new() new()
{ {
Name = "Name", Name = "Name",
Key = "Key", Key = "Key",
TotalAmount = 0.1m DisplayName = "DisplayName",
TotalAmount = 4.18m // 4.18m > 4.17m
}
}
}
}
};
var request3 = new CreateEShopRefundInput
{
DisplayReason = "Reason",
CustomerRemark = "Customer Remark",
PaymentId = PaymentsTestData.Payment1,
StaffRemark = "StaffRemark",
RefundItems = new List<CreateEShopRefundItemInput>
{
new()
{
CustomerRemark = "CustomerRemark",
OrderId = PaymentsTestData.Order1,
StaffRemark = "StaffRemark",
OrderLines = new List<OrderLineRefundInfoModel>
{
new()
{
OrderLineId = PaymentsTestData.OrderLine1,
Quantity = 1,
TotalAmount = 100m // 100m >>> 5m (the order payment amount)
}
}
}
}
};
// Act & Assert
await Should.ThrowAsync<InvalidOrderLineRefundAmountException>(async () =>
{
await _refundAppService.CreateAsync(request1);
});
await Should.ThrowAsync<InvalidOrderExtraFeeRefundAmountException>(async () =>
{
await _refundAppService.CreateAsync(request2);
});
await Should.ThrowAsync<InvalidOrderRefundAmountException>(async () =>
{
await _refundAppService.CreateAsync(request3);
});
}
[Fact]
public async Task Should_Avoid_Over_Quantity_Refund()
{
// Arrange
var request = new CreateEShopRefundInput
{
DisplayReason = "Reason",
CustomerRemark = "Customer Remark",
PaymentId = PaymentsTestData.Payment1,
StaffRemark = "StaffRemark",
RefundItems = new List<CreateEShopRefundItemInput>
{
new()
{
CustomerRemark = "CustomerRemark",
OrderId = PaymentsTestData.Order1,
StaffRemark = "StaffRemark",
OrderLines = new List<OrderLineRefundInfoModel>
{
new()
{
OrderLineId = PaymentsTestData.OrderLine1,
Quantity = 2, // 2 > 1
TotalAmount = 0.83m
} }
} }
} }
@ -283,7 +387,7 @@ namespace EasyAbp.EShop.Payments.Refunds
}; };
// Act & Assert // Act & Assert
await Should.ThrowAsync<InvalidRefundAmountException>(async () => await Should.ThrowAsync<InvalidRefundQuantityException>(async () =>
{ {
await _refundAppService.CreateAsync(request); await _refundAppService.CreateAsync(request);
}); });
@ -308,7 +412,7 @@ namespace EasyAbp.EShop.Payments.Refunds
StaffRemark = "StaffRemark", StaffRemark = "StaffRemark",
OrderLines = new List<OrderLineRefundInfoModel> OrderLines = new List<OrderLineRefundInfoModel>
{ {
new OrderLineRefundInfoModel new()
{ {
OrderLineId = PaymentsTestData.OrderLine1, OrderLineId = PaymentsTestData.OrderLine1,
Quantity = 1, Quantity = 1,
@ -327,7 +431,7 @@ namespace EasyAbp.EShop.Payments.Refunds
} }
[Fact] [Fact]
public async Task Should_Check_OrderLines_Exist_When_Refunding() public Task Should_Check_OrderLines_Exist_When_Refunding()
{ {
// Arrange // Arrange
var request = new CreateEShopRefundInput var request = new CreateEShopRefundInput
@ -355,6 +459,8 @@ namespace EasyAbp.EShop.Payments.Refunds
} }
} }
}; };
return Task.CompletedTask;
} }
[Fact] [Fact]
@ -380,6 +486,7 @@ namespace EasyAbp.EShop.Payments.Refunds
{ {
Name = "FakeName", Name = "FakeName",
Key = "FakeKey", Key = "FakeKey",
DisplayName = "FakeDisplayName",
TotalAmount = 0.6m TotalAmount = 0.6m
} }
} }
@ -393,7 +500,7 @@ namespace EasyAbp.EShop.Payments.Refunds
await _refundAppService.CreateAsync(request); await _refundAppService.CreateAsync(request);
}); });
} }
[Fact] [Fact]
public async Task Should_Avoid_Non_Positive_Refund_Amount() public async Task Should_Avoid_Non_Positive_Refund_Amount()
{ {
@ -413,7 +520,7 @@ namespace EasyAbp.EShop.Payments.Refunds
StaffRemark = "StaffRemark", StaffRemark = "StaffRemark",
OrderLines = new List<OrderLineRefundInfoModel> OrderLines = new List<OrderLineRefundInfoModel>
{ {
new OrderLineRefundInfoModel new()
{ {
OrderLineId = PaymentsTestData.OrderLine1, OrderLineId = PaymentsTestData.OrderLine1,
Quantity = 1, Quantity = 1,
@ -425,10 +532,10 @@ namespace EasyAbp.EShop.Payments.Refunds
}; };
// Act & Assert // Act & Assert
await Should.ThrowAsync<AbpValidationException>(async () => (await Should.ThrowAsync<AbpValidationException>(async () =>
{ {
await _refundAppService.CreateAsync(request); await _refundAppService.CreateAsync(request);
}, "RefundAmount should be greater than 0."); })).ValidationErrors[0].ErrorMessage.ShouldBe("RefundAmount should be greater than 0.");
} }
[Fact] [Fact]
@ -447,27 +554,30 @@ namespace EasyAbp.EShop.Payments.Refunds
}; };
var now = DateTime.Now; var now = DateTime.Now;
refundItem.SetProperty(nameof(RefundItem.StoreId), PaymentsTestData.Store1); refundItem.SetProperty(nameof(RefundItem.StoreId), PaymentsTestData.Store1);
refundItem.SetProperty(nameof(RefundItem.OrderId), PaymentsTestData.Order1); refundItem.SetProperty(nameof(RefundItem.OrderId), PaymentsTestData.Order1);
refundItem.SetProperty(nameof(RefundItem.OrderLines), _jsonSerializer.Serialize(new List<OrderLineRefundInfoModel> refundItem.SetProperty(nameof(RefundItem.OrderLines), _jsonSerializer.Serialize(
{ new List<OrderLineRefundInfoModel>
new()
{ {
OrderLineId = PaymentsTestData.OrderLine1, new()
Quantity = 2, {
TotalAmount = 1m OrderLineId = PaymentsTestData.OrderLine1,
} Quantity = 2,
})); TotalAmount = 1m
refundItem.SetProperty(nameof(RefundItem.OrderExtraFees), _jsonSerializer.Serialize(new List<OrderExtraFeeRefundInfoModel> }
{ }));
new() refundItem.SetProperty(nameof(RefundItem.OrderExtraFees), _jsonSerializer.Serialize(
new List<OrderExtraFeeRefundInfoModel>
{ {
Name = "Name", new()
Key = "Key", {
TotalAmount = 0.5m Name = "Name",
} Key = "Key",
})); DisplayName = "DisplayName",
TotalAmount = 0.5m
}
}));
await synchronizer.HandleEventAsync(new EntityCreatedEto<RefundEto>(new RefundEto await synchronizer.HandleEventAsync(new EntityCreatedEto<RefundEto>(new RefundEto
{ {
@ -487,7 +597,7 @@ namespace EasyAbp.EShop.Payments.Refunds
})); }));
var refundDto = await _refundAppService.GetAsync(PaymentsTestData.Refund1); var refundDto = await _refundAppService.GetAsync(PaymentsTestData.Refund1);
refundDto.PaymentId.ShouldBe(PaymentsTestData.Payment1); refundDto.PaymentId.ShouldBe(PaymentsTestData.Payment1);
refundDto.ExternalTradingCode.ShouldBe("testcode"); refundDto.ExternalTradingCode.ShouldBe("testcode");
refundDto.Currency.ShouldBe("USD"); refundDto.Currency.ShouldBe("USD");
@ -517,7 +627,8 @@ namespace EasyAbp.EShop.Payments.Refunds
var orderExtraFee = refundItemDto.OrderExtraFees.First(); var orderExtraFee = refundItemDto.OrderExtraFees.First();
orderExtraFee.Name.ShouldBe("Name"); orderExtraFee.Name.ShouldBe("Name");
orderExtraFee.Key.ShouldBe("Key"); orderExtraFee.Key.ShouldBe("Key");
orderExtraFee.DisplayName.ShouldBe("DisplayName");
orderExtraFee.RefundAmount.ShouldBe(0.5m); orderExtraFee.RefundAmount.ShouldBe(0.5m);
} }
} }
} }

6553
samples/EShopSample/aspnet-core/src/EShopSample.EntityFrameworkCore/Migrations/20230729082400_AddedPaymentAmount.Designer.cs

File diff suppressed because it is too large

48
samples/EShopSample/aspnet-core/src/EShopSample.EntityFrameworkCore/Migrations/20230729082400_AddedPaymentAmount.cs

@ -0,0 +1,48 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace EShopSample.Migrations
{
/// <inheritdoc />
public partial class AddedPaymentAmount : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<decimal>(
name: "PaymentAmount",
table: "EasyAbpEShopOrdersOrders",
type: "decimal(18,2)",
nullable: true);
migrationBuilder.AddColumn<decimal>(
name: "PaymentAmount",
table: "EasyAbpEShopOrdersOrderLines",
type: "decimal(18,2)",
nullable: true);
migrationBuilder.AddColumn<decimal>(
name: "PaymentAmount",
table: "EasyAbpEShopOrdersOrderExtraFees",
type: "decimal(18,2)",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "PaymentAmount",
table: "EasyAbpEShopOrdersOrders");
migrationBuilder.DropColumn(
name: "PaymentAmount",
table: "EasyAbpEShopOrdersOrderLines");
migrationBuilder.DropColumn(
name: "PaymentAmount",
table: "EasyAbpEShopOrdersOrderExtraFees");
}
}
}

15
samples/EShopSample/aspnet-core/src/EShopSample.EntityFrameworkCore/Migrations/EShopSampleDbContextModelSnapshot.cs

@ -564,6 +564,9 @@ namespace EShopSample.Migrations
b.Property<DateTime?>("PaidTime") b.Property<DateTime?>("PaidTime")
.HasColumnType("datetime2"); .HasColumnType("datetime2");
b.Property<decimal?>("PaymentAmount")
.HasColumnType("decimal(18,2)");
b.Property<DateTime?>("PaymentExpiration") b.Property<DateTime?>("PaymentExpiration")
.HasColumnType("datetime2"); .HasColumnType("datetime2");
@ -652,6 +655,9 @@ namespace EShopSample.Migrations
b.Property<decimal>("Fee") b.Property<decimal>("Fee")
.HasColumnType("decimal(20,8)"); .HasColumnType("decimal(20,8)");
b.Property<decimal?>("PaymentAmount")
.HasColumnType("decimal(18,2)");
b.Property<decimal>("RefundAmount") b.Property<decimal>("RefundAmount")
.HasColumnType("decimal(20,8)"); .HasColumnType("decimal(20,8)");
@ -712,6 +718,9 @@ namespace EShopSample.Migrations
b.Property<Guid?>("OrderId") b.Property<Guid?>("OrderId")
.HasColumnType("uniqueidentifier"); .HasColumnType("uniqueidentifier");
b.Property<decimal?>("PaymentAmount")
.HasColumnType("decimal(18,2)");
b.Property<Guid?>("ProductDetailId") b.Property<Guid?>("ProductDetailId")
.HasColumnType("uniqueidentifier"); .HasColumnType("uniqueidentifier");
@ -2974,7 +2983,8 @@ namespace EShopSample.Migrations
.HasColumnType("decimal(20,8)"); .HasColumnType("decimal(20,8)");
b.Property<Guid?>("TenantId") b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier"); .HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");
b.Property<Guid>("UserId") b.Property<Guid>("UserId")
.HasColumnType("uniqueidentifier"); .HasColumnType("uniqueidentifier");
@ -3426,7 +3436,8 @@ namespace EShopSample.Migrations
.HasColumnType("nvarchar(max)"); .HasColumnType("nvarchar(max)");
b.Property<Guid?>("TenantId") b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier"); .HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");
b.HasKey("Id"); b.HasKey("Id");

Loading…
Cancel
Save