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. 2
      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. 2
      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. 131
      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. 65
      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. 14
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/OrderExtraFee.cs
  23. 14
      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. 4
      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. 11
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/PaymentCreatedEventHandler.cs
  28. 51
      modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Application.Tests/Orders/OrderAppServiceTests.cs
  29. 36
      modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Domain.Tests/Orders/InventoryReductionResultTests.cs
  30. 166
      modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Domain.Tests/Orders/OrderDomainTests.cs
  31. 2
      modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Application.Contracts/EasyAbp/EShop/Payments/Refunds/Dtos/CreateEShopRefundInput.cs
  32. 17
      modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Application/EasyAbp/EShop/Payments/Refunds/InvalidOrderExtraFeeRefundAmountException.cs
  33. 17
      modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Application/EasyAbp/EShop/Payments/Refunds/InvalidOrderLineRefundAmountException.cs
  34. 16
      modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Application/EasyAbp/EShop/Payments/Refunds/InvalidOrderRefundAmountException.cs
  35. 16
      modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Application/EasyAbp/EShop/Payments/Refunds/InvalidRefundAmountException.cs
  36. 22
      modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Application/EasyAbp/EShop/Payments/Refunds/RefundAppService.cs
  37. 4
      modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Domain.Shared/EasyAbp/EShop/Payments/Localization/Payments/en.json
  38. 4
      modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Domain.Shared/EasyAbp/EShop/Payments/Localization/Payments/zh-Hans.json
  39. 4
      modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Domain.Shared/EasyAbp/EShop/Payments/Localization/Payments/zh-Hant.json
  40. 4
      modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Domain.Shared/EasyAbp/EShop/Payments/PaymentsErrorCodes.cs
  41. 159
      modules/EasyAbp.EShop.Payments/test/EasyAbp.EShop.Payments.Application.Tests/Refunds/RefundAppServiceTests.cs
  42. 6553
      samples/EShopSample/aspnet-core/src/EShopSample.EntityFrameworkCore/Migrations/20230729082400_AddedPaymentAmount.Designer.cs
  43. 48
      samples/EShopSample/aspnet-core/src/EShopSample.EntityFrameworkCore/Migrations/20230729082400_AddedPaymentAmount.cs
  44. 15
      samples/EShopSample/aspnet-core/src/EShopSample.EntityFrameworkCore/Migrations/EShopSampleDbContextModelSnapshot.cs

2
common.props

@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<LangVersion>latest</LangVersion>
<Version>4.3.0</Version>
<Version>5.0.0-preview.1</Version>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<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 decimal? PaymentAmount { get; set; }
public DateTime? CompletionTime { 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 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 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",
"OrderStaffRemark": "Staff remark",
"OrderPaidTime": "Paid time",
"OrderPaymentAmount": "Payment amount",
"OrderCompletionTime": "Completion time",
"OrderCanceledTime": "Canceled time",
"OrderReducedInventoryAfterPlacingTime": "Time of inventory reduced after placing",
@ -45,6 +46,9 @@
"OrderLineTotalDiscount": "Total discount",
"OrderLineActualTotalPrice": "Actual total price",
"OrderLineQuantity": "Quantity",
"OrderLineRefundedQuantity": "Refunded quantity",
"OrderLineRefundAmount": "Refund amount",
"OrderLinePaymentAmount": "Payment amount",
"OrderDiscount": "Order discount",
"OrderDiscountOrderId": "Order ID",
"OrderDiscountOrderLineId": "Order line ID",
@ -60,6 +64,7 @@
"OrderExtraFeeDisplayName": "Display name",
"OrderExtraFeeFee": "Fee",
"OrderExtraFeeRefundAmount": "Refund amount",
"OrderExtraFeePaymentAmount": "Payment amount",
"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: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": "客户备注",
"OrderStaffRemark": "员工备注",
"OrderPaidTime": "支付时间",
"OrderPaymentAmount": "支付金额",
"OrderCompletionTime": "完成时间",
"OrderCanceledTime": "取消时间",
"OrderReducedInventoryAfterPlacingTime": "下单后减库存时间",
@ -45,6 +46,9 @@
"OrderLineTotalDiscount": "总折扣",
"OrderLineActualTotalPrice": "折后总价",
"OrderLineQuantity": "数量",
"OrderLineRefundedQuantity": "已退款的数量",
"OrderLineRefundAmount": "已退款金额",
"OrderLinePaymentAmount": "支付金额",
"OrderDiscount": "订单折扣项",
"OrderDiscountOrderId": "订单 ID",
"OrderDiscountOrderLineId": "订单项 ID",
@ -60,6 +64,7 @@
"OrderExtraFeeDisplayName": "显示名称",
"OrderExtraFeeFee": "金额",
"OrderExtraFeeRefundAmount": "已退款金额",
"OrderExtraFeePaymentAmount": "支付金额",
"EasyAbp.EShop.Orders:UnexpectedCurrency": "只允许指定的{expectedCurrency}货币",
"EasyAbp.EShop.Orders:OrderLineInvalidQuantity": "产品{productId}(SKU: {productSkuId})的{quantity}数量无效",
"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": "客戶備註",
"OrderStaffRemark": "員工備註",
"OrderPaidTime": "支付時間",
"OrderPaymentAmount": "支付金額",
"OrderCompletionTime": "完成時間",
"OrderCanceledTime": "取消時間",
"OrderReducedInventoryAfterPlacingTime": "下單後減庫存時間",
@ -45,6 +46,9 @@
"OrderLineTotalDiscount": "總折扣",
"OrderLineActualTotalPrice": "折後總價",
"OrderLineQuantity": "數量",
"OrderLineRefundedQuantity": "已退款的數量",
"OrderLineRefundAmount": "已退款金額",
"OrderLinePaymentAmount": "支付金額",
"OrderDiscount": "訂單折扣項",
"OrderDiscountOrderId": "訂單 ID",
"OrderDiscountOrderLineId": "訂單項 ID",
@ -60,6 +64,7 @@
"OrderExtraFeeDisplayName": "顯示名稱",
"OrderExtraFeeFee": "金額",
"OrderExtraFeeRefundAmount": "已退款金額",
"OrderExtraFeePaymentAmount": "支付金額",
"EasyAbp.EShop.Orders:UnexpectedCurrency": "只允許指定的{expectedCurrency}貨幣",
"EasyAbp.EShop.Orders:OrderLineInvalidQuantity": "產品{productId}(SKU: {productSkuId})的{quantity}數量無效",
"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; }
decimal? PaymentAmount { get; }
DateTime? CompletionTime { 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 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; }
decimal RefundAmount { get; }
decimal? PaymentAmount { get; }
}
}

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

