From d91a7f9436eabbe9f61a4192fc4e190319068862 Mon Sep 17 00:00:00 2001 From: Anderson Fernandes do Nascimento Date: Fri, 21 May 2021 02:26:35 -0300 Subject: [PATCH 1/4] Product recommendation updated daily based on real ecommerce sales using machine learning. Removed sample data from Amazon. --- SimplCommerce.sln | 15 ++ .../Areas/Catalog/ViewModels/ProductDetail.cs | 2 + ...erCreatedCreateDataRecomendationHandler.cs | 61 +++++++ .../ModuleInitializer.cs | 1 + .../IRecommendationService.cs | 11 ++ .../Models/ProductInfo.cs | 10 ++ .../Models/ProductPrediction.cs | 7 + .../RecommendService.cs | 149 ++++++++++++++++++ ...RecommendationTrainingBackgroundService.cs | 43 +++++ .../SimplCommerce.AI.Recommendation.csproj | 14 ++ src/SimplCommerce.WebHost/MLData/data.txt | 0 .../Views/Product/ProductDetail.cshtml | 14 ++ 12 files changed, 327 insertions(+) create mode 100644 src/Modules/SimplCommerce.Module.Orders/Events/OrderCreatedCreateDataRecomendationHandler.cs create mode 100644 src/SimplCommerce.AI.Recommendation/IRecommendationService.cs create mode 100644 src/SimplCommerce.AI.Recommendation/Models/ProductInfo.cs create mode 100644 src/SimplCommerce.AI.Recommendation/Models/ProductPrediction.cs create mode 100644 src/SimplCommerce.AI.Recommendation/RecommendService.cs create mode 100644 src/SimplCommerce.AI.Recommendation/RecommendationTrainingBackgroundService.cs create mode 100644 src/SimplCommerce.AI.Recommendation/SimplCommerce.AI.Recommendation.csproj create mode 100644 src/SimplCommerce.WebHost/MLData/data.txt diff --git a/SimplCommerce.sln b/SimplCommerce.sln index 78a141ca08..36dee55280 100644 --- a/SimplCommerce.sln +++ b/SimplCommerce.sln @@ -134,6 +134,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SimplCommerce.Module.Paymen EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SimplCommerce.Module.PaymentCashfree", "src\Modules\SimplCommerce.Module.PaymentCashfree\SimplCommerce.Module.PaymentCashfree.csproj", "{E30CF10F-FABF-4917-8BEB-CB81E4CE2C92}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimplCommerce.AI.Recommendation", "src\SimplCommerce.AI.Recommendation\SimplCommerce.AI.Recommendation.csproj", "{632EECD7-C8FD-460B-A8D0-F01FD4DA3C38}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -696,6 +698,18 @@ Global {E30CF10F-FABF-4917-8BEB-CB81E4CE2C92}.Release|x64.Build.0 = Release|Any CPU {E30CF10F-FABF-4917-8BEB-CB81E4CE2C92}.Release|x86.ActiveCfg = Release|Any CPU {E30CF10F-FABF-4917-8BEB-CB81E4CE2C92}.Release|x86.Build.0 = Release|Any CPU + {632EECD7-C8FD-460B-A8D0-F01FD4DA3C38}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {632EECD7-C8FD-460B-A8D0-F01FD4DA3C38}.Debug|Any CPU.Build.0 = Debug|Any CPU + {632EECD7-C8FD-460B-A8D0-F01FD4DA3C38}.Debug|x64.ActiveCfg = Debug|Any CPU + {632EECD7-C8FD-460B-A8D0-F01FD4DA3C38}.Debug|x64.Build.0 = Debug|Any CPU + {632EECD7-C8FD-460B-A8D0-F01FD4DA3C38}.Debug|x86.ActiveCfg = Debug|Any CPU + {632EECD7-C8FD-460B-A8D0-F01FD4DA3C38}.Debug|x86.Build.0 = Debug|Any CPU + {632EECD7-C8FD-460B-A8D0-F01FD4DA3C38}.Release|Any CPU.ActiveCfg = Release|Any CPU + {632EECD7-C8FD-460B-A8D0-F01FD4DA3C38}.Release|Any CPU.Build.0 = Release|Any CPU + {632EECD7-C8FD-460B-A8D0-F01FD4DA3C38}.Release|x64.ActiveCfg = Release|Any CPU + {632EECD7-C8FD-460B-A8D0-F01FD4DA3C38}.Release|x64.Build.0 = Release|Any CPU + {632EECD7-C8FD-460B-A8D0-F01FD4DA3C38}.Release|x86.ActiveCfg = Release|Any CPU + {632EECD7-C8FD-460B-A8D0-F01FD4DA3C38}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -750,6 +764,7 @@ Global {1A8B6FA0-8341-4D27-9B71-57F70AB37571} = {0A27C140-4CCB-40DD-BE48-F5DE16D1177B} {14586564-62CC-4117-AC1B-858ED53C2D6C} = {7EFA2FA7-32DD-4047-B021-50E77A83D714} {E30CF10F-FABF-4917-8BEB-CB81E4CE2C92} = {7EFA2FA7-32DD-4047-B021-50E77A83D714} + {632EECD7-C8FD-460B-A8D0-F01FD4DA3C38} = {C9BFDDC4-5671-47A3-B57D-197C2A51FA8A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {B9D0D8F0-1AB9-44DD-839F-ED8CEE7DDB10} diff --git a/src/Modules/SimplCommerce.Module.Catalog/Areas/Catalog/ViewModels/ProductDetail.cs b/src/Modules/SimplCommerce.Module.Catalog/Areas/Catalog/ViewModels/ProductDetail.cs index 2273c29eca..6e17e5ae77 100644 --- a/src/Modules/SimplCommerce.Module.Catalog/Areas/Catalog/ViewModels/ProductDetail.cs +++ b/src/Modules/SimplCommerce.Module.Catalog/Areas/Catalog/ViewModels/ProductDetail.cs @@ -73,6 +73,8 @@ into g public IList RelatedProducts { get; set; } = new List(); + public IList RecommendProducts { get; set; } = new List(); + public IList CrossSellProducts { get; set; } = new List(); public Brand Brand { get; set; } diff --git a/src/Modules/SimplCommerce.Module.Orders/Events/OrderCreatedCreateDataRecomendationHandler.cs b/src/Modules/SimplCommerce.Module.Orders/Events/OrderCreatedCreateDataRecomendationHandler.cs new file mode 100644 index 0000000000..ba1f071d63 --- /dev/null +++ b/src/Modules/SimplCommerce.Module.Orders/Events/OrderCreatedCreateDataRecomendationHandler.cs @@ -0,0 +1,61 @@ +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using SimplCommerce.Infrastructure.Data; +using SimplCommerce.Module.Orders.Models; + +namespace SimplCommerce.Module.Orders.Events +{ + public class OrderCreatedCreateDataRecomendationHandler : INotificationHandler + { + private readonly ILogger _logger; + private readonly IRepository _orderHistoryRepository; + private static string dataPath = Path.Combine(Environment.CurrentDirectory, "MLData/data.txt"); + + public OrderCreatedCreateDataRecomendationHandler( + IRepository orderHistoryRepository, + ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + _orderHistoryRepository = orderHistoryRepository; + } + + public async Task Handle(OrderCreated notification, CancellationToken cancellationToken) + { + try + { + if (notification.Order != null + && notification.Order.OrderItems != null && notification.Order.OrderItems.Count > 1) + { + StringBuilder sb = new StringBuilder(); + for (var i = 0; i < notification.Order.OrderItems.Count - 1; i++) + { + var item = notification.Order.OrderItems[i]; + for (var i2 = i + 1; i2 <= i + 2; i2++) + { + if (i2 < notification.Order.OrderItems.Count) + { + var item2 = notification.Order.OrderItems[i2]; + if (item.ProductId != item2.ProductId) + { + sb.AppendLine(string.Format("{0}\t{1}", item.ProductId, item2.ProductId)); + } + } + } + } + await File.AppendAllTextAsync(dataPath, sb.ToString()); + } + } + catch (Exception ex) + { + _logger.LogError(ex, $"Failed executing event handler {GetType().Name}: {ex.Message}"); + } + } + } +} diff --git a/src/Modules/SimplCommerce.Module.Orders/ModuleInitializer.cs b/src/Modules/SimplCommerce.Module.Orders/ModuleInitializer.cs index b179f7b34f..61b0cba73d 100644 --- a/src/Modules/SimplCommerce.Module.Orders/ModuleInitializer.cs +++ b/src/Modules/SimplCommerce.Module.Orders/ModuleInitializer.cs @@ -18,6 +18,7 @@ public void ConfigureServices(IServiceCollection services) services.AddTransient, OrderChangedCreateOrderHistoryHandler>(); services.AddTransient, OrderCreatedCreateOrderHistoryHandler>(); //services.AddTransient, AfterOrderCreatedSendEmailHanlder>(); + services.AddTransient, OrderCreatedCreateDataRecomendationHandler>(); GlobalConfiguration.RegisterAngularModule("simplAdmin.orders"); } diff --git a/src/SimplCommerce.AI.Recommendation/IRecommendationService.cs b/src/SimplCommerce.AI.Recommendation/IRecommendationService.cs new file mode 100644 index 0000000000..31a3e82152 --- /dev/null +++ b/src/SimplCommerce.AI.Recommendation/IRecommendationService.cs @@ -0,0 +1,11 @@ +using Microsoft.ML; +using SimplCommerce.AI.Recommendation.Models; + +namespace SimplCommerce.AI.Recommendation +{ + public interface IRecommendationService + { + void BuildRecommendationModel(); + ProductPrediction Predict(ProductInfo product); + } +} \ No newline at end of file diff --git a/src/SimplCommerce.AI.Recommendation/Models/ProductInfo.cs b/src/SimplCommerce.AI.Recommendation/Models/ProductInfo.cs new file mode 100644 index 0000000000..e54c5c9df4 --- /dev/null +++ b/src/SimplCommerce.AI.Recommendation/Models/ProductInfo.cs @@ -0,0 +1,10 @@ +using Microsoft.ML.Data; + +namespace SimplCommerce.AI.Recommendation.Models +{ + public class ProductInfo + { + [LoadColumn(0)] public float ProductID { get; set; } + [LoadColumn(1)] public float CombinedProductID { get; set; } + } +} \ No newline at end of file diff --git a/src/SimplCommerce.AI.Recommendation/Models/ProductPrediction.cs b/src/SimplCommerce.AI.Recommendation/Models/ProductPrediction.cs new file mode 100644 index 0000000000..dcefc4c402 --- /dev/null +++ b/src/SimplCommerce.AI.Recommendation/Models/ProductPrediction.cs @@ -0,0 +1,7 @@ +namespace SimplCommerce.AI.Recommendation.Models +{ + public class ProductPrediction + { + public float Score { get; set; } + } +} \ No newline at end of file diff --git a/src/SimplCommerce.AI.Recommendation/RecommendService.cs b/src/SimplCommerce.AI.Recommendation/RecommendService.cs new file mode 100644 index 0000000000..35ceed9d22 --- /dev/null +++ b/src/SimplCommerce.AI.Recommendation/RecommendService.cs @@ -0,0 +1,149 @@ +using System; +using System.IO; +using Microsoft.Extensions.Logging; +using Microsoft.ML; +using Microsoft.ML.Data; +using Microsoft.ML.Trainers; +using Microsoft.ML.Trainers.Recommender; +using SimplCommerce.AI.Recommendation.Models; +using static Microsoft.ML.DataOperationsCatalog; + +namespace SimplCommerce.AI.Recommendation +{ + public class RecommendationService : IRecommendationService + { + private readonly MLContext _mlContext; + private readonly ILogger _logger; + private TrainTestData _trainTestData; + private EstimatorChain _pipeline; + private ITransformer _model; + private IDataView _data; + public PredictionEngine predictEngine; + + private static string dataPath = Path.Combine(Environment.CurrentDirectory, "MLData/data.txt"); + private static string modelPath = Path.Combine(Environment.CurrentDirectory, "MLModels/model.zip"); + + public RecommendationService(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + _mlContext = new MLContext(); + } + + public void BuildRecommendationModel() + { + _logger.LogInformation($"{nameof(RecommendationService)} is training."); + + // setting and initialize + Initialize(dataPath); + + // train the model + TrainModel(); + + // evaluate the model performance + EvaluateModel(); + + // check how well products 3 and 63 go together + CreatePredictEngine(); + + // expose model to filePath + ExposeModelToPath(modelPath); + + _logger.LogInformation($"{nameof(RecommendationService)} is trained."); + } + + private void LoadRecommendationModel() + { + // load model from filePath + LoadTrainedModel(modelPath); + CreatePredictEngine(); + } + + private void Initialize(string dataPath) + { + // load the dataset in memory + _logger.LogInformation("Loading data..."); + + _data = _mlContext.Data.LoadFromTextFile( + dataPath, + hasHeader: false, + separatorChar: '\t'); + + // split the data into 80% training and 20% testing partitions + _trainTestData = _mlContext.Data.TrainTestSplit(_data, testFraction: 0.2); + + // prepare matrix factorization options + var options = new MatrixFactorizationTrainer.Options() + { + MatrixColumnIndexColumnName = "ProductIDEncoded", + MatrixRowIndexColumnName = "CombinedProductIDEncoded", + LabelColumnName = "CombinedProductID", + LossFunction = MatrixFactorizationTrainer.LossFunctionType.SquareLossOneClass, + Alpha = 0.01, + Lambda = 0.025, + }; + + // set up a training pipeline + // step 1: map ProductID and CombinedProductID to keys + _pipeline = _mlContext.Transforms.Conversion.MapValueToKey( + inputColumnName: "ProductID", + outputColumnName: "ProductIDEncoded") + .Append(_mlContext.Transforms.Conversion.MapValueToKey( + inputColumnName: "CombinedProductID", + outputColumnName: "CombinedProductIDEncoded")) + + // step 2: find recommendations using matrix factorization + .Append(_mlContext.Recommendation().Trainers.MatrixFactorization(options)); + } + + private void TrainModel() + { + _logger.LogInformation("Training the model..."); + _model = _pipeline.Fit(_trainTestData.TrainSet); + } + + private void EvaluateModel() + { + // evaluate the model performance + _logger.LogInformation("Evaluating the model..."); + var predictions = _model.Transform(_trainTestData.TestSet); + var metrics = _mlContext.Regression.Evaluate(predictions, labelColumnName: "CombinedProductID", scoreColumnName: "Score"); + _logger.LogInformation($" RMSE: {metrics.RootMeanSquaredError:#.##}"); + _logger.LogInformation($" L1: {metrics.LossFunction:#.##}"); + } + + private void CreatePredictEngine() + { + _logger.LogInformation("Create predict engine..."); + predictEngine = _mlContext.Model.CreatePredictionEngine(_model); + } + + public ProductPrediction Predict(ProductInfo product) + { + if (predictEngine == null) + { + return new ProductPrediction() { Score = 1 }; + } + + var result = predictEngine.Predict(product); + // Console.WriteLine($" Score:{result.Score}\tProductID: {product.ProductID}\tCombinedProductID: {product.CombinedProductID}"); + return result; + } + + private void ExposeModelToPath(string filePath) + { + _logger.LogInformation($"Expose model to {filePath}"); + + if (!Directory.Exists(Path.GetDirectoryName(filePath))) + Directory.CreateDirectory(Path.GetDirectoryName(filePath)); + + _mlContext.Model.Save(_model, _data.Schema, filePath); + } + + private void LoadTrainedModel(string filePath) + { + _logger.LogInformation($"Load model from {filePath}"); + DataViewSchema modelSchema; + _model = _mlContext.Model.Load(filePath, out modelSchema); + } + } +} diff --git a/src/SimplCommerce.AI.Recommendation/RecommendationTrainingBackgroundService.cs b/src/SimplCommerce.AI.Recommendation/RecommendationTrainingBackgroundService.cs new file mode 100644 index 0000000000..84e88e70a4 --- /dev/null +++ b/src/SimplCommerce.AI.Recommendation/RecommendationTrainingBackgroundService.cs @@ -0,0 +1,43 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace SimplCommerce.AI.Recommendation +{ + public class RecommendationTrainingBackgroundService : BackgroundService + { + private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; + + public RecommendationTrainingBackgroundService(IServiceProvider serviceProvider, ILoggerFactory loggerFactory) + { + _serviceProvider = serviceProvider; + _logger = loggerFactory.CreateLogger(); + } + + protected async override Task ExecuteAsync(CancellationToken stoppingToken) + { + _logger.LogInformation($"{nameof(RecommendationTrainingBackgroundService)} is starting."); + + while (!stoppingToken.IsCancellationRequested) + { + _logger.LogInformation($"{nameof(RecommendationTrainingBackgroundService)} is working."); + + using (var scope = _serviceProvider.CreateScope()) + { + var recommendationService = scope.ServiceProvider.GetRequiredService(); + + recommendationService.BuildRecommendationModel(); + } + + await Task.Delay(TimeSpan.FromDays(1), stoppingToken); + } + } + + } +} \ No newline at end of file diff --git a/src/SimplCommerce.AI.Recommendation/SimplCommerce.AI.Recommendation.csproj b/src/SimplCommerce.AI.Recommendation/SimplCommerce.AI.Recommendation.csproj new file mode 100644 index 0000000000..b512897408 --- /dev/null +++ b/src/SimplCommerce.AI.Recommendation/SimplCommerce.AI.Recommendation.csproj @@ -0,0 +1,14 @@ + + + + netcoreapp3.1 + + + + + + + + + + diff --git a/src/SimplCommerce.WebHost/MLData/data.txt b/src/SimplCommerce.WebHost/MLData/data.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/SimplCommerce.WebHost/Themes/CozaStore/Areas/Catalog/Views/Product/ProductDetail.cshtml b/src/SimplCommerce.WebHost/Themes/CozaStore/Areas/Catalog/Views/Product/ProductDetail.cshtml index e8536a9b27..7bc049bc45 100644 --- a/src/SimplCommerce.WebHost/Themes/CozaStore/Areas/Catalog/Views/Product/ProductDetail.cshtml +++ b/src/SimplCommerce.WebHost/Themes/CozaStore/Areas/Catalog/Views/Product/ProductDetail.cshtml @@ -274,6 +274,20 @@ @await Component.InvokeAsync("ProductRecentlyViewed", new { productId = Model.Id }) +@if (Model.RecommendProducts.Any()) +{ + +

@Localizer["Recommend Products"]

+
+ @foreach (var product in Model.RecommendProducts) + { +
+ +
+ } +
+} +

