Skip to content

Commit

Permalink
Merge pull request #12 from Kentico/feat/standalone_sku_synchronization
Browse files Browse the repository at this point in the history
Feat/standalone SKU synchronization
  • Loading branch information
michalJakubis authored Jul 19, 2024
2 parents 089b63b + b4d7bd0 commit 3e6a166
Show file tree
Hide file tree
Showing 22 changed files with 339 additions and 101 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment environment)
"ClientId": "3ef7fe1b-696c-4afa-8b56-d3176b7bea95",
"ClientSecret": "********************",
"ProductSyncEnabled": true,
"StandaloneProductSync": true,
"ProductSyncInterval": 10
}
}
Expand Down
6 changes: 4 additions & 2 deletions docs/Usage-Guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,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 `StandaloneProductsSync` setting.
- Product variants - Content type `K13Store.ProductVariant`
- All products variant for parent products
- Product images - Content type `K13Store.ProductImage`
Expand All @@ -241,7 +241,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 (`StandaloneProductsSync` setting).

No price data is synced, because catalog prices need
calculator evaluation in context of user's cart and standalone requests via `IProductService` are required.
Expand Down Expand Up @@ -294,6 +294,7 @@ dotnet add package Kentico.Xperience.Store.Rcl
"ClientId": "3ef7fe1b-696c-4afa-8b56-d3176b7bea95",
"ClientSecret": "********************",
"ProductSyncEnabled": true,
"StandaloneProductSync": true,
"ProductSyncInterval": 10
}
}
Expand All @@ -306,6 +307,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 true, [standalone SKUs](https://docs.kentico.com/x/3gqRBg) are synchronized as well |
| ProductSyncInterval | Interval in minutes specifies how often synchronization is running |


Expand Down
1 change: 1 addition & 0 deletions examples/DancingGoat-K13Ecommerce/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"ClientId": "YourUniqueClientIdentifier",
"ClientSecret": "********************",
"ProductSyncEnabled": true,
"StandaloneProductSync": true,
"ProductSyncInterval": 10
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ namespace DancingGoat.ShopApi;
/// </summary>
public class CustomSKUConverter : ProductSKUConverter<CustomSKU>
{
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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ public class KenticoStoreConfig
/// </summary>
public required string ClientSecret { get; set; } = string.Empty;

/// <summary>
/// Synchronize also standalone products without page representation.
/// </summary>
public required bool StandaloneProductSync { get; set; } = true;

/// <summary>
/// When true, product synchronization is enabled.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,12 @@ public interface IProductService
/// <param name="skuId">SKUID for product or variant.</param>
/// <param name="currencyCode">Currency code.</param>
Task<ProductInventoryPriceInfo> GetVariantInventoryPriceInfo(int skuId, string? currencyCode = null);


/// <summary>
/// Gets all standalone products.
/// </summary>
/// <param name="request">Stanadlone product params.</param>
/// <returns></returns>
Task<ICollection<KProductSKU>> GetStandaloneProducts(ProductRequest request);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,44 +3,14 @@
/// <summary>
/// Model for request to filter product pages
/// </summary>
public class ProductPageRequest
public class ProductPageRequest : ProductRequest
{
/// <summary>
/// Node alias path prefix
/// </summary>
public required string Path { get; set; }


/// <summary>
/// Document culture
/// </summary>
public string? Culture { get; set; }


/// <summary>
/// Product currency
/// </summary>
public string? Currency { get; set; }


/// <summary>
/// Order by SQL expression
/// </summary>
public string? OrderBy { get; set; }


/// <summary>
/// Limit how many products to return.
/// </summary>
public int? Limit { get; set; }


/// <summary>
/// If true variants are loaded too for products with variants (default false).
/// </summary>
public bool WithVariants { get; set; }


/// <summary>
/// If true, DocumentSKUDescription is filled too (default false).
/// </summary>
Expand Down
39 changes: 39 additions & 0 deletions src/Kentico.Xperience.K13Ecommerce/Products/ProductRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System.ComponentModel.DataAnnotations;

namespace Kentico.Xperience.K13Ecommerce.Products;

/// <summary>
/// Model for request to filter standalone products
/// </summary>
public class ProductRequest
{
/// <summary>
/// To determine if product has its page, culture needs to be provided
/// so documents in the specific culture will be checked.
/// </summary>
[RegularExpression("[a-zA-Z]{2}-[a-zA-Z]{2}")]
public string? Culture { get; set; }

/// <summary>
/// Product currency.
/// </summary>
public string? Currency { get; set; }

/// <summary>
/// Order by SQL expression.
/// </summary>
public string? OrderBy { get; set; }

/// <summary>
/// Limit how many products to return.
/// </summary>
[DefaultValue(12)]
[Range(1, 1000)]
public int Limit { get; set; }

/// <summary>
/// If true variants are loaded too for products with variants (default false).
/// </summary>
public bool WithVariants { get; set; }
}

8 changes: 6 additions & 2 deletions src/Kentico.Xperience.K13Ecommerce/Products/ProductService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ internal class ProductService(IKenticoStoreApiClient storeApiClient) : IProductS
{
/// <inheritdoc/>
public async Task<ICollection<KProductNode>> 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);


/// <inheritdoc/>
Expand All @@ -24,4 +24,8 @@ public async Task<ProductPricesResponse> GetProductPrices(int skuId, string? cur
/// <inheritdoc/>
public async Task<ProductInventoryPriceInfo> GetVariantInventoryPriceInfo(int skuId, string? currencyCode = null)
=> await storeApiClient.GetInventoryPricesAsync(skuId, currencyCode);

/// <inheritdoc/>
public async Task<ICollection<KProductSKU>> GetStandaloneProducts(ProductRequest request) =>
await storeApiClient.GetStandaloneProductsAsync(request.Culture, request.Currency, request.OrderBy, request.Limit, request.WithVariants);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,20 @@ public partial class KProductNode : IItemIdentifier<int>
{
/// <inheritdoc/>
public int ExternalId => Sku.Skuid;

/// <summary>
/// Create product node from product SKU.
/// </summary>
/// <param name="sku"></param>
public KProductNode(KProductSKU sku)
{
Sku = sku;
DocumentSKUName = sku.SkuName;
DocumentSKUDescription = sku.SkuLongDescription;
DocumentSKUShortDescription = sku.SkuShortDescription;
ClassName = string.Empty;
NodeAliasPath = string.Empty;
}
}

/// <summary>
Expand Down
123 changes: 104 additions & 19 deletions src/Kentico.Xperience.K13Ecommerce/StoreApi/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -367,10 +367,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"
Expand Down Expand Up @@ -411,22 +427,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": {
Expand Down Expand Up @@ -659,6 +659,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": [
Expand Down Expand Up @@ -2751,6 +2832,10 @@
"type": "string",
"nullable": true
},
"skuLongDescription": {
"type": "string",
"nullable": true
},
"skuNumber": {
"type": "string",
"nullable": true
Expand Down Expand Up @@ -3311,7 +3396,7 @@
"nullable": true
}
},
"additionalProperties": { }
"additionalProperties": {}
},
"ProductInventoryPriceInfo": {
"type": "object",
Expand Down Expand Up @@ -3418,7 +3503,7 @@
},
"security": [
{
"Bearer": [ ]
"Bearer": []
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ public bool GetModifiedProperties(ProductSKU contentItem, out Dictionary<string,
[nameof(ProductSKU.SKUReorderAt)] = Item.Sku.SkuReorderAt,
[nameof(ProductSKU.SKUProductType)] = Item.Sku.SkuProductType.ToString(),
[nameof(ProductSKU.SKUNeedsShipping)] = Item.Sku.SkuNeedsShipping,
[nameof(ProductSKU.SKUAvailableItems)] = Item.Sku.SkuAvailableItems,
[nameof(ProductSKU.SKUMinItemsInOrder)] = Item.Sku.SkuMinItemsInOrder,
[nameof(ProductSKU.SKUMaxItemsInOrder)] = Item.Sku.SkuMaxItemsInOrder,
[nameof(ProductSKU.SKUInStoreFrom)] = Item.Sku.SkuInStoreFrom,
Expand Down
Loading

0 comments on commit 3e6a166

Please sign in to comment.