diff --git a/README.md b/README.md index 205d6cf3..d36eec50 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # Xperience by Kentico - KX 13 E-Commerce integration [![Kentico Labs](https://img.shields.io/badge/Kentico_Labs-grey?labelColor=orange&logo=data:image/svg+xml;base64,PHN2ZyBjbGFzcz0ic3ZnLWljb24iIHN0eWxlPSJ3aWR0aDogMWVtOyBoZWlnaHQ6IDFlbTt2ZXJ0aWNhbC1hbGlnbjogbWlkZGxlO2ZpbGw6IGN1cnJlbnRDb2xvcjtvdmVyZmxvdzogaGlkZGVuOyIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGQ9Ik05NTYuMjg4IDgwNC40OEw2NDAgMjc3LjQ0VjY0aDMyYzE3LjYgMCAzMi0xNC40IDMyLTMycy0xNC40LTMyLTMyLTMyaC0zMjBjLTE3LjYgMC0zMiAxNC40LTMyIDMyczE0LjQgMzIgMzIgMzJIMzg0djIxMy40NEw2Ny43MTIgODA0LjQ4Qy00LjczNiA5MjUuMTg0IDUxLjIgMTAyNCAxOTIgMTAyNGg2NDBjMTQwLjggMCAxOTYuNzM2LTk4Ljc1MiAxMjQuMjg4LTIxOS41MnpNMjQxLjAyNCA2NDBMNDQ4IDI5NS4wNFY2NGgxMjh2MjMxLjA0TDc4Mi45NzYgNjQwSDI0MS4wMjR6IiAgLz48L3N2Zz4=)](https://github.com/Kentico/.github/blob/main/SUPPORT.md#labs-limited-support) -[![CI: Build and Test](https://github.com/Kentico/xperience-by-kentico-ecommerce/actions/workflows/ci.yml/badge.svg)](https://github.com/Kentico/xperience-by-kentico-ecommerce/actions/workflows/ci.yml) +[![CI: Build and Test](https://github.com/Kentico/xperience-by-kentico-k13ecommerce/actions/workflows/ci.yml/badge.svg)](https://github.com/Kentico/xperience-by-kentico-k13ecommerce/actions/workflows/ci.yml) -**This integration is currently a Proof of Concept (PoC). For further details, please refer to the [Support](#support) section and the [KenticoLabs](https://github.com/Kentico/.github/blob/main/SUPPORT.md#labs-limited-support) tag associated with this feature.** +**This integration is currently a Proof of Concept (PoC). For further details, please refer to the [Support](#support) section and the [Kentico Labs](https://github.com/Kentico/.github/blob/main/SUPPORT.md#labs-limited-support) tag associated with this feature.** | Name | Package | | ------------- |:-------------:| @@ -43,8 +43,10 @@ is located in [Dancing Goat XbyK example project](./examples/DancingGoat-K13Ecom The integration provides an API with services for implementing the following scenarios: - Listing products based on parameters, product categories, prices and inventory - Actions with shopping cart, changing currency and order creation - - Listing of orders (currently suitable for implementing listing orders in administration) - - **Order updates and listing for specific customers are under development** + - Listing of orders in administration + - Listing of orders for current customer + - Update order + - Listing site order statuses - Listing site cultures and currencies - Check [this part of User Guide](./docs/Usage-Guide.md#kx-13-e-commerce-integration-in-xperience-by-kentico) for more specific description @@ -137,7 +139,7 @@ dotnet add package Kentico.Xperience.StoreApi } ``` -2. Add [Store API services](https://github.com/Kentico/xperience-by-kentico-ecommerce/blob/main/examples/Kentico13_DancingGoatStore/Startup.cs#L130) to application services and configure Swagger +2. Add [Store API services](https://github.com/Kentico/xperience-by-kentico-k13ecommerce/blob/main/examples/Kentico13_DancingGoatStore/Startup.cs#L130) to application services and configure Swagger ```csharp // Startup.cs @@ -174,7 +176,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment environment) } } ``` -2. Add K13Ecommerce library to the [application services](https://github.com/Kentico/xperience-by-kentico-ecommerce/blob/main/examples/DancingGoat-K13Ecommerce/Program.cs#L61) +2. Add K13Ecommerce library to the [application services](https://github.com/Kentico/xperience-by-kentico-k13ecommerce/blob/main/examples/DancingGoat-K13Ecommerce/Program.cs#L61) ```csharp // Program.cs diff --git a/docs/Usage-Guide.md b/docs/Usage-Guide.md index 112b78e3..2d1fb153 100644 --- a/docs/Usage-Guide.md +++ b/docs/Usage-Guide.md @@ -77,6 +77,10 @@ via API. ### Orders - Endpoint `api/store/order/list` for retrieving list of orders for current customer based on request (supports paging) - Endpoint `api/store/order/admin/list` for retrieving list of orders (for all customers) based on request (supports paging) to display in XbyK administration (supports paging) +- Endpoint `api/store/order/detail/{orderID}` for retrieving order detail for current customer. If the order order belongs to another customer, no order is retrieved +- Endpoint `api/store/order/admin/detail/{orderID}` for retrieving order detail(without verifying if order belongs to current customer) +- Endpoint `api/store/order/statuses/list` for retrieving all order statuses +- Endpoint `api/store/order/update` for updating order(update order status, set order payment, etc.) ### Customers - Endpoint `api/store/customer/addresses` for retrieving current customer's addresses @@ -218,8 +222,12 @@ and to browser cookie (uses `IShoppingCartClientStorage`) - Service is used e.g. in [CheckoutService in Dancing Goat example](../examples/DancingGoat-K13Ecommerce/Services/CheckoutService.cs) where customer's addresses are retrieved in cart's second step. - `IOrderService` - - List of orders - currently suitable for implementing listing orders in administration - - **Order updates and listing for specific customers are under development** + - List of orders from all customers(for implementing listing orders in administration) + - List of orders for current customer(based on request) + - Order detail for current customer(only for orders that belong to the customer) + - Order detail for administrator(without verifying if order belongs to current customer) + - List of all order statuses + - Update order - `ISiteStoreService` - Use for retrieving site's [list of enabled cultures](https://github.com/Kentico/xperience-by-kentico-ecommerce/blob/main/src/Kentico.Xperience.K13Ecommerce/SiteStore/ISiteStoreService.cs#L13), e.g. for implementation of language selector - Use for retrieving site's [list of enabled currencies](https://github.com/Kentico/xperience-by-kentico-ecommerce/blob/main/src/Kentico.Xperience.K13Ecommerce/SiteStore/ISiteStoreService.cs#L18), e.g. for implementation of currency selector @@ -414,10 +422,11 @@ Here are links for some specific parts of shopping cart: - [Discount / Coupon codes](https://github.com/Kentico/xperience-by-kentico-ecommerce/blob/main/examples/DancingGoat-K13Ecommerce/Controllers/KStore/CheckoutController.cs#L163) - [Delivery details + shipping](https://github.com/Kentico/xperience-by-kentico-ecommerce/blob/main/examples/DancingGoat-K13Ecommerce/Controllers/KStore/CheckoutController.cs#L194) - [Payment](https://github.com/Kentico/xperience-by-kentico-ecommerce/blob/main/examples/DancingGoat-K13Ecommerce/Controllers/KStore/CheckoutController.cs#L330) -- Payment gateway - Is not part of this PoC solution, you need to implement integration with specific payment gateway. **API for updating orders (and their statuses) is under development**. +- Payment gateway - Is not part of this PoC solution, you need to implement integration with specific payment gateway. - [Order creation](https://github.com/Kentico/xperience-by-kentico-ecommerce/blob/main/examples/DancingGoat-K13Ecommerce/Controllers/KStore/CheckoutController.cs#L315) - - - +### How to handle order payments? +1. Implement your own payment method. +2. Retrieve all order statuses using `IOrderService` if needed. +3. Use `UpdateOrder` method of `IOrderService` to update order status and to set `OrderIsPaid` flag according to the payment result. \ No newline at end of file diff --git a/examples/DancingGoat-K13Ecommerce/Controllers/TestController.cs b/examples/DancingGoat-K13Ecommerce/Controllers/TestController.cs index de833ceb..3e4ca008 100644 --- a/examples/DancingGoat-K13Ecommerce/Controllers/TestController.cs +++ b/examples/DancingGoat-K13Ecommerce/Controllers/TestController.cs @@ -29,5 +29,43 @@ public async Task TestSetCurrency(string currencyCode) public async Task TestOrders([FromServices] IOrderService orderService) => Json(await orderService.GetCurrentCustomerOrderList(new OrderListRequest { Page = 1, PageSize = 10, OrderBy = "OrderID DESC" })); + + public async Task TestOrderStatuses([FromServices] IOrderService orderService) + { + var statuses = await orderService.GetOrderStatuses(); + return Json(statuses); + } + + public async Task TestUpdateOrder([FromServices] IOrderService orderService) + { + var orders = + await orderService.GetAdminOrderList(new OrderListRequest { Page = 1, PageSize = 10, OrderBy = "OrderID DESC" }); + + var order = orders.Orders.First(o => o.OrderId == 25); + + order.OrderGrandTotal = 999; + order.OrderIsPaid = true; + var newStatus = (await orderService.GetOrderStatuses()).First(o => o.StatusName == "Completed"); + order.OrderStatus = newStatus; + + order.OrderBillingAddress.AddressLine1 = "123 Main St"; + order.OrderBillingAddress.AddressCity = "New York"; + + order.OrderShippingAddress.AddressZip = "10001"; + + order.OrderShippingOption = new KShippingOption { ShippingOptionId = 2 }; + order.OrderPaymentOption = new KPaymentOption { PaymentOptionId = 1 }; + + order.OrderPaymentResult = new KPaymentResult + { + PaymentIsCompleted = true, + PaymentMethodName = "Test", + PaymentStatusName = "Test status" + }; + + await orderService.UpdateOrder(order); + + return Ok(); + } } #endif diff --git a/src/Kentico.Xperience.K13Ecommerce/Orders/IOrderService.cs b/src/Kentico.Xperience.K13Ecommerce/Orders/IOrderService.cs index fd397946..f87dcb53 100644 --- a/src/Kentico.Xperience.K13Ecommerce/Orders/IOrderService.cs +++ b/src/Kentico.Xperience.K13Ecommerce/Orders/IOrderService.cs @@ -21,4 +21,31 @@ public interface IOrderService /// Request parameters for order listing. /// Paged list of orders. Task GetAdminOrderList(OrderListRequest request); + + + /// + /// Get order by id for current customer. + /// + /// Order ID. + Task GetCurrentCustomerOrder(int orderId); + + + /// + /// Get order by ID to display in XbyK administration. + /// + /// Order ID. + Task GetAdminOrder(int orderId); + + + /// + /// Returns list of enabled order statuses. + /// + Task> GetOrderStatuses(); + + + /// + /// Updates order. + /// + /// Order Dto, send full data for order - retrieve order data first. + Task UpdateOrder(KOrder order); } diff --git a/src/Kentico.Xperience.K13Ecommerce/Orders/OrderListRequest.cs b/src/Kentico.Xperience.K13Ecommerce/Orders/OrderListRequest.cs index 5b17d59d..565e8ed7 100644 --- a/src/Kentico.Xperience.K13Ecommerce/Orders/OrderListRequest.cs +++ b/src/Kentico.Xperience.K13Ecommerce/Orders/OrderListRequest.cs @@ -19,4 +19,9 @@ public class OrderListRequest /// Order by. /// public required string OrderBy { get; set; } + + /// + /// Customer ID, leave null or zero for all customers. + /// + public int? CustomerId { get; set; } } diff --git a/src/Kentico.Xperience.K13Ecommerce/Orders/OrderService.cs b/src/Kentico.Xperience.K13Ecommerce/Orders/OrderService.cs index 4680615e..b8f28e2d 100644 --- a/src/Kentico.Xperience.K13Ecommerce/Orders/OrderService.cs +++ b/src/Kentico.Xperience.K13Ecommerce/Orders/OrderService.cs @@ -12,4 +12,20 @@ public async Task GetCurrentCustomerOrderList(OrderListReques /// public async Task GetAdminOrderList(OrderListRequest request) => await storeApiClient.AdminOrderListAsync(request.Page, request.PageSize, request.OrderBy); + + + /// + public async Task GetCurrentCustomerOrder(int orderId) => await storeApiClient.OrderDetailAsync(orderId); + + + /// + public async Task GetAdminOrder(int orderId) => await storeApiClient.AdminOrderDetailAsync(orderId); + + + /// + public async Task> GetOrderStatuses() => await storeApiClient.OrderStatusesListAsync(); + + + /// + public async Task UpdateOrder(KOrder order) => await storeApiClient.UpdateOrderAsync(order); } diff --git a/src/Kentico.Xperience.K13Ecommerce/StoreApi/swagger.json b/src/Kentico.Xperience.K13Ecommerce/StoreApi/swagger.json index 44dd8176..b24af5c4 100644 --- a/src/Kentico.Xperience.K13Ecommerce/StoreApi/swagger.json +++ b/src/Kentico.Xperience.K13Ecommerce/StoreApi/swagger.json @@ -243,6 +243,14 @@ "schema": { "type": "string" } + }, + { + "name": "CustomerId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } } ], "responses": { @@ -301,6 +309,14 @@ "schema": { "type": "string" } + }, + { + "name": "CustomerId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } } ], "responses": { @@ -327,6 +343,170 @@ } } }, + "/api/store/order/detail/{orderId}": { + "get": { + "tags": [ + "Order" + ], + "summary": "Returns order by ID only if order belongs to current customer.", + "operationId": "OrderDetail", + "parameters": [ + { + "name": "orderId", + "in": "path", + "description": "Order ID.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KOrder" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/api/store/order/admin/detail/{orderId}": { + "get": { + "tags": [ + "Order" + ], + "summary": "Returns order by ID.", + "operationId": "AdminOrderDetail", + "parameters": [ + { + "name": "orderId", + "in": "path", + "description": "Order ID.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KOrder" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/api/store/order/statuses/list": { + "get": { + "tags": [ + "Order" + ], + "summary": "Endpoint for listing order statuses.", + "operationId": "OrderStatusesList", + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/KOrderStatus" + } + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/api/store/order/update": { + "put": { + "tags": [ + "Order" + ], + "summary": "Updates order. Updates all fields on order level and addresses on sub-level. Customer data and order items cannot be updated.", + "operationId": "UpdateOrder", + "requestBody": { + "description": "Order dto", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KOrder" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/KOrder" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/KOrder" + } + } + } + }, + "responses": { + "200": { + "description": "Success" + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, "/api/store/products/test": { "get": { "tags": [ @@ -2360,6 +2540,10 @@ "KCurrency": { "type": "object", "properties": { + "currencyId": { + "type": "integer", + "format": "int32" + }, "currencyCode": { "type": "string", "nullable": true @@ -2370,7 +2554,7 @@ } }, "additionalProperties": false, - "description": "Represents currency from Store configuration." + "description": "Represents currency from Store configuration. CMS.Ecommerce.CurrencyInfo" }, "KCustomer": { "required": [ @@ -2480,6 +2664,12 @@ "description": "Dto for CMS.Ecommerce.ManufacturerInfo." }, "KOrder": { + "required": [ + "orderGrandTotal", + "orderId", + "orderTotalPrice", + "orderTotalTax" + ], "type": "object", "properties": { "orderId": { @@ -2498,9 +2688,8 @@ "type": "string", "nullable": true }, - "orderCurrencyCode": { - "type": "string", - "nullable": true + "orderCurrency": { + "$ref": "#/components/schemas/KCurrency" }, "orderShippingOption": { "$ref": "#/components/schemas/KShippingOption" @@ -2599,6 +2788,9 @@ "$ref": "#/components/schemas/KOrderItem" }, "nullable": true + }, + "orderPaymentResult": { + "$ref": "#/components/schemas/KPaymentResult" } }, "additionalProperties": false, @@ -2712,6 +2904,57 @@ "additionalProperties": false, "description": "Dto for CMS.Ecommerce.PaymentOptionInfo." }, + "KPaymentResult": { + "type": "object", + "properties": { + "paymentDate": { + "type": "string", + "nullable": true + }, + "paymentIsCompleted": { + "type": "boolean" + }, + "paymentIsFailed": { + "type": "boolean" + }, + "paymentIsAuthorized": { + "type": "boolean" + }, + "paymentDescription": { + "type": "string", + "nullable": true + }, + "paymentTransactionId": { + "type": "string", + "nullable": true + }, + "paymentAuthorizationID": { + "type": "string", + "nullable": true + }, + "paymentMethodName": { + "type": "string", + "nullable": true + }, + "paymentMethodID": { + "type": "integer", + "format": "int32" + }, + "paymentStatusValue": { + "type": "string", + "nullable": true + }, + "paymentStatusName": { + "type": "string", + "nullable": true + }, + "paymentApprovalUrl": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, "KProductCatalogPrices": { "type": "object", "properties": { diff --git a/src/Kentico.Xperience.StoreApi/Currencies/KCurrency.cs b/src/Kentico.Xperience.StoreApi/Currencies/KCurrency.cs index f261bb27..d8b0f213 100644 --- a/src/Kentico.Xperience.StoreApi/Currencies/KCurrency.cs +++ b/src/Kentico.Xperience.StoreApi/Currencies/KCurrency.cs @@ -1,10 +1,11 @@ namespace Kentico.Xperience.StoreApi.Currencies; /// -/// Represents currency from Store configuration. +/// Represents currency from Store configuration. /// public class KCurrency { + public int CurrencyId { get; set; } public string CurrencyCode { get; set; } public string CurrencyFormatString { get; set; } } diff --git a/src/Kentico.Xperience.StoreApi/Mapping/StoreApiMappingProfile.cs b/src/Kentico.Xperience.StoreApi/Mapping/StoreApiMappingProfile.cs index bb479123..04d3ffbd 100644 --- a/src/Kentico.Xperience.StoreApi/Mapping/StoreApiMappingProfile.cs +++ b/src/Kentico.Xperience.StoreApi/Mapping/StoreApiMappingProfile.cs @@ -56,21 +56,34 @@ public StoreApiMappingProfile() CreateMap() .AfterMap((source, dest, ctx) => { - dest.OrderCurrencyCode = CurrencyInfoProvider.ProviderObject.Get(source.OrderCurrencyID)?.CurrencyCode; + dest.OrderCurrency = + ctx.Mapper.Map(CurrencyInfoProvider.ProviderObject.Get(source.OrderCurrencyID)); dest.OrderCustomer = ctx.Mapper.Map(CustomerInfoProvider.ProviderObject.Get(source.OrderCustomerID)); dest.OrderShippingOption = ctx.Mapper.Map(ShippingOptionInfo.Provider.Get(source.OrderShippingOptionID)); - dest.OrderPaymentOption = ctx.Mapper.Map(PaymentOptionInfo.Provider.Get(source.OrderPaymentOptionID)); - dest.OrderItems = ctx.Mapper.Map>(OrderItemInfoProvider.GetOrderItems(source.OrderID)); - dest.OrderStatus = ctx.Mapper.Map(OrderStatusInfoProvider.ProviderObject.Get(source.OrderStatusID)); - }); + dest.OrderPaymentOption = + ctx.Mapper.Map(PaymentOptionInfo.Provider.Get(source.OrderPaymentOptionID)); + dest.OrderItems = + ctx.Mapper.Map>(OrderItemInfoProvider.GetOrderItems(source.OrderID)); + dest.OrderStatus = + ctx.Mapper.Map(OrderStatusInfoProvider.ProviderObject.Get(source.OrderStatusID)); + }) + .ReverseMap() + .ForPath(s => s.OrderCurrencyID, m => m.MapFrom(d => d.OrderCurrency.CurrencyId)) + .ForPath(s => s.OrderCustomerID, m => m.MapFrom(d => d.OrderCustomer.CustomerId)) + .ForPath(s => s.OrderShippingOptionID, m => m.MapFrom(d => d.OrderShippingOption.ShippingOptionId)) + .ForPath(s => s.OrderPaymentOptionID, m => m.MapFrom(d => d.OrderPaymentOption.PaymentOptionId)) + .ForPath(s => s.OrderStatusID, m => m.MapFrom(d => d.OrderStatus.StatusId)) + .ForPath(s => s.OrderBillingAddress, m => m.MapFrom(d => d.OrderBillingAddress)) + .ForPath(s => s.OrderShippingAddress, m => m.MapFrom(d => d.OrderShippingAddress)); CreateMap(); - CreateMap(); + CreateMap().ReverseMap(); CreateMap(); CreateMap>().ConvertUsing(); CreateMap(); + CreateMap().ReverseMap(); CreateMap() .AfterMap((source, dest, ctx) => diff --git a/src/Kentico.Xperience.StoreApi/Orders/KOrder.cs b/src/Kentico.Xperience.StoreApi/Orders/KOrder.cs index 4d10a332..901daf44 100644 --- a/src/Kentico.Xperience.StoreApi/Orders/KOrder.cs +++ b/src/Kentico.Xperience.StoreApi/Orders/KOrder.cs @@ -1,22 +1,25 @@ -using Kentico.Xperience.StoreApi.Customers; +using System.ComponentModel.DataAnnotations; + +using Kentico.Xperience.StoreApi.Currencies; +using Kentico.Xperience.StoreApi.Customers; using Kentico.Xperience.StoreApi.ShoppingCart; namespace Kentico.Xperience.StoreApi.Orders; /// -/// Dto for . +/// Dto for . /// public class KOrder { - public int OrderId { get; set; } + [Required] public int OrderId { get; set; } - public decimal OrderTotalTax { get; set; } + [Required] public decimal OrderTotalTax { get; set; } public string OrderTaxSummary { get; set; } public string OrderInvoiceNumber { get; set; } - public string OrderCurrencyCode { get; set; } + public KCurrency OrderCurrency { get; set; } public KShippingOption OrderShippingOption { get; set; } @@ -28,11 +31,11 @@ public class KOrder public KOrderStatus OrderStatus { get; set; } - public decimal OrderGrandTotal { get; set; } + [Required] public decimal OrderGrandTotal { get; set; } public decimal OrderGrandTotalInMainCurrency { get; set; } - public decimal OrderTotalPrice { get; set; } + [Required] public decimal OrderTotalPrice { get; set; } public decimal OrderTotalPriceInMainCurrency { get; set; } @@ -67,4 +70,6 @@ public class KOrder public KAddress OrderCompanyAddress { get; set; } public IEnumerable OrderItems { get; set; } + + public KPaymentResult OrderPaymentResult { get; set; } } diff --git a/src/Kentico.Xperience.StoreApi/Orders/KPaymentResult.cs b/src/Kentico.Xperience.StoreApi/Orders/KPaymentResult.cs new file mode 100644 index 00000000..e3ff9e58 --- /dev/null +++ b/src/Kentico.Xperience.StoreApi/Orders/KPaymentResult.cs @@ -0,0 +1,28 @@ +namespace Kentico.Xperience.StoreApi.Orders; + +public class KPaymentResult +{ + public string PaymentDate { get; set; } + + public bool PaymentIsCompleted { get; set; } + + public bool PaymentIsFailed { get; set; } + + public bool PaymentIsAuthorized { get; set; } + + public string PaymentDescription { get; set; } + + public string PaymentTransactionId { get; set; } + + public string PaymentAuthorizationID { get; set; } + + public string PaymentMethodName { get; set; } + + public int PaymentMethodID { get; set; } + + public string PaymentStatusValue { get; set; } + + public string PaymentStatusName { get; set; } + + public string PaymentApprovalUrl { get; set; } +} diff --git a/src/Kentico.Xperience.StoreApi/Orders/OrderController.cs b/src/Kentico.Xperience.StoreApi/Orders/OrderController.cs index 2a0b78d2..d1199fb9 100644 --- a/src/Kentico.Xperience.StoreApi/Orders/OrderController.cs +++ b/src/Kentico.Xperience.StoreApi/Orders/OrderController.cs @@ -25,17 +25,20 @@ public class OrderController : ControllerBase private readonly IMapper mapper; private readonly ISiteService siteService; private readonly IShoppingService shoppingService; + private readonly IOrderStatusInfoProvider orderStatusInfoProvider; public OrderController( IOrderInfoProvider orderInfoProvider, IMapper mapper, ISiteService siteService, - IShoppingService shoppingService) + IShoppingService shoppingService, + IOrderStatusInfoProvider orderStatusInfoProvider) { this.orderInfoProvider = orderInfoProvider; this.mapper = mapper; this.siteService = siteService; this.shoppingService = shoppingService; + this.orderStatusInfoProvider = orderStatusInfoProvider; } /// @@ -83,7 +86,7 @@ public async Task> CurrentCustomerOrderList([Fro /// /// Endpoint for getting list of orders based on request to display in XbyK administration. /// - /// + /// Order list request. /// [HttpGet("admin/list", Name = nameof(AdminOrderList))] public async Task> AdminOrderList([FromQuery] OrderListRequest request) @@ -93,11 +96,17 @@ public async Task> AdminOrderList([FromQuery] Or { request.OrderBy = $"{nameof(OrderInfo.OrderDate)} DESC"; } + var orderQuery = orderInfoProvider.Get() .OnSite(siteService.CurrentSite.SiteID) .Page(page, request.PageSize) .OrderBy(request.OrderBy); + if (request.CustomerId is > 0) + { + orderQuery = orderQuery.WhereEquals(nameof(OrderInfo.OrderCustomerID), request.CustomerId); + } + var orders = mapper.Map>(await orderQuery.GetEnumerableTypedResultAsync()); return Ok(new OrderListResponse @@ -107,4 +116,94 @@ public async Task> AdminOrderList([FromQuery] Or MaxPage = (orderQuery.TotalRecords / request.PageSize) + 1 }); } + + + /// + /// Returns order by ID only if order belongs to current customer. + /// + /// Order ID. + [HttpGet("detail/{orderId:int}", Name = nameof(OrderDetail))] + public async Task> OrderDetail([FromRoute] int orderId) + { + var customer = shoppingService.GetCurrentCustomer(); + if (customer == null) + { + return NotFound(); + } + + var order = await orderInfoProvider.GetAsync(orderId); + + if (order == null || order.OrderCustomerID != customer.CustomerID) + { + return NotFound(); + } + + return Ok(mapper.Map(order)); + } + + + /// + /// Returns order by ID. + /// + /// Order ID. + [HttpGet("admin/detail/{orderId:int}", Name = nameof(AdminOrderDetail))] + public async Task> AdminOrderDetail([FromRoute] int orderId) + { + var order = await orderInfoProvider.GetAsync(orderId); + + if (order == null) + { + return NotFound(); + } + + return Ok(mapper.Map(order)); + } + + + /// + /// Endpoint for listing order statuses. + /// + /// List of order statuses. + [HttpGet("statuses/list", Name = nameof(OrderStatusesList))] + public async Task>> OrderStatusesList() + { + int siteId = ECommerceHelper.GetSiteID(siteService.CurrentSite.SiteID, "CMSStoreUseGlobalOrderStatus"); + + var orderStatuses = await orderStatusInfoProvider.Get() + .OnSite(siteId, includeGlobal: siteId == 0) + .OrderBy(nameof(OrderStatusInfo.StatusOrder)) + .GetEnumerableTypedResultAsync(); + + return Ok(mapper.Map>(orderStatuses)); + } + + /// + /// Updates order. Updates all fields on order level and addresses on sub-level. Customer data and order items cannot be updated. + /// + /// Order dto + [HttpPut("update", Name = nameof(UpdateOrder))] + public async Task UpdateOrder([FromBody] KOrder order) + { + var orderInfo = await orderInfoProvider.GetAsync(order.OrderId); + if (orderInfo == null) + { + return NotFound(); + } + + orderInfo = mapper.Map(order, orderInfo); + + if (orderInfo.OrderBillingAddress.HasChanged) + { + orderInfo.OrderBillingAddress.Update(); + } + + if (orderInfo.OrderShippingAddress.HasChanged) + { + orderInfo.OrderShippingAddress.Update(); + } + + orderInfoProvider.Set(orderInfo); + + return Ok(); + } } diff --git a/src/Kentico.Xperience.StoreApi/Orders/OrderListRequest.cs b/src/Kentico.Xperience.StoreApi/Orders/OrderListRequest.cs index 795c59f2..bdcb2e2a 100644 --- a/src/Kentico.Xperience.StoreApi/Orders/OrderListRequest.cs +++ b/src/Kentico.Xperience.StoreApi/Orders/OrderListRequest.cs @@ -13,4 +13,6 @@ public class OrderListRequest public int PageSize { get; set; } public string OrderBy { get; set; } + + public int? CustomerId { get; set; } } diff --git a/submodules/xperience-by-kentico-ecommerce-common b/submodules/xperience-by-kentico-ecommerce-common index cf7540fd..91a62d6b 160000 --- a/submodules/xperience-by-kentico-ecommerce-common +++ b/submodules/xperience-by-kentico-ecommerce-common @@ -1 +1 @@ -Subproject commit cf7540fddfe1e5ca7e376aaf1467d29dce65267e +Subproject commit 91a62d6bcbcde1faa8d06d5f8d5cc0fa68fc68a4 diff --git a/test/Kentico.Xperience.K13Ecommerce.IntegrationTests/StoreApiOrderTests.cs b/test/Kentico.Xperience.K13Ecommerce.IntegrationTests/StoreApiOrderTests.cs new file mode 100644 index 00000000..8bc46856 --- /dev/null +++ b/test/Kentico.Xperience.K13Ecommerce.IntegrationTests/StoreApiOrderTests.cs @@ -0,0 +1,85 @@ +using Kentico.Xperience.K13Ecommerce.StoreApi; + +namespace Kentico.Xperience.KStore.Tests; + +/// +/// Store API order integration tests. +/// +[TestFixture] +[Category("IntegrationTests")] +public class StoreApiOrderTests : StoreApiTestBase +{ + /// + /// Test for existing order. + /// + /// Order ID. + /// Customer email. + [Test] + [TestCase(27, "automation.test@test.com")] + public async Task OrderDetail_ExistingOrder(int orderId, string customerEmail) + { + var order = await StoreApiClient.OrderDetailAsync(orderId); + + Assert.That(order.OrderId, Is.EqualTo(orderId)); + Assert.That(order.OrderItems, Has.Count.EqualTo(2)); + + Assert.That(order.OrderCustomer.CustomerEmail, Is.EqualTo(customerEmail)); + Assert.That(order.OrderBillingAddress.AddressLine1, Is.EqualTo("Automation Street 1")); + Assert.That(order.OrderBillingAddress.AddressCity, Is.EqualTo("Automation City A")); + Assert.That(order.OrderBillingAddress.AddressZip, Is.EqualTo("00001")); + Assert.That(order.OrderBillingAddress.AddressCountryId, Is.EqualTo(326)); + + Assert.That(order.OrderShippingAddress.AddressLine1, Is.EqualTo("Automation Street 2")); + Assert.That(order.OrderShippingAddress.AddressCity, Is.EqualTo("Automation City B")); + Assert.That(order.OrderShippingAddress.AddressZip, Is.EqualTo("00002")); + Assert.That(order.OrderShippingAddress.AddressCountryId, Is.EqualTo(326)); + + Assert.That(order.OrderStatus.StatusName, Is.EqualTo("New")); + Assert.That(order.OrderShippingOption.ShippingOptionName, Is.EqualTo("StandardDelivery")); + Assert.That(order.OrderPaymentOption.PaymentOptionName, Is.EqualTo("DancingGoatCore.MoneyTransfer")); + } + + /// + /// Test order detail when order does not exist. + /// + /// Order ID. + [Test] + [TestCase(99999)] + public async Task OrderDetail_NonExistingOrder(int orderId) + { + // test that exception has status code 404 + var exception = Assert.ThrowsAsync(async () => await StoreApiClient.OrderDetailAsync(orderId)); + Assert.That(exception!.StatusCode, Is.EqualTo(404)); + } + + /// + /// Test for order update. + /// + /// Order ID. + [Test] + [TestCase(27)] + public async Task OrderUpdate_ChangeStatus_And_IsPaid(int orderId) + { + var order = await StoreApiClient.OrderDetailAsync(orderId); + + order.OrderStatus = new KOrderStatus { StatusId = 2 }; + order.OrderIsPaid = true; + + await StoreApiClient.UpdateOrderAsync(order); + + var updatedOrder = await StoreApiClient.OrderDetailAsync(orderId); + + Assert.That(updatedOrder.OrderStatus.StatusName, Is.EqualTo("PaymentReceived")); + Assert.That(updatedOrder.OrderIsPaid, Is.True); + + order.OrderIsPaid = false; + order.OrderStatus = new KOrderStatus { StatusId = 3 }; + + await StoreApiClient.UpdateOrderAsync(order); + + var rollbackedOrder = await StoreApiClient.OrderDetailAsync(orderId); + + Assert.That(rollbackedOrder.OrderStatus.StatusName, Is.EqualTo("New")); + Assert.That(rollbackedOrder.OrderIsPaid, Is.False); + } +}