@Localizer["Customer reviews"]

@await Component.InvokeAsync("Review", new { entityId = Model.Id, entityTypeId = "Product" }) From ce1962d69f1aaec9d0c8f29e25b4bc830d1019bd Mon Sep 17 00:00:00 2001 From: Anderson Fernandes do Nascimento Date: Fri, 21 May 2021 02:38:31 -0300 Subject: [PATCH 2/4] fix dotnet version --- .../SimplCommerce.AI.Recommendation.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SimplCommerce.AI.Recommendation/SimplCommerce.AI.Recommendation.csproj b/src/SimplCommerce.AI.Recommendation/SimplCommerce.AI.Recommendation.csproj index b512897408..8cf1b1c17c 100644 --- a/src/SimplCommerce.AI.Recommendation/SimplCommerce.AI.Recommendation.csproj +++ b/src/SimplCommerce.AI.Recommendation/SimplCommerce.AI.Recommendation.csproj @@ -1,7 +1,7 @@ - netcoreapp3.1 + net5.0 From 81f766f03215b027948390c302b0abf9208261e0 Mon Sep 17 00:00:00 2001 From: Anderson Fernandes do Nascimento Date: Fri, 21 May 2021 21:01:12 -0300 Subject: [PATCH 3/4] merge fix --- .../Catalog/Controllers/ProductController.cs | 34 ++++++++++++++++++- .../Views/Product/ProductDetail.cshtml | 14 ++++++++ .../ModuleInitializer.cs | 3 ++ .../SimplCommerce.Module.Catalog.csproj | 1 + .../Views/Product/ProductDetail.cshtml | 4 +-- 5 files changed, 53 insertions(+), 3 deletions(-) diff --git a/src/Modules/SimplCommerce.Module.Catalog/Areas/Catalog/Controllers/ProductController.cs b/src/Modules/SimplCommerce.Module.Catalog/Areas/Catalog/Controllers/ProductController.cs index 79c5d71d89..0ce9c1d04e 100644 --- a/src/Modules/SimplCommerce.Module.Catalog/Areas/Catalog/Controllers/ProductController.cs +++ b/src/Modules/SimplCommerce.Module.Catalog/Areas/Catalog/Controllers/ProductController.cs @@ -5,6 +5,8 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; +using SimplCommerce.AI.Recommendation; +using SimplCommerce.AI.Recommendation.Models; using SimplCommerce.Infrastructure.Data; using SimplCommerce.Module.Catalog.Areas.Catalog.ViewModels; using SimplCommerce.Module.Catalog.Models; @@ -24,18 +26,21 @@ public class ProductController : Controller private readonly IMediator _mediator; private readonly IProductPricingService _productPricingService; private readonly IContentLocalizationService _contentLocalizationService; + private readonly IRecommendationService _recommendationService; public ProductController(IRepository productRepository, IMediaService mediaService, IMediator mediator, IProductPricingService productPricingService, - IContentLocalizationService contentLocalizationService) + IContentLocalizationService contentLocalizationService, + IRecommendationService recommendationService) { _productRepository = productRepository; _mediaService = mediaService; _mediator = mediator; _productPricingService = productPricingService; _contentLocalizationService = contentLocalizationService; + _recommendationService = recommendationService; } [HttpGet("product/product-overview")] @@ -116,6 +121,7 @@ public async Task ProductDetail(long id) MapRelatedProductToProductVm(product, model); MapProductOptionToProductVm(product, model); MapProductImagesToProductVm(product, model); + MapRecommendProductsToVm(model); await _mediator.Publish(new EntityViewed { EntityId = product.Id, EntityTypeId = "Product" }); _productRepository.SaveChanges(); @@ -218,5 +224,31 @@ private void MapRelatedProductToProductVm(Product product, ProductDetail model) } } } + + private void MapRecommendProductsToVm(ProductDetail model) + { + var top5 = (from m in _productRepository.Query().Where(x => x.IsPublished && x.IsVisibleIndividually && x.Id != model.Id) + .Include(x => x.OptionValues) + .Include(x => x.ThumbnailImage) + .Include(x => x.Medias).ThenInclude(m => m.Media) + .AsEnumerable() + let p = _recommendationService.Predict( + new ProductInfo() + { + ProductID = model.Id, + CombinedProductID = (uint)m.Id + }) + orderby p.Score descending + select (Product: m, Score: p.Score)).Take(4); + + foreach (var dict in top5) + { + var productThumbnail = ProductThumbnail.FromProduct(dict.Product); + productThumbnail.Name = _contentLocalizationService.GetLocalizedProperty(nameof(Product), productThumbnail.Id, nameof(productThumbnail.Name), productThumbnail.Name); + productThumbnail.ThumbnailUrl = _mediaService.GetThumbnailUrl(productThumbnail.ThumbnailImage); + productThumbnail.CalculatedProductPrice = _productPricingService.CalculateProductPrice(productThumbnail); + model.RecommendProducts.Add(productThumbnail); + } + } } } diff --git a/src/Modules/SimplCommerce.Module.Catalog/Areas/Catalog/Views/Product/ProductDetail.cshtml b/src/Modules/SimplCommerce.Module.Catalog/Areas/Catalog/Views/Product/ProductDetail.cshtml index fcac14b389..7cfe6e262f 100644 --- a/src/Modules/SimplCommerce.Module.Catalog/Areas/Catalog/Views/Product/ProductDetail.cshtml +++ b/src/Modules/SimplCommerce.Module.Catalog/Areas/Catalog/Views/Product/ProductDetail.cshtml @@ -295,6 +295,20 @@ @await Component.InvokeAsync("ProductRecentlyViewed", new { productId = Model.Id }) +@if (Model.RecommendProducts.Any()) +{ + +

@Localizer["Recommended Products"]

+
+ @foreach (var product in Model.RecommendProducts) + { +
+ +
+ } +
+} +

