Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix/hardcoded currency #14

Merged
merged 9 commits into from
Aug 1, 2024
Merged
179 changes: 115 additions & 64 deletions README.md

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion docs/Usage-Guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ Class library consists of 2 main parts - Shopify products synchronization and th
The **Product listing** widget is a standalone widget that displays Shopify products using the Shopify Admin REST API. The Shopify integration must be set up in `appsettings.json` or in the `Shopify integration` application for this widget to work (no products need to be synchronized). The widget can be used to display products from your Shopify store outside of the [content type structure](#integration-specific-content-types) the integration provides.

#### Limitations
Shopify API can return maximum of 250 items in one API request. For larger number of products, pagination needs to be implemented. Detailed information on Shopify API pagination can be found [here](https://shopify.dev/docs/api/usage/pagination-rest). The `ShopifyProductService` wraps result in [ListResultWrapper](../src/Kentico.Xperience.Shopify/Products/Models/ListResultWrapper.cs). This wrapper returns retrieved items along with next page and previous page [PagingFilterParams](../src/Kentico.Xperience.Shopify/Products/Models/PagingFilterParams.cs). These filters can then be used in the `GetProductsAsync` method parameters to retrieve next or previous page from Shopify API. Due to this limitation, the maximum number of retrieved results in the [product listing widget](#product-listing-widget) is 250. To increase this limit, pagination must be implemented in the widget. This limitation also affects product synchronization, where only first 250 products are synchronized, and shopping cart, which can have maximum of 250 cart items.
- Shopify API can return maximum of 250 items in one API request. For larger number of products, pagination needs to be implemented. Detailed information on Shopify API pagination can be found [here](https://shopify.dev/docs/api/usage/pagination-rest). The `ShopifyProductService` wraps result in [ListResultWrapper](../src/Kentico.Xperience.Shopify/Products/Models/ListResultWrapper.cs). This wrapper returns retrieved items along with next page and previous page [PagingFilterParams](../src/Kentico.Xperience.Shopify/Products/Models/PagingFilterParams.cs). These filters can then be used in the `GetProductsAsync` method parameters to retrieve next or previous page from Shopify API. Due to this limitation, the maximum number of retrieved results in the [product listing widget](#product-listing-widget) is 250. To increase this limit, pagination must be implemented in the widget. This limitation also affects product synchronization, where only first 250 products are synchronized, and shopping cart, which can have maximum of 250 cart items.
- Only one currency per website channel is supported.

### Shopify products synchronization
Synchronization running in background thread worker periodically every 15 minutes and all synchronization items are stored as following content items:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ public class ShopifyProductListWidgetProperties : IWidgetProperties
[EditingComponent(TextInputComponent.IDENTIFIER, Label = "Title", Order = 0)]
public string Title { get; set; }

// TODO - create selector for long datatype
[EditingComponent(ShopifyCollectionSelectorComponent.IDENTIFIER, Label = "Collection", Order = 10)]
public string CollectionID { get; set; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

using Kentico.Content.Web.Mvc;
using Kentico.Content.Web.Mvc.Routing;
using Kentico.Xperience.Shopify.Config;
using Kentico.Xperience.Shopify.Products;
using Kentico.Xperience.Shopify.Products.Models;

Expand All @@ -28,6 +29,7 @@ public class ShopifyCategoryController : Controller
private readonly ISettingsService settingsService;
private readonly IConversionService conversionService;
private readonly ILogger<ShopifyCategoryController> logger;
private readonly IShopifyIntegrationSettingsService shopifySettingsService;

public ShopifyCategoryController(
CategoryPageRepository categoryPageRepository,
Expand All @@ -37,7 +39,8 @@ public ShopifyCategoryController(
IProgressiveCache progressiveCache,
ISettingsService settingsService,
IConversionService conversionService,
ILogger<ShopifyCategoryController> logger)
ILogger<ShopifyCategoryController> logger,
IShopifyIntegrationSettingsService shopifySettingsService)
{
this.categoryPageRepository = categoryPageRepository;
this.webPageDataContextRetriever = webPageDataContextRetriever;
Expand All @@ -47,6 +50,7 @@ public ShopifyCategoryController(
this.settingsService = settingsService;
this.conversionService = conversionService;
this.logger = logger;
this.shopifySettingsService = shopifySettingsService;
}
public async Task<IActionResult> Index()
{
Expand All @@ -62,7 +66,9 @@ public async Task<IActionResult> Index()
async (_) => await GetProductPrices(products.Select(x => x.Product.FirstOrDefault())),
new CacheSettings(cacheMinutes, webPage.WebsiteChannelName, webPage.LanguageName, categoryPage.SystemFields.WebPageItemGUID));

return View(CategoryPageViewModel.GetViewModel(categoryPage, prices, products, urls, logger));
string currencyCode = shopifySettingsService.GetWebsiteChannelSettings()?.CurrencyCode ?? string.Empty;

return View(CategoryPageViewModel.GetViewModel(categoryPage, prices, products, urls, logger, currencyCode));
}

private async Task<IDictionary<string, ProductPriceModel>> GetProductPrices(IEnumerable<Product> productContentItems)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@

using Kentico.Content.Web.Mvc;
using Kentico.Content.Web.Mvc.Routing;
using Kentico.Xperience.Shopify.Config;
using Kentico.Xperience.Shopify.Products.Models;
using Kentico.Xperience.Shopify.ShoppingCart;

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;

using Shopify.Controllers;

Expand All @@ -24,28 +26,33 @@ public class ShopifyProductDetailController : Controller
private readonly ProductDetailPageRepository productDetailPageRepository;
private readonly IWebPageDataContextRetriever webPageDataContextRetriever;
private readonly IShoppingService shoppingService;
private readonly IShopifyIntegrationSettingsService settingsService;

public ShopifyProductDetailController(ProductDetailPageRepository productDetailPageRepository,
IWebPageDataContextRetriever webPageDataContextRetriever,
IShoppingService shoppingService)
IShoppingService shoppingService,
IShopifyIntegrationSettingsService settingsService)
{
this.productDetailPageRepository = productDetailPageRepository;
this.webPageDataContextRetriever = webPageDataContextRetriever;
this.shoppingService = shoppingService;
this.settingsService = settingsService;
}

[HttpGet]
public async Task<IActionResult> Index(string variantID = null)
{
// TODO - dynamic resolve country
string country = "CZ";
string currency = "CZK";

if (!TempData.TryGetValue(ERROR_MESSAGES_KEY, out object tempDataErrors) || tempDataErrors is not string[] errorMessages)
{
errorMessages = [];
}

var config = settingsService.GetWebsiteChannelSettings();
if (config == null)
{
return View(new ProductDetailViewModel());
}

var webPage = webPageDataContextRetriever.Retrieve().WebPage;
var productDetail = await productDetailPageRepository.GetProductDetailPage(webPage.WebPageItemID, webPage.LanguageName, HttpContext.RequestAborted);

Expand All @@ -54,7 +61,7 @@ public async Task<IActionResult> Index(string variantID = null)
return View(new ProductDetailViewModel());
}

return View(ProductDetailViewModel.GetViewModel(productDetail, variantID ?? string.Empty, country, currency, errorMessages));
return View(ProductDetailViewModel.GetViewModel(productDetail, variantID ?? string.Empty, config.Country, config.CurrencyCode, errorMessages));
}

[HttpPost]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

using Kentico.Content.Web.Mvc;
using Kentico.Content.Web.Mvc.Routing;
using Kentico.Xperience.Shopify.Config;
using Kentico.Xperience.Shopify.Products;
using Kentico.Xperience.Shopify.Products.Models;

Expand All @@ -32,6 +33,7 @@ public class ShopifyStoreController : Controller
private readonly ISettingsService settingsService;
private readonly IConversionService conversionService;
private readonly IShopifyPriceService priceService;
private readonly IShopifyIntegrationSettingsService shopifySettingsService;

public ShopifyStoreController(StorePageRepository storePageRepository,
IWebPageDataContextRetriever webPageDataContextRetriever,
Expand All @@ -42,7 +44,8 @@ public ShopifyStoreController(StorePageRepository storePageRepository,
IProgressiveCache progressiveCache,
ISettingsService settingsService,
IConversionService conversionService,
IShopifyPriceService priceService)
IShopifyPriceService priceService,
IShopifyIntegrationSettingsService shopifySettingsService)
{
this.storePageRepository = storePageRepository;
this.webPageDataContextRetriever = webPageDataContextRetriever;
Expand All @@ -54,6 +57,7 @@ public ShopifyStoreController(StorePageRepository storePageRepository,
this.settingsService = settingsService;
this.conversionService = conversionService;
this.priceService = priceService;
this.shopifySettingsService = shopifySettingsService;
}

public async Task<IActionResult> Index()
Expand Down Expand Up @@ -86,7 +90,9 @@ private async Task<IEnumerable<ShopifyProductListItemViewModel>> GetProductListV

if (!string.IsNullOrEmpty(shopifyProductId) && prices.TryGetValue(shopifyProductId, out var price))
{
productViewModels.Add(ShopifyProductListItemViewModel.GetViewModel(productPage.Product.FirstOrDefault(), url, price));
string currencyCode = shopifySettingsService.GetWebsiteChannelSettings()?.CurrencyCode ?? string.Empty;

productViewModels.Add(ShopifyProductListItemViewModel.GetViewModel(productPage.Product.FirstOrDefault(), url, price, currencyCode));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ public static CategoryPageViewModel GetViewModel(
IDictionary<string, ProductPriceModel> productPrices,
IEnumerable<ProductDetailPage> products,
IDictionary<Guid, WebPageUrl> productUrls,
ILogger logger)
ILogger logger,
string currencyCode)
{
var productListItems = new List<ShopifyProductListItemViewModel>();
foreach (var product in products)
Expand All @@ -25,7 +26,7 @@ public static CategoryPageViewModel GetViewModel(
}
else
{
productListItems.Add(GetProductListItem(product, productUrls, productPrices));
productListItems.Add(GetProductListItem(product, productUrls, productPrices, currencyCode));
}
}
var model = new CategoryPageViewModel
Expand All @@ -37,7 +38,11 @@ public static CategoryPageViewModel GetViewModel(
return model;
}

private static ShopifyProductListItemViewModel GetProductListItem(ProductDetailPage productPage, IDictionary<Guid, WebPageUrl> productUrls, IDictionary<string, ProductPriceModel> productPrices)
private static ShopifyProductListItemViewModel GetProductListItem(
ProductDetailPage productPage,
IDictionary<Guid, WebPageUrl> productUrls,
IDictionary<string, ProductPriceModel> productPrices,
string currencyCode)
{
var product = productPage.Product.FirstOrDefault();
if (product == null)
Expand All @@ -47,6 +52,6 @@ private static ShopifyProductListItemViewModel GetProductListItem(ProductDetailP

productUrls.TryGetValue(productPage.SystemFields.WebPageItemGUID, out var url);
productPrices.TryGetValue(product.ShopifyProductID, out var productPriceModel);
return ShopifyProductListItemViewModel.GetViewModel(product, url, productPriceModel);
return ShopifyProductListItemViewModel.GetViewModel(product, url, productPriceModel, currencyCode);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,9 @@ public record ShopifyProductListItemViewModel
public string ListPrice { get; init; }
public bool HasMultipleVariants { get; init; }

public static ShopifyProductListItemViewModel GetViewModel(Product product, WebPageUrl productUrl, ProductPriceModel priceModel)
public static ShopifyProductListItemViewModel GetViewModel(Product product, WebPageUrl productUrl, ProductPriceModel priceModel, string currencyCode)
{
// TODO resolve currency
string currency = "CZK";
string currency = currencyCode;
var mainImage = product.Images.FirstOrDefault() ?? product.Variants.FirstOrDefault(x => x.Image.Any())?.Image.FirstOrDefault();
return new ShopifyProductListItemViewModel()
{
Expand Down
18 changes: 18 additions & 0 deletions examples/DancingGoat-Shopify/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,23 @@
"AdminApiToken": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"StorefrontApiToken": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"StorefrontApiVersion": "YYYY-MM"
},
"CMSShopifyWebsiteChannelsConfig": {
"Settings": [
{
"ChannelName": "MyWebsiteChannel1",
"CurrencyCode": "CZK",
"Country": "CZ"
},
{
"ChannelName": "MyWebsiteChannel2",
"CurrencyCode": "USD",
"Country": "US"
}
],
"DefaultSetting": {
"CurrencyCode": "CZK",
"Country": "CZ"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

@{
var price = Model.PriceDetail;
var currencyFormat = "0:00#";//TODO;
var currencyFormat = "0:00#";
}

<article class="product-tile">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,13 @@ public interface IShopifyIntegrationSettingsService
/// </summary>
/// <returns><see cref="ShopifyConfig"/> containing the settings or NULL if no configuration is found.</returns>
ShopifyConfig? GetSettings();

/// <summary>
/// Get current website channel configuration from appsettings.
/// </summary>
/// <returns>
/// <see cref="ShopifyWebsiteChannelConfig"/> containing configuration for current website channel or default value if no configuration is found.
/// </returns>
ShopifyWebsiteChannelConfig? GetWebsiteChannelSettings();
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using CMS.DataEngine;
using CMS.Helpers;
using CMS.Websites.Routing;

using Kentico.Xperience.Shopify.Admin;

Expand All @@ -10,26 +11,30 @@ namespace Kentico.Xperience.Shopify.Config
internal class ShopifyIntegrationSettingsService : IShopifyIntegrationSettingsService
{
private readonly IProgressiveCache cache;
private readonly IOptionsMonitor<ShopifyConfig> monitor;
private readonly ShopifyConfig shopifyConfig;
private readonly IInfoProvider<IntegrationSettingsInfo> integrationSettingsProvider;
private readonly ShopifyWebsiteChannelConfigOptions websiteChannelConfig;
private readonly IWebsiteChannelContext websiteChannelContext;

public ShopifyIntegrationSettingsService(
IProgressiveCache cache,
IOptionsMonitor<ShopifyConfig> monitor,
IInfoProvider<IntegrationSettingsInfo> integrationSettingsProvider)
IOptionsMonitor<ShopifyConfig> shopifyConfigMonitor,
IInfoProvider<IntegrationSettingsInfo> integrationSettingsProvider,
IOptionsMonitor<ShopifyWebsiteChannelConfigOptions> websiteChannelConfigMonitor,
IWebsiteChannelContext websiteChannelContext)
{
this.cache = cache;
this.monitor = monitor;
this.integrationSettingsProvider = integrationSettingsProvider;
this.websiteChannelContext = websiteChannelContext;
shopifyConfig = shopifyConfigMonitor.CurrentValue;
websiteChannelConfig = websiteChannelConfigMonitor.CurrentValue;
}

public ShopifyConfig? GetSettings()
{
var monitorValue = monitor.CurrentValue;

if (ShopifyConfigIsFilled(monitorValue))
if (ShopifyConfigIsFilled(shopifyConfig))
{
return monitorValue;
return shopifyConfig;
}

return cache.Load(cs => GetConfigFromSettings(),
Expand All @@ -40,6 +45,21 @@ public ShopifyIntegrationSettingsService(

}

public ShopifyWebsiteChannelConfig? GetWebsiteChannelSettings()
{
if (websiteChannelConfig == null)
{
return null;
}

string? currentChannel = websiteChannelContext.WebsiteChannelName;
if (string.IsNullOrEmpty(currentChannel))
{
return websiteChannelConfig.DefaultSetting;
}
return websiteChannelConfig.Settings?.Find(x => x.ChannelName == currentChannel) ?? websiteChannelConfig.DefaultSetting;
}

private ShopifyConfig? GetConfigFromSettings()
{
var settingsInfo = integrationSettingsProvider.Get()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
namespace Kentico.Xperience.Shopify.Config
{
/// <summary>
/// Class with list of website channel configurations.
/// </summary>
public class ShopifyWebsiteChannelConfigOptions
{
/// <summary>
/// The name of configuration section.
/// </summary>
public const string SECTION_NAME = "CMSShopifyWebsiteChannelsConfig";

/// <summary>
/// Website channel configurations list.
/// </summary>
public required List<ShopifySpecificWebsiteChannelConfig> Settings { get; set; }

/// <summary>
/// Default setting used if no setting for current website channel is found.
/// </summary>
public required ShopifyWebsiteChannelConfig? DefaultSetting { get; set; }
}


/// <summary>
/// Class for website channel configuration
/// </summary>
public class ShopifySpecificWebsiteChannelConfig : ShopifyWebsiteChannelConfig
{
/// <summary>
/// Website channel name.
/// </summary>
public required string ChannelName { get; set; }
}


/// <summary>
/// Class for default channel configuration.
/// </summary>
public class ShopifyWebsiteChannelConfig
{
/// <summary>
/// ISO 4217 currency code.
/// </summary>
public required string CurrencyCode { get; set; }

/// <summary>
/// Two letter country code.
/// </summary>
public required string Country { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public static void RegisterShopifyServices(this IServiceCollection services, ICo

// Add options monitor
services.Configure<ShopifyConfig>(configuration.GetSection(ShopifyConfig.SECTION_NAME));
services.Configure<ShopifyWebsiteChannelConfigOptions>(configuration.GetSection(ShopifyWebsiteChannelConfigOptions.SECTION_NAME));

// Add HTTP session services
services.AddSession();
Expand Down
Loading