From 1e841467ec4495c0caa14ba47185b2dce116ca39 Mon Sep 17 00:00:00 2001 From: Loretta Date: Fri, 24 Oct 2025 08:12:35 +0200 Subject: [PATCH 1/2] Implement CheckAndUpdateProductManageInventoryMethodToManageStock to MgEventConsumer; improvements; --- .../Areas/Admin/Controllers/CustomOrderController.cs | 1 + .../Admin/Controllers/CustomOrderSignalREndpoint.cs | 6 ++++++ .../Domains/DataLayer/FruitBankDbContext.cs | 3 +-- .../Domains/DataLayer/OrderDtoDbTable.cs | 3 +++ .../Domains/EventConsumers/FruitBankEventConsumer.cs | 10 ++++++---- 5 files changed, 17 insertions(+), 6 deletions(-) diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/CustomOrderController.cs b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/CustomOrderController.cs index 8c40534..976b51b 100644 --- a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/CustomOrderController.cs +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/CustomOrderController.cs @@ -78,6 +78,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers [NonAction] public Task> GetAllOrderDtos() => _customOrderSignalREndpoint.GetAllOrderDtos(); [NonAction]public Task GetOrderDtoById(int orderId) => _customOrderSignalREndpoint.GetOrderDtoById(orderId); [NonAction]public Task> GetPendingOrderDtos() => _customOrderSignalREndpoint.GetPendingOrderDtos(); + [NonAction]public Task> GetPendingOrderDtosForMeasuring() => _customOrderSignalREndpoint.GetPendingOrderDtosForMeasuring(); [NonAction] public Task StartMeasuring(int orderId, int userId) => _customOrderSignalREndpoint.StartMeasuring(orderId, userId); [NonAction]public Task SetOrderStatusToComplete(int orderId, int revisorId) => _customOrderSignalREndpoint.SetOrderStatusToComplete(orderId, revisorId); [NonAction] public Task> GetAllOrderDtoByIds(int[] orderIds) => _customOrderSignalREndpoint.GetAllOrderDtoByIds(orderIds); diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/CustomOrderSignalREndpoint.cs b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/CustomOrderSignalREndpoint.cs index 7a0373f..f960399 100644 --- a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/CustomOrderSignalREndpoint.cs +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/CustomOrderSignalREndpoint.cs @@ -35,6 +35,12 @@ public class CustomOrderSignalREndpoint(FruitBankDbContext ctx, IWorkContext wor return await ctx.OrderDtos.GetAllByOrderStatus(OrderStatus.Pending).ToListAsync(); } + [SignalR(SignalRTags.GetPendingOrderDtosForMeasuring)] + public async Task> GetPendingOrderDtosForMeasuring() + { + return await ctx.OrderDtos.GetAllForMeasuring().ToListAsync(); + } + [SignalR(SignalRTags.GetAllOrderDtoByIds)] public async Task> GetAllOrderDtoByIds(int[] orderIds) { diff --git a/Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/FruitBankDbContext.cs b/Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/FruitBankDbContext.cs index 5c6c0ab..f921bab 100644 --- a/Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/FruitBankDbContext.cs +++ b/Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/FruitBankDbContext.cs @@ -55,7 +55,6 @@ public class FruitBankDbContext : MgDbContextBase, public FilesDbTable Files { get; set; } public ShippingDocumentToFilesDbTable ShippingDocumentToFiles { get; set; } - public IRepository Products { get; set; } public IRepository Customers { get; set; } public IRepository CustomerRoles { get; set; } public IRepository CustomerRoleMappings { get; set; } @@ -69,7 +68,7 @@ public class FruitBankDbContext : MgDbContextBase, IRepository customerRepository, IRepository customerCustomerRoleMappingRepository, IRepository customerRoleRepository, - IEnumerable logWriters) : base(dataProvider, lockService, new Logger(logWriters.ToArray())) + IEnumerable logWriters) : base(productRepository, dataProvider, lockService, new Logger(logWriters.ToArray())) { _storeContext = storeContext; _productService = productService; diff --git a/Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/OrderDtoDbTable.cs b/Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/OrderDtoDbTable.cs index e930071..5257fac 100644 --- a/Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/OrderDtoDbTable.cs +++ b/Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/OrderDtoDbTable.cs @@ -33,5 +33,8 @@ public class OrderDtoDbTable : MgDtoDbTableBase public IQueryable GetAllByOrderStatus(OrderStatus orderStatus, bool loadRelations = true) => GetAll(loadRelations).Where(o => o.OrderStatusId == (int)orderStatus); + public IQueryable GetAllForMeasuring(bool loadRelations = true) + => GetAllByOrderStatus(OrderStatus.Pending, loadRelations).Where(o => o.DateOfReceipt != null); + public IQueryable GetAllByIds(IEnumerable orderIds, bool loadRelations = true) => GetAll(loadRelations).Where(o => orderIds.Contains(o.Id)); } diff --git a/Nop.Plugin.Misc.AIPlugin/Domains/EventConsumers/FruitBankEventConsumer.cs b/Nop.Plugin.Misc.AIPlugin/Domains/EventConsumers/FruitBankEventConsumer.cs index 3f323f8..4b45f16 100644 --- a/Nop.Plugin.Misc.AIPlugin/Domains/EventConsumers/FruitBankEventConsumer.cs +++ b/Nop.Plugin.Misc.AIPlugin/Domains/EventConsumers/FruitBankEventConsumer.cs @@ -14,7 +14,7 @@ using Mango.Nop.Core.Extensions; namespace Nop.Plugin.Misc.FruitBankPlugin.Domains.EventConsumers; public class FruitBankEventConsumer(IHttpContextAccessor httpContextAcc, FruitBankDbContext ctx, FruitBankAttributeService fruitBankAttributeService, IEnumerable logWriters) : - MgEventConsumer(httpContextAcc, logWriters), + MgEventConsumer(ctx, httpContextAcc, logWriters), IConsumer>, IConsumer>, IConsumer>, @@ -28,9 +28,9 @@ public class FruitBankEventConsumer(IHttpContextAccessor httpContextAcc, FruitBa { public override async Task HandleEventAsync(EntityUpdatedEvent eventMessage) { - var product = eventMessage.Entity; + var product = await CheckAndUpdateProductManageInventoryMethodToManageStock(eventMessage.Entity); - var saveProductCustomAttributesResult = await SaveProductCustomAttributesAsync(eventMessage.Entity); + var saveProductCustomAttributesResult = await SaveProductCustomAttributesAsync(product); //var isMeasurableProduct = await fruitBankAttributeService.IsMeasurableEntityAsync(product.Id); @@ -52,7 +52,9 @@ public class FruitBankEventConsumer(IHttpContextAccessor httpContextAcc, FruitBa public override async Task HandleEventAsync(EntityInsertedEvent eventMessage) { - await SaveProductCustomAttributesAsync(eventMessage.Entity); //TODO: ez ide miért kell? - J. + var product = await CheckAndUpdateProductManageInventoryMethodToManageStock(eventMessage.Entity); + + await SaveProductCustomAttributesAsync(product); //TODO: ez ide miért kell? - J. await base.HandleEventAsync(eventMessage); } From f9b302c5d915e5db0850c0cc0fea2d3eef4cadbf Mon Sep 17 00:00:00 2001 From: Loretta Date: Fri, 24 Oct 2025 12:01:48 +0200 Subject: [PATCH 2/2] fixes --- .../Controllers/CustomOrderController.cs | 23 +- .../Areas/Admin/Views/Order/Edit.cshtml | 82 ++++ .../Areas/Admin/Views/Order/List.cshtml | 4 +- .../Order/_CustomOrderDetails.Products.cshtml | 399 ++++++++++++++++++ .../Areas/Admin/Views/_ViewImports.cshtml | 3 +- .../Domains/DataLayer/FruitBankDbContext.cs | 23 +- .../EventConsumers/FruitBankEventConsumer.cs | 52 ++- .../Factories/CustomOrderModelFactory.cs | 2 +- .../Nop.Plugin.Misc.FruitBankPlugin.csproj | 11 + .../Services/AICalculationService.cs | 6 +- .../Services/CerebrasAPIService.cs | 77 ++-- .../Services/CustomPriceCalculationService.cs | 2 - .../Services/LockService.cs | 2 +- .../Services/OpenAIApiService.cs | 130 +++--- 14 files changed, 675 insertions(+), 141 deletions(-) create mode 100644 Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/Edit.cshtml create mode 100644 Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/_CustomOrderDetails.Products.cshtml diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/CustomOrderController.cs b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/CustomOrderController.cs index 976b51b..3ec06ad 100644 --- a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/CustomOrderController.cs +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/CustomOrderController.cs @@ -52,11 +52,12 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers private readonly IProductService _productService; private readonly IStoreContext _storeContext; private readonly IWorkContext _workContext; + private readonly IPriceCalculationService _priceCalculationService; // ... other dependencies private readonly ILogger _logger; - public CustomOrderController(FruitBankDbContext fruitBankDbContext, IOrderService orderService, IOrderModelFactory orderModelFactory, ICustomOrderSignalREndpointServer customOrderSignalREndpoint, IPermissionService permissionService, IGenericAttributeService genericAttributeService, INotificationService notificationService, ICustomerService customerService, IProductService productService, IEnumerable logWriters, IStoreContext storeContext, IWorkContext workContext) + public CustomOrderController(FruitBankDbContext fruitBankDbContext, IOrderService orderService,IPriceCalculationService priceCalculationService, IOrderModelFactory orderModelFactory, ICustomOrderSignalREndpointServer customOrderSignalREndpoint, IPermissionService permissionService, IGenericAttributeService genericAttributeService, INotificationService notificationService, ICustomerService customerService, IProductService productService, IEnumerable logWriters, IStoreContext storeContext, IWorkContext workContext) { _logger = new Logger(logWriters.ToArray()); @@ -71,6 +72,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers _productService = productService; _storeContext = storeContext; _workContext = workContext; + _priceCalculationService = priceCalculationService; // ... initialize other deps } @@ -117,10 +119,10 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers //return _customOrderService. var orderListModel = await _orderModelFactory.PrepareOrderListModelExtendedAsync(searchModel); - _logger.Debug($"Total: {orderListModel.RecordsTotal}, Data Count: {orderListModel.Data.Count()}"); + _logger.Detail($"Total: {orderListModel.RecordsTotal}, Data Count: {orderListModel.Data.Count()}"); foreach (var item in orderListModel.Data.Take(3)) { - _logger.Debug($"Order: {item.Id}, {item.CustomOrderNumber}"); + _logger.Detail($"Order: {item.Id}, {item.CustomOrderNumber}"); } return orderListModel; @@ -223,7 +225,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers }; //var productDtosById = await _dbContext.ProductDtos.GetAllByIds(orderProducts.Select(op => op.Id)).ToDictionaryAsync(p => p.Id, prodDto => prodDto); - + var store = _storeContext.GetCurrentStore(); var transactionSuccess = await _dbContext.TransactionSafeAsync(async _ => { await _orderService.InsertOrderAsync(order); @@ -233,6 +235,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers foreach (var item in orderProducts) { var product = await _productService.GetProductByIdAsync(item.Id); + if (product == null || product.StockQuantity - item.Quantity < 0) { var errorText = $"product == null || product.StockQuantity - item.Quantity < 0; productId: {product?.Id}; product?.StockQuantity - item.Quantity: {product?.StockQuantity - item.Quantity}"; @@ -241,6 +244,16 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers throw new Exception($"{errorText}"); } + //var (_, finalUnitPrice, _, _) = await _priceCalculationService.GetFinalPriceAsync(product, customer, store, item.Price, 0, true, item.Quantity, null, null); + + //var (presetPriceInclTax, _) = await _taxService.GetProductPriceAsync(product, finalPrice, true, customer); + //var (presetPriceExclTax, _) = await _taxService.GetProductPriceAsync(product, finalPrice, false, customer); + //model.UnitPriceExclTax = presetPriceExclTax; + //model.UnitPriceInclTax = presetPriceInclTax; + //model.Quantity = item.Quantity; + //model.SubTotalExclTax = presetPriceExclTax; + //model.SubTotalInclTax = presetPriceInclTax; + //if (productDtosById.TryGetValue(item.Id, out var productDto)) { var orderItem = new OrderItem @@ -453,7 +466,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers { result.Add(new { - label = $"{product.Name} [KÉSZLET: {product.StockQuantity}]", + label = $"{product.Name} [KÉSZLET: {product.StockQuantity}] [ÁR: {product.Price}]", value = product.Id, sku = product.Sku, price = product.Price, diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/Edit.cshtml b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/Edit.cshtml new file mode 100644 index 0000000..1464356 --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/Edit.cshtml @@ -0,0 +1,82 @@ +@model OrderModel + + +@{ + //page title + ViewBag.PageTitle = T("Admin.Orders.EditOrderDetails").Text + "ANYD"; + //active menu item (system name) + NopHtml.SetActiveMenuItemSystemName("Orders"); +} + +@{ + const string hideInfoBlockAttributeName = "OrderPage.HideInfoBlock"; + var customer = await workContext.GetCurrentCustomerAsync(); + var hideInfoBlock = await genericAttributeService.GetAttributeAsync(customer, hideInfoBlockAttributeName); + + const string hideBillingAndShippingBlockAttributeName = "OrderPage.HideBillingAndShippingBlock"; + var hideBillingAndShippingBlock = await genericAttributeService.GetAttributeAsync(customer, hideBillingAndShippingBlockAttributeName); + + const string hideProductsBlockAttributeName = "OrderPage.HideProductsBlock"; + var hideProductsBlock = await genericAttributeService.GetAttributeAsync(customer, hideProductsBlockAttributeName); + + const string hideNotesBlockAttributeName = "OrderPage.HideNotesBlock"; + var hideNotesBlock = await genericAttributeService.GetAttributeAsync(customer, hideNotesBlockAttributeName); +} + +
+
+

+ @T("Admin.Orders.EditOrderDetails") - @Model.CustomOrderNumber + + + @T("Admin.Orders.BackToList") + +

+
+ + + @T("Admin.Orders.PdfInvoice") + + @if (!Model.IsLoggedInAsVendor) + { + + + @T("Admin.Common.Delete") + + } + + + @await Component.InvokeAsync(typeof(AdminWidgetViewComponent), new { widgetZone = AdminWidgetZones.OrderDetailsButtons, additionalData = Model }) +
+
+ +
+ +
+
+
+ + + @await Component.InvokeAsync(typeof(AdminWidgetViewComponent), new { widgetZone = AdminWidgetZones.OrderDetailsBlock, additionalData = Model }) + @await Html.PartialAsync("_OrderDetails.Info", Model) + @await Html.PartialAsync("_OrderDetails.BillingShipping", Model) + @await Html.PartialAsync("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Order/_CustomOrderDetails.Products.cshtml", Model) + + @if (!Model.IsLoggedInAsVendor) + { + @await Html.PartialAsync("_OrderDetails.Notes", Model) + } + + +
+
+
+
+ \ No newline at end of file diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/List.cshtml b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/List.cshtml index 88cc570..66882a0 100644 --- a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/List.cshtml +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/List.cshtml @@ -751,8 +751,8 @@ Product - Quantity - Price + Mennyisség + Egységár diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/_CustomOrderDetails.Products.cshtml b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/_CustomOrderDetails.Products.cshtml new file mode 100644 index 0000000..acf1a25 --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/_CustomOrderDetails.Products.cshtml @@ -0,0 +1,399 @@ +@model OrderModel + +@using Nop.Core.Domain.Tax; +@using Nop.Core.Domain.Catalog; + +
+
+
ANYÁD!!!!! + @foreach (var item in Model.Items) + { + + + + } + + + + + @if (Model.HasDownloadableProducts) + { + + } + + + + + @if (!Model.IsLoggedInAsVendor) + { + + } + + + + + @if (Model.HasDownloadableProducts) + { + + } + + + + + @if (!Model.IsLoggedInAsVendor) + { + + } + + + + @foreach (var item in Model.Items) + { + + + + @if (Model.HasDownloadableProducts) + { + + } + + + + + @if (!Model.IsLoggedInAsVendor) + { + + } + + } + +
+ @T("Admin.Orders.Products.Picture") + + @T("Admin.Orders.Products.ProductName") + + @T("Admin.Orders.Products.Download") + + @T("Admin.Orders.Products.Price") + + @T("Admin.Orders.Products.Quantity") + + @T("Admin.Orders.Products.Discount") + + @T("Admin.Orders.Products.Total") + + @T("Admin.Common.Edit") +
+ + + @item.ProductName + @if (!string.IsNullOrEmpty(item.AttributeInfo)) + { +

+ @Html.Raw(item.AttributeInfo) +

+ } + @if (!string.IsNullOrEmpty(item.RecurringInfo)) + { +

+ @Html.Raw(item.RecurringInfo) +

+ } + @if (!string.IsNullOrEmpty(item.RentalInfo)) + { +

+ @Html.Raw(item.RentalInfo) +

+ } + @if (!string.IsNullOrEmpty(item.Sku)) + { +

+ @T("Admin.Orders.Products.SKU"): + @item.Sku +

+ } + @if (!string.IsNullOrEmpty(item.VendorName)) + { +

+ @T("Admin.Orders.Products.Vendor"): + @item.VendorName +

+ } + @if (item.ReturnRequests.Count > 0) + { +

+ @T("Admin.Orders.Products.ReturnRequests"): + @for (var i = 0; i < item.ReturnRequests.Count; i++) + { + var returnRequest = item.ReturnRequests[i]; + @returnRequest.CustomNumber + if (i != item.ReturnRequests.Count - 1) + { + , + } + } +

+ } +
+ @if (item.IsDownload) + { + + @T("Admin.Orders.Products.Download.Download") + + + } + else + { + @T("Admin.Orders.Products.Download.NotAvailable") + } + + @if (Model.AllowCustomersToSelectTaxDisplayType) + { +
@Html.Raw(item.UnitPriceInclTax)
+
@Html.Raw(item.UnitPriceExclTax)
+ } + else + { + switch (Model.TaxDisplayType) + { + case TaxDisplayType.ExcludingTax: + { + @Html.Raw(item.UnitPriceExclTax) + } + break; + case TaxDisplayType.IncludingTax: + { + @Html.Raw(item.UnitPriceInclTax) + } + break; + default: + break; + } + } +
+
+
+ @T("Admin.Orders.Products.Edit.InclTax") +
+
+ +
+
+
+
+ @T("Admin.Orders.Products.Edit.ExclTax") +
+
+ +
+
+
+
+
@item.Quantity
+
+
+
+ +
+
+
+
+ @if (Model.AllowCustomersToSelectTaxDisplayType) + { +
@Html.Raw(item.DiscountInclTax)
+
@Html.Raw(item.DiscountExclTax)
+ } + else + { + switch (Model.TaxDisplayType) + { + case TaxDisplayType.ExcludingTax: + { + @Html.Raw(item.DiscountExclTax) + } + break; + case TaxDisplayType.IncludingTax: + { + @Html.Raw(item.DiscountInclTax) + } + break; + default: + break; + } + } +
+
+
+ @T("Admin.Orders.Products.Edit.InclTax") +
+
+ +
+
+
+
+ @T("Admin.Orders.Products.Edit.ExclTax") +
+
+ +
+
+
+
+ @if (Model.AllowCustomersToSelectTaxDisplayType) + { +
@Html.Raw(item.SubTotalInclTax)
+
@Html.Raw(item.SubTotalExclTax)
+ } + else + { + switch (Model.TaxDisplayType) + { + case TaxDisplayType.ExcludingTax: + { + @Html.Raw(item.SubTotalExclTax) + } + break; + case TaxDisplayType.IncludingTax: + { + @Html.Raw(item.SubTotalInclTax) + } + break; + default: + break; + } + } +
+
+
+ @T("Admin.Orders.Products.Edit.InclTax") +
+
+ +
+
+
+
+ @T("Admin.Orders.Products.Edit.ExclTax") +
+
+ +
+
+
+
+ + + + + + + + + +
+
+
+ @if (!string.IsNullOrEmpty(Model.CheckoutAttributeInfo) && !Model.IsLoggedInAsVendor) + { +
+
+ @Html.Raw(Model.CheckoutAttributeInfo) +
+
+ } + @if (!Model.IsLoggedInAsVendor) + { +
+
+ +
+
+ } +
\ No newline at end of file diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/_ViewImports.cshtml b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/_ViewImports.cshtml index afa453c..7ced1cd 100644 --- a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/_ViewImports.cshtml +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/_ViewImports.cshtml @@ -74,4 +74,5 @@ @using Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin @* @using Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Components *@ -@using DevExtreme.AspNet.Mvc \ No newline at end of file +@using DevExtreme.AspNet.Mvc +@using Nop.Web.Areas.Admin.Models.Orders; \ No newline at end of file diff --git a/Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/FruitBankDbContext.cs b/Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/FruitBankDbContext.cs index f921bab..d9b953b 100644 --- a/Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/FruitBankDbContext.cs +++ b/Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/FruitBankDbContext.cs @@ -2,22 +2,24 @@ using AyCode.Core.Extensions; using AyCode.Core.Loggers; using AyCode.Utils.Extensions; +using FruitBank.Common.Dtos; using FruitBank.Common.Entities; using FruitBank.Common.Interfaces; using FruitBank.Common.Models; +using Mango.Nop.Core.Extensions; +using Mango.Nop.Core.Loggers; using Mango.Nop.Core.Repositories; using Nop.Core; using Nop.Core.Caching; using Nop.Core.Domain.Catalog; using Nop.Core.Domain.Customers; +using Nop.Core.Domain.Orders; +using Nop.Core.Events; using Nop.Data; using Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer.Interfaces; using Nop.Plugin.Misc.FruitBankPlugin.Services; using Nop.Services.Catalog; -using FruitBank.Common.Dtos; -using Mango.Nop.Core.Extensions; -using Mango.Nop.Core.Loggers; -using Nop.Core.Domain.Orders; +using Nop.Services.Security; using WebMarkupMin.Core.Loggers; namespace Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer; @@ -38,6 +40,7 @@ public class FruitBankDbContext : MgDbContextBase, private readonly IStoreContext _storeContext; private readonly IProductService _productService; private readonly IStaticCacheManager _staticCacheManager; + protected readonly IEventPublisher _eventPublisher; public ProductDtoDbTable ProductDtos { get; set; } @@ -64,12 +67,15 @@ public class FruitBankDbContext : MgDbContextBase, ShippingItemPalletDbTable shippingItemPalletDbTable, FilesDbTable filesDbTable, ShippingDocumentToFilesDbTable shippingDocumentToFilesDbTable, ProductDtoDbTable productDtoDbTable, OrderDtoDbTable orderDtoDbTable, OrderItemDtoDbTable orderItemDtoDbTable, OrderItemPalletDbTable orderItemPalletDbTable, IProductService productService, IStaticCacheManager staticCacheManager, + IRepository orderRepository, + IRepository orderItemRepository, IRepository productRepository, IRepository customerRepository, IRepository customerCustomerRoleMappingRepository, - IRepository customerRoleRepository, - IEnumerable logWriters) : base(productRepository, dataProvider, lockService, new Logger(logWriters.ToArray())) + IRepository customerRoleRepository,IEventPublisher eventPublisher, + IEnumerable logWriters) : base(productRepository, orderRepository, orderItemRepository, dataProvider, lockService, new Logger(logWriters.ToArray())) { + _eventPublisher = eventPublisher; _storeContext = storeContext; _productService = productService; _staticCacheManager = staticCacheManager; @@ -410,6 +416,8 @@ public class FruitBankDbContext : MgDbContextBase, if (orderDto == null) return null; if (!orderDto.IsMeasuredAndValid() || orderDto.OrderStatus == OrderStatus.Complete) return null; //throw new Exception($"SetOrderDtoToComplete; orderDto.IsMeasured == false; {orderDto}"); + + var prevOrderStatus = orderDto.OrderStatus; orderDto.OrderStatus = OrderStatus.Complete; await OrderDtos.UpdateAsync(orderDto); @@ -434,6 +442,9 @@ public class FruitBankDbContext : MgDbContextBase, (orderItemDto.ProductId, -(orderItemDto.NetWeight-gaNetWeight), orderItemDto.IsMeasurable, true); } + var order = Orders.GetById(orderDto.Id); + await _eventPublisher.PublishAsync(new OrderStatusChangedEvent(order, prevOrderStatus)); + return orderDto; } diff --git a/Nop.Plugin.Misc.AIPlugin/Domains/EventConsumers/FruitBankEventConsumer.cs b/Nop.Plugin.Misc.AIPlugin/Domains/EventConsumers/FruitBankEventConsumer.cs index 4b45f16..4454e21 100644 --- a/Nop.Plugin.Misc.AIPlugin/Domains/EventConsumers/FruitBankEventConsumer.cs +++ b/Nop.Plugin.Misc.AIPlugin/Domains/EventConsumers/FruitBankEventConsumer.cs @@ -10,11 +10,12 @@ using Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer; using Nop.Plugin.Misc.FruitBankPlugin.Services; using Nop.Services.Events; using Mango.Nop.Core.Extensions; +using Nop.Core.Domain.Orders; namespace Nop.Plugin.Misc.FruitBankPlugin.Domains.EventConsumers; public class FruitBankEventConsumer(IHttpContextAccessor httpContextAcc, FruitBankDbContext ctx, FruitBankAttributeService fruitBankAttributeService, IEnumerable logWriters) : - MgEventConsumer(ctx, httpContextAcc, logWriters), + MgEventConsumerBase(ctx, httpContextAcc, logWriters), IConsumer>, IConsumer>, IConsumer>, @@ -24,7 +25,10 @@ public class FruitBankEventConsumer(IHttpContextAccessor httpContextAcc, FruitBa IConsumer>, IConsumer>, IConsumer>, - IConsumer> + IConsumer>, + + IConsumer>, + IConsumer> { public override async Task HandleEventAsync(EntityUpdatedEvent eventMessage) { @@ -126,7 +130,7 @@ public class FruitBankEventConsumer(IHttpContextAccessor httpContextAcc, FruitBa { return; - Logger.Info($"HandleEventAsync EntityInsertedEvent; id: {eventMessage.Entity.Id}"); + Logger.Info($"HandleEventAsync->EntityInsertedEvent; id: {eventMessage.Entity.Id}"); await UpdateShippingItemMeasuringValuesAsync(eventMessage.Entity); } @@ -134,13 +138,13 @@ public class FruitBankEventConsumer(IHttpContextAccessor httpContextAcc, FruitBa { return; - Logger.Info($"HandleEventAsync EntityUpdatedEvent; id: {eventMessage.Entity.Id}"); + Logger.Info($"HandleEventAsync->EntityUpdatedEvent; id: {eventMessage.Entity.Id}"); await UpdateShippingItemMeasuringValuesAsync(eventMessage.Entity); } public async Task HandleEventAsync(EntityDeletedEvent eventMessage) { - Logger.Info($"HandleEventAsync EntityDeletedEvent; id: {eventMessage.Entity.Id}"); + Logger.Info($"HandleEventAsync->EntityDeletedEvent; id: {eventMessage.Entity.Id}"); await UpdateShippingItemMeasuringValuesAsync(eventMessage.Entity); } @@ -153,7 +157,7 @@ public class FruitBankEventConsumer(IHttpContextAccessor httpContextAcc, FruitBa public async Task HandleEventAsync(EntityInsertedEvent eventMessage) { - Logger.Info($"HandleEventAsync EntityInsertedEvent; id: {eventMessage.Entity.Id}"); + Logger.Info($"HandleEventAsync->EntityInsertedEvent; id: {eventMessage.Entity.Id}"); await UpdateShippingDocumentIsAllMeasuredAsync(eventMessage.Entity); } @@ -162,7 +166,7 @@ public class FruitBankEventConsumer(IHttpContextAccessor httpContextAcc, FruitBa public async Task HandleEventAsync(EntityUpdatedEvent eventMessage) { - Logger.Info($"HandleEventAsync EntityUpdatedEvent; id: {eventMessage.Entity.Id}"); + Logger.Info($"HandleEventAsync->EntityUpdatedEvent; id: {eventMessage.Entity.Id}"); var shippingItem = eventMessage.Entity; //var isMeasured = shippingItem.IsValidMeasuringValues(); @@ -191,14 +195,14 @@ public class FruitBankEventConsumer(IHttpContextAccessor httpContextAcc, FruitBa public async Task HandleEventAsync(EntityInsertedEvent eventMessage) { - Logger.Info($"HandleEventAsync EntityInsertedEvent; id: {eventMessage.Entity.Id}"); + Logger.Info($"HandleEventAsync->EntityInsertedEvent; id: {eventMessage.Entity.Id}"); await UpdateShippingIsAllMeasuredAsync(eventMessage.Entity); } public async Task HandleEventAsync(EntityUpdatedEvent eventMessage) { - Logger.Info($"HandleEventAsync EntityUpdatedEvent; id: {eventMessage.Entity.Id}"); + Logger.Info($"HandleEventAsync->EntityUpdatedEvent; id: {eventMessage.Entity.Id}"); await UpdateShippingIsAllMeasuredAsync(eventMessage.Entity); } @@ -221,24 +225,48 @@ public class FruitBankEventConsumer(IHttpContextAccessor httpContextAcc, FruitBa public async Task HandleEventAsync(EntityDeletedEvent eventMessage) { - Logger.Info($"HandleEventAsync EntityDeletedEvent; id: {eventMessage.Entity.Id}"); + Logger.Info($"HandleEventAsync->EntityDeletedEvent; id: {eventMessage.Entity.Id}"); await ctx.ShippingDocuments.DeleteAsync(sd => sd.ShippingId == eventMessage.Entity.Id, true); } public async Task HandleEventAsync(EntityDeletedEvent eventMessage) { - Logger.Info($"HandleEventAsync EntityDeletedEvent; id: {eventMessage.Entity.Id}"); + Logger.Info($"HandleEventAsync->EntityDeletedEvent; id: {eventMessage.Entity.Id}"); await ctx.ShippingItems.DeleteAsync(si => si.ShippingDocumentId == eventMessage.Entity.Id, true); } public async Task HandleEventAsync(EntityDeletedEvent eventMessage) { - Logger.Info($"HandleEventAsync EntityDeletedEvent; id: {eventMessage.Entity.Id}"); + Logger.Info($"HandleEventAsync->EntityDeletedEvent; id: {eventMessage.Entity.Id}"); await ctx.ShippingItemPallets.DeleteAsync(sp => sp.ShippingItemId == eventMessage.Entity.Id, false); } + public async Task HandleEventAsync(EntityUpdatedEvent eventMessage) => await CheckAndUpdateOrderItemFinalPrices(eventMessage.Entity); + + public async Task HandleEventAsync(EntityInsertedEvent eventMessage) => await CheckAndUpdateOrderItemFinalPrices(eventMessage.Entity); + + private async Task CheckAndUpdateOrderItemFinalPrices(OrderItem orderItem) + { + Logger.Info($"HandleEventAsync->CheckAndUpdateOrderItemFinalPrices; orderItem.Id: {orderItem.Id}"); + + var finalPriceInclTax = decimal.Round(orderItem.UnitPriceInclTax * orderItem.Quantity, 0); + var finalPriceExclTax = decimal.Round(orderItem.UnitPriceExclTax * orderItem.Quantity, 0); + + if (orderItem.PriceInclTax != finalPriceInclTax || orderItem.PriceExclTax != finalPriceExclTax) + { + Logger.Error($"HandleEventAsync->CheckAndUpdateOrderItemFinalPrices; " + + $"orderItem.PriceInclTax({orderItem.PriceInclTax}) != finalPriceInclTax({finalPriceInclTax}) || " + + $"orderItem.PriceExclTax({orderItem.PriceExclTax}) != finalPriceExclTax({finalPriceExclTax})"); + + orderItem.PriceInclTax = finalPriceInclTax; + orderItem.PriceExclTax = finalPriceExclTax; + + await ctx.OrderItems.UpdateAsync(orderItem, false); + } + } + #endregion Delete } \ No newline at end of file diff --git a/Nop.Plugin.Misc.AIPlugin/Factories/CustomOrderModelFactory.cs b/Nop.Plugin.Misc.AIPlugin/Factories/CustomOrderModelFactory.cs index 50b956b..bcccce9 100644 --- a/Nop.Plugin.Misc.AIPlugin/Factories/CustomOrderModelFactory.cs +++ b/Nop.Plugin.Misc.AIPlugin/Factories/CustomOrderModelFactory.cs @@ -176,7 +176,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Factories //orderModelExtended.IsMeasurable = await ShouldMarkAsNeedsMeasurementAsync(orderModel); //orderModelExtended.DateOfReceipt = await GetPickupDateTimeAsync(orderModel); - Console.WriteLine(orderModelExtended.Id); + //Console.WriteLine(orderModelExtended.Id); }); return orderListModelExtended; diff --git a/Nop.Plugin.Misc.AIPlugin/Nop.Plugin.Misc.FruitBankPlugin.csproj b/Nop.Plugin.Misc.AIPlugin/Nop.Plugin.Misc.FruitBankPlugin.csproj index 026afda..f9e70c8 100644 --- a/Nop.Plugin.Misc.AIPlugin/Nop.Plugin.Misc.FruitBankPlugin.csproj +++ b/Nop.Plugin.Misc.AIPlugin/Nop.Plugin.Misc.FruitBankPlugin.csproj @@ -9,6 +9,7 @@ + @@ -40,6 +41,16 @@ PreserveNewest Always + + true + PreserveNewest + Always + + + true + PreserveNewest + Always + true PreserveNewest diff --git a/Nop.Plugin.Misc.AIPlugin/Services/AICalculationService.cs b/Nop.Plugin.Misc.AIPlugin/Services/AICalculationService.cs index 749645f..6ef979e 100644 --- a/Nop.Plugin.Misc.AIPlugin/Services/AICalculationService.cs +++ b/Nop.Plugin.Misc.AIPlugin/Services/AICalculationService.cs @@ -21,9 +21,9 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services var store = await _storeContext.GetCurrentStoreAsync(); var storeName = store.Name; var storeCompanyName = store.CompanyName; - string systemMessage = $"You are a helpful assistant of a webshop called {storeName}, of the company {storeCompanyName}, you work in the administration area, with the ADMIN user. The ADMIN user is {customer.FirstName}. Date and time is {DateTime.Now} When the user greets you, answer with a warm HUNGARIAN welcome message, incorporating some reference of the time, and maybe the weather too, making it pleasant for the ADMIN user to start working with the shop system. Assure the user that AI connection is established and you are ready to assist them."; + var systemMessage = $"You are a helpful assistant of a webshop called {storeName}, of the company {storeCompanyName}, you work in the administration area, with the ADMIN user. The ADMIN user is {customer.FirstName}. Date and time is {DateTime.Now} When the user greets you, answer with a warm HUNGARIAN welcome message, incorporating some reference of the time, and maybe the weather too, making it pleasant for the ADMIN user to start working with the shop system. Assure the user that AI connection is established and you are ready to assist them."; - string userMessage = "Hello"; + const string userMessage = "Hello"; var response = await _openAIApiService.GetSimpleResponseAsync(systemMessage, userMessage) ?? string.Empty; return response; @@ -31,7 +31,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services public async Task GetOpenAIPDFAnalysisFromText(string pdfText, string userQuestion) { - string systemMessage = $"You are a pdf analyzis assistant, the user is asking you questions about a PDF document, that you have access to. The content of the PDF document is the following: {pdfText}"; + var systemMessage = $"You are a pdf analyzis assistant, the user is asking you questions about a PDF document, that you have access to. The content of the PDF document is the following: {pdfText}"; var response = await _openAIApiService.GetSimpleResponseAsync(systemMessage, userQuestion); if (response == null) return string.Empty; diff --git a/Nop.Plugin.Misc.AIPlugin/Services/CerebrasAPIService.cs b/Nop.Plugin.Misc.AIPlugin/Services/CerebrasAPIService.cs index 17a5d5e..d55a62b 100644 --- a/Nop.Plugin.Misc.AIPlugin/Services/CerebrasAPIService.cs +++ b/Nop.Plugin.Misc.AIPlugin/Services/CerebrasAPIService.cs @@ -1,14 +1,18 @@ -using System.Text.Json; -using System.Text; -using Microsoft.Extensions.Configuration; +using AyCode.Core.Loggers; +using Mango.Nop.Core.Loggers; +using Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer; using Nop.Plugin.Misc.FruitBankPlugin.Models; -using Nop.Plugin.Misc.FruitBankPlugin.Services; using Nop.Services.Configuration; +using System.Text; +using System.Text.Json; +#nullable enable namespace Nop.Plugin.Misc.FruitBankPlugin.Services { public class CerebrasAPIService : IAIAPIService - { + { + private readonly ILogger _logger; + private readonly ISettingService _settingService; private readonly FruitBankSettings _fruitBankSettings; private readonly HttpClient _httpClient; @@ -18,23 +22,20 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services private const string CerebrasEndpoint = "https://api.cerebras.ai/v1/chat/completions"; - public CerebrasAPIService(ISettingService settingService, HttpClient httpClient) + public CerebrasAPIService(ISettingService settingService, HttpClient httpClient, IEnumerable logWriters) { + _logger = new Logger(logWriters.ToArray()); + _settingService = settingService; _fruitBankSettings = _settingService.LoadSetting(); + _httpClient = httpClient; _httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {GetApiKey()}"); } - public string GetApiKey() => - _fruitBankSettings.ApiKey; - //_configuration?.GetSection("Cerebras")?.GetValue("ApiKey") ?? string.Empty; + public string GetApiKey() => _fruitBankSettings.ApiKey; //_configuration?.GetSection("Cerebras")?.GetValue("ApiKey") ?? string.Empty; - - - public string GetModelName() => - _fruitBankSettings.ModelName; - //_configuration?.GetSection("Cerebras")?.GetValue("Model") ?? string.Empty; + public string GetModelName() => _fruitBankSettings.ModelName; //_configuration?.GetSection("Cerebras")?.GetValue("Model") ?? string.Empty; public void RegisterCallback(Action callback, Action onCompleteCallback, Action onErrorCallback) { @@ -44,27 +45,26 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services } - public async Task GetSimpleResponseAsync(string systemMessage, string userMessage, string? assistantMessage = null) - { - - string modelName = GetModelName(); + public async Task GetSimpleResponseAsync(string systemMessage, string userMessage, string? assistantMessage = null) + { + var modelName = GetModelName(); var requestBody = new CerebrasAIChatRequest { Model = modelName, Temperature = 0.2, - Messages = assistantMessage == null || assistantMessage == string.Empty - ? new[] - { + Messages = string.IsNullOrEmpty(assistantMessage) + ? + [ new AIChatMessage { Role = "system", Content = systemMessage }, new AIChatMessage { Role = "user", Content = userMessage } - } - : new[] - { + ] + : + [ new AIChatMessage { Role = "system", Content = systemMessage }, new AIChatMessage { Role = "assistant", Content = assistantMessage }, new AIChatMessage { Role = "user", Content = userMessage } - }, + ], Stream = false }; @@ -78,12 +78,13 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services using var response = await _httpClient.PostAsync(CerebrasEndpoint, requestContent); response.EnsureSuccessStatusCode(); - using var responseStream = await response.Content.ReadAsStreamAsync(); + await using var responseStream = await response.Content.ReadAsStreamAsync(); using var document = await JsonDocument.ParseAsync(responseStream); var inputTokens = document.RootElement.GetProperty("usage").GetProperty("prompt_tokens").GetInt32(); var outputTokens = document.RootElement.GetProperty("usage").GetProperty("completion_tokens").GetInt32(); var sum = inputTokens + outputTokens; - Console.WriteLine($"USAGE STATS - Tokens: {inputTokens.ToString()} + {outputTokens.ToString()} = {sum.ToString()}"); + + _logger.Info($"USAGE STATS - Tokens: {inputTokens.ToString()} + {outputTokens.ToString()} = {sum.ToString()}"); return document.RootElement .GetProperty("choices")[0] @@ -95,24 +96,24 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services public async Task GetStreamedResponseAsync(string sessionId, string systemMessage, string userMessage, string? assistantMessage = null) { - string modelName = GetModelName(); + var modelName = GetModelName(); var requestBody = new CerebrasAIChatRequest { Model = modelName, Temperature = 0.2, Messages = assistantMessage == null - ? new[] - { + ? + [ new AIChatMessage { Role = "system", Content = systemMessage }, new AIChatMessage { Role = "user", Content = userMessage } - } - : new[] - { + ] + : + [ new AIChatMessage { Role = "system", Content = systemMessage }, new AIChatMessage { Role = "assistant", Content = assistantMessage }, new AIChatMessage { Role = "user", Content = userMessage } - }, + ], Stream = true }; @@ -122,16 +123,14 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services }); var requestContent = new StringContent(requestJson, Encoding.UTF8, "application/json"); - using var httpRequest = new HttpRequestMessage(HttpMethod.Post, CerebrasEndpoint) - { - Content = requestContent - }; + using var httpRequest = new HttpRequestMessage(HttpMethod.Post, CerebrasEndpoint); + httpRequest.Content = requestContent; using var response = await _httpClient.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead); response.EnsureSuccessStatusCode(); var stringBuilder = new StringBuilder(); - using var responseStream = await response.Content.ReadAsStreamAsync(); + await using var responseStream = await response.Content.ReadAsStreamAsync(); using var reader = new StreamReader(responseStream); try diff --git a/Nop.Plugin.Misc.AIPlugin/Services/CustomPriceCalculationService.cs b/Nop.Plugin.Misc.AIPlugin/Services/CustomPriceCalculationService.cs index 7f12a25..a6487d5 100644 --- a/Nop.Plugin.Misc.AIPlugin/Services/CustomPriceCalculationService.cs +++ b/Nop.Plugin.Misc.AIPlugin/Services/CustomPriceCalculationService.cs @@ -53,8 +53,6 @@ public class CustomPriceCalculationService : PriceCalculationService } - //decimal? overriddenProductPrice = null - public override async Task<(decimal priceWithoutDiscounts, decimal finalPrice, decimal appliedDiscountAmount, List appliedDiscounts)> GetFinalPriceAsync( Product product, Customer customer, Store store, decimal? overriddenProductPrice, decimal additionalCharge = 0, bool includeDiscounts = true, int quantity = 1, DateTime? rentalStartDate = null, DateTime? rentalEndDate = null) diff --git a/Nop.Plugin.Misc.AIPlugin/Services/LockService.cs b/Nop.Plugin.Misc.AIPlugin/Services/LockService.cs index 787a38f..e725781 100644 --- a/Nop.Plugin.Misc.AIPlugin/Services/LockService.cs +++ b/Nop.Plugin.Misc.AIPlugin/Services/LockService.cs @@ -2,7 +2,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services; -public class LockService : MgLockService, ILockService +public class LockService : MgLockServiceBase, ILockService { public LockService() : this(new SemaphoreSlim(1)) {} diff --git a/Nop.Plugin.Misc.AIPlugin/Services/OpenAIApiService.cs b/Nop.Plugin.Misc.AIPlugin/Services/OpenAIApiService.cs index 03c6596..e94d49f 100644 --- a/Nop.Plugin.Misc.AIPlugin/Services/OpenAIApiService.cs +++ b/Nop.Plugin.Misc.AIPlugin/Services/OpenAIApiService.cs @@ -1,12 +1,11 @@ -using Microsoft.Extensions.Configuration; -using Nop.Plugin.Misc.FruitBankPlugin.Models; -using Nop.Plugin.Misc.FruitBankPlugin.Services; +using Nop.Plugin.Misc.FruitBankPlugin.Models; using Nop.Services.Configuration; -using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Text.Json; +using AyCode.Utils.Extensions; +#nullable enable namespace Nop.Plugin.Misc.FruitBankPlugin.Services { public class OpenAIApiService : IAIAPIService @@ -45,29 +44,30 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services } #region === CHAT (TEXT INPUT) === + public async Task GetSimpleResponseAsync(string systemMessage, string userMessage, string? assistantMessage = null) { - string modelName = GetModelName(); - StringContent requestContent = new(""); + var modelName = GetModelName(); + StringContent requestContent; - if (modelName == "gpt-4.1-mini" || modelName == "gpt-4o-mini" || modelName == "gpt-4.1-nano" || modelName == "gpt-5-nano") + if (modelName is "gpt-4.1-mini" or "gpt-4o-mini" or "gpt-4.1-nano" or "gpt-5-nano") { var requestBody = new OpenAIGpt4MiniAIChatRequest { Model = modelName, Temperature = 0.2, - Messages = assistantMessage == null || assistantMessage == string.Empty - ? new[] - { + Messages = string.IsNullOrEmpty(assistantMessage) + ? + [ new AIChatMessage { Role = "system", Content = systemMessage }, new AIChatMessage { Role = "user", Content = userMessage } - } - : new[] - { + ] + : + [ new AIChatMessage { Role = "system", Content = systemMessage }, new AIChatMessage { Role = "assistant", Content = assistantMessage }, new AIChatMessage { Role = "user", Content = userMessage } - }, + ], Stream = false }; @@ -84,18 +84,18 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services { Model = modelName, Temperature = 1, - Messages = assistantMessage == null || assistantMessage == string.Empty - ? new[] - { + Messages = string.IsNullOrEmpty(assistantMessage) + ? + [ new AIChatMessage { Role = "system", Content = systemMessage }, new AIChatMessage { Role = "user", Content = userMessage } - } - : new[] - { + ] + : + [ new AIChatMessage { Role = "system", Content = systemMessage }, new AIChatMessage { Role = "assistant", Content = assistantMessage }, new AIChatMessage { Role = "user", Content = userMessage } - }, + ], ReasoningEffort = "minimal", Verbosity = "high", Stream = false @@ -134,12 +134,14 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services return null; } } + #endregion #region === CHAT (STREAMING) === + public async Task GetStreamedResponseAsync(string sessionId, string systemMessage, string userMessage, string? assistantMessage = null) { - string modelName = GetModelName(); + var modelName = GetModelName(); StringContent requestContent = new(""); if (modelName == "gpt-4.1-mini" || modelName == "gpt-4o-mini" || modelName == "gpt-4.1-nano" || modelName == "gpt-5-nano") @@ -148,18 +150,18 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services { Model = modelName, Temperature = 0.2, - Messages = assistantMessage == null || assistantMessage == string.Empty - ? new[] - { + Messages = string.IsNullOrEmpty(assistantMessage) + ? + [ new AIChatMessage { Role = "system", Content = systemMessage }, new AIChatMessage { Role = "user", Content = userMessage } - } - : new[] - { + ] + : + [ new AIChatMessage { Role = "system", Content = systemMessage }, new AIChatMessage { Role = "assistant", Content = assistantMessage }, new AIChatMessage { Role = "user", Content = userMessage } - }, + ], Stream = true }; @@ -176,18 +178,18 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services { Model = modelName, Temperature = 1, - Messages = assistantMessage == null || assistantMessage == string.Empty - ? new[] - { + Messages = string.IsNullOrEmpty(assistantMessage) + ? + [ new AIChatMessage { Role = "system", Content = systemMessage }, new AIChatMessage { Role = "user", Content = userMessage } - } - : new[] - { + ] + : + [ new AIChatMessage { Role = "system", Content = systemMessage }, new AIChatMessage { Role = "assistant", Content = assistantMessage }, new AIChatMessage { Role = "user", Content = userMessage } - }, + ], Stream = true }; @@ -200,7 +202,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services } using var httpRequest = new HttpRequestMessage(HttpMethod.Post, OpenAiEndpoint); - httpRequest.Content = requestContent; + httpRequest.Content = new StringContent(""); using var response = await _httpClient.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead); response.EnsureSuccessStatusCode(); @@ -216,7 +218,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services var line = await reader.ReadLineAsync(); if (string.IsNullOrWhiteSpace(line) || !line.StartsWith("data: ")) continue; - var jsonResponse = line.Substring(6); + var jsonResponse = line[6..]; if (jsonResponse == "[DONE]") { @@ -259,9 +261,11 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services return stringBuilder.ToString(); } + #endregion #region === IMAGE GENERATION === + public async Task GenerateImageAsync(string prompt) { var request = new HttpRequestMessage(HttpMethod.Post, OpenAiImageEndpoint); @@ -294,6 +298,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services return $"data:image/png;base64,{base64Image}"; } + #endregion #region === PDF ANALYSIS (NEW) === @@ -301,16 +306,10 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services private async Task EnsureAssistantAndVectorStoreAsync() { // Find or create vector store - if (_vectorStoreId == null) - { - _vectorStoreId = await FindOrCreateVectorStoreAsync("pdf-analysis-store"); - } + _vectorStoreId ??= await FindOrCreateVectorStoreAsync("pdf-analysis-store"); // Find or create assistant - if (_assistantId == null) - { - _assistantId = await FindOrCreateAssistantAsync("PDF and Image Analyzer Assistant"); - } + _assistantId ??= await FindOrCreateAssistantAsync("PDF and Image Analyzer Assistant"); } //TEMPORARY: Cleanup all assistants (for testing purposes) - A. @@ -348,7 +347,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services public async Task AnalyzePdfAsync(Stream file, string fileName, string userPrompt) { - + await EnsureAssistantAndVectorStoreAsync(); var fileId = await UploadFileAsync(file, fileName); var isImage = IsImageFile(fileName); @@ -361,7 +360,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services var threadId = await CreateThreadAsync(); if (isImage) - { + { await AddUserMessageWithImageAsync(threadId, userPrompt, fileId); } else @@ -375,7 +374,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services return await GetAssistantResponseAsync(threadId); } - private bool IsImageFile(string fileName) + private static bool IsImageFile(string fileName) { var extension = Path.GetExtension(fileName).ToLowerInvariant(); return extension == ".jpg" || extension == ".jpeg" || extension == ".png" || extension == ".gif" || extension == ".webp"; @@ -461,8 +460,8 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services role = "user", content = new object[] { - new { type = "text", text = userPrompt }, - new { type = "image_file", image_file = new { file_id = fileId } } + new { type = "text", text = userPrompt }, + new { type = "image_file", image_file = new { file_id = fileId } } } }; @@ -507,7 +506,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services { const int pollIntervalMs = 1000; const int maxAttempts = 60; // 1 minute timeout - int attempts = 0; + var attempts = 0; while (attempts < maxAttempts) { @@ -560,7 +559,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services return firstMessage ?? "No response"; } - private HttpRequestMessage CreateAssistantRequest(HttpMethod method, string url, object? body = null) + private static HttpRequestMessage CreateAssistantRequest(HttpMethod method, string url, object? body = null) { var request = new HttpRequestMessage(method, url); request.Headers.Add("OpenAI-Beta", "assistants=v2"); @@ -577,7 +576,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services return request; } - private async Task EnsureSuccessAsync(HttpResponseMessage response, string operation) + private static async Task EnsureSuccessAsync(HttpResponseMessage response, string operation) { if (!response.IsSuccessStatusCode) { @@ -588,8 +587,6 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services } } - - private async Task FindOrCreateVectorStoreAsync(string name) { // List existing vector stores @@ -602,12 +599,12 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services using var json = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync()); var stores = json.RootElement.GetProperty("data"); - foreach (var store in stores.EnumerateArray()) + //var getString = stores.EnumerateArray().FirstOrDefault(store => store.GetProperty("name").GetString() == name)?.GetProperty("id").GetString(); + //if (!getString.IsNullOrWhiteSpace()) return getString; + + foreach (var store in stores.EnumerateArray().Where(store => store.GetProperty("name").GetString() == name)) { - if (store.GetProperty("name").GetString() == name) - { - return store.GetProperty("id").GetString()!; - } + return store.GetProperty("id").GetString()!; } } @@ -633,12 +630,9 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services using var json = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync()); var assistants = json.RootElement.GetProperty("data"); - foreach (var assistant in assistants.EnumerateArray()) + foreach (var assistant in assistants.EnumerateArray().Where(assistant => assistant.GetProperty("name").GetString() == name)) { - if (assistant.GetProperty("name").GetString() == name) - { - return assistant.GetProperty("id").GetString()!; - } + return assistant.GetProperty("id").GetString()!; } } @@ -659,9 +653,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services return createJson.RootElement.GetProperty("id").GetString()!; } - #endregion - + #endregion } - }