diff --git a/README.md b/README.md
index 77bb8b29..64463452 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,8 @@
-# Xperience by Kentico - KX 13 E-Commerce integration
+# Xperience by Kentico - Kentico Xperience 13 E-commerce
-[![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)
+[![7-day bug-fix policy](https://img.shields.io/badge/-7--days_bug--fixing_policy-grey?labelColor=orange&logo=data:image/svg+xml;base64,PHN2ZyBjbGFzcz0ic3ZnLWljb24iIHN0eWxlPSJ3aWR0aDogMWVtOyBoZWlnaHQ6IDFlbTt2ZXJ0aWNhbC1hbGlnbjogbWlkZGxlO2ZpbGw6IGN1cnJlbnRDb2xvcjtvdmVyZmxvdzogaGlkZGVuOyIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGQ9Ik04ODguNDkgMjIyLjY4NnYtMzEuNTRsLTY1LjY3Mi0wLjk1NWgtMC4yMDVhNDY1LjcxNSA0NjUuNzE1IDAgMCAxLTE0NC4zMTUtMzEuMzM0Yy03Ny4wMDUtMzEuMTk4LTEyNi4yOTQtNjYuNzY1LTEyNi43MDMtNjcuMTA3bC0zOS44LTI4LjY3Mi0zOS4xODUgMjguNDY4Yy0yLjA0OCAxLjUwMS00OS45MDMgMzYuMDQ0LTEyNi45MDggNjcuMzFhNDQ3LjQyIDQ0Ny40MiAwIDAgMS0xNDQuNTIgMzEuMzM1bC02NS44NzcgMC45NTZ2Mzc4Ljg4YzAgODcuMDQgNDkuODM0IDE4NC42NjEgMTM3LjAxIDI2Ny44MSAzNy41NDcgMzUuODQgNzkuMjU4IDY2LjM1NSAxMjAuODMzIDg4LjIgNDMuMjggMjIuNzMzIDg0LjI0IDM0LjYxMiAxMTguODUyIDM0LjYxMiAzNC40MDYgMCA3NS43NzYtMTIuMTUyIDExOS42MDMtMzUuMTU4YTU0Ny45NzcgNTQ3Ljk3NyAwIDAgMCAxMjAuMDEzLTg3LjY1NCA1MTUuMjA5IDUxNS4yMDkgMCAwIDAgOTYuMTg4LTEyMi44OGMyNy4xMDItNDkuNTYyIDQwLjgyMy05OC4zMDQgNDAuODIzLTE0NC45OTlsLTAuMTM2LTM0Ny4yMDR6TTUxMC4wOSAxNDMuNDI4bDEuNzA2LTEuMzY1IDEuNzc1IDEuMzY1YzUuODAzIDQuMTY1IDU5LjUyOSA0MS44NDggMTQwLjM1NiA3NC43NTIgNzkuMTkgMzIuMDg2IDE1My42IDM1LjYzNSAxNjcuNjYzIDM2LjA0NWwyLjU5NCAwLjA2OCAwLjIwNSAzMTUuNzM0YzAuMTM3IDY5LjQ5NS00Mi41OTggMTUwLjE4Ni0xMTcuMDc3IDIyMS40NTdDNjQxLjU3IDg1NC4yODkgNTYzLjEzIDg5Ni40NzggNTEyIDg5Ni40NzhjLTIzLjY4OSAwLTU1LjU3LTkuODk5LTg5LjcwMi0yNy43ODVhNDc4LjgyMiA0NzguODIyIDAgMCAxLTEwNS42MDktNzcuMjc4QzI0Mi4yMSA3MjAuMjEzIDE5OS40NzUgNjM5LjUyMiAxOTkuNDc1IDU2OS44OVYyNTQuMjI1bDIuNzMtMC4xMzZjMy4yNzggMCA4Mi42MDQtMS41MDIgMTY3LjY2NC0zNS45NzdhNzM5Ljk0MiA3MzkuOTQyIDAgMCAwIDE0MC4yMi03NC42MTV2LTAuMDY5eiIgIC8+PHBhdGggZD0iTTcxMy4zMTggMzY4LjY0YTMyLjIyMiAzMi4yMjIgMCAwIDAtNDUuMzI5IDBMNDQ5LjE5NSA1ODcuNDM1bC05My4xODQtOTMuMTE2YTMyLjIyMiAzMi4yMjIgMCAwIDAtNDUuMzMgMCAzMi4yMjIgMzIuMjIyIDAgMCAwIDAgNDUuMjZsMTE1Ljg1IDExNS44NWEzMi4yOSAzMi4yOSAwIDAgMCA0NS4zMjggMEw3MTMuMzIgNDEzLjlhMzIuMjIyIDMyLjIyMiAwIDAgMCAwLTQ1LjMzeiIgIC8+PC9zdmc+)](https://github.com/Kentico/.github/blob/main/SUPPORT.md#full-support)
[![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 [Kentico Labs](https://github.com/Kentico/.github/blob/main/SUPPORT.md#labs-limited-support) tag associated with this feature.**
-
| Name | Package |
| ------------- |:-------------:|
| Kentico.Xperience.K13Ecommerce | [![NuGet Package](https://img.shields.io/nuget/v/Kentico.Xperience.K13Ecommerce.svg)](https://www.nuget.org/packages/Kentico.Xperience.K13Ecommerce) |
@@ -43,8 +41,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 the administration
+ - Listing of orders on the live site for the current customer
+ - Updates of existing orders
+ - Retrieving and listing 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
@@ -169,6 +169,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment environment)
"ClientId": "3ef7fe1b-696c-4afa-8b56-d3176b7bea95",
"ClientSecret": "********************",
"ProductSyncEnabled": true,
+ "StandaloneProductSync": true,
"ProductSyncInterval": 10
}
}
@@ -207,8 +208,8 @@ Distributed under the MIT License. See [`LICENSE.md`](./LICENSE.md) for more inf
## Support
-This contribution has __Kentico Labs limited support__.
+This contribution has __Full Support__.
-See [`SUPPORT.md`](https://github.com/Kentico/.github/blob/main/SUPPORT.md#labs-limited-support) for more information.
+See [`SUPPORT.md`](https://github.com/Kentico/.github/blob/main/SUPPORT.md) for more information.
For any security issues see [`SECURITY.md`](https://github.com/Kentico/.github/blob/main/SECURITY.md).
diff --git a/docs/Usage-Guide.md b/docs/Usage-Guide.md
index 53de2128..bccd9948 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 details for the current customer. If the order belongs to another customer, no order is retrieved
+- Endpoint `api/store/order/admin/detail/{orderID}` for retrieving order details (without verifying if the order belongs to the current customer)
+- Endpoint `api/store/order/statuses/list` for retrieving all order statuses
+- Endpoint `api/store/order/update` for updating orders (update order status, set order payment, etc.). Primarily intended to be used via `IOrderService` available in the [integration API](https://github.com/Kentico/xperience-by-kentico-k13ecommerce/pull/16#kx-13-e-commerce-integration-in-xperience-by-kentico)
### Customers
- Endpoint `api/store/customer/addresses` for retrieving current customer's addresses
@@ -90,7 +94,7 @@ via API.
When [member](https://docs.kentico.com/x/BIsuCw) is created on XbyK (for example when a new customer registers), this member needs to be synchronized to KX 13 as a user.
It is subsequently used for API authorization (member/user identity is generated in JWT).
Before you start using the Store API, you need to synchronize all website members between the client (XbyK) and your KX 13 application.
-Complete synchronization is not part of this PoC solution.
+Complete synchronization is currently not a part of this solution.
- Endpoint `api/store/synchronization/user-synchronization` can be used to create a new user in KX 13
- The client application (XbyK) should use this to ensure that all new members are synchronized to KX 13. This is necessary when client's
@@ -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 orders from all customers (for implementing order listings in the administration)
+ - List orders for the current customer (based on the request context)
+ - Retrieve order details for the current customer (only for orders that belong to the customer)
+ - Retrieve order details for administrators (without verifying if the order belongs to the current customer)
+ - List all order statuses
+ - Update existing orders (order status, payment, etc.)
- `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
@@ -232,7 +240,7 @@ and to browser cookie (uses `IShoppingCartClientStorage`)
Library also implements product synchronization to Content hub. These are 3 entities synchronized to reusable content items:
- Products - Content type `K13Store.ProductSKU`
- - All products associated with product pages are synced. **Standalone SKUs** aren't currently supported.
+ - All products associated with product pages are synced. **Standalone SKUs** synchronization can be set via `StandaloneProductSync` setting.
- Product variants - Content type `K13Store.ProductVariant`
- All products variant for parent products
- Product images - Content type `K13Store.ProductImage`
@@ -241,7 +249,7 @@ Library also implements product synchronization to Content hub. These are 3 enti
The synchronization runs in a background thread worker periodically and can be disabled (`ProductSyncEnabled` setting).
Interval can be set in minutes (`ProductSyncInterval` setting). Synchronized data is updated when source value
changes, so data cannot be edited in XbyK safely, but new custom or reusable fields can be added and edited
-safely.
+safely. You can decide, whether include [standalone SKUs](https://docs.kentico.com/x/3gqRBg) or not (`StandaloneProductSync` setting).
You can select content item folders where content items are synchronized. Content item folders can be selected independently for each content type in XbyK administration UI. Go to
**Configuration** -> **K13Ecommerce** -> **K13Ecommerce settings**. Content items are not moved if root folder is selected.
@@ -302,6 +310,7 @@ dotnet add package Kentico.Xperience.Store.Rcl
"ClientId": "3ef7fe1b-696c-4afa-8b56-d3176b7bea95",
"ClientSecret": "********************",
"ProductSyncEnabled": true,
+ "StandaloneProductSync": true,
"ProductSyncInterval": 10
}
}
@@ -314,6 +323,7 @@ dotnet add package Kentico.Xperience.Store.Rcl
| ClientId | Fill same value which is defined on KX 13 side |
| ClientSecret | Fill same value which is defined on KX 13 side |
| ProductSyncEnabled | If true, product synchronization is enabled |
+| StandaloneProductSync | If this setting along with `ProductSyncEnabled` is true, [standalone SKUs](https://docs.kentico.com/x/3gqRBg) are synchronized as well (if `ProductSyncEnabled` is false, no products are synchronized). |
| ProductSyncInterval | Interval in minutes specifies how often synchronization is running |
@@ -420,7 +430,7 @@ 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 currently part of the solution. You need to implement integration with a 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 enable automatic product page synchronization?
@@ -473,4 +483,9 @@ mapped e.g. in another folder or website channel:
#### Known limitations
Avoid creating rules that map the NodeAliasPath of different content items to a single product page tree path. Such rules cause the linked content item to be overwritten for particular pages.
-If you change existing mapping rules, already created pages will not be moved accordingly. Instead, they will be created in the new location.
\ No newline at end of file
+If you change existing mapping rules, already created pages will not be moved accordingly. Instead, they will be created in the new location.
+
+### 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/examples/DancingGoat-K13Ecommerce/appsettings.json b/examples/DancingGoat-K13Ecommerce/appsettings.json
index e97cc90b..8ffed5c8 100644
--- a/examples/DancingGoat-K13Ecommerce/appsettings.json
+++ b/examples/DancingGoat-K13Ecommerce/appsettings.json
@@ -19,6 +19,7 @@
"ClientId": "YourUniqueClientIdentifier",
"ClientSecret": "********************",
"ProductSyncEnabled": true,
+ "StandaloneProductSync": true,
"ProductSyncInterval": 10
}
}
\ No newline at end of file
diff --git a/examples/Kentico13_DancingGoatStore/ShopApi/CustomSKUConverter.cs b/examples/Kentico13_DancingGoatStore/ShopApi/CustomSKUConverter.cs
index 33140a37..5dcf28ff 100644
--- a/examples/Kentico13_DancingGoatStore/ShopApi/CustomSKUConverter.cs
+++ b/examples/Kentico13_DancingGoatStore/ShopApi/CustomSKUConverter.cs
@@ -10,9 +10,9 @@ namespace DancingGoat.ShopApi;
///
public class CustomSKUConverter : ProductSKUConverter
{
- public override CustomSKU Convert(SKUInfo skuInfo, string currencyCode, bool withVariants)
+ public override CustomSKU Convert(SKUInfo skuInfo, string currencyCode, bool withVariants, bool withLongDescription)
{
- var model = base.Convert(skuInfo, currencyCode, withVariants);
+ var model = base.Convert(skuInfo, currencyCode, withVariants, withLongDescription);
return model;
}
diff --git a/src/Kentico.Xperience.K13Ecommerce/Config/KenticoStoreConfig.cs b/src/Kentico.Xperience.K13Ecommerce/Config/KenticoStoreConfig.cs
index 6d99c2f9..12a822ab 100644
--- a/src/Kentico.Xperience.K13Ecommerce/Config/KenticoStoreConfig.cs
+++ b/src/Kentico.Xperience.K13Ecommerce/Config/KenticoStoreConfig.cs
@@ -20,6 +20,11 @@ public class KenticoStoreConfig
///
public required string ClientSecret { get; set; } = string.Empty;
+ ///
+ /// Synchronize also standalone products without page representation.
+ ///
+ public required bool StandaloneProductSync { get; set; } = true;
+
///
/// When true, product synchronization is enabled.
///
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/Products/IProductService.cs b/src/Kentico.Xperience.K13Ecommerce/Products/IProductService.cs
index 49d49537..479f2d7b 100644
--- a/src/Kentico.Xperience.K13Ecommerce/Products/IProductService.cs
+++ b/src/Kentico.Xperience.K13Ecommerce/Products/IProductService.cs
@@ -36,4 +36,12 @@ public interface IProductService
/// SKUID for product or variant.
/// Currency code.
Task GetVariantInventoryPriceInfo(int skuId, string? currencyCode = null);
+
+
+ ///
+ /// Gets all standalone products.
+ ///
+ /// Stanadlone product params.
+ ///
+ Task> GetStandaloneProducts(ProductRequest request);
}
diff --git a/src/Kentico.Xperience.K13Ecommerce/Products/ProductPageRequest.cs b/src/Kentico.Xperience.K13Ecommerce/Products/ProductPageRequest.cs
index a0d35390..dff2db93 100644
--- a/src/Kentico.Xperience.K13Ecommerce/Products/ProductPageRequest.cs
+++ b/src/Kentico.Xperience.K13Ecommerce/Products/ProductPageRequest.cs
@@ -3,7 +3,7 @@
///
/// Model for request to filter product pages
///
-public class ProductPageRequest
+public class ProductPageRequest : ProductRequest
{
///
/// Node alias path prefix
@@ -11,36 +11,6 @@ public class ProductPageRequest
public required string Path { get; set; }
- ///
- /// Document culture
- ///
- public string? Culture { get; set; }
-
-
- ///
- /// Product currency
- ///
- public string? Currency { get; set; }
-
-
- ///
- /// Order by SQL expression
- ///
- public string? OrderBy { get; set; }
-
-
- ///
- /// Limit how many products to return.
- ///
- public int? Limit { get; set; }
-
-
- ///
- /// If true variants are loaded too for products with variants (default false).
- ///
- public bool WithVariants { get; set; }
-
-
///
/// If true, DocumentSKUDescription is filled too (default false).
///
diff --git a/src/Kentico.Xperience.K13Ecommerce/Products/ProductRequest.cs b/src/Kentico.Xperience.K13Ecommerce/Products/ProductRequest.cs
new file mode 100644
index 00000000..1758d47a
--- /dev/null
+++ b/src/Kentico.Xperience.K13Ecommerce/Products/ProductRequest.cs
@@ -0,0 +1,39 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Kentico.Xperience.K13Ecommerce.Products;
+
+///
+/// Model for request to filter standalone products
+///
+public class ProductRequest
+{
+ ///
+ /// To determine if product has its page, culture needs to be provided
+ /// so documents in the specific culture will be checked.
+ ///
+ [RegularExpression("[a-zA-Z]{2}-[a-zA-Z]{2}")]
+ public string? Culture { get; set; }
+
+ ///
+ /// Product currency.
+ ///
+ public string? Currency { get; set; }
+
+ ///
+ /// Order by SQL expression.
+ ///
+ public string? OrderBy { get; set; }
+
+ ///
+ /// Limit how many products to return.
+ ///
+ [DefaultValue(12)]
+ [Range(1, 1000)]
+ public int Limit { get; set; }
+
+ ///
+ /// If true variants are loaded too for products with variants (default false).
+ ///
+ public bool WithVariants { get; set; }
+}
+
diff --git a/src/Kentico.Xperience.K13Ecommerce/Products/ProductService.cs b/src/Kentico.Xperience.K13Ecommerce/Products/ProductService.cs
index fc7ce94b..776b4d8e 100644
--- a/src/Kentico.Xperience.K13Ecommerce/Products/ProductService.cs
+++ b/src/Kentico.Xperience.K13Ecommerce/Products/ProductService.cs
@@ -6,8 +6,8 @@ internal class ProductService(IKenticoStoreApiClient storeApiClient) : IProductS
{
///
public async Task> GetProductPages(ProductPageRequest request)
- => await storeApiClient.GetProductPagesAsync(request.Path, request.Culture, request.Currency, request.OrderBy,
- request.Limit, request.WithVariants, request.WithLongDescription, request.NoLinks);
+ => await storeApiClient.GetProductPagesAsync(request.Path, request.WithLongDescription, request.NoLinks, request.Culture,
+ request.Currency, request.OrderBy, request.Limit, request.WithVariants);
///
@@ -24,4 +24,8 @@ public async Task GetProductPrices(int skuId, string? cur
///
public async Task GetVariantInventoryPriceInfo(int skuId, string? currencyCode = null)
=> await storeApiClient.GetInventoryPricesAsync(skuId, currencyCode);
+
+ ///
+ public async Task> GetStandaloneProducts(ProductRequest request) =>
+ await storeApiClient.GetStandaloneProductsAsync(request.Culture, request.Currency, request.OrderBy, request.Limit, request.WithVariants);
}
diff --git a/src/Kentico.Xperience.K13Ecommerce/StoreApi/StoreItemIdentifiers.cs b/src/Kentico.Xperience.K13Ecommerce/StoreApi/StoreItemIdentifiers.cs
index 1263c1f3..246ddb3a 100644
--- a/src/Kentico.Xperience.K13Ecommerce/StoreApi/StoreItemIdentifiers.cs
+++ b/src/Kentico.Xperience.K13Ecommerce/StoreApi/StoreItemIdentifiers.cs
@@ -9,6 +9,20 @@ public partial class KProductNode : IItemIdentifier
{
///
public int ExternalId => Sku.Skuid;
+
+ ///
+ /// Create product node from product SKU.
+ ///
+ ///
+ public KProductNode(KProductSKU sku)
+ {
+ Sku = sku;
+ DocumentSKUName = sku.SkuName;
+ DocumentSKUDescription = sku.SkuLongDescription;
+ DocumentSKUShortDescription = sku.SkuShortDescription;
+ ClassName = string.Empty;
+ NodeAliasPath = string.Empty;
+ }
}
///
diff --git a/src/Kentico.Xperience.K13Ecommerce/StoreApi/swagger.json b/src/Kentico.Xperience.K13Ecommerce/StoreApi/swagger.json
index ba166abd..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": [
@@ -367,10 +547,26 @@
"type": "string"
}
},
+ {
+ "name": "WithLongDescription",
+ "in": "query",
+ "description": "If true, DocumentSKUDescription is filled too (default false).",
+ "schema": {
+ "type": "boolean"
+ }
+ },
+ {
+ "name": "NoLinks",
+ "in": "query",
+ "description": "If true, only not-linked product pages are returned (default false).",
+ "schema": {
+ "type": "boolean"
+ }
+ },
{
"name": "Culture",
"in": "query",
- "description": "Document culture.",
+ "description": "To determine if product has its page, culture needs to be provided\r\nso documents in the specific culture will be checked.",
"schema": {
"pattern": "[a-zA-Z]{2}-[a-zA-Z]{2}",
"type": "string"
@@ -411,22 +607,6 @@
"schema": {
"type": "boolean"
}
- },
- {
- "name": "WithLongDescription",
- "in": "query",
- "description": "If true, DocumentSKUDescription is filled too (default false).",
- "schema": {
- "type": "boolean"
- }
- },
- {
- "name": "NoLinks",
- "in": "query",
- "description": "If true, only not-linked product pages are returned (default false).",
- "schema": {
- "type": "boolean"
- }
}
],
"responses": {
@@ -659,6 +839,87 @@
}
}
},
+ "/api/store/products/standalone-products": {
+ "get": {
+ "tags": [
+ "Products"
+ ],
+ "summary": "Returns all standalone products.",
+ "operationId": "GetStandaloneProducts",
+ "parameters": [
+ {
+ "name": "Culture",
+ "in": "query",
+ "description": "To determine if product has its page, culture needs to be provided\r\nso documents in the specific culture will be checked.",
+ "schema": {
+ "pattern": "[a-zA-Z]{2}-[a-zA-Z]{2}",
+ "type": "string"
+ }
+ },
+ {
+ "name": "Currency",
+ "in": "query",
+ "description": "Product currency.",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "OrderBy",
+ "in": "query",
+ "description": "Order by SQL expression.",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "Limit",
+ "in": "query",
+ "description": "Limit how many products to return.",
+ "schema": {
+ "maximum": 1000,
+ "minimum": 1,
+ "type": "integer",
+ "format": "int32",
+ "default": 12
+ }
+ },
+ {
+ "name": "WithVariants",
+ "in": "query",
+ "description": "If true variants are loaded too for products with variants (default false).",
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/KProductSKU"
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
"/api/store/cart/content": {
"get": {
"tags": [
@@ -2279,6 +2540,10 @@
"KCurrency": {
"type": "object",
"properties": {
+ "currencyId": {
+ "type": "integer",
+ "format": "int32"
+ },
"currencyCode": {
"type": "string",
"nullable": true
@@ -2289,7 +2554,7 @@
}
},
"additionalProperties": false,
- "description": "Represents currency from Store configuration."
+ "description": "Represents currency from Store configuration. CMS.Ecommerce.CurrencyInfo"
},
"KCustomer": {
"required": [
@@ -2399,6 +2664,12 @@
"description": "Dto for CMS.Ecommerce.ManufacturerInfo."
},
"KOrder": {
+ "required": [
+ "orderGrandTotal",
+ "orderId",
+ "orderTotalPrice",
+ "orderTotalTax"
+ ],
"type": "object",
"properties": {
"orderId": {
@@ -2417,9 +2688,8 @@
"type": "string",
"nullable": true
},
- "orderCurrencyCode": {
- "type": "string",
- "nullable": true
+ "orderCurrency": {
+ "$ref": "#/components/schemas/KCurrency"
},
"orderShippingOption": {
"$ref": "#/components/schemas/KShippingOption"
@@ -2518,6 +2788,9 @@
"$ref": "#/components/schemas/KOrderItem"
},
"nullable": true
+ },
+ "orderPaymentResult": {
+ "$ref": "#/components/schemas/KPaymentResult"
}
},
"additionalProperties": false,
@@ -2631,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": {
@@ -2751,6 +3075,10 @@
"type": "string",
"nullable": true
},
+ "skuLongDescription": {
+ "type": "string",
+ "nullable": true
+ },
"skuNumber": {
"type": "string",
"nullable": true
@@ -3311,7 +3639,7 @@
"nullable": true
}
},
- "additionalProperties": { }
+ "additionalProperties": {}
},
"ProductInventoryPriceInfo": {
"type": "object",
@@ -3418,7 +3746,7 @@
},
"security": [
{
- "Bearer": [ ]
+ "Bearer": []
}
]
}
\ No newline at end of file
diff --git a/src/Kentico.Xperience.K13Ecommerce/Synchronization/Products/ProductSynchronizationItem.cs b/src/Kentico.Xperience.K13Ecommerce/Synchronization/Products/ProductSynchronizationItem.cs
index 526ba7b3..1c8de2f5 100644
--- a/src/Kentico.Xperience.K13Ecommerce/Synchronization/Products/ProductSynchronizationItem.cs
+++ b/src/Kentico.Xperience.K13Ecommerce/Synchronization/Products/ProductSynchronizationItem.cs
@@ -90,6 +90,7 @@ public bool GetModifiedProperties(ProductSKU contentItem, out Dictionary config) : SynchronizationServiceCommon(httpClientFactory), IProductSynchronizationService
{
public async Task SynchronizeProducts()
{
@@ -33,7 +36,7 @@ public async Task SynchronizeProducts()
string language = defaultCultureCode[..2];
var kenticoStoreProducts =
- await productService.GetProductPages(new ProductPageRequest
+ (await productService.GetProductPages(new ProductPageRequest
{
Path = "/",
Culture = defaultCultureCode,
@@ -41,7 +44,20 @@ await productService.GetProductPages(new ProductPageRequest
WithVariants = true,
WithLongDescription = true,
NoLinks = true
- });
+ })).ToList();
+
+ if (config.CurrentValue.StandaloneProductSync)
+ {
+ var kenticoStandaloneProducts = (await productService.GetStandaloneProducts(new ProductRequest
+ {
+ Culture = defaultCultureCode,
+ Limit = 1000,
+ WithVariants = true
+ })).Select(p => new KProductNode(p));
+
+ kenticoStoreProducts.AddRange(kenticoStandaloneProducts);
+ }
+
var contentItemProducts =
await contentItemService.GetContentItems(ProductSKU.CONTENT_TYPE_NAME, linkedItemsLevel: 2);
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/src/Kentico.Xperience.StoreApi/Products/IKProductService.cs b/src/Kentico.Xperience.StoreApi/Products/IKProductService.cs
index 41fc8f5e..5a6c2643 100644
--- a/src/Kentico.Xperience.StoreApi/Products/IKProductService.cs
+++ b/src/Kentico.Xperience.StoreApi/Products/IKProductService.cs
@@ -1,6 +1,7 @@
using Kentico.Xperience.StoreApi.Products.Categories;
using Kentico.Xperience.StoreApi.Products.Pages;
using Kentico.Xperience.StoreApi.Products.Prices;
+using Kentico.Xperience.StoreApi.Products.SKU;
namespace Kentico.Xperience.StoreApi.Products;
@@ -49,4 +50,12 @@ public interface IKProductService
/// Currency code.
///
Task GetProductInventoryAndPrices(int skuId, string currencyCode);
+
+
+ ///
+ /// Get all standalone products in specific culture.
+ ///
+ /// Standalone products parameters.
+ ///
+ Task> GetStandaloneProducts(ProductRequest request);
}
diff --git a/src/Kentico.Xperience.StoreApi/Products/KProductService.cs b/src/Kentico.Xperience.StoreApi/Products/KProductService.cs
index cfc49d78..8068498a 100644
--- a/src/Kentico.Xperience.StoreApi/Products/KProductService.cs
+++ b/src/Kentico.Xperience.StoreApi/Products/KProductService.cs
@@ -10,6 +10,7 @@
using Kentico.Xperience.StoreApi.Products.Categories;
using Kentico.Xperience.StoreApi.Products.Pages;
using Kentico.Xperience.StoreApi.Products.Prices;
+using Kentico.Xperience.StoreApi.Products.SKU;
namespace Kentico.Xperience.StoreApi.Products;
@@ -18,6 +19,7 @@ internal class KProductService : IKProductService
{
private readonly IPageRetriever pageRetriever;
private readonly IProductPageConverter productPageConverter;
+ private readonly IProductSKUConverter productSKUConverter;
private readonly ISKUInfoProvider skuInfoProvider;
private readonly ICatalogPriceCalculatorFactory catalogPriceCalculatorFactory;
private readonly ISiteService siteService;
@@ -25,12 +27,14 @@ internal class KProductService : IKProductService
private readonly IShoppingService shoppingService;
- public KProductService(IPageRetriever pageRetriever, IProductPageConverter productPageConverter,
+ public KProductService(
+ IPageRetriever pageRetriever, IProductPageConverter productPageConverter, IProductSKUConverter productSKUConverter,
ISKUInfoProvider skuInfoProvider, ICatalogPriceCalculatorFactory catalogPriceCalculatorFactory,
ISiteService siteService, IMapper mapper, IShoppingService shoppingService)
{
this.pageRetriever = pageRetriever;
this.productPageConverter = productPageConverter;
+ this.productSKUConverter = productSKUConverter;
this.skuInfoProvider = skuInfoProvider;
this.catalogPriceCalculatorFactory = catalogPriceCalculatorFactory;
this.siteService = siteService;
@@ -48,10 +52,7 @@ public async Task> GetProductPages(ProductPageRequest
orderBy = "DocumentSKUName ASC";
}
- var productTypes = (await DataClassInfoProvider.ProviderObject.Get()
- .WhereTrue(nameof(DataClassInfo.ClassIsProduct))
- .Columns(nameof(DataClassInfo.ClassName), nameof(DataClassInfo.ClassFormDefinition))
- .GetEnumerableTypedResultAsync())
+ var productTypes = (await GetProductDataClasses())
.Select(p => new
{
p.ClassName,
@@ -161,6 +162,35 @@ public async Task GetProductInventoryAndPrices(int sk
}
+ ///
+ public async Task> GetStandaloneProducts(ProductRequest request)
+ {
+ var (culture, currencyCode, orderBy, limit, withVariants) = request;
+
+ var skuInfos = skuInfoProvider.Get()
+ .WhereEqualsOrNull(nameof(SKUInfo.SKUOptionCategoryID), 0)
+ .WhereEqualsOrNull(nameof(SKUInfo.SKUParentSKUID), 0)
+ .TopN(limit)
+ .OrderBy(orderBy);
+
+
+ var query = DocumentHelper.GetDocuments()
+ .WhereNotNull(nameof(TreeNode.NodeSKUID))
+ .Column(nameof(TreeNode.NodeSKUID));
+
+ if (!string.IsNullOrEmpty(culture))
+ {
+ query = query.Culture(culture);
+ }
+
+ skuInfos = skuInfos.WhereNotIn(nameof(SKUInfo.SKUID), query);
+
+
+ return (await skuInfos.GetEnumerableTypedResultAsync())
+ .Select(x => productSKUConverter.Convert(x, currencyCode, withVariants, true));
+ }
+
+
private ProductPricesResponse GetProductPrices(SKUInfo sku, string currencyCode)
{
var calculator = catalogPriceCalculatorFactory.GetCalculator(siteService.CurrentSite.SiteID);
@@ -185,4 +215,11 @@ private ProductPricesResponse GetProductPrices(SKUInfo sku, string currencyCode)
return response;
}
+
+
+ private async Task> GetProductDataClasses()
+ => await DataClassInfoProvider.ProviderObject.Get()
+ .WhereTrue(nameof(DataClassInfo.ClassIsProduct))
+ .Columns(nameof(DataClassInfo.ClassName), nameof(DataClassInfo.ClassFormDefinition))
+ .GetEnumerableTypedResultAsync();
}
diff --git a/src/Kentico.Xperience.StoreApi/Products/Pages/ProductPageConverter.cs b/src/Kentico.Xperience.StoreApi/Products/Pages/ProductPageConverter.cs
index 266f5307..9efdf768 100644
--- a/src/Kentico.Xperience.StoreApi/Products/Pages/ProductPageConverter.cs
+++ b/src/Kentico.Xperience.StoreApi/Products/Pages/ProductPageConverter.cs
@@ -30,7 +30,7 @@ public virtual TProduct Convert(SKUTreeNode skuTreeNode, IEnumerable cus
{
var model = mapper.Map(skuTreeNode);
model.AbsoluteUrl = DocumentURLProvider.GetAbsoluteUrl(skuTreeNode);
- model.SKU = skuConverter.Convert(skuTreeNode.SKU, currencyCode, withVariants);
+ model.SKU = skuConverter.Convert(skuTreeNode.SKU, currencyCode, withVariants, false);
model.CustomFields = customFields.ToDictionary(f => f, f => skuTreeNode.GetValue(f));
if (withLongDescription)
{
diff --git a/src/Kentico.Xperience.StoreApi/Products/ProductPageRequest.cs b/src/Kentico.Xperience.StoreApi/Products/ProductPageRequest.cs
index b26a1326..498203ee 100644
--- a/src/Kentico.Xperience.StoreApi/Products/ProductPageRequest.cs
+++ b/src/Kentico.Xperience.StoreApi/Products/ProductPageRequest.cs
@@ -1,14 +1,11 @@
-using System.ComponentModel;
-using System.ComponentModel.DataAnnotations;
-
-using Kentico.Xperience.StoreApi.Currencies;
+using System.ComponentModel.DataAnnotations;
namespace Kentico.Xperience.StoreApi.Products;
///
/// Model for product pages request used in API.
///
-public class ProductPageRequest
+public class ProductPageRequest : ProductRequest
{
///
/// Node alias path prefix.
@@ -16,35 +13,6 @@ public class ProductPageRequest
[Required]
public string Path { get; set; }
- ///
- /// Document culture.
- ///
- [RegularExpression("[a-zA-Z]{2}-[a-zA-Z]{2}")]
- public string Culture { get; set; }
-
- ///
- /// Product currency.
- ///
- [CurrencyValidation]
- public string Currency { get; set; }
-
- ///
- /// Order by SQL expression.
- ///
- public string OrderBy { get; set; }
-
- ///
- /// Limit how many products to return.
- ///
- [DefaultValue(12)]
- [Range(1, 1000)]
- public int Limit { get; set; }
-
- ///
- /// If true variants are loaded too for products with variants (default false).
- ///
- public bool WithVariants { get; set; }
-
///
/// If true, DocumentSKUDescription is filled too (default false).
///
diff --git a/src/Kentico.Xperience.StoreApi/Products/ProductRequest.cs b/src/Kentico.Xperience.StoreApi/Products/ProductRequest.cs
new file mode 100644
index 00000000..7d4773c5
--- /dev/null
+++ b/src/Kentico.Xperience.StoreApi/Products/ProductRequest.cs
@@ -0,0 +1,49 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel;
+
+using Kentico.Xperience.StoreApi.Currencies;
+
+namespace Kentico.Xperience.StoreApi.Products;
+
+public class ProductRequest
+{
+ ///
+ /// To determine if product has its page, culture needs to be provided
+ /// so documents in the specific culture will be checked.
+ ///
+ [RegularExpression("[a-zA-Z]{2}-[a-zA-Z]{2}")]
+ public string Culture { get; set; }
+
+ ///
+ /// Product currency.
+ ///
+ [CurrencyValidation]
+ public string Currency { get; set; }
+
+ ///
+ /// Order by SQL expression.
+ ///
+ public string OrderBy { get; set; }
+
+ ///
+ /// Limit how many products to return.
+ ///
+ [DefaultValue(12)]
+ [Range(1, 1000)]
+ public int Limit { get; set; }
+
+ ///
+ /// If true variants are loaded too for products with variants (default false).
+ ///
+ public bool WithVariants { get; set; }
+
+ public void Deconstruct(out string culture, out string currency, out string orderBy,
+ out int limit, out bool withVariants)
+ {
+ culture = Culture;
+ currency = Currency;
+ orderBy = OrderBy;
+ limit = Limit;
+ withVariants = WithVariants;
+ }
+}
diff --git a/src/Kentico.Xperience.StoreApi/Products/ProductsController.cs b/src/Kentico.Xperience.StoreApi/Products/ProductsController.cs
index c4f32a35..e02f4ac4 100644
--- a/src/Kentico.Xperience.StoreApi/Products/ProductsController.cs
+++ b/src/Kentico.Xperience.StoreApi/Products/ProductsController.cs
@@ -8,6 +8,7 @@
using Kentico.Xperience.StoreApi.Products.Categories;
using Kentico.Xperience.StoreApi.Products.Pages;
using Kentico.Xperience.StoreApi.Products.Prices;
+using Kentico.Xperience.StoreApi.Products.SKU;
using Kentico.Xperience.StoreApi.Routing;
using Microsoft.AspNetCore.Http;
@@ -146,4 +147,24 @@ public async Task> GetInventoryPrices(in
return ValidationProblem();
}
}
+
+
+ ///
+ /// Returns all standalone products.
+ ///
+ /// Product request parameters.
+ ///
+ [HttpGet("standalone-products", Name = nameof(GetStandaloneProducts))]
+ [AuthorizeStore]
+ public async Task>> GetStandaloneProducts([FromQuery] ProductRequest request)
+ {
+ if (request.Culture is not null &&
+ !CultureSiteInfoProvider.IsCultureOnSite(request.Culture, SiteContext.CurrentSiteName))
+ {
+ return BadRequest($"Culture '{request.Culture}' is not assigned for site");
+ }
+
+ var products = await productService.GetStandaloneProducts(request);
+ return Ok(products);
+ }
}
diff --git a/src/Kentico.Xperience.StoreApi/Products/SKU/IProductSKUConverter.cs b/src/Kentico.Xperience.StoreApi/Products/SKU/IProductSKUConverter.cs
index b6ab646a..c3e5e76c 100644
--- a/src/Kentico.Xperience.StoreApi/Products/SKU/IProductSKUConverter.cs
+++ b/src/Kentico.Xperience.StoreApi/Products/SKU/IProductSKUConverter.cs
@@ -14,6 +14,7 @@ public interface IProductSKUConverter
/// SKU info.
/// Currency code in which evaluates prices.
/// If true, variants are included.
+ /// If true, is included.
///
- TModel Convert(SKUInfo skuInfo, string currencyCode, bool withVariants);
+ TModel Convert(SKUInfo skuInfo, string currencyCode, bool withVariants, bool withLongDescription);
}
diff --git a/src/Kentico.Xperience.StoreApi/Products/SKU/KProductSKU.cs b/src/Kentico.Xperience.StoreApi/Products/SKU/KProductSKU.cs
index cddeeeb6..095e59c3 100644
--- a/src/Kentico.Xperience.StoreApi/Products/SKU/KProductSKU.cs
+++ b/src/Kentico.Xperience.StoreApi/Products/SKU/KProductSKU.cs
@@ -18,6 +18,8 @@ public class KProductSKU
public string SKUShortDescription { get; set; }
+ public string SKULongDescription { get; set; }
+
public string SKUNumber { get; set; }
public bool SKUEnabled { get; set; }
diff --git a/src/Kentico.Xperience.StoreApi/Products/SKU/ProductSKUConverter.cs b/src/Kentico.Xperience.StoreApi/Products/SKU/ProductSKUConverter.cs
index aeb68d6b..4a15dbbd 100644
--- a/src/Kentico.Xperience.StoreApi/Products/SKU/ProductSKUConverter.cs
+++ b/src/Kentico.Xperience.StoreApi/Products/SKU/ProductSKUConverter.cs
@@ -37,7 +37,7 @@ public ProductSKUConverter(ICatalogPriceCalculatorFactory catalogPriceCalculator
///
- public virtual TModel Convert(SKUInfo skuInfo, string currencyCode, bool withVariants)
+ public virtual TModel Convert(SKUInfo skuInfo, string currencyCode, bool withVariants, bool withLongDescription)
{
var model = mapper.Map(skuInfo);
@@ -66,6 +66,11 @@ public virtual TModel Convert(SKUInfo skuInfo, string currencyCode, bool withVar
model.Variants = variants;
}
+ if (withLongDescription)
+ {
+ model.SKULongDescription = skuInfo.SKUDescription;
+ }
+
model.Prices = mapper.Map(prices);
return model;
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);
+ }
+}