@ -42,6 +42,8 @@ namespace EasyAbp.EShop.Orders.Orders
public DateTime? PaidTime { get; set; }
public decimal? PaymentAmount { get; set; }
public DateTime? CompletionTime { get; set; }
public DateTime? CanceledTime { 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 RefundAmount { get; set; }
public decimal? PaymentAmount { get; set; }
}
}

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

@ -50,5 +50,7 @@ namespace EasyAbp.EShop.Orders.Orders
public int RefundedQuantity { 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 />
</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>
<PackageReference Include="NodaMoney" Version="$(NodaMoneyVersion)" />
<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));
}
}

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

@ -1,9 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using EasyAbp.EShop.Products.Products;
using System.Threading.Tasks;
using JetBrains.Annotations;
using NodaMoney;
using Volo.Abp.Domain.Entities.Auditing;
using Volo.Abp.MultiTenancy;
@ -41,6 +40,8 @@ namespace EasyAbp.EShop.Orders.Orders
public virtual DateTime? PaidTime { get; protected set; }
public virtual decimal? PaymentAmount { get; protected set; }
public virtual DateTime? CompletionTime { 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? tenantId,
Guid storeId,
@ -99,55 +100,103 @@ namespace EasyAbp.EShop.Orders.Orders
OrderExtraFees = new List<OrderExtraFee>();
}
public void SetOrderNumber([NotNull] string orderNumber)
internal void SetOrderNumber([NotNull] string orderNumber)
{
OrderNumber = orderNumber;
}
public void SetOrderLines(List<OrderLine> orderLines)
internal void SetOrderLines(List<OrderLine> orderLines)
{
OrderLines = orderLines;
}
public void SetReducedInventoryAfterPlacingTime(DateTime? time)
internal void SetReducedInventoryAfterPlacingTime(DateTime time)
{
if (ReducedInventoryAfterPlacingTime.HasValue)
{
throw new OrderIsInWrongStageException(Id);
}
ReducedInventoryAfterPlacingTime = time;
}
public void SetReducedInventoryAfterPaymentTime(DateTime? time)
internal void SetReducedInventoryAfterPaymentTime(DateTime time)
{
if (ReducedInventoryAfterPaymentTime.HasValue)
{
throw new OrderIsInWrongStageException(Id);
}
ReducedInventoryAfterPaymentTime = time;
}
public void SetPaymentExpiration(DateTime? paymentExpiration)
internal async Task StartPaymentAsync(Guid paymentId, decimal? paymentAmount, IMoneyDistributor distributor)
{
PaymentExpiration = paymentExpiration;
PaymentId = paymentId;
PaymentAmount = paymentAmount;
if (paymentAmount is null)
{
// PaymentAmount is always null before EShop v5
return;
}
public void SetPaymentId(Guid? paymentId)
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)
{
PaymentId = paymentId;
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;
OrderStatus = OrderStatus.Completed;
}
public void SetCanceled(DateTime canceledTime, [CanBeNull] string cancellationReason)
internal void SetCanceled(DateTime canceledTime, [CanBeNull] string cancellationReason, bool forceCancel)
{
CanceledTime = canceledTime;
CancellationReason = cancellationReason;
OrderStatus = OrderStatus.Canceled;
}
public bool IsCanceled()
{
return CanceledTime.HasValue;
}
public bool IsPaid()
@ -155,9 +204,16 @@ namespace EasyAbp.EShop.Orders.Orders
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);
}
@ -169,9 +225,16 @@ namespace EasyAbp.EShop.Orders.Orders
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);
}
@ -185,14 +248,19 @@ namespace EasyAbp.EShop.Orders.Orders
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)
{
var orderLine = OrderLines.Single(x => x.Id == orderLineId);
throw new OrderIsInWrongStageException(Id);
}
foreach (var (o, discountAmount) in model.Distributions)
{
var orderLine = OrderLines.Single(x => x.Id == o.Id);
orderLine.AddDiscount(discountAmount);
@ -205,14 +273,14 @@ namespace EasyAbp.EShop.Orders.Orders
}
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))
{
throw new DuplicateOrderDiscountException(orderLineId, model.DiscountInfoModel.Name,
throw new DuplicateOrderDiscountException(orderLine.Id, model.DiscountInfoModel.Name,
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,
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)
{
if (OrderStatus != OrderStatus.Pending)
{
throw new OrderIsInWrongStageException(Id);
}
if (extraFee <= decimal.Zero)
{
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.Linq;
using Volo.Abp;
@ -9,18 +8,23 @@ public class OrderDiscountDistributionModel
{
public OrderDiscountInfoModel DiscountInfoModel { get; set; }
public Dictionary<IOrderLine, decimal> CurrentTotalPrices { get; set; }
/// <summary>
/// OrderLine to discount amount mapping.
/// </summary>
public Dictionary<Guid, decimal> Distributions { get; set; }
public Dictionary<IOrderLine, decimal> Distributions { get; set; }
public OrderDiscountDistributionModel(OrderDiscountInfoModel discountInfoModel,
Dictionary<Guid, decimal> distributions)
public OrderDiscountDistributionModel(
OrderDiscountInfoModel discountInfoModel,
Dictionary<IOrderLine, decimal> currentTotalPrices,
Dictionary<IOrderLine, decimal> distributions)
{
DiscountInfoModel = Check.NotNull(discountInfoModel, nameof(discountInfoModel));
CurrentTotalPrices = Check.NotNull(currentTotalPrices, nameof(currentTotalPrices));
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.");
}

65
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.Linq;
using System.Threading.Tasks;
using EasyAbp.EShop.Products.Products;
using NodaMoney;
using Volo.Abp.DependencyInjection;
@ -10,58 +8,37 @@ namespace EasyAbp.EShop.Orders.Orders;
public class OrderDiscountDistributor : IOrderDiscountDistributor, ITransientDependency
{
public virtual Task<OrderDiscountDistributionModel> DistributeAsync(IOrder order,
Dictionary<IOrderLine, decimal> currentTotalPrices, OrderDiscountInfoModel discount)
{
var affectedOrderLines = discount.AffectedOrderLineIds
.Select(orderLineId => order.OrderLines.Single(x => x.Id == orderLineId))
.ToList();
var affectedOrderLinesCurrentTotalPrice =
new Money(affectedOrderLines.Sum(x => currentTotalPrices[x]), order.Currency);
protected IMoneyDistributor MoneyDistributor { get; }
var totalDiscountAmount =
discount.CalculateDiscountAmount(affectedOrderLinesCurrentTotalPrice.Amount, order.Currency);
var distributions = new Dictionary<Guid, decimal>();
var remainingDiscountAmount = totalDiscountAmount;
foreach (var orderLine in affectedOrderLines)
public OrderDiscountDistributor(IMoneyDistributor moneyDistributor)
{
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;
currentTotalPrices[orderLine] -= discountAmount;
remainingDiscountAmount -= discountAmount;
MoneyDistributor = moneyDistributor;
}
foreach (var orderLine in affectedOrderLines.OrderByDescending(x => currentTotalPrices[x]))
{
if (remainingDiscountAmount == decimal.Zero)
public virtual async Task<OrderDiscountDistributionModel> DistributeAsync(IOrder order,
Dictionary<IOrderLine, decimal> currentTotalPrices, OrderDiscountInfoModel discount)
{
break;
}
var affectedCurrentTotalPrices = discount.AffectedOrderLineIds
.Select(orderLineId => order.OrderLines.Single(x => x.Id == orderLineId))
.ToDictionary(x => x, x => currentTotalPrices[x]);
var discountAmount = remainingDiscountAmount > currentTotalPrices[orderLine]
? currentTotalPrices[orderLine]
: remainingDiscountAmount;
var affectedPriceSum = new Money(affectedCurrentTotalPrices.Sum(x => x.Value), order.Currency);
distributions[orderLine.Id] += discountAmount;
currentTotalPrices[orderLine] -= discountAmount;
remainingDiscountAmount -= discountAmount;
}
var totalDiscountAmount = discount.CalculateDiscountAmount(affectedPriceSum.Amount, order.Currency);
var result = await MoneyDistributor.DistributeAsync(
order.Currency, affectedCurrentTotalPrices, -totalDiscountAmount);
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 =
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));

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

@ -21,6 +21,8 @@ namespace EasyAbp.EShop.Orders.Orders
public virtual decimal RefundAmount { get; protected set; }
public virtual decimal? PaymentAmount { get; protected set; }
protected OrderExtraFee()
{
}
@ -41,9 +43,21 @@ namespace EasyAbp.EShop.Orders.Orders
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;
}
internal void SetPaymentAmount(decimal? paymentAmount)
{
PaymentAmount = paymentAmount;
}
public override object[] GetKeys()
{
return new object[] { OrderId, Name, Key };

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

@ -51,6 +51,8 @@ namespace EasyAbp.EShop.Orders.Orders
public virtual decimal RefundAmount { get; protected set; }
public virtual decimal? PaymentAmount { get; protected set; }
public ExtraPropertyDictionary ExtraProperties { get; protected set; }
protected OrderLine()
@ -115,6 +117,13 @@ namespace EasyAbp.EShop.Orders.Orders
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;
RefundAmount += amount;
}
@ -129,5 +138,10 @@ namespace EasyAbp.EShop.Orders.Orders
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]
public virtual async Task<Order> CompleteAsync(Order order)
{
if (order.CompletionTime.HasValue || !order.ReducedInventoryAfterPaymentTime.HasValue)
if (order.CompletionTime.HasValue)
{
throw new OrderIsInWrongStageException(order.Id);
}
@ -46,8 +46,7 @@ namespace EasyAbp.EShop.Orders.Orders
await provider.CheckAsync(order);
}
order.SetCompletionTime(_clock.Now);
order.SetOrderStatus(OrderStatus.Completed);
order.Complete(_clock.Now);
await _orderRepository.UpdateAsync(order, true);
@ -59,7 +58,7 @@ namespace EasyAbp.EShop.Orders.Orders
[UnitOfWork]
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);
}
@ -69,8 +68,7 @@ namespace EasyAbp.EShop.Orders.Orders
throw new OrderIsInWrongStageException(order.Id);
}
order.SetCanceled(_clock.Now, cancellationReason);
order.SetOrderStatus(OrderStatus.Canceled);
order.SetCanceled(_clock.Now, cancellationReason, forceCancel);
await _orderRepository.UpdateAsync(order, true);

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

@ -33,12 +33,12 @@ namespace EasyAbp.EShop.Orders.Orders
{
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;
}
order.SetPaymentId(null);
order.CancelPayment();
// OrderAutoCancelOnUpdatedHandler may auto cancel the unpaid order.
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);
}
order.SetPaidTime(_clock.Now);
order.SetOrderStatus(OrderStatus.Processing);
order.SetPaid(_clock.Now);
await _orderRepository.UpdateAsync(order, true);

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

@ -15,15 +15,18 @@ namespace EasyAbp.EShop.Orders.Orders
private readonly ICurrentTenant _currentTenant;
private readonly IOrderPaymentChecker _orderPaymentChecker;
private readonly IOrderRepository _orderRepository;
private readonly IMoneyDistributor _moneyDistributor;
public PaymentCreatedEventHandler(
ICurrentTenant currentTenant,
IOrderPaymentChecker orderPaymentChecker,
IOrderRepository orderRepository)
IOrderRepository orderRepository,
IMoneyDistributor moneyDistributor)
{
_currentTenant = currentTenant;
_orderPaymentChecker = orderPaymentChecker;
_orderRepository = orderRepository;
_moneyDistributor = moneyDistributor;
}
[UnitOfWork(true)]
@ -31,7 +34,8 @@ namespace EasyAbp.EShop.Orders.Orders
{
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);
@ -49,7 +53,8 @@ namespace EasyAbp.EShop.Orders.Orders
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);
}

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

@ -14,7 +14,6 @@ using NSubstitute;
using Shouldly;
using Volo.Abp;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Settings;
using Volo.Abp.Timing;
using Xunit;
@ -24,6 +23,7 @@ namespace EasyAbp.EShop.Orders.Orders
{
private readonly IClock _clock;
private readonly IOrderAppService _orderAppService;
private readonly IMoneyDistributor _moneyDistributor;
private ProductDto Product1 { get; set; }
@ -31,6 +31,7 @@ namespace EasyAbp.EShop.Orders.Orders
{
_clock = GetRequiredService<IClock>();
_orderAppService = GetRequiredService<IOrderAppService>();
_moneyDistributor = GetRequiredService<IMoneyDistributor>();
}
protected override void AfterAddApplication(IServiceCollection services)
@ -243,10 +244,10 @@ namespace EasyAbp.EShop.Orders.Orders
order.StaffRemark.ShouldBeNullOrEmpty();
order.StoreId.ShouldBe(OrderTestData.Store1Id);
order.TotalDiscount.ShouldBe(0m);
order.TotalPrice.ShouldBe(12m);
order.ActualTotalPrice.ShouldBe(12m);
order.TotalPrice.ShouldBe(order.ActualTotalPrice);
order.ActualTotalPrice.ShouldBe(order.ActualTotalPrice);
order.CustomerUserId.ShouldBe(Guid.Parse("2e701e62-0953-4dd3-910b-dc6cc93ccb0d"));
order.ProductTotalPrice.ShouldBe(12m);
order.ProductTotalPrice.ShouldBe(order.ActualTotalPrice);
order.ReducedInventoryAfterPaymentTime.ShouldBeNull();
order.ReducedInventoryAfterPlacingTime.ShouldNotBeNull();
order.OrderLines.Count.ShouldBe(2);
@ -288,7 +289,7 @@ namespace EasyAbp.EShop.Orders.Orders
order.OrderStatus.ShouldNotBe(OrderStatus.Completed);
order.CompletionTime.ShouldBeNull();
orderId = order.Id;
order.SetPaidTime(DateTime.Now);
order.SetPaid(DateTime.Now);
order.SetReducedInventoryAfterPaymentTime(DateTime.Now);
db.SaveChanges();
});
@ -345,12 +346,12 @@ namespace EasyAbp.EShop.Orders.Orders
// Arrange
await Order_Should_Be_Created();
Guid orderId = Guid.Empty;
UsingDbContext(db =>
UsingDbContext(async db =>
{
var order = db.Orders.First();
orderId = order.Id;
order.SetPaymentId(Guid.NewGuid());
db.SaveChanges();
await order.StartPaymentAsync(Guid.NewGuid(), order.ActualTotalPrice, _moneyDistributor);
await db.SaveChangesAsync();
});
// Act
@ -360,8 +361,8 @@ namespace EasyAbp.EShop.Orders.Orders
{
var orderRepository = ServiceProvider.GetRequiredService<IOrderRepository>();
var order = await orderRepository.GetAsync(orderId);
order.SetPaymentExpiration(now);
order.SetPaymentId(null);
typeof(Order).GetProperty(nameof(Order.PaymentExpiration))!.SetValue(order, now);
order.CancelPayment();
await orderRepository.UpdateAsync(order, true);
});
@ -393,28 +394,28 @@ namespace EasyAbp.EShop.Orders.Orders
// Arrange
await Order_Should_Be_Created();
Guid orderId = Guid.Empty;
UsingDbContext(db =>
await WithUnitOfWorkAsync(async () =>
{
var order = db.Orders.First();
orderId = order.Id;
order.SetPaymentId(Guid.NewGuid());
order.SetPaidTime(_clock.Now);
order.SetOrderStatus(OrderStatus.Processing);
db.SaveChanges();
var orderRepository = ServiceProvider.GetRequiredService<IOrderRepository>();
orderId = await (await orderRepository.GetQueryableAsync()).Select(x => x.Id).FirstAsync();
var order = await orderRepository.GetAsync(orderId);
await order.StartPaymentAsync(Guid.NewGuid(), order.ActualTotalPrice, _moneyDistributor);
order.SetPaid(_clock.Now);
await orderRepository.UpdateAsync(order, true);
});
// Act
var now = _clock.Now;
UsingDbContext(async db =>
await WithUnitOfWorkAsync(async () =>
{
var orderRepository = ServiceProvider.GetRequiredService<IOrderRepository>();
var order = await orderRepository.GetAsync(orderId);
order.SetPaymentExpiration(now);
typeof(Order).GetProperty(nameof(Order.PaymentExpiration))!.SetValue(order, now);
await orderRepository.UpdateAsync(order, true);
});
UsingDbContext(async db =>
await WithUnitOfWorkAsync(async () =>
{
var backgroundJob = ServiceProvider.GetRequiredService<UnpaidOrderAutoCancelJob>();
await backgroundJob.ExecuteAsync(new UnpaidOrderAutoCancelArgs
@ -465,7 +466,7 @@ namespace EasyAbp.EShop.Orders.Orders
{
var orderRepository = ServiceProvider.GetRequiredService<IOrderRepository>();
var order = await orderRepository.GetAsync(orderId);
order.SetPaymentExpiration(now);
typeof(Order).GetProperty(nameof(Order.PaymentExpiration))!.SetValue(order, now);
await orderRepository.UpdateAsync(order, true);
});
@ -507,12 +508,12 @@ namespace EasyAbp.EShop.Orders.Orders
// Arrange
await Order_Should_Be_Created();
Guid orderId = Guid.Empty;
UsingDbContext(db =>
UsingDbContext(async db =>
{
var order = db.Orders.First();
orderId = order.Id;
order.SetPaymentId(Guid.NewGuid());
db.SaveChanges();
await order.StartPaymentAsync(Guid.NewGuid(), order.ActualTotalPrice, _moneyDistributor);
await db.SaveChangesAsync();
});
// Act
@ -522,7 +523,7 @@ namespace EasyAbp.EShop.Orders.Orders
{
var orderRepository = ServiceProvider.GetRequiredService<IOrderRepository>();
var order = await orderRepository.GetAsync(orderId);
order.SetPaymentExpiration(now);
typeof(Order).GetProperty(nameof(Order.PaymentExpiration))!.SetValue(order, now);
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.CancellationReason))!.SetValue(Order1, null);
Order1.SetReducedInventoryAfterPlacingTime(null);
Order1.SetReducedInventoryAfterPaymentTime(null);
Order1.SetOrderStatus(OrderStatus.Pending);
Order1.SetPaymentId(null);
Order1.SetPaidTime(null);
typeof(Order).GetProperty(nameof(Order.ReducedInventoryAfterPlacingTime))!.SetValue(Order1, null);
typeof(Order).GetProperty(nameof(Order.ReducedInventoryAfterPaymentTime))!.SetValue(Order1, null);
typeof(Order).GetProperty(nameof(Order.OrderStatus))!.SetValue(Order1, OrderStatus.Pending);
typeof(Order).GetProperty(nameof(Order.PaidTime))!.SetValue(Order1, null);
Order1.CancelPayment();
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.CancellationReason))!.SetValue(Order1, null);
Order1.SetReducedInventoryAfterPlacingTime(null);
Order1.SetReducedInventoryAfterPaymentTime(null);
Order1.SetOrderStatus(OrderStatus.Pending);
Order1.SetPaymentId(null);
Order1.SetPaidTime(null);
typeof(Order).GetProperty(nameof(Order.ReducedInventoryAfterPlacingTime))!.SetValue(Order1, null);
typeof(Order).GetProperty(nameof(Order.ReducedInventoryAfterPaymentTime))!.SetValue(Order1, null);
typeof(Order).GetProperty(nameof(Order.OrderStatus))!.SetValue(Order1, OrderStatus.Pending);
typeof(Order).GetProperty(nameof(Order.PaidTime))!.SetValue(Order1, null);
Order1.CancelPayment();
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.CancellationReason))!.SetValue(Order1, null);
Order1.SetReducedInventoryAfterPlacingTime(DateTime.Now);
Order1.SetReducedInventoryAfterPaymentTime(null);
Order1.SetOrderStatus(OrderStatus.Processing);
Order1.SetPaymentId(OrderTestData.Payment1Id);
Order1.SetPaidTime(DateTime.Now);
typeof(Order).GetProperty(nameof(Order.ReducedInventoryAfterPaymentTime))!.SetValue(Order1, null);
await Order1.StartPaymentAsync(OrderTestData.Payment1Id, Order1.ActualTotalPrice,
GetRequiredService<IMoneyDistributor>());
Order1.SetPaid(DateTime.Now);
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.CancellationReason))!.SetValue(Order1, null);
Order1.SetReducedInventoryAfterPlacingTime(DateTime.Now);
Order1.SetReducedInventoryAfterPaymentTime(null);
Order1.SetOrderStatus(OrderStatus.Processing);
Order1.SetPaymentId(OrderTestData.Payment1Id);
Order1.SetPaidTime(DateTime.Now);
typeof(Order).GetProperty(nameof(Order.ReducedInventoryAfterPaymentTime))!.SetValue(Order1, null);
await Order1.StartPaymentAsync(OrderTestData.Payment1Id, Order1.ActualTotalPrice,
GetRequiredService<IMoneyDistributor>());
Order1.SetPaid(DateTime.Now);
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 readonly IOrderRepository _orderRepository;
private readonly IMoneyDistributor _moneyDistributor;
public OrderDomainTests()
{
_orderRepository = ServiceProvider.GetRequiredService<IOrderRepository>();
_moneyDistributor = ServiceProvider.GetRequiredService<IMoneyDistributor>();
}
protected override void AfterAddApplication(IServiceCollection services)
@ -33,8 +35,8 @@ namespace EasyAbp.EShop.Orders.Orders
"USD",
1m,
0m,
1.5m,
1.5m,
1.36m,
1.36m,
null,
null);
Order1.OrderLines.Add(new OrderLine(
@ -53,10 +55,10 @@ namespace EasyAbp.EShop.Orders.Orders
null,
null,
"USD",
0.5m,
1m,
0.53m,
1.06m,
0m,
1m,
1.06m,
2
));
Order1.OrderExtraFees.Add(new OrderExtraFee(
@ -77,6 +79,10 @@ namespace EasyAbp.EShop.Orders.Orders
{
var handler = ServiceProvider.GetRequiredService<RefundCompletedEventHandler>();
await Order1.StartPaymentAsync(OrderTestData.Payment1Id, Order1.ActualTotalPrice,
_moneyDistributor);
Order1.SetPaid(DateTime.Now);
await handler.HandleEventAsync(new EShopRefundCompletedEto
{
Refund = new EShopRefundEto
@ -110,6 +116,7 @@ namespace EasyAbp.EShop.Orders.Orders
{
Name = "Name",
Key = "Key",
DisplayName = "DisplayName",
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);
var orderLine1 = Order1.OrderLines.Single(x => x.Id == OrderTestData.OrderLine1Id);
orderLine1.RefundAmount.ShouldBe(0.2m);
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);
}
@ -136,8 +141,9 @@ namespace EasyAbp.EShop.Orders.Orders
{
var handler = ServiceProvider.GetRequiredService<RefundCompletedEventHandler>();
Order1.SetPaymentId(OrderTestData.Payment1Id);
Order1.SetPaidTime(DateTime.Now);
await Order1.StartPaymentAsync(OrderTestData.Payment1Id, Order1.ActualTotalPrice,
_moneyDistributor);
Order1.SetPaid(DateTime.Now);
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]
public async Task Should_Avoid_Over_Quantity_Refund()
{
var handler = ServiceProvider.GetRequiredService<RefundCompletedEventHandler>();
Order1.SetPaymentId(OrderTestData.Payment1Id);
Order1.SetPaidTime(DateTime.Now);
await Order1.StartPaymentAsync(OrderTestData.Payment1Id, Order1.ActualTotalPrice,
_moneyDistributor);
Order1.SetPaid(DateTime.Now);
await Should.ThrowAsync<InvalidRefundQuantityException>(async () =>
{
@ -193,14 +235,14 @@ namespace EasyAbp.EShop.Orders.Orders
TenantId = null,
PaymentId = OrderTestData.Payment1Id,
Currency = "USD",
RefundAmount = 0.3m,
RefundAmount = 0.2m,
RefundItems = new List<EShopRefundItemEto>
{
new()
{
Id = Guid.NewGuid(),
PaymentItemId = Guid.NewGuid(),
RefundAmount = 0.3m,
RefundAmount = 0.2m,
StoreId = OrderTestData.Store1Id,
OrderId = OrderTestData.Order1Id,
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]
public async Task Should_Forbid_Canceling_Order_During_Payment_State()
{
var orderManager = ServiceProvider.GetRequiredService<IOrderManager>();
var order = await _orderRepository.GetAsync(OrderTestData.Order1Id);
order.SetPaymentId(Guid.NewGuid());
order.SetPaidTime(null);
await Order1.StartPaymentAsync(Guid.NewGuid(), Order1.ActualTotalPrice, _moneyDistributor);
order.PaidTime.ShouldBeNull();
await Should.ThrowAsync<OrderIsInWrongStageException>(() => orderManager.CancelAsync(order, "my-reason"));
}
@ -236,13 +359,14 @@ namespace EasyAbp.EShop.Orders.Orders
var orderManager = ServiceProvider.GetRequiredService<IOrderManager>();
var order = await _orderRepository.GetAsync(OrderTestData.Order1Id);
order.SetReducedInventoryAfterPlacingTime(null);
order.ReducedInventoryAfterPlacingTime.ShouldBeNull();
await Should.ThrowAsync<OrderIsInWrongStageException>(() => orderManager.CancelAsync(order, "my-reason"));
order.SetReducedInventoryAfterPlacingTime(DateTime.Now);
order.SetPaymentId(Guid.NewGuid());
order.SetPaidTime(DateTime.Now);
order.SetReducedInventoryAfterPlacingTime(null);
await order.StartPaymentAsync(Guid.NewGuid(), order.ActualTotalPrice, _moneyDistributor);
order.SetPaid(DateTime.Now);
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"));
}
}

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]
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)
{

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);
}
}
}

22
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);
// todo: needs a lock.
var payment = await _paymentRepository.GetAsync(input.PaymentId);
if (payment.PendingRefundAmount != decimal.Zero)
@ -117,7 +118,7 @@ namespace EasyAbp.EShop.Payments.Refunds
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)
@ -129,6 +130,14 @@ namespace EasyAbp.EShop.Payments.Refunds
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)
{
throw new InvalidRefundQuantityException(model.Quantity);
@ -143,6 +152,14 @@ namespace EasyAbp.EShop.Payments.Refunds
{
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
@ -156,7 +173,8 @@ namespace EasyAbp.EShop.Payments.Refunds
eto.SetProperty(nameof(RefundItem.StoreId), order.StoreId);
eto.SetProperty(nameof(RefundItem.OrderId), order.Id);
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);
}

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: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: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: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})",

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:OrderIsNotInSpecifiedPayment": "订单({orderId})不在指定的支付({paymentId})中",
"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:StoreIdNotFound": "无法从ExtraProperties获得有效的StoreId",
"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:OrderIsNotInSpecifiedPayment": "訂單({orderId})不在指定的支付({paymentId})中",
"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:StoreIdNotFound": "無法從ExtraProperties獲得有效的StoreId",
"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 OrderIsNotInSpecifiedPayment = "EasyAbp.EShop.Payments:OrderIsNotInSpecifiedPayment";
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 StoreIdNotFound = "EasyAbp.EShop.Payments:StoreIdNotFound";
public const string OrderLineNotFound = "EasyAbp.EShop.Payments:OrderLineNotFound";

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

