Browse Source

Merge pull request #190 from EasyAbp/basket-item-product-info-updater

Improve the baskets module and add documents
pull/226/head
Super 3 years ago
committed by GitHub
parent
commit
a222d4c1ab
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      EShop.sln
  2. 2
      docs/README.md
  3. 72
      docs/plugins/baskets/README.md
  4. 2
      docs/plugins/booking/README.md
  5. 8
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Application.Contracts/EasyAbp/EShop/Orders/Orders/Dtos/CheckCreateOrderInput.cs
  6. 11
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Application.Contracts/EasyAbp/EShop/Orders/Orders/Dtos/CheckCreateOrderResultDto.cs
  7. 2
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Application.Contracts/EasyAbp/EShop/Orders/Orders/IOrderAppService.cs
  8. 42
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Application/EasyAbp/EShop/Orders/Orders/OrderAppService.cs
  9. 7
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.HttpApi/EasyAbp/EShop/Orders/Orders/OrderController.cs
  10. 83
      modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Application.Tests/Orders/OrderAppServiceTests.cs
  11. 1
      plugins/Baskets/README.md
  12. 3
      plugins/Baskets/src/EasyAbp.EShop.Plugins.Baskets.Application.Contracts/EasyAbp/EShop/Plugins/Baskets/BasketItems/Dtos/ClientSideBasketItemModel.cs
  13. 4
      plugins/Baskets/src/EasyAbp.EShop.Plugins.Baskets.Application.Contracts/EasyAbp/EShop/Plugins/Baskets/BasketItems/Dtos/GetBasketItemListDto.cs
  14. 70
      plugins/Baskets/src/EasyAbp.EShop.Plugins.Baskets.Application/EasyAbp/EShop/Plugins/Baskets/BasketItems/BasicBasketItemProductInfoUpdater.cs
  15. 89
      plugins/Baskets/src/EasyAbp.EShop.Plugins.Baskets.Application/EasyAbp/EShop/Plugins/Baskets/BasketItems/BasketItemAppService.cs
  16. 11
      plugins/Baskets/src/EasyAbp.EShop.Plugins.Baskets.Application/EasyAbp/EShop/Plugins/Baskets/BasketItems/IBasketItemProductInfoUpdater.cs
  17. 3
      plugins/Baskets/src/EasyAbp.EShop.Plugins.Baskets.Domain.Shared/EasyAbp/EShop/Plugins/Baskets/BasketItems/IBasketItem.cs
  18. 1
      plugins/Baskets/src/EasyAbp.EShop.Plugins.Baskets.Domain.Shared/EasyAbp/EShop/Plugins/Baskets/BasketsErrorCodes.cs
  19. 1
      plugins/Baskets/src/EasyAbp.EShop.Plugins.Baskets.Domain.Shared/EasyAbp/EShop/Plugins/Baskets/Localization/en.json
  20. 1
      plugins/Baskets/src/EasyAbp.EShop.Plugins.Baskets.Domain.Shared/EasyAbp/EShop/Plugins/Baskets/Localization/zh-Hans.json
  21. 1
      plugins/Baskets/src/EasyAbp.EShop.Plugins.Baskets.Domain.Shared/EasyAbp/EShop/Plugins/Baskets/Localization/zh-Hant.json
  22. 12
      plugins/Baskets/src/EasyAbp.EShop.Plugins.Baskets.Domain/EasyAbp/EShop/Plugins/Baskets/BasketItems/IProductUpdateRecorder.cs
  23. 6
      plugins/Baskets/src/EasyAbp.EShop.Plugins.Baskets.Domain/EasyAbp/EShop/Plugins/Baskets/ProductUpdates/IProductUpdateRecorder.cs
  24. 3
      plugins/Baskets/src/EasyAbp.EShop.Plugins.Baskets.Domain/EasyAbp/EShop/Plugins/Baskets/ProductUpdates/ProductUpdateRecorder.cs
  25. 4
      plugins/Baskets/src/EasyAbp.EShop.Plugins.Baskets.Web/EShopPluginsBasketsWebModule.cs
  26. 1
      plugins/Baskets/src/EasyAbp.EShop.Plugins.Baskets.Web/EasyAbp.EShop.Plugins.Baskets.Web.csproj
  27. 41
      plugins/Baskets/src/EasyAbp.EShop.Plugins.Baskets.Web/Pages/EShop/Plugins/Baskets/BasketItems/BasketItem/CreateModal.cshtml
  28. 40
      plugins/Baskets/src/EasyAbp.EShop.Plugins.Baskets.Web/Pages/EShop/Plugins/Baskets/BasketItems/BasketItem/CreateModal.cshtml.cs
  29. 29
      plugins/Baskets/src/EasyAbp.EShop.Plugins.Baskets.Web/Pages/EShop/Plugins/Baskets/BasketItems/BasketItem/index.js
  30. 20
      plugins/Booking/src/EasyAbp.EShop.Plugins.Baskets.Booking.Application/EasyAbp.EShop.Plugins.Baskets.Booking.Application.csproj
  31. 125
      plugins/Booking/src/EasyAbp.EShop.Plugins.Baskets.Booking.Application/EasyAbp/EShop/Plugins/Baskets/Booking/BasketItems/BookingBasketItemProductInfoUpdater.cs
  32. 6
      plugins/Booking/src/EasyAbp.EShop.Plugins.Baskets.Booking.Application/EasyAbp/EShop/Plugins/Baskets/Booking/BasketItems/BookingBasketItemUpdatedCacheItem.cs
  33. 19
      plugins/Booking/src/EasyAbp.EShop.Plugins.Baskets.Booking.Application/EasyAbp/EShop/Plugins/Baskets/Booking/EShopPluginsBasketsBookingApplicationModule.cs
  34. 20
      plugins/Booking/src/EasyAbp.EShop.Plugins.Baskets.Booking.Application/EasyAbp/EShop/Plugins/Baskets/Booking/ObjectExtending/BookingBasketItemProperties.cs
  35. 55
      plugins/Booking/src/EasyAbp.EShop.Plugins.Baskets.Booking.Application/EasyAbp/EShop/Plugins/Baskets/Booking/ObjectExtending/EShopPluginsBasketsBookingObjectExtensions.cs
  36. 97
      plugins/Booking/src/EasyAbp.EShop.Plugins.Baskets.Booking.Application/EasyAbp/EShop/Plugins/Baskets/Booking/ObjectExtending/OrderLineExtensions.cs
  37. 3
      plugins/Booking/src/EasyAbp.EShop.Plugins.Baskets.Booking.Application/FodyWeavers.xml
  38. 30
      plugins/Booking/src/EasyAbp.EShop.Plugins.Baskets.Booking.Application/FodyWeavers.xsd

7
EShop.sln