@Localizer["Customer reviews"]

@await Component.InvokeAsync("Review", new { entityId = Model.Id, entityTypeId = "Product" }) diff --git a/src/Modules/SimplCommerce.Module.Catalog/ModuleInitializer.cs b/src/Modules/SimplCommerce.Module.Catalog/ModuleInitializer.cs index 440cc3e5eb..ccc41404ac 100644 --- a/src/Modules/SimplCommerce.Module.Catalog/ModuleInitializer.cs +++ b/src/Modules/SimplCommerce.Module.Catalog/ModuleInitializer.cs @@ -8,6 +8,7 @@ using SimplCommerce.Module.Catalog.Events; using SimplCommerce.Module.Catalog.Services; using SimplCommerce.Module.Core.Events; +using SimplCommerce.AI.Recommendation; namespace SimplCommerce.Module.Catalog { @@ -15,12 +16,14 @@ public class ModuleInitializer : IModuleInitializer { public void ConfigureServices(IServiceCollection services) { + services.AddSingleton(); services.AddTransient(); services.AddTransient, ReviewSummaryChangedHandler>(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddHostedService(); GlobalConfiguration.RegisterAngularModule("simplAdmin.catalog"); } diff --git a/src/Modules/SimplCommerce.Module.Catalog/SimplCommerce.Module.Catalog.csproj b/src/Modules/SimplCommerce.Module.Catalog/SimplCommerce.Module.Catalog.csproj index c270ebf644..d292b2dddb 100644 --- a/src/Modules/SimplCommerce.Module.Catalog/SimplCommerce.Module.Catalog.csproj +++ b/src/Modules/SimplCommerce.Module.Catalog/SimplCommerce.Module.Catalog.csproj @@ -9,6 +9,7 @@ + diff --git a/src/SimplCommerce.WebHost/Themes/CozaStore/Areas/Catalog/Views/Product/ProductDetail.cshtml b/src/SimplCommerce.WebHost/Themes/CozaStore/Areas/Catalog/Views/Product/ProductDetail.cshtml index 7bc049bc45..8c1edfa079 100644 --- a/src/SimplCommerce.WebHost/Themes/CozaStore/Areas/Catalog/Views/Product/ProductDetail.cshtml +++ b/src/SimplCommerce.WebHost/Themes/CozaStore/Areas/Catalog/Views/Product/ProductDetail.cshtml @@ -276,8 +276,8 @@ @if (Model.RecommendProducts.Any()) { - -

@Localizer["Recommend Products"]

+ +

@Localizer["Recommended Products"]

@foreach (var product in Model.RecommendProducts) { From a3468b61c5a000eb4a194078f43c1c23b9dcfaab Mon Sep 17 00:00:00 2001 From: Anderson Fernandes do Nascimento Date: Sat, 28 Oct 2023 21:39:53 -0300 Subject: [PATCH 4/4] Fix build ML --- .../SimplCommerce.AI.Recommendation.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/SimplCommerce.AI.Recommendation/SimplCommerce.AI.Recommendation.csproj b/src/SimplCommerce.AI.Recommendation/SimplCommerce.AI.Recommendation.csproj index 8cf1b1c17c..f7b742e55d 100644 --- a/src/SimplCommerce.AI.Recommendation/SimplCommerce.AI.Recommendation.csproj +++ b/src/SimplCommerce.AI.Recommendation/SimplCommerce.AI.Recommendation.csproj @@ -1,14 +1,14 @@ - net5.0 + net7.0 - - + +