@ -43,19 +43,24 @@ namespace EasyAbp.EShop.Payments.Refunds
var paymentItem = Activator.CreateInstance(paymentItemType, true) as PaymentItem;
paymentItem.ShouldNotBeNull();
paymentItemType.GetProperty(nameof(PaymentItem.Id))?.SetValue(paymentItem, PaymentsTestData.PaymentItem1);
paymentItemType.GetProperty(nameof(PaymentItem.ActualPaymentAmount))?.SetValue(paymentItem, 1m);
paymentItemType.GetProperty(nameof(PaymentItem.ItemType))?.SetValue(paymentItem, PaymentsConsts.PaymentItemType);
paymentItemType.GetProperty(nameof(PaymentItem.ItemKey))?.SetValue(paymentItem, PaymentsTestData.Order1.ToString());
paymentItemType.GetProperty(nameof(PaymentItem.StoreId))?.SetValue(paymentItem, PaymentsTestData.Store1);
paymentItemType.GetProperty(nameof(PaymentItem.Id))
?.SetValue(paymentItem, PaymentsTestData.PaymentItem1);
paymentItemType.GetProperty(nameof(PaymentItem.ActualPaymentAmount))?.SetValue(paymentItem, 5m);
paymentItemType.GetProperty(nameof(PaymentItem.ItemType))
?.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);
var payment = Activator.CreateInstance(paymentType, true) as Payment;
payment.ShouldNotBeNull();
paymentType.GetProperty(nameof(Payment.Id))?.SetValue(payment, PaymentsTestData.Payment1);
paymentType.GetProperty(nameof(Payment.Currency))?.SetValue(payment, "USD");
paymentType.GetProperty(nameof(Payment.ActualPaymentAmount))?.SetValue(payment, 1m);
paymentType.GetProperty(nameof(Payment.PaymentItems))?.SetValue(payment, new List<PaymentItem> {paymentItem});
paymentType.GetProperty(nameof(Payment.ActualPaymentAmount))?.SetValue(payment, 5m);
paymentType.GetProperty(nameof(Payment.PaymentItems))
?.SetValue(payment, new List<PaymentItem> { paymentItem });
return payment;
};
@ -109,7 +114,8 @@ namespace EasyAbp.EShop.Payments.Refunds
{
Id = PaymentsTestData.Order1,
Currency = "USD",
ActualTotalPrice = 0,
ActualTotalPrice = 6m,
PaymentAmount = 5m,
StoreId = PaymentsTestData.Store1,
OrderLines = new List<OrderLineDto>
{
@ -118,6 +124,7 @@ namespace EasyAbp.EShop.Payments.Refunds
Id = PaymentsTestData.OrderLine1,
Currency = "USD",
ActualTotalPrice = 1m,
PaymentAmount = 0.83m,
Quantity = 1
}
},
@ -127,7 +134,9 @@ namespace EasyAbp.EShop.Payments.Refunds
{
Name = "Name",
Key = "Key",
Fee = 5m
DisplayName = "DisplayName",
Fee = 5m,
PaymentAmount = 4.17m
}
},
PaymentId = PaymentsTestData.Payment1
@ -176,6 +185,7 @@ namespace EasyAbp.EShop.Payments.Refunds
{
Name = "Name",
Key = "Key",
DisplayName = "DisplayName",
TotalAmount = 0.6m
}
}
@ -210,6 +220,7 @@ namespace EasyAbp.EShop.Payments.Refunds
orderExtraFees.Count.ShouldBe(1);
orderExtraFees[0].Name.ShouldBe("Name");
orderExtraFees[0].Key.ShouldBe("Key");
orderExtraFees[0].DisplayName.ShouldBe("DisplayName");
orderExtraFees[0].TotalAmount.ShouldBe(0.6m);
}
@ -237,17 +248,18 @@ namespace EasyAbp.EShop.Payments.Refunds
};
// Act & Assert
await Should.ThrowAsync<AbpValidationException>(async () =>
(await Should.ThrowAsync<AbpValidationException>(async () =>
{
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]
public async Task Should_Avoid_Over_Refund()
public async Task Should_Avoid_Over_Amount_Refund()
{
// Arrange
var request = new CreateEShopRefundInput
var request1 = new CreateEShopRefundInput
{
DisplayReason = "Reason",
CustomerRemark = "Customer Remark",
@ -266,16 +278,61 @@ namespace EasyAbp.EShop.Payments.Refunds
{
OrderLineId = PaymentsTestData.OrderLine1,
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>
{
new()
{
Name = "Name",
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)
}
}
}
@ -283,7 +340,54 @@ namespace EasyAbp.EShop.Payments.Refunds
};
// Act & Assert
await Should.ThrowAsync<InvalidRefundAmountException>(async () =>
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
}
}
}
}
};
// Act & Assert
await Should.ThrowAsync<InvalidRefundQuantityException>(async () =>
{
await _refundAppService.CreateAsync(request);
});
@ -308,7 +412,7 @@ namespace EasyAbp.EShop.Payments.Refunds
StaffRemark = "StaffRemark",
OrderLines = new List<OrderLineRefundInfoModel>
{
new OrderLineRefundInfoModel
new()
{
OrderLineId = PaymentsTestData.OrderLine1,
Quantity = 1,
@ -327,7 +431,7 @@ namespace EasyAbp.EShop.Payments.Refunds
}
[Fact]
public async Task Should_Check_OrderLines_Exist_When_Refunding()
public Task Should_Check_OrderLines_Exist_When_Refunding()
{
// Arrange
var request = new CreateEShopRefundInput
@ -355,6 +459,8 @@ namespace EasyAbp.EShop.Payments.Refunds
}
}
};
return Task.CompletedTask;
}
[Fact]
@ -380,6 +486,7 @@ namespace EasyAbp.EShop.Payments.Refunds
{
Name = "FakeName",
Key = "FakeKey",
DisplayName = "FakeDisplayName",
TotalAmount = 0.6m
}
}
@ -413,7 +520,7 @@ namespace EasyAbp.EShop.Payments.Refunds
StaffRemark = "StaffRemark",
OrderLines = new List<OrderLineRefundInfoModel>
{
new OrderLineRefundInfoModel
new()
{
OrderLineId = PaymentsTestData.OrderLine1,
Quantity = 1,
@ -425,10 +532,10 @@ namespace EasyAbp.EShop.Payments.Refunds
};
// Act & Assert
await Should.ThrowAsync<AbpValidationException>(async () =>
(await Should.ThrowAsync<AbpValidationException>(async () =>
{
await _refundAppService.CreateAsync(request);
}, "RefundAmount should be greater than 0.");
})).ValidationErrors[0].ErrorMessage.ShouldBe("RefundAmount should be greater than 0.");
}
[Fact]
@ -450,7 +557,8 @@ namespace EasyAbp.EShop.Payments.Refunds
refundItem.SetProperty(nameof(RefundItem.StoreId), PaymentsTestData.Store1);
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()
{
@ -459,12 +567,14 @@ namespace EasyAbp.EShop.Payments.Refunds
TotalAmount = 1m
}
}));
refundItem.SetProperty(nameof(RefundItem.OrderExtraFees), _jsonSerializer.Serialize(new List<OrderExtraFeeRefundInfoModel>
refundItem.SetProperty(nameof(RefundItem.OrderExtraFees), _jsonSerializer.Serialize(
new List<OrderExtraFeeRefundInfoModel>
{
new()
{
Name = "Name",
Key = "Key",
DisplayName = "DisplayName",
TotalAmount = 0.5m
}
}));
@ -517,6 +627,7 @@ namespace EasyAbp.EShop.Payments.Refunds
var orderExtraFee = refundItemDto.OrderExtraFees.First();
orderExtraFee.Name.ShouldBe("Name");
orderExtraFee.Key.ShouldBe("Key");
orderExtraFee.DisplayName.ShouldBe("DisplayName");
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")
.HasColumnType("datetime2");
b.Property<decimal?>("PaymentAmount")
.HasColumnType("decimal(18,2)");
b.Property<DateTime?>("PaymentExpiration")
.HasColumnType("datetime2");
@ -652,6 +655,9 @@ namespace EShopSample.Migrations
b.Property<decimal>("Fee")
.HasColumnType("decimal(20,8)");
b.Property<decimal?>("PaymentAmount")
.HasColumnType("decimal(18,2)");
b.Property<decimal>("RefundAmount")
.HasColumnType("decimal(20,8)");
@ -712,6 +718,9 @@ namespace EShopSample.Migrations
b.Property<Guid?>("OrderId")
.HasColumnType("uniqueidentifier");
b.Property<decimal?>("PaymentAmount")
.HasColumnType("decimal(18,2)");
b.Property<Guid?>("ProductDetailId")
.HasColumnType("uniqueidentifier");
@ -2974,7 +2983,8 @@ namespace EShopSample.Migrations
.HasColumnType("decimal(20,8)");
b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier");
.HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");
b.Property<Guid>("UserId")
.HasColumnType("uniqueidentifier");
@ -3426,7 +3436,8 @@ namespace EShopSample.Migrations
.HasColumnType("nvarchar(max)");
b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier");
.HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");
b.HasKey("Id");

Loading…
Cancel
Save