@ -417,6 +417,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAbp.EShop.Orders.Bookin
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAbp.EShop.Payments.Booking.Application", "plugins\Booking\src\EasyAbp.EShop.Payments.Booking.Application\EasyAbp.EShop.Payments.Booking.Application.csproj", "{4480BFAF-C981-4242-A509-EDA6F572E45C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAbp.EShop.Plugins.Baskets.Booking.Application", "plugins\Booking\src\EasyAbp.EShop.Plugins.Baskets.Booking.Application\EasyAbp.EShop.Plugins.Baskets.Booking.Application.csproj", "{F9FD36E6-0ED0-4649-8E1C-33B2CDE85B14}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "FlashSales", "FlashSales", "{867AD9F8-FD56-469D-A90D-C569EB9C3D2A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Plugins.FlashSales.Application", "plugins\FlashSales\src\EasyAbp.EShop.Plugins.FlashSales.Application\EasyAbp.EShop.Plugins.FlashSales.Application.csproj", "{26611C4C-6910-498A-9FBA-BECC09392ADE}"
@ -1133,6 +1135,10 @@ Global
{4480BFAF-C981-4242-A509-EDA6F572E45C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4480BFAF-C981-4242-A509-EDA6F572E45C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4480BFAF-C981-4242-A509-EDA6F572E45C}.Release|Any CPU.Build.0 = Release|Any CPU
{F9FD36E6-0ED0-4649-8E1C-33B2CDE85B14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F9FD36E6-0ED0-4649-8E1C-33B2CDE85B14}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F9FD36E6-0ED0-4649-8E1C-33B2CDE85B14}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F9FD36E6-0ED0-4649-8E1C-33B2CDE85B14}.Release|Any CPU.Build.0 = Release|Any CPU
{26611C4C-6910-498A-9FBA-BECC09392ADE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{26611C4C-6910-498A-9FBA-BECC09392ADE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{26611C4C-6910-498A-9FBA-BECC09392ADE}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -1429,6 +1435,7 @@ Global
{739CAE6A-14E6-44FC-8863-DA905CBD289F} = {7087FDFF-196A-4C9F-8C66-EEBC2C49F2F8}
{C0399352-1278-4D91-8D4E-7491FD77C18B} = {A8C4583C-034E-47AF-B7EC-1A34EE288E2F}
{4480BFAF-C981-4242-A509-EDA6F572E45C} = {A8C4583C-034E-47AF-B7EC-1A34EE288E2F}
{F9FD36E6-0ED0-4649-8E1C-33B2CDE85B14} = {A8C4583C-034E-47AF-B7EC-1A34EE288E2F}
{867AD9F8-FD56-469D-A90D-C569EB9C3D2A} = {94CC5A11-DA0F-413C-96CA-01DB0FC426E0}
{26611C4C-6910-498A-9FBA-BECC09392ADE} = {F29C5BCD-E6C0-4556-A631-CACA41B1050B}
{FCD72398-F832-4914-8ACF-EA4C1DD24BBF} = {F29C5BCD-E6C0-4556-A631-CACA41B1050B}

2
docs/README.md

@ -104,7 +104,7 @@ We can customize the EShop for complex application scenarios.
* Stores
* Plugin modules
* Baskets
* [Baskets](https://github.com/EasyAbp/EShop/tree/dev/plugins/Baskets)
* Coupons
* Inventories
* [DaprActors](https://github.com/EasyAbp/EShop/tree/dev/plugins/Inventories/DaprActors)

72
docs/plugins/baskets/README.md

@ -0,0 +1,72 @@
# EShop.Plugins.Baskets
[![ABP version](https://img.shields.io/badge/dynamic/xml?style=flat-square&color=yellow&label=abp&query=%2F%2FProject%2FPropertyGroup%2FAbpVersion&url=https%3A%2F%2Fraw.githubusercontent.com%2FEasyAbp%2FEShop%2Fmaster%2FDirectory.Build.props)](https://abp.io)
[![NuGet](https://img.shields.io/nuget/v/EasyAbp.EShop.Plugins.Baskets.Domain.Shared.svg?style=flat-square)](https://www.nuget.org/packages/EasyAbp.EShop.Plugins.Baskets.Domain.Shared)
[![NuGet Download](https://img.shields.io/nuget/dt/EasyAbp.EShop.Plugins.Baskets.Domain.Shared.svg?style=flat-square)](https://www.nuget.org/packages/EasyAbp.EShop.Plugins.Baskets.Domain.Shared)
[![Discord online](https://badgen.net/discord/online-members/S6QaezrCRq?label=Discord)](https://discord.gg/S6QaezrCRq)
[![GitHub stars](https://img.shields.io/github/stars/EasyAbp/EShop?style=social)](https://www.github.com/EasyAbp/EShop)
🛒 A baskets (cart) plugin for EShop. It supports both the server-side pattern and the client-side pattern.
## Installation
1. Install the following NuGet packages. ([see how](https://github.com/EasyAbp/EasyAbpGuide/blob/master/docs/How-To.md#add-nuget-packages))
* EasyAbp.EShop.Plugins.Baskets.Application
* EasyAbp.EShop.Plugins.Baskets.Application.Contracts
* EasyAbp.EShop.Plugins.Baskets.Domain
* EasyAbp.EShop.Plugins.Baskets.Domain.Shared
* EasyAbp.EShop.Plugins.Baskets.EntityFrameworkCore
* EasyAbp.EShop.Plugins.Baskets.HttpApi
* EasyAbp.EShop.Plugins.Baskets.HttpApi.Client
* (Optional) EasyAbp.EShop.Plugins.Baskets.MongoDB
* (Optional) EasyAbp.EShop.Plugins.Baskets.Web
1. Add `DependsOn(typeof(EShopXxxModule))` attribute to configure the module dependencies. ([see how](https://github.com/EasyAbp/EasyAbpGuide/blob/master/docs/How-To.md#add-module-dependencies))
1. Add `builder.ConfigureEShopPluginsBaskets();` to the `OnModelCreating()` method in **MyProjectMigrationsDbContext.cs**.
1. Add EF Core migrations and update your database. See: [ABP document](https://docs.abp.io/en/abp/latest/Tutorials/Part-1?UI=MVC&DB=EF#add-database-migration).
## Server-side Baskets Pattern
The server-side basket is for identified(logon) users. It requests the basket [backend service APIs](https://github.com/EasyAbp/EShop/blob/dev/plugins/Baskets/src/EasyAbp.EShop.Plugins.Baskets.HttpApi/EasyAbp/EShop/Plugins/Baskets/BasketItems/BasketItemController.cs) are available.
1. Before you create a basket item, use `/api/e-shop/orders/order/check-create` (POST) to check whether the current user is allowed to create an order with it.
1. Use `/api/e-shop/plugins/baskets/basket-item` (POST) to create basket items on the server side. Extra properties are allowed.
1. Use `/api/e-shop/plugins/baskets/basket-item` (GET) to get basket the item list. The returned "IsInvalid" property shows whether you can use the item to create an order.
1. Use `/api/e-shop/plugins/baskets/basket-item/{id}` (PUT) or `/api/e-shop/plugins/baskets/basket-item/{id}` (DELETE) to change an item's quantity or remove an item.
## Client-side Baskets Pattern
You should store the basket items in the client(browser) cache as a collection of [IBasketItem](https://github.com/EasyAbp/EShop/blob/dev/plugins/Baskets/src/EasyAbp.EShop.Plugins.Baskets.Domain.Shared/EasyAbp/EShop/Plugins/Baskets/BasketItems/IBasketItem.cs). The client-side doesn't depend on the baskets module backend service APIs.
### With Backend
If you install the backend, it provides APIs to help refresh your client-side basket items.
1. Before you create a basket item, use `/api/e-shop/orders/order/check-create` (POST) to check whether the current user is allowed to create an order with it.
1. Use `/api/e-shop/plugins/baskets/basket-item/generate-client-side-data` (POST) to refresh your client-side basket items anytime.
### Without Backend
If you don't install the backend, you can still create and store the basket items locally, but the client-side basket items will not validate and never update.
## How To Determine a Basket Pattern
The server-side baskets pattern will take effect if all the following conditions are met:
1. The baskets module backend has been installed and is available.
1. The `EasyAbp.EShop.Plugins.Baskets.EnableServerSideBaskets` setting value is "True".
1. The current user has the `EasyAbp.EShop.Plugins.Baskets.BasketItem` permission.
## What if Anonymous Users Add Basket Items and Then Log In?
The client(browser) should try to add the existing items to the server-side and remove them from the client-side. You can implement it by referring to the [index.js](https://github.com/EasyAbp/EShop/blob/dev/plugins/Baskets/src/EasyAbp.EShop.Plugins.Baskets.Web/Pages/EShop/Plugins/Baskets/BasketItems/BasketItem/index.js).
## Use Basket Items To Create an Order
We don't provide a built-in way to convert basket items to an order creation DTO.
Create an order manually by the front end, and remember to map the basket item's extra properties to the order creation DTO since some special product may need them.
After creating the order, the front end should clear the unused basket items.

2
docs/plugins/booking/README.md

@ -16,7 +16,7 @@ A booking-business plugin for EShop. It extends EShop to use the [EasyAbp.Bookin
* EasyAbp.EShop.Orders.Booking.Application (install at EasyAbp.EShop.Orders.Application location)
* (Optional) EasyAbp.EShop.Payments.Booking.Application (install at EasyAbp.EShop.Payments.Application location)
* EasyAbp.EShop.Plugins.Booking.Application
* (Optional) EasyAbp.EShop.Plugins.Baskets.Booking.Application (install at EasyAbp.EShop.Plugins.Baskets.Application location)
* EasyAbp.EShop.Plugins.Booking.Application
* EasyAbp.EShop.Plugins.Booking.Application.Contracts
* EasyAbp.EShop.Plugins.Booking.Domain

8
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Application.Contracts/EasyAbp/EShop/Orders/Orders/Dtos/CheckCreateOrderInput.cs

@ -0,0 +1,8 @@
using System;
namespace EasyAbp.EShop.Orders.Orders.Dtos;
[Serializable]
public class CheckCreateOrderInput : CreateOrderDto
{
}

11
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Application.Contracts/EasyAbp/EShop/Orders/Orders/Dtos/CheckCreateOrderResultDto.cs

@ -0,0 +1,11 @@
using System;
namespace EasyAbp.EShop.Orders.Orders.Dtos;
[Serializable]
public class CheckCreateOrderResultDto
{
public bool CanCreate { get; set; }
public string Reason { get; set; }
}

2
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Application.Contracts/EasyAbp/EShop/Orders/Orders/IOrderAppService.cs

@ -19,5 +19,7 @@ namespace EasyAbp.EShop.Orders.Orders
Task<OrderDto> CancelAsync(Guid id, CancelOrderInput input);
Task<OrderDto> UpdateStaffRemarkAsync(Guid id, UpdateStaffRemarkInput input);
Task<CheckCreateOrderResultDto> CheckCreateAsync(CheckCreateOrderInput input);
}
}

42
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Application/EasyAbp/EShop/Orders/Orders/OrderAppService.cs

@ -12,6 +12,7 @@ using EasyAbp.EShop.Stores.Stores;
using Microsoft.AspNetCore.Authorization;
using Volo.Abp;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Uow;
using Volo.Abp.Users;
namespace EasyAbp.EShop.Orders.Orders
@ -91,7 +92,7 @@ namespace EasyAbp.EShop.Orders.Orders
var productDict = await GetProductDictionaryAsync(input.OrderLines.Select(dto => dto.ProductId).ToList());
ThrowIfExistFlashSalesProduct(productDict);
await AuthorizationService.CheckAsync(
new OrderCreationResource
{
@ -108,11 +109,12 @@ namespace EasyAbp.EShop.Orders.Orders
.Where(x => x.HasValue)
.Select(x => x.Value)
.ToList();
var productDetailDict = await GetProductDetailDictionaryAsync(productDetailIds);
// Todo: Can we use IProductDataScopedCache/IProductDetailDataScopedCache instead of productDict/productDetailDict?
var order = await _newOrderGenerator.GenerateAsync(CurrentUser.GetId(), input, productDict, productDetailDict);
var order = await _newOrderGenerator.GenerateAsync(CurrentUser.GetId(), input, productDict,
productDetailDict);
await DiscountOrderAsync(order, productDict);
@ -203,11 +205,11 @@ namespace EasyAbp.EShop.Orders.Orders
return await MapToGetOutputDtoAsync(order);
}
public virtual async Task<OrderDto> CancelAsync(Guid id, CancelOrderInput input)
{
var order = await GetEntityByIdAsync(id);
await AuthorizationService.CheckAsync(
order,
new OrderOperationAuthorizationRequirement(OrderOperation.Cancellation)
@ -225,10 +227,38 @@ namespace EasyAbp.EShop.Orders.Orders
await CheckMultiStorePolicyAsync(order.StoreId, OrdersPermissions.Orders.Manage);
order.SetStaffRemark(input.StaffRemark);
await Repository.UpdateAsync(order, true);
return await MapToGetOutputDtoAsync(order);
}
public virtual async Task<CheckCreateOrderResultDto> CheckCreateAsync(CheckCreateOrderInput input)
{
using var uow = UnitOfWorkManager.Begin(new AbpUnitOfWorkOptions(true), true);
try
{
await CreateAsync(input);
await uow.RollbackAsync();
}
catch
{
await uow.RollbackAsync();
return new CheckCreateOrderResultDto
{
CanCreate = false,
Reason = "Unknown" // Todo: get failure reason
};
}
return new CheckCreateOrderResultDto
{
CanCreate = true,
Reason = null,
};
}
}
}

7
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.HttpApi/EasyAbp/EShop/Orders/Orders/OrderController.cs

@ -81,5 +81,12 @@ namespace EasyAbp.EShop.Orders.Orders
{
return _service.UpdateStaffRemarkAsync(id, input);
}
[HttpPost]
[Route("check-create")]
public virtual Task<CheckCreateOrderResultDto> CheckCreateAsync(CheckCreateOrderInput input)
{
return _service.CheckCreateAsync(input);
}
}
}

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

@ -13,6 +13,7 @@ using Microsoft.Extensions.DependencyInjection;
using NSubstitute;
using Shouldly;
using Volo.Abp;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Settings;
using Volo.Abp.Timing;
using Xunit;
@ -113,6 +114,88 @@ namespace EasyAbp.EShop.Orders.Orders
services.AddTransient(_ => productDetailAppService);
}
[Fact]
public async Task Check_Create_Order_Should_Succeed()
{
var orderRepository = ServiceProvider.GetRequiredService<IOrderRepository>();
var orderCount = 0;
await WithUnitOfWorkAsync(async () =>
{
orderCount = await orderRepository.CountAsync();
});
// Arrange
var checkCreateOrderInput = new CheckCreateOrderInput
{
CustomerRemark = "customer remark",
StoreId = OrderTestData.Store1Id,
OrderLines = new List<CreateOrderLineDto>
{
new CreateOrderLineDto
{
ProductId = OrderTestData.Product1Id,
ProductSkuId = OrderTestData.ProductSku1Id,
Quantity = 10
}
}
};
CheckCreateOrderResultDto resultDto = null;
// Act
await WithUnitOfWorkAsync(async () =>
{
resultDto = await _orderAppService.CheckCreateAsync(checkCreateOrderInput);
});
// Assert
resultDto.CanCreate.ShouldBeTrue();
await WithUnitOfWorkAsync(async () =>
{
orderCount.ShouldBeEquivalentTo(await orderRepository.CountAsync());
});
}
[Fact]
public async Task Check_Create_Order_Should_Fail()
{
var orderRepository = ServiceProvider.GetRequiredService<IOrderRepository>();
var orderCount = 0;
await WithUnitOfWorkAsync(async () =>
{
orderCount = await orderRepository.CountAsync();
});
// Arrange
var checkCreateOrderInput = new CheckCreateOrderInput
{
CustomerRemark = "customer remark",
StoreId = OrderTestData.Store1Id,
OrderLines = new List<CreateOrderLineDto>
{
new CreateOrderLineDto
{
ProductId = OrderTestData.Product1Id,
ProductSkuId = OrderTestData.ProductSku1Id,
Quantity = 101 // limited range: 1-100
}
}
};
CheckCreateOrderResultDto resultDto = null;
// Act
await WithUnitOfWorkAsync(async () =>
{
resultDto = await _orderAppService.CheckCreateAsync(checkCreateOrderInput);
});
// Assert
resultDto.CanCreate.ShouldBeFalse();
await WithUnitOfWorkAsync(async () =>
{
orderCount.ShouldBeEquivalentTo(await orderRepository.CountAsync());
});
}
[Fact]
public async Task Order_Should_Be_Created()
{

1
plugins/Baskets/README.md

@ -0,0 +1 @@
../../docs/plugins/baskets/README.md

3
plugins/Baskets/src/EasyAbp.EShop.Plugins.Baskets.Application.Contracts/EasyAbp/EShop/Plugins/Baskets/BasketItems/Dtos/ClientSideBasketItemModel.cs

@ -1,10 +1,11 @@
using System;
using JetBrains.Annotations;
using Volo.Abp.ObjectExtending;
namespace EasyAbp.EShop.Plugins.Baskets.BasketItems.Dtos;
[Serializable]
public class ClientSideBasketItemModel : IBasketItem
public class ClientSideBasketItemModel : ExtensibleObject, IBasketItem
{
public Guid Id { get; set; }

4
plugins/Baskets/src/EasyAbp.EShop.Plugins.Baskets.Application.Contracts/EasyAbp/EShop/Plugins/Baskets/BasketItems/Dtos/GetBasketItemListDto.cs

@ -1,13 +1,11 @@
using System;
using System.ComponentModel.DataAnnotations;
namespace EasyAbp.EShop.Plugins.Baskets.BasketItems.Dtos
{
[Serializable]
public class GetBasketItemListDto
{
[Required]
public string BasketName { get; set; }
public string BasketName { get; set; } = BasketsConsts.DefaultBasketName;
/// <summary>
/// Specify the basket item owner user ID. Use current user ID if this property is null.

70
plugins/Baskets/src/EasyAbp.EShop.Plugins.Baskets.Application/EasyAbp/EShop/Plugins/Baskets/BasketItems/BasicBasketItemProductInfoUpdater.cs

@ -0,0 +1,70 @@
using System;
using System.Threading.Tasks;
using EasyAbp.EShop.Products.Products;
using EasyAbp.EShop.Products.Products.Dtos;
using Volo.Abp.DependencyInjection;
namespace EasyAbp.EShop.Plugins.Baskets.BasketItems;
public class BasicBasketItemProductInfoUpdater : IBasketItemProductInfoUpdater, ITransientDependency
{
protected IProductSkuDescriptionProvider ProductSkuDescriptionProvider { get; }
public BasicBasketItemProductInfoUpdater(IProductSkuDescriptionProvider productSkuDescriptionProvider)
{
ProductSkuDescriptionProvider = productSkuDescriptionProvider;
}
public virtual Task UpdateForAnonymousAsync(int targetQuantity, IBasketItem item, ProductDto productDto)
{
return InternalUpdateAsync(targetQuantity, item, productDto);
}
public virtual Task UpdateForIdentifiedAsync(int targetQuantity, IBasketItem item, ProductDto productDto)
{
return InternalUpdateAsync(targetQuantity, item, productDto);
}
protected virtual async Task InternalUpdateAsync(int targetQuantity, IBasketItem item, ProductDto productDto)
{
var productSkuDto = productDto.FindSkuById(item.ProductSkuId);
if (productSkuDto == null)
{
item.SetIsInvalid(true);
return;
}
item.UpdateProductData(targetQuantity, new ProductDataModel
{
MediaResources = productSkuDto.MediaResources ?? productDto.MediaResources,
ProductUniqueName = productDto.UniqueName,
ProductDisplayName = productDto.DisplayName,
SkuName = productSkuDto.Name,
SkuDescription = await ProductSkuDescriptionProvider.GenerateAsync(productDto, productSkuDto),
Currency = productSkuDto.Currency,
UnitPrice = productSkuDto.DiscountedPrice,
TotalPrice = productSkuDto.DiscountedPrice * item.Quantity,
TotalDiscount = (productSkuDto.Price - productSkuDto.DiscountedPrice) * item.Quantity,
Inventory = productSkuDto.Inventory
});
if (productDto.InventoryStrategy != InventoryStrategy.NoNeed && targetQuantity > productSkuDto.Inventory)
{
item.SetIsInvalid(true);
return;
}
if (!productDto.IsPublished)
{
item.SetIsInvalid(true);
return;
}
if (!targetQuantity.IsBetween(productSkuDto.OrderMinQuantity, productSkuDto.OrderMaxQuantity))
{
item.SetIsInvalid(true);
return;
}
}
}

89
plugins/Baskets/src/EasyAbp.EShop.Plugins.Baskets.Application/EasyAbp/EShop/Plugins/Baskets/BasketItems/BasketItemAppService.cs

@ -11,11 +11,13 @@ using Microsoft.AspNetCore.Authorization;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Authorization;
using Volo.Abp.ObjectExtending;
using Volo.Abp.Users;
namespace EasyAbp.EShop.Plugins.Baskets.BasketItems
{
public class BasketItemAppService : CrudAppService<BasketItem, BasketItemDto, Guid, GetBasketItemListDto, CreateBasketItemDto, UpdateBasketItemDto>,
public class BasketItemAppService : CrudAppService<BasketItem, BasketItemDto, Guid, GetBasketItemListDto,
CreateBasketItemDto, UpdateBasketItemDto>,
IBasketItemAppService
{
protected override string GetPolicyName { get; set; } = BasketsPermissions.BasketItem.Default;
@ -27,18 +29,15 @@ namespace EasyAbp.EShop.Plugins.Baskets.BasketItems
private readonly IBasketItemRepository _repository;
private readonly IProductUpdateRepository _productUpdateRepository;
private readonly IProductAppService _productAppService;
private readonly IProductSkuDescriptionProvider _productSkuDescriptionProvider;
public BasketItemAppService(
IBasketItemRepository repository,
IProductUpdateRepository productUpdateRepository,
IProductAppService productAppService,
IProductSkuDescriptionProvider productSkuDescriptionProvider) : base(repository)
IProductAppService productAppService) : base(repository)
{
_repository = repository;
_productUpdateRepository = productUpdateRepository;
_productAppService = productAppService;
_productSkuDescriptionProvider = productSkuDescriptionProvider;
}
public override async Task<BasketItemDto> GetAsync(Guid id)
@ -46,14 +45,14 @@ namespace EasyAbp.EShop.Plugins.Baskets.BasketItems
await CheckGetPolicyAsync();
var item = await GetEntityByIdAsync(id);
if (item.UserId != CurrentUser.GetId() && !await IsCurrentUserManagerAsync())
{
throw new AbpAuthorizationException();
}
var productUpdate = await _productUpdateRepository.FindAsync(x => x.ProductSkuId == item.ProductSkuId);
if (productUpdate != null)
{
var itemUpdateTime = item.LastModificationTime ?? item.CreationTime;
@ -62,9 +61,9 @@ namespace EasyAbp.EShop.Plugins.Baskets.BasketItems
if (itemUpdateTime < productUpdateTime)
{
var productDto = await _productAppService.GetAsync(item.ProductId);
await UpdateProductDataAsync(item.Quantity, item, productDto);
await _repository.UpdateAsync(item, true);
}
}
@ -75,7 +74,7 @@ namespace EasyAbp.EShop.Plugins.Baskets.BasketItems
public override async Task<PagedResultDto<BasketItemDto>> GetListAsync(GetBasketItemListDto input)
{
await CheckGetListPolicyAsync();
if (input.UserId != CurrentUser.GetId())
{
await AuthorizationService.CheckAsync(BasketsPermissions.BasketItem.Manage);
@ -124,49 +123,33 @@ namespace EasyAbp.EShop.Plugins.Baskets.BasketItems
await MapToGetListOutputDtosAsync(items)
);
}
protected virtual async Task UpdateProductDataAsync(int quantity, IBasketItem item, ProductDto productDto)
protected virtual async Task UpdateProductDataAsync(int targetQuantity, IBasketItem item, ProductDto productDto)
{
item.SetIsInvalid(false);
var productSkuDto = productDto.FindSkuById(item.ProductSkuId);
if (productSkuDto == null)
{
item.SetIsInvalid(true);
return;
}
var updaters = LazyServiceProvider.LazyGetRequiredService<IEnumerable<IBasketItemProductInfoUpdater>>();
if (productDto.InventoryStrategy != InventoryStrategy.NoNeed && quantity > productSkuDto.Inventory)
if (CurrentUser.IsAuthenticated)
{
item.SetIsInvalid(true);
foreach (var updater in updaters)
{
await updater.UpdateForIdentifiedAsync(targetQuantity, item, productDto);
}
}
item.UpdateProductData(quantity, new ProductDataModel
{
MediaResources = productSkuDto.MediaResources ?? productDto.MediaResources,
ProductUniqueName = productDto.UniqueName,
ProductDisplayName = productDto.DisplayName,
SkuName = productSkuDto.Name,
SkuDescription = await _productSkuDescriptionProvider.GenerateAsync(productDto, productSkuDto),
Currency = productSkuDto.Currency,
UnitPrice = productSkuDto.DiscountedPrice,
TotalPrice = productSkuDto.DiscountedPrice * item.Quantity,
TotalDiscount = (productSkuDto.Price - productSkuDto.DiscountedPrice) * item.Quantity,
Inventory = productSkuDto.Inventory
});
if (!productDto.IsPublished)
else
{
item.SetIsInvalid(true);
foreach (var updater in updaters)
{
await updater.UpdateForAnonymousAsync(targetQuantity, item, productDto);
}
}
}
protected override async Task<IQueryable<BasketItem>> CreateFilteredQueryAsync(GetBasketItemListDto input)
{
var userId = input.UserId ?? CurrentUser.GetId();
return (await ReadOnlyRepository.GetQueryableAsync())
.Where(item => item.UserId == userId && item.BasketName == input.BasketName);
}
@ -190,12 +173,12 @@ namespace EasyAbp.EShop.Plugins.Baskets.BasketItems
if (item != null)
{
await UpdateProductDataAsync(input.Quantity + item.Quantity, item, productDto);
await Repository.UpdateAsync(item, autoSave: true);
return await MapToGetOutputDtoAsync(item);
}
var productSkuDto = productDto.FindSkuById(input.ProductSkuId);
if (productSkuDto == null)
@ -205,9 +188,11 @@ namespace EasyAbp.EShop.Plugins.Baskets.BasketItems
item = new BasketItem(GuidGenerator.Create(), CurrentTenant.Id, input.BasketName, CurrentUser.GetId(),
productDto.StoreId, input.ProductId, input.ProductSkuId);
input.MapExtraPropertiesTo(item);
await UpdateProductDataAsync(input.Quantity, item, productDto);
await Repository.InsertAsync(item, autoSave: true);
return await MapToGetOutputDtoAsync(item);
@ -218,7 +203,7 @@ namespace EasyAbp.EShop.Plugins.Baskets.BasketItems
await CheckUpdatePolicyAsync();
var item = await GetEntityByIdAsync(id);
if (item.UserId != CurrentUser.GetId() && !await IsCurrentUserManagerAsync())
{
throw new AbpAuthorizationException();
@ -226,8 +211,10 @@ namespace EasyAbp.EShop.Plugins.Baskets.BasketItems
var productDto = await _productAppService.GetAsync(item.ProductId);
input.MapExtraPropertiesTo(item);
await UpdateProductDataAsync(input.Quantity, item, productDto);
await Repository.UpdateAsync(item, autoSave: true);
return await MapToGetOutputDtoAsync(item);
@ -243,20 +230,20 @@ namespace EasyAbp.EShop.Plugins.Baskets.BasketItems
{
throw new AbpAuthorizationException();
}
await _repository.DeleteAsync(item, true);
}
public virtual async Task BatchDeleteAsync(IEnumerable<Guid> ids)
{
await CheckDeletePolicyAsync();
var isCurrentUserManager = await IsCurrentUserManagerAsync();
foreach (var id in ids)
{
var item = await GetEntityByIdAsync(id);
if (item.UserId != CurrentUser.GetId() && !isCurrentUserManager)
{
throw new AbpAuthorizationException();
@ -270,7 +257,7 @@ namespace EasyAbp.EShop.Plugins.Baskets.BasketItems
GenerateClientSideDataInput input)
{
var itemList = new List<ClientSideBasketItemModel>();
var products = new Dictionary<Guid, ProductDto>();
foreach (var dto in input.Items)
@ -290,7 +277,7 @@ namespace EasyAbp.EShop.Plugins.Baskets.BasketItems
}
var id = dto.Id ?? GuidGenerator.Create();
var item = new ClientSideBasketItemModel(id, dto.BasketName, productDto.StoreId,
dto.ProductId, dto.ProductSkuId);

11
plugins/Baskets/src/EasyAbp.EShop.Plugins.Baskets.Application/EasyAbp/EShop/Plugins/Baskets/BasketItems/IBasketItemProductInfoUpdater.cs

@ -0,0 +1,11 @@
using System.Threading.Tasks;
using EasyAbp.EShop.Products.Products.Dtos;
namespace EasyAbp.EShop.Plugins.Baskets.BasketItems;
public interface IBasketItemProductInfoUpdater
{
Task UpdateForAnonymousAsync(int targetQuantity, IBasketItem item, ProductDto productDto);
Task UpdateForIdentifiedAsync(int targetQuantity, IBasketItem item, ProductDto productDto);
}

3
plugins/Baskets/src/EasyAbp.EShop.Plugins.Baskets.Domain.Shared/EasyAbp/EShop/Plugins/Baskets/BasketItems/IBasketItem.cs

@ -1,9 +1,10 @@
using System;
using JetBrains.Annotations;
using Volo.Abp.Data;
namespace EasyAbp.EShop.Plugins.Baskets.BasketItems;
public interface IBasketItem : IProductData
public interface IBasketItem : IProductData, IHasExtraProperties
{
Guid Id { get; }

1
plugins/Baskets/src/EasyAbp.EShop.Plugins.Baskets.Domain.Shared/EasyAbp/EShop/Plugins/Baskets/BasketsErrorCodes.cs

@ -2,6 +2,7 @@
{
public static class BasketsErrorCodes
{
public const string CheckCreateOrderFailed = "EasyAbp.EShop.Plugins.Baskets:CheckCreateOrderFailed";
public const string ProductSkuNotFound = "EasyAbp.EShop.Plugins.Baskets:ProductSkuNotFound";
}
}

1
plugins/Baskets/src/EasyAbp.EShop.Plugins.Baskets.Domain.Shared/EasyAbp/EShop/Plugins/Baskets/Localization/en.json

@ -27,6 +27,7 @@
"EditBasketItem": "Edit",
"BasketItemDeletionConfirmationMessage": "Are you sure to delete the basket item {0}?",
"SuccessfullyDeleted": "Successfully deleted",
"EasyAbp.EShop.Plugins.Baskets:CheckCreateOrderFailed": "You are not allowed to purchase the specified item. Reason: {reason}",
"EasyAbp.EShop.Plugins.Baskets:ProductSkuNotFound": "Product {productId} (SKU: {productSkuId}) not found."
}
}

1
plugins/Baskets/src/EasyAbp.EShop.Plugins.Baskets.Domain.Shared/EasyAbp/EShop/Plugins/Baskets/Localization/zh-Hans.json

@ -27,6 +27,7 @@
"EditBasketItem": "编辑",
"BasketItemDeletionConfirmationMessage": "确认删除购物车项 {0}?",
"SuccessfullyDeleted": "删除成功",
"EasyAbp.EShop.Plugins.Baskets:CheckCreateOrderFailed": "您无法购买指定商品,原因:{reason}",
"EasyAbp.EShop.Plugins.Baskets:ProductSkuNotFound": "商品{productId}(SKU: {productSkuId})未找到"
}
}

1
plugins/Baskets/src/EasyAbp.EShop.Plugins.Baskets.Domain.Shared/EasyAbp/EShop/Plugins/Baskets/Localization/zh-Hant.json

@ -27,6 +27,7 @@
"EditBasketItem": "編輯",
"BasketItemDeletionConfirmationMessage": "確認刪除購物車項 {0}?",
"SuccessfullyDeleted": "刪除成功",
"EasyAbp.EShop.Plugins.Baskets:CheckCreateOrderFailed": "您無法購買指定商品,原因:{reason}",
"EasyAbp.EShop.Plugins.Baskets:ProductSkuNotFound": "商品{productId}(SKU: {productSkuId})未找到"
}
}

12
plugins/Baskets/src/EasyAbp.EShop.Plugins.Baskets.Domain/EasyAbp/EShop/Plugins/Baskets/BasketItems/IProductUpdateRecorder.cs

@ -1,12 +0,0 @@
using EasyAbp.EShop.Products.ProductInventories;
using EasyAbp.EShop.Products.Products;
using Volo.Abp.Domain.Entities.Events.Distributed;
using Volo.Abp.EventBus.Distributed;
namespace EasyAbp.EShop.Plugins.Baskets.BasketItems
{
public interface IProductUpdateRecorder
{
}
}

6
plugins/Baskets/src/EasyAbp.EShop.Plugins.Baskets.Domain/EasyAbp/EShop/Plugins/Baskets/ProductUpdates/IProductUpdateRecorder.cs

@ -0,0 +1,6 @@
namespace EasyAbp.EShop.Plugins.Baskets.ProductUpdates;
public interface IProductUpdateRecorder
{
}

3
plugins/Baskets/src/EasyAbp.EShop.Plugins.Baskets.Domain/EasyAbp/EShop/Plugins/Baskets/BasketItems/ProductUpdateRecorder.cs → plugins/Baskets/src/EasyAbp.EShop.Plugins.Baskets.Domain/EasyAbp/EShop/Plugins/Baskets/ProductUpdates/ProductUpdateRecorder.cs

@ -1,7 +1,6 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using EasyAbp.EShop.Plugins.Baskets.ProductUpdates;
using EasyAbp.EShop.Products.ProductInventories;
using EasyAbp.EShop.Products.Products;
using Volo.Abp.DependencyInjection;
@ -11,7 +10,7 @@ using Volo.Abp.Guids;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Uow;
namespace EasyAbp.EShop.Plugins.Baskets.BasketItems
namespace EasyAbp.EShop.Plugins.Baskets.ProductUpdates
{
public class ProductUpdateRecorder :
IProductUpdateRecorder,

4
plugins/Baskets/src/EasyAbp.EShop.Plugins.Baskets.Web/EShopPluginsBasketsWebModule.cs

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc.RazorPages;
using EasyAbp.EShop.Orders;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.DependencyInjection;
using EasyAbp.EShop.Plugins.Baskets.Localization;
using EasyAbp.EShop.Plugins.Baskets.Web.Menus;
@ -12,6 +13,7 @@ using Volo.Abp.VirtualFileSystem;
namespace EasyAbp.EShop.Plugins.Baskets.Web
{
[DependsOn(
typeof(EShopOrdersApplicationContractsModule),
typeof(EShopPluginsBasketsApplicationContractsModule),
typeof(AbpAspNetCoreMvcUiThemeSharedModule),
typeof(AbpAutoMapperModule)

1
plugins/Baskets/src/EasyAbp.EShop.Plugins.Baskets.Web/EasyAbp.EShop.Plugins.Baskets.Web.csproj

@ -17,6 +17,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\modules\EasyAbp.EShop.Orders\src\EasyAbp.EShop.Orders.Application.Contracts\EasyAbp.EShop.Orders.Application.Contracts.csproj" />
<ProjectReference Include="..\EasyAbp.EShop.Plugins.Baskets.Application.Contracts\EasyAbp.EShop.Plugins.Baskets.Application.Contracts.csproj" />
</ItemGroup>

41
plugins/Baskets/src/EasyAbp.EShop.Plugins.Baskets.Web/Pages/EShop/Plugins/Baskets/BasketItems/BasketItem/CreateModal.cshtml

@ -1,6 +1,6 @@
@page
@using EasyAbp.EShop.Plugins.Baskets
@using EasyAbp.EShop.Plugins.Baskets.Localization
@using EasyAbp.EShop.Plugins.Baskets.Permissions
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Mvc.Localization
@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal
@ -25,16 +25,15 @@
<script>
var localStorageItemKey = localStorageItemKey || "EShopBasket:" + basketName;
var l = abp.localization.getResource('EasyAbpEShopPluginsBaskets');
$(document).ready(function() {
if (@(Model.ServerSide ? "true" : "false")) return;
var $form = $("form");
$form.off('submit');
$form.on('submit', function(e){
e.preventDefault();
var service = easyAbp.eShop.plugins.baskets.basketItems.basketItem;
service.generateClientSideData({ items: [ $form.serializeFormToObject().viewModel ] }, {
function generateClientSideData(formObj) {
var basketItemService = easyAbp.eShop.plugins.baskets.basketItems.basketItem;
basketItemService.generateClientSideData({ items: [ formObj.viewModel ] }, {
success: function (responseText, statusText, xhr, form) {
var basketItems = JSON.parse(localStorage.getItem(localStorageItemKey)) || [];
basketItems = pushOrUpdateBasketItem(basketItems, responseText.items[0])
@ -48,6 +47,34 @@
})
}
});
}
var $form = $("form");
$form.off('submit');
$form.on('submit', function(e){
e.preventDefault();
var orderService = easyAbp.eShop.orders.orders.order;
var formObj = $form.serializeFormToObject();
if (abp.currentUser.isAuthenticated) {
orderService.checkCreate({
storeId: formObj.viewModel.storeId,
orderLines: [{
productId: formObj.viewModel.productId,
productSkuId: formObj.viewModel.productSkuId,
quantity: formObj.viewModel.quantity
}]
}, {
success: function (responseText, statusText, xhr, form) {
if (responseText.canCreate) {
generateClientSideData(formObj);
} else {
abp.message.error(l('@BasketsErrorCodes.CheckCreateOrderFailed', responseText.reason));
}
}
});
} else {
generateClientSideData(formObj);
}
});
});

40
plugins/Baskets/src/EasyAbp.EShop.Plugins.Baskets.Web/Pages/EShop/Plugins/Baskets/BasketItems/BasketItem/CreateModal.cshtml.cs

@ -1,10 +1,14 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using EasyAbp.EShop.Orders.Orders;
using EasyAbp.EShop.Orders.Orders.Dtos;
using Microsoft.AspNetCore.Mvc;
using EasyAbp.EShop.Plugins.Baskets.BasketItems;
using EasyAbp.EShop.Plugins.Baskets.BasketItems.Dtos;
using EasyAbp.EShop.Plugins.Baskets.Permissions;
using EasyAbp.EShop.Plugins.Baskets.Web.Pages.EShop.Plugins.Baskets.BasketItems.BasketItem.ViewModels;
using Microsoft.AspNetCore.Authorization;
using Volo.Abp;
using Volo.Abp.Settings;
namespace EasyAbp.EShop.Plugins.Baskets.Web.Pages.EShop.Plugins.Baskets.BasketItems.BasketItem
@ -13,14 +17,18 @@ namespace EasyAbp.EShop.Plugins.Baskets.Web.Pages.EShop.Plugins.Baskets.BasketIt
{
[BindProperty]
public CreateBasketItemViewModel ViewModel { get; set; } = new();
public bool ServerSide { get; set; }
private readonly IBasketItemAppService _service;
private readonly IOrderAppService _orderAppService;
private readonly IBasketItemAppService _basketItemAppService;
public CreateModalModel(IBasketItemAppService service)
public CreateModalModel(
IOrderAppService orderAppService,
IBasketItemAppService basketItemAppService)
{
_service = service;
_orderAppService = orderAppService;
_basketItemAppService = basketItemAppService;
}
public virtual async Task OnGetAsync()
@ -32,7 +40,29 @@ namespace EasyAbp.EShop.Plugins.Baskets.Web.Pages.EShop.Plugins.Baskets.BasketIt
public virtual async Task<IActionResult> OnPostAsync()
{
var dto = ObjectMapper.Map<CreateBasketItemViewModel, CreateBasketItemDto>(ViewModel);
await _service.CreateAsync(dto);
var checkCreateOrderResult = await _orderAppService.CheckCreateAsync(new CheckCreateOrderInput
{
StoreId = ViewModel.StoreId,
OrderLines = new List<CreateOrderLineDto>
{
new()
{
ProductId = ViewModel.ProductId,
ProductSkuId = ViewModel.ProductSkuId,
Quantity = ViewModel.Quantity
}
}
});
if (!checkCreateOrderResult.CanCreate)
{
throw new BusinessException(BasketsErrorCodes.CheckCreateOrderFailed)
.WithData("reason", checkCreateOrderResult.Reason);
}
await _basketItemAppService.CreateAsync(dto);
return NoContent();
}
}

29
plugins/Baskets/src/EasyAbp.EShop.Plugins.Baskets.Web/Pages/EShop/Plugins/Baskets/BasketItems/BasketItem/index.js

@ -120,15 +120,30 @@ $(function () {
function createManyServerSideBasketItems(items, autoReloadDataTable = true) {
var item = items.shift();
service.create(item, {
success: function () {
if (items.length > 0) {
createManyServerSideBasketItems(items);
} else if (autoReloadDataTable) {
dataTable.ajax.reload();
var orderService = easyAbp.eShop.orders.orders.order;
orderService.checkCreate({
storeId: item.storeId,
orderLines: [{
productId: item.productId,
productSkuId: item.productSkuId,
quantity: item.quantity
}]
}, {
success: function (responseText, statusText, xhr, form) {
console.log(responseText);
if (responseText.canCreate) {
service.create(item, {
success: function () {
if (items.length > 0) {
createManyServerSideBasketItems(items);
} else if (autoReloadDataTable) {
dataTable.ajax.reload();
}
}
})
}
}
})
});
}
$('#NewBasketItemButton').click(function (e) {

20
plugins/Booking/src/EasyAbp.EShop.Plugins.Baskets.Booking.Application/EasyAbp.EShop.Plugins.Baskets.Booking.Application.csproj

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\common.props" />
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\Baskets\src\EasyAbp.EShop.Plugins.Baskets.Application\EasyAbp.EShop.Plugins.Baskets.Application.csproj" />
<ProjectReference Include="..\EasyAbp.EShop.Plugins.Booking.Application.Contracts\EasyAbp.EShop.Plugins.Booking.Application.Contracts.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Volo.Abp.Caching" Version="$(AbpVersion)" />
<PackageReference Include="EasyAbp.BookingService.Application.Contracts" Version="$(EasyAbpBookingServiceModuleVersion)" />
</ItemGroup>
</Project>

125
plugins/Booking/src/EasyAbp.EShop.Plugins.Baskets.Booking.Application/EasyAbp/EShop/Plugins/Baskets/Booking/BasketItems/BookingBasketItemProductInfoUpdater.cs

@ -0,0 +1,125 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using EasyAbp.BookingService.AssetOccupancies;
using EasyAbp.BookingService.AssetOccupancies.Dtos;
using EasyAbp.EShop.Plugins.Baskets.BasketItems;
using EasyAbp.EShop.Plugins.Baskets.Booking.ObjectExtending;
using EasyAbp.EShop.Plugins.Booking.BookingProductGroupDefinitions;
using EasyAbp.EShop.Products.Products.Dtos;
using Microsoft.Extensions.Caching.Distributed;
using Volo.Abp.Caching;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Users;
namespace EasyAbp.EShop.Plugins.Baskets.Booking.BasketItems;
public class BookingBasketItemProductInfoUpdater : IBasketItemProductInfoUpdater, ITransientDependency
{
protected List<string> ProductGroupNames { get; set; }
protected bool? AllowedToUpdate { get; set; }
protected ICurrentUser CurrentUser { get; }
protected IDistributedCache<BookingBasketItemUpdatedCacheItem> Cache { get; }
protected IAssetOccupancyAppService AssetOccupancyAppService { get; }
protected IBookingProductGroupDefinitionAppService DefinitionAppService { get; }
public BookingBasketItemProductInfoUpdater(
ICurrentUser currentUser,
IDistributedCache<BookingBasketItemUpdatedCacheItem> cache,
IAssetOccupancyAppService assetOccupancyAppService,
IBookingProductGroupDefinitionAppService definitionAppService)
{
CurrentUser = currentUser;
Cache = cache;
AssetOccupancyAppService = assetOccupancyAppService;
DefinitionAppService = definitionAppService;
}
public virtual Task UpdateForAnonymousAsync(int targetQuantity, IBasketItem item, ProductDto productDto)
{
return Task.CompletedTask;
}
public virtual async Task UpdateForIdentifiedAsync(int targetQuantity, IBasketItem item, ProductDto productDto)
{
if (!await IsAllowedToUpdateAsync(item, productDto) ||
!await IsBookingProductGroupAsync(productDto.ProductGroupName))
{
return;
}
var assetId = item.FindBookingAssetId();
var assetCategoryId = item.FindBookingAssetCategoryId();
if (assetId is not null)
{
try
{
await AssetOccupancyAppService.CheckCreateAsync(new CreateAssetOccupancyDto
{
AssetId = assetId.Value,
Volume = item.GetBookingVolume(),
Date = item.GetBookingDate(),
StartingTime = item.GetBookingStartingTime(),
Duration = item.GetBookingDuration()
});
}
catch
{
item.SetIsInvalid(true);
}
}
else
{
try
{
await AssetOccupancyAppService.CheckCreateByCategoryIdAsync(new CreateAssetOccupancyByCategoryIdDto
{
AssetCategoryId = assetCategoryId!.Value,
Volume = item.GetBookingVolume(),
Date = item.GetBookingDate(),
StartingTime = item.GetBookingStartingTime(),
Duration = item.GetBookingDuration()
});
}
catch
{
item.SetIsInvalid(true);
}
}
}
protected virtual async Task<bool> IsAllowedToUpdateAsync(IBasketItem item, ProductDto productDto)
{
if (AllowedToUpdate.HasValue)
{
return AllowedToUpdate.Value;
}
var key = GetCacheItemKey(item, productDto);
AllowedToUpdate = await Cache.GetAsync(key) is null;
await Cache.SetAsync(key, new BookingBasketItemUpdatedCacheItem(), new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(30)
});
return AllowedToUpdate.Value;
}
protected virtual string GetCacheItemKey(IBasketItem item, ProductDto productDto)
{
return CurrentUser.GetId().ToString();
}
protected virtual async Task<bool> IsBookingProductGroupAsync(string productGroupName)
{
ProductGroupNames ??=
(await DefinitionAppService.GetListAsync()).Items.Select(x => x.ProductGroupName).ToList();
return ProductGroupNames.Contains(productGroupName);
}
}

6
plugins/Booking/src/EasyAbp.EShop.Plugins.Baskets.Booking.Application/EasyAbp/EShop/Plugins/Baskets/Booking/BasketItems/BookingBasketItemUpdatedCacheItem.cs

@ -0,0 +1,6 @@
namespace EasyAbp.EShop.Plugins.Baskets.Booking.BasketItems;
public class BookingBasketItemUpdatedCacheItem
{
}

19
plugins/Booking/src/EasyAbp.EShop.Plugins.Baskets.Booking.Application/EasyAbp/EShop/Plugins/Baskets/Booking/EShopPluginsBasketsBookingApplicationModule.cs

@ -0,0 +1,19 @@
using EasyAbp.EShop.Orders;
using EasyAbp.EShop.Plugins.Baskets.Booking.ObjectExtending;
using Volo.Abp.Caching;
using Volo.Abp.Modularity;
namespace EasyAbp.EShop.Plugins.Baskets.Booking;
[DependsOn(
typeof(AbpCachingModule),
typeof(EShopPluginsBasketsApplicationModule),
typeof(EShopOrdersApplicationContractsModule)
)]
public class EShopPluginsBasketsBookingApplicationModule : AbpModule
{
public override void PreConfigureServices(ServiceConfigurationContext context)
{
EShopPluginsBasketsBookingObjectExtensions.Configure();
}
}

20
plugins/Booking/src/EasyAbp.EShop.Plugins.Baskets.Booking.Application/EasyAbp/EShop/Plugins/Baskets/Booking/ObjectExtending/BookingBasketItemProperties.cs

@ -0,0 +1,20 @@
namespace EasyAbp.EShop.Plugins.Baskets.Booking.ObjectExtending;
public class BookingBasketItemProperties
{
public const string BasketItemBookingAssetId = "BookingAssetId";
public const string BasketItemBookingAssetCategoryId = "BookingAssetCategoryId";
public const string BasketItemBookingPeriodSchemeId = "BookingPeriodSchemeId";
public const string BasketItemBookingPeriodId = "BookingPeriodId";
public const string BasketItemBookingDate = "BookingDate";
public const string BasketItemBookingStartingTime = "BookingStartingTime";
public const string BasketItemBookingDuration = "BookingDuration";
public const string BasketItemBookingAssetOccupancyId = "BookingAssetOccupancyId";
}

55
plugins/Booking/src/EasyAbp.EShop.Plugins.Baskets.Booking.Application/EasyAbp/EShop/Plugins/Baskets/Booking/ObjectExtending/EShopPluginsBasketsBookingObjectExtensions.cs

@ -0,0 +1,55 @@
using System;
using EasyAbp.EShop.Orders;
using EasyAbp.EShop.Plugins.Baskets.BasketItems;
using EasyAbp.EShop.Plugins.Baskets.BasketItems.Dtos;
using Volo.Abp.ObjectExtending;
using Volo.Abp.Threading;
namespace EasyAbp.EShop.Plugins.Baskets.Booking.ObjectExtending;
public static class EShopPluginsBasketsBookingObjectExtensions
{
private static readonly OneTimeRunner OneTimeRunner = new OneTimeRunner();
public static void Configure()
{
OneTimeRunner.Run(() =>
{
/* You can configure extension properties to entities or other object types
* defined in the depended modules.
*
* If you are using EF Core and want to map the entity extension properties to new
* table fields in the database, then configure them in the EShopSampleEfCoreEntityExtensionMappings
*
* Example:
*
* ObjectExtensionManager.Instance
* .AddOrUpdateProperty<IdentityRole, string>("Title");
*
* See the documentation for more:
* https://docs.abp.io/en/abp/latest/Object-Extensions
*/
ObjectExtensionManager.Instance
.AddOrUpdate(
new[]
{
typeof(BasketItem),
typeof(BasketItemDto),
typeof(CreateBasketItemDto),
typeof(UpdateBasketItemDto)
},
config =>
{
config.AddOrUpdateProperty<Guid?>(BookingBasketItemProperties.BasketItemBookingAssetId);
config.AddOrUpdateProperty<Guid?>(BookingBasketItemProperties.BasketItemBookingAssetCategoryId);
config.AddOrUpdateProperty<Guid?>(BookingBasketItemProperties.BasketItemBookingPeriodSchemeId);
config.AddOrUpdateProperty<Guid?>(BookingBasketItemProperties.BasketItemBookingPeriodId);
config.AddOrUpdateProperty<DateTime?>(BookingBasketItemProperties.BasketItemBookingDate);
config.AddOrUpdateProperty<TimeSpan?>(BookingBasketItemProperties.BasketItemBookingStartingTime);
config.AddOrUpdateProperty<TimeSpan?>(BookingBasketItemProperties.BasketItemBookingDuration);
}
);
});
}
}

97
plugins/Booking/src/EasyAbp.EShop.Plugins.Baskets.Booking.Application/EasyAbp/EShop/Plugins/Baskets/Booking/ObjectExtending/OrderLineExtensions.cs

@ -0,0 +1,97 @@
using System;
using EasyAbp.EShop.Orders;
using EasyAbp.EShop.Plugins.Baskets.BasketItems;
using Volo.Abp;
using Volo.Abp.Data;
namespace EasyAbp.EShop.Plugins.Baskets.Booking.ObjectExtending;
public static class OrderLineExtensions
{
public static Guid? FindBookingAssetId(this IBasketItem basketItem)
{
return basketItem.GetProperty<Guid?>(BookingOrderProperties.OrderLineBookingAssetId);
}
public static Guid GetBookingAssetId(this IBasketItem basketItem)
{
return Check.NotNull(FindBookingAssetId(basketItem),
BookingOrderProperties.OrderLineBookingAssetId)!.Value;
}
public static Guid? FindBookingAssetCategoryId(this IBasketItem basketItem)
{
return basketItem.GetProperty<Guid?>(BookingOrderProperties.OrderLineBookingAssetCategoryId);
}
public static Guid GetBookingAssetCategoryId(this IBasketItem basketItem)
{
return Check.NotNull(FindBookingAssetCategoryId(basketItem),
BookingOrderProperties.OrderLineBookingAssetCategoryId)!.Value;
}
public static Guid? FindBookingPeriodSchemeId(this IBasketItem basketItem)
{
return basketItem.GetProperty<Guid?>(BookingOrderProperties.OrderLineBookingPeriodSchemeId);
}
public static Guid GetBookingPeriodSchemeId(this IBasketItem basketItem)
{
return Check.NotNull(FindBookingPeriodSchemeId(basketItem),
BookingOrderProperties.OrderLineBookingPeriodSchemeId)!.Value;
}
public static Guid? FindBookingPeriodId(this IBasketItem basketItem)
{
return basketItem.GetProperty<Guid?>(BookingOrderProperties.OrderLineBookingPeriodId);
}
public static Guid GetBookingPeriodId(this IBasketItem basketItem)
{
return Check.NotNull(FindBookingPeriodId(basketItem),
BookingOrderProperties.OrderLineBookingPeriodId)!.Value;
}
public static int? FindBookingVolume(this IBasketItem basketItem)
{
return basketItem.Quantity;
}
public static int GetBookingVolume(this IBasketItem basketItem)
{
return FindBookingVolume(basketItem)!.Value;
}
public static DateTime? FindBookingDate(this IBasketItem basketItem)
{
return basketItem.FindDateTimeProperty(BookingOrderProperties.OrderLineBookingDate);
}
public static DateTime GetBookingDate(this IBasketItem basketItem)
{
return Check.NotNull(FindBookingDate(basketItem),
BookingOrderProperties.OrderLineBookingDate)!.Value;
}
public static TimeSpan? FindBookingStartingTime(this IBasketItem basketItem)
{
return basketItem.FindTimeSpanProperty(BookingOrderProperties.OrderLineBookingStartingTime);
}
public static TimeSpan GetBookingStartingTime(this IBasketItem basketItem)
{
return Check.NotNull(FindBookingStartingTime(basketItem),
BookingOrderProperties.OrderLineBookingStartingTime)!.Value;
}
public static TimeSpan? FindBookingDuration(this IBasketItem basketItem)
{
return basketItem.FindTimeSpanProperty(BookingOrderProperties.OrderLineBookingDuration);
}
public static TimeSpan GetBookingDuration(this IBasketItem basketItem)
{
return Check.NotNull(FindBookingDuration(basketItem),
BookingOrderProperties.OrderLineBookingDuration)!.Value;
}
}

3
plugins/Booking/src/EasyAbp.EShop.Plugins.Baskets.Booking.Application/FodyWeavers.xml

@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait ContinueOnCapturedContext="false" />
</Weavers>

30
plugins/Booking/src/EasyAbp.EShop.Plugins.Baskets.Booking.Application/FodyWeavers.xsd

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
<xs:element name="Weavers">
<xs:complexType>
<xs:all>
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" />
</xs:complexType>
</xs:element>
</xs:all>
<xs:attribute name="VerifyAssembly" type="xs:boolean">
<xs:annotation>
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
<xs:annotation>
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="GenerateXsd" type="xs:boolean">
<xs:annotation>
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:schema>
Loading…
Cancel
Save