-
+
@@ -342,11 +341,18 @@
};
gridModel.ColumnCollection.Add(new ColumnProperty(nameof(OrderModelExtended.IsMeasurable))
{
- Title = "Needs Measurement",
+ Title = T("Admin.Orders.Fields.ToBeMeasured").Text,
Width = "100",
Render = new RenderCustom("renderColumnIsMeasurable"),
ClassName = NopColumnClassDefaults.CenterAll
});
+ gridModel.ColumnCollection.Add(new ColumnProperty(nameof(IOrderDto.DateOfReceipt))
+ {
+ Title = T("Admin.Orders.Fields.PickupDate").Text,
+ Width = "100",
+ Render = new RenderCustom("renderColumnPickupDateAndTime"),
+ ClassName = NopColumnClassDefaults.CenterAll
+ });
//a vendor does not have access to this functionality
if (!Model.IsLoggedInAsVendor)
@@ -443,9 +449,14 @@
function renderColumnIsMeasurable(data, type, row, meta) {
if(data === true) {
- return '
Yes';
+ return '
Yes';
}
- return '
No';
+ return '
No';
+ }
+
+ function renderColumnPickupDateAndTime(data, type, row, meta) {
+
+ return `
${data}`;
}
$(function() {
diff --git a/Nop.Plugin.Misc.AIPlugin/Components/OrderAttributesViewComponent.cs b/Nop.Plugin.Misc.AIPlugin/Components/OrderAttributesViewComponent.cs
new file mode 100644
index 0000000..6cc2ddc
--- /dev/null
+++ b/Nop.Plugin.Misc.AIPlugin/Components/OrderAttributesViewComponent.cs
@@ -0,0 +1,55 @@
+// File: Plugins/YourCompany.ProductAttributes/Components/ProductAttributesViewComponent.cs
+
+using FruitBank.Common.Interfaces;
+using Microsoft.AspNetCore.Mvc;
+using Nop.Core;
+using Nop.Core.Domain.Catalog;
+using Nop.Core.Domain.Orders;
+using Nop.Plugin.Misc.FruitBankPlugin.Models;
+using Nop.Plugin.Misc.FruitBankPlugin.Services;
+using Nop.Services.Common;
+using Nop.Web.Areas.Admin.Models.Catalog;
+using Nop.Web.Areas.Admin.Models.Orders;
+using Nop.Web.Framework.Components;
+
+namespace Nop.Plugin.Misc.FruitBankPlugin.Components
+{
+ [ViewComponent(Name = "OrderAttributes")]
+ public class OrderAttributesViewComponent : NopViewComponent
+ {
+ private readonly FruitBankAttributeService _fruitBankAttributeService;
+ private readonly IWorkContext _workContext;
+ private readonly IStoreContext _storeContext;
+
+ public OrderAttributesViewComponent(FruitBankAttributeService fruitBankAttributeService, IWorkContext workContext, IStoreContext storeContext)
+ {
+ _workContext = workContext;
+ _storeContext = storeContext;
+ _fruitBankAttributeService = fruitBankAttributeService;
+ }
+
+ public async Task
InvokeAsync(string widgetZone, object additionalData)
+ {
+ if (additionalData is not OrderModel orderModel) return Content("");
+
+ var model = new OrderAttributesModel { OrderId = orderModel.Id };
+
+ if (model.OrderId > 0)
+ {
+ var orderPickupAttributeValue = await _fruitBankAttributeService.GetGenericAttributeValueAsync(model.OrderId, nameof(IOrderDto.DateOfReceipt));
+ var orderMeasurableAttributeValue = await _fruitBankAttributeService.GetGenericAttributeValueAsync(model.OrderId, nameof(OrderModelExtended.IsMeasurable));
+
+ model.IsMeasurable = orderMeasurableAttributeValue;
+ if(orderPickupAttributeValue.HasValue && orderPickupAttributeValue.Value != DateTime.MinValue)
+ {
+ model.DateOfReceipt = orderPickupAttributeValue;
+ }
+ else
+ {
+ model.DateOfReceipt = null;
+ }
+ }
+ return View("~/Plugins/Misc.FruitBankPlugin/Views/OrderAttributes.cshtml", model);
+ }
+ }
+}
diff --git a/Nop.Plugin.Misc.AIPlugin/Factories/CustomOrderModelFactory.cs b/Nop.Plugin.Misc.AIPlugin/Factories/CustomOrderModelFactory.cs
index 3783369..f313eb8 100644
--- a/Nop.Plugin.Misc.AIPlugin/Factories/CustomOrderModelFactory.cs
+++ b/Nop.Plugin.Misc.AIPlugin/Factories/CustomOrderModelFactory.cs
@@ -1,4 +1,5 @@
using AyCode.Core.Extensions;
+using FruitBank.Common.Interfaces;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.TagHelpers;
@@ -41,6 +42,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Factories
public class CustomOrderModelFactory : OrderModelFactory
{
private readonly IOrderMeasurementService _orderMeasurementService;
+ private readonly IGenericAttributeService _genericAttributeService;
public CustomOrderModelFactory(
IOrderMeasurementService orderMeasurementService,
@@ -88,36 +90,37 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Factories
OrderSettings orderSettings,
ShippingSettings shippingSettings,
IUrlRecordService urlRecordService,
- TaxSettings taxSettings
- ) : base(addressSettings,
- catalogSettings,
- currencySettings,
- actionContextAccessor,
- addressModelFactory,
- addressService,
- affiliateService,
- baseAdminModelFactory,
- countryService,
- currencyService,
- customerService,
- dateTimeHelper,
- discountService,
- downloadService,
- encryptionService,
- giftCardService,
- localizationService,
- measureService,
- orderProcessingService,
- orderReportService,
- orderService,
- paymentPluginManager,
- paymentService,
- pictureService,
- priceCalculationService,
- priceFormatter,
- productAttributeService,
- productService,
- returnRequestService,
+ TaxSettings taxSettings,
+ IGenericAttributeService genericAttributeService
+ ) : base(addressSettings,
+ catalogSettings,
+ currencySettings,
+ actionContextAccessor,
+ addressModelFactory,
+ addressService,
+ affiliateService,
+ baseAdminModelFactory,
+ countryService,
+ currencyService,
+ customerService,
+ dateTimeHelper,
+ discountService,
+ downloadService,
+ encryptionService,
+ giftCardService,
+ localizationService,
+ measureService,
+ orderProcessingService,
+ orderReportService,
+ orderService,
+ paymentPluginManager,
+ paymentService,
+ pictureService,
+ priceCalculationService,
+ priceFormatter,
+ productAttributeService,
+ productService,
+ returnRequestService,
rewardPointService,
settingService,
shipmentService,
@@ -137,6 +140,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Factories
)
{
_orderMeasurementService = orderMeasurementService;
+ _genericAttributeService = genericAttributeService;
}
public override async Task PrepareOrderSearchModelAsync(OrderSearchModel searchModel)
@@ -169,7 +173,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Factories
}
public override async Task PrepareOrderListModelAsync(OrderSearchModel searchModel)
- =>await base.PrepareOrderListModelAsync(searchModel);
+ => await base.PrepareOrderListModelAsync(searchModel);
public async Task PrepareOrderListModelExtendedAsync(OrderSearchModel searchModel)
{
@@ -182,6 +186,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Factories
PropertyHelper.CopyPublicValueTypeProperties(orderModel, orderModelExtended);
orderModelExtended.IsMeasurable = await ShouldMarkAsNeedsMeasurementAsync(orderModel);
+ orderModelExtended.DateOfReceipt = await GetPickupDateTimeAsync(orderModel);
Console.WriteLine(orderModelExtended.Id);
extendedRows.Add(orderModelExtended);
@@ -210,7 +215,20 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Factories
return await Task.FromResult(false);
}
-
+ private async Task GetPickupDateTimeAsync(OrderModel order)
+ {
+ DateTime? dateTime = DateTime.MinValue;
+ var fullOrder = await _orderService.GetOrderByIdAsync(order.Id);
+ if (fullOrder != null)
+ {
+ dateTime = await _genericAttributeService.GetAttributeAsync(fullOrder, nameof(IOrderDto.DateOfReceipt));
+ if(dateTime == DateTime.MinValue || !dateTime.HasValue)
+ {
+ dateTime = null;
+ }
+ }
+ return dateTime;
+ }
}
}
diff --git a/Nop.Plugin.Misc.AIPlugin/FruitBankPlugin.cs b/Nop.Plugin.Misc.AIPlugin/FruitBankPlugin.cs
index afaeca5..4d5f0dd 100644
--- a/Nop.Plugin.Misc.AIPlugin/FruitBankPlugin.cs
+++ b/Nop.Plugin.Misc.AIPlugin/FruitBankPlugin.cs
@@ -84,7 +84,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin
public Task> GetWidgetZonesAsync()
{
- return Task.FromResult>(new List { PublicWidgetZones.ProductBoxAddinfoBefore, PublicWidgetZones.ProductDetailsBottom, AdminWidgetZones.ProductDetailsBlock });
+ return Task.FromResult>(new List { PublicWidgetZones.ProductBoxAddinfoBefore, PublicWidgetZones.ProductDetailsBottom, AdminWidgetZones.ProductDetailsBlock, AdminWidgetZones.OrderDetailsBlock });
}
//public string GetWidgetViewComponentName(string widgetZone)
@@ -138,6 +138,11 @@ namespace Nop.Plugin.Misc.FruitBankPlugin
{
return zones.Any(widgetZone.Equals) ? typeof(ProductAttributesViewComponent) : null;
}
+
+ else if (widgetZone == AdminWidgetZones.OrderDetailsBlock)
+ {
+ return zones.Any(widgetZone.Equals) ? typeof(OrderAttributesViewComponent) : null;
+ }
}
return null;
diff --git a/Nop.Plugin.Misc.AIPlugin/Infrastructure/RouteProvider.cs b/Nop.Plugin.Misc.AIPlugin/Infrastructure/RouteProvider.cs
index ae953a6..c37686b 100644
--- a/Nop.Plugin.Misc.AIPlugin/Infrastructure/RouteProvider.cs
+++ b/Nop.Plugin.Misc.AIPlugin/Infrastructure/RouteProvider.cs
@@ -126,6 +126,11 @@ public class RouteProvider : IRouteProvider
name: "Plugin.FruitBank.Admin.Products.ProductList",
pattern: "Admin/Product/ProductList",
defaults: new { controller = "CustomProduct", action = "ProductList", area = AreaNames.ADMIN });
+
+ endpointRouteBuilder.MapControllerRoute(
+ name: "Plugin.FruitBank.Admin.Orders.SaveOrderAttributes",
+ pattern: "Admin/CustomOrder/SaveOrderAttributes",
+ defaults: new { controller = "CustomOrder", action = "SaveOrderAttributes", area = AreaNames.ADMIN });
}
///
diff --git a/Nop.Plugin.Misc.AIPlugin/Models/OrderAttributesModel.cs b/Nop.Plugin.Misc.AIPlugin/Models/OrderAttributesModel.cs
new file mode 100644
index 0000000..080e9f6
--- /dev/null
+++ b/Nop.Plugin.Misc.AIPlugin/Models/OrderAttributesModel.cs
@@ -0,0 +1,18 @@
+using FruitBank.Common.Interfaces;
+using Nop.Web.Framework.Models;
+using Nop.Web.Framework.Mvc.ModelBinding;
+
+namespace Nop.Plugin.Misc.FruitBankPlugin.Models
+{
+ public record OrderAttributesModel : BaseNopModel, IMeasurable
+ {
+ public int OrderId { get; set; }
+
+ [NopResourceDisplayName("Plugins.YourCompany.ProductAttributes.Fields.IsMeasurable")]
+ public bool IsMeasurable { get; set; }
+
+ [NopResourceDisplayName("Plugins.YourCompany.ProductAttributes.Fields.DateOfReceipt")]
+ public DateTime? DateOfReceipt { get; set; }
+
+ }
+}
\ No newline at end of file
diff --git a/Nop.Plugin.Misc.AIPlugin/Models/OrderModelExtended.cs b/Nop.Plugin.Misc.AIPlugin/Models/OrderModelExtended.cs
index 5d6c936..8687e20 100644
--- a/Nop.Plugin.Misc.AIPlugin/Models/OrderModelExtended.cs
+++ b/Nop.Plugin.Misc.AIPlugin/Models/OrderModelExtended.cs
@@ -5,6 +5,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Models
public partial record OrderModelExtended : OrderModel
{
public bool IsMeasurable { get; set; }
+ public DateTime? DateOfReceipt { get; set; }
}
}
diff --git a/Nop.Plugin.Misc.AIPlugin/Nop.Plugin.Misc.FruitBankPlugin.csproj b/Nop.Plugin.Misc.AIPlugin/Nop.Plugin.Misc.FruitBankPlugin.csproj
index c76f8b8..ac55a51 100644
--- a/Nop.Plugin.Misc.AIPlugin/Nop.Plugin.Misc.FruitBankPlugin.csproj
+++ b/Nop.Plugin.Misc.AIPlugin/Nop.Plugin.Misc.FruitBankPlugin.csproj
@@ -7,6 +7,9 @@
true
true
+
+
+
@@ -28,7 +31,10 @@
+
+
+
@@ -151,9 +157,6 @@
-
- Always
-
Always
@@ -616,6 +619,9 @@
Always
+
+ Always
+
Always
@@ -631,6 +637,10 @@
+
+
+
+
diff --git a/Nop.Plugin.Misc.AIPlugin/Services/AICalculationService.cs b/Nop.Plugin.Misc.AIPlugin/Services/AICalculationService.cs
index e90d7c1..f5e65f8 100644
--- a/Nop.Plugin.Misc.AIPlugin/Services/AICalculationService.cs
+++ b/Nop.Plugin.Misc.AIPlugin/Services/AICalculationService.cs
@@ -1,14 +1,6 @@
-using Azure;
-using Nop.Core;
+using Nop.Core;
using Nop.Core.Domain.Customers;
-using Nop.Core.Domain.Stores;
using Nop.Plugin.Misc.FruitBankPlugin.Helpers;
-using StackExchange.Redis;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
namespace Nop.Plugin.Misc.FruitBankPlugin.Services
{
@@ -37,16 +29,6 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services
return response;
}
- //public async Task GetOpenAIPDFAnalyzisFromText(string pdfText, string userQuestion)
- //{
- // string systemMessage = $"You are a helpful assistant of a webshop, you work in the administration area, with the ADMIN user. The ADMIN 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);
-
- // var fixedResponse = TextHelper.FixJsonWithoutAI(response);
-
- // return fixedResponse;
- //}
-
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}";
@@ -57,15 +39,5 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services
return fixedResponse;
}
- //public async Task GetOpenAIPartnerInfoFromText(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 response = await _openAIApiService.GetSimpleResponseAsync(systemMessage, userQuestion);
-
- // var fixedResponse = TextHelper.FixJsonWithoutAI(response);
-
- // return fixedResponse;
- //}
-
}
}
diff --git a/Nop.Plugin.Misc.AIPlugin/Services/InnvoiceApiService.cs b/Nop.Plugin.Misc.AIPlugin/Services/InnvoiceApiService.cs
new file mode 100644
index 0000000..ad3a08b
--- /dev/null
+++ b/Nop.Plugin.Misc.AIPlugin/Services/InnvoiceApiService.cs
@@ -0,0 +1,496 @@
+using System;
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Threading.Tasks;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Xml.Linq;
+using System.Linq;
+using System.Text;
+
+namespace Nop.Plugin.Misc.FruitBankPlugin.Services
+{
+ ///
+ /// Service for interacting with InnVoice Invoice API
+ /// API Documentation: https://help.innvoice.hu/hc/hu/articles/360003142839
+ ///
+ public class InnVoiceApiService
+ {
+ private readonly HttpClient _httpClient;
+ private readonly string _companyName;
+ private readonly string _username;
+ private readonly string _password;
+ private readonly string _baseUrl;
+
+ public InnVoiceApiService(string companyName, string username, string password, string baseUrl = "https://api.innvoice.hu")
+ {
+ _companyName = companyName ?? throw new ArgumentNullException(nameof(companyName));
+ _username = username ?? throw new ArgumentNullException(nameof(username));
+ _password = password ?? throw new ArgumentNullException(nameof(password));
+ _baseUrl = baseUrl.TrimEnd('/');
+
+ _httpClient = new HttpClient();
+ SetupAuthentication();
+ }
+
+ private void SetupAuthentication()
+ {
+ var authToken = Convert.ToBase64String(
+ System.Text.Encoding.ASCII.GetBytes($"{_username}:{_password}")
+ );
+ _httpClient.DefaultRequestHeaders.Authorization =
+ new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", authToken);
+ }
+
+ ///
+ /// Get all invoices
+ /// Rate limit: 20 times per hour without ID parameter
+ ///
+ public async Task> GetAllInvoicesAsync()
+ {
+ var url = $"{_baseUrl}/{_companyName}/invoice";
+ return await GetInvoicesFromUrlAsync(url);
+ }
+
+ ///
+ /// Get invoice by internal table ID
+ ///
+ public async Task GetInvoiceByIdAsync(int tableId)
+ {
+ var url = $"{_baseUrl}/{_companyName}/invoice/id/{tableId}";
+ var invoices = await GetInvoicesFromUrlAsync(url);
+ return invoices.Count > 0 ? invoices[0] : null;
+ }
+
+ ///
+ /// Get invoice by invoice number
+ ///
+ public async Task> GetInvoiceByNumberAsync(string invoiceNumber)
+ {
+ var url = $"{_baseUrl}/{_companyName}/invoice/invoicenumber/{Uri.EscapeDataString(invoiceNumber)}";
+ return await GetInvoicesFromUrlAsync(url);
+ }
+
+ ///
+ /// Get invoices by creation date
+ /// Format: YYYYMMDD
+ ///
+ public async Task> GetInvoicesByCreationDateAsync(DateTime date)
+ {
+ var dateStr = date.ToString("yyyyMMdd");
+ var url = $"{_baseUrl}/{_companyName}/invoice/created/{dateStr}";
+ return await GetInvoicesFromUrlAsync(url);
+ }
+
+ ///
+ /// Get invoices by fulfillment date
+ /// Format: YYYYMMDD
+ ///
+ public async Task> GetInvoicesByFulfillmentDateAsync(DateTime date)
+ {
+ var dateStr = date.ToString("yyyyMMdd");
+ var url = $"{_baseUrl}/{_companyName}/invoice/fulfillment/{dateStr}";
+ return await GetInvoicesFromUrlAsync(url);
+ }
+
+ ///
+ /// Get invoices by due date
+ /// Format: YYYYMMDD
+ ///
+ public async Task> GetInvoicesByDueDateAsync(DateTime date)
+ {
+ var dateStr = date.ToString("yyyyMMdd");
+ var url = $"{_baseUrl}/{_companyName}/invoice/duedate/{dateStr}";
+ return await GetInvoicesFromUrlAsync(url);
+ }
+
+ ///
+ /// Get invoices by payment date
+ /// Format: YYYYMMDD
+ ///
+ public async Task> GetInvoicesByPaymentDateAsync(DateTime date)
+ {
+ var dateStr = date.ToString("yyyyMMdd");
+ var url = $"{_baseUrl}/{_companyName}/invoice/paymentdate/{dateStr}";
+ return await GetInvoicesFromUrlAsync(url);
+ }
+
+ ///
+ /// Get invoices by customer tax number
+ ///
+ public async Task> GetInvoicesByCustomerTaxNumberAsync(string taxNumber)
+ {
+ var url = $"{_baseUrl}/{_companyName}/invoice/taxnumber/{Uri.EscapeDataString(taxNumber)}";
+ return await GetInvoicesFromUrlAsync(url);
+ }
+
+ ///
+ /// Get invoices modified since a specific timestamp
+ /// Format: YYYYMMDDHHmmss (year, month, day, hour, minute, second)
+ /// Recommended for tracking changes every 10 minutes
+ /// Rate limit: Full queries or queries older than current month limited to 10 times per 30 days
+ /// Recommended: Only use current month dates
+ ///
+ public async Task> GetInvoicesByUpdateTimeAsync(DateTime updateTime)
+ {
+ var timeStr = updateTime.ToString("yyyyMMddHHmmss");
+ var url = $"{_baseUrl}/{_companyName}/invoice/updatedtime/{timeStr}";
+ return await GetInvoicesFromUrlAsync(url);
+ }
+
+ private async Task> GetInvoicesFromUrlAsync(string url)
+ {
+ try
+ {
+ var response = await _httpClient.GetAsync(url);
+ response.EnsureSuccessStatusCode();
+
+ var content = await response.Content.ReadAsStringAsync();
+ var invoices = JsonSerializer.Deserialize>(content, new JsonSerializerOptions
+ {
+ PropertyNameCaseInsensitive = true
+ });
+
+ return invoices ?? new List();
+ }
+ catch (HttpRequestException ex)
+ {
+ throw new InnVoiceApiException($"Error calling InnVoice API: {ex.Message}", ex);
+ }
+ catch (JsonException ex)
+ {
+ throw new InnVoiceApiException($"Error parsing API response: {ex.Message}", ex);
+ }
+ }
+ }
+
+ // Models
+ public class Invoice
+ {
+ [JsonPropertyName("TABLE_ID")]
+ public int TableId { get; set; }
+
+ [JsonPropertyName("InvoiceNumber")]
+ public string InvoiceNumber { get; set; }
+
+ [JsonPropertyName("Created")]
+ public string Created { get; set; }
+
+ [JsonPropertyName("Fulfillment")]
+ public string Fulfillment { get; set; }
+
+ [JsonPropertyName("DueDate")]
+ public string DueDate { get; set; }
+
+ [JsonPropertyName("PaymentDate")]
+ public string PaymentDate { get; set; }
+
+ [JsonPropertyName("CustomerName")]
+ public string CustomerName { get; set; }
+
+ [JsonPropertyName("CustomerTaxNumber")]
+ public string CustomerTaxNumber { get; set; }
+
+ [JsonPropertyName("CustomerAddress")]
+ public string CustomerAddress { get; set; }
+
+ [JsonPropertyName("TotalNet")]
+ public decimal TotalNet { get; set; }
+
+ [JsonPropertyName("TotalGross")]
+ public decimal TotalGross { get; set; }
+
+ [JsonPropertyName("Currency")]
+ public string Currency { get; set; }
+
+ [JsonPropertyName("Status")]
+ public string Status { get; set; }
+
+ [JsonPropertyName("InvoiceType")]
+ public string InvoiceType { get; set; }
+
+ [JsonPropertyName("PaymentMethod")]
+ public string PaymentMethod { get; set; }
+
+ // Add more properties as needed based on actual API response
+ }
+
+ public class InnVoiceApiException : Exception
+ {
+ public InnVoiceApiException(string message) : base(message) { }
+ public InnVoiceApiException(string message, Exception innerException) : base(message, innerException) { }
+ }
+
+ // Invoice Creation Models
+ public class InvoiceCreateRequest
+ {
+ public int VevoID { get; set; } = 0;
+ public string VevoNev { get; set; }
+ public string VevoIrsz { get; set; }
+ public string VevoOrszag { get; set; }
+ public string VevoTelep { get; set; }
+ public string VevoUtcaHsz { get; set; }
+ public string VevoEPNev { get; set; }
+ public string VevoEPKod { get; set; }
+ public string SzallNev { get; set; }
+ public string SzallIrsz { get; set; }
+ public string SzallTelep { get; set; }
+ public string SzallUtcaHsz { get; set; }
+ public string SzallOrszag { get; set; }
+ public int SzamlatombID { get; set; }
+ public DateTime SzamlaKelte { get; set; }
+ public DateTime TeljesitesKelte { get; set; }
+ public DateTime Hatarido { get; set; }
+ public string Devizanem { get; set; }
+ public string FizetesiMod { get; set; }
+ public string Megjegyzes { get; set; }
+ public string Nyelv1 { get; set; }
+ public string Nyelv2 { get; set; }
+ public decimal? Arfolyam { get; set; }
+ public string ArfolyamDeviza { get; set; }
+ public bool Fizetve { get; set; }
+ public bool Eszamla { get; set; }
+ public string VevoAdoszam { get; set; }
+ public string VevoCsAdoszam { get; set; }
+ public string Telefon { get; set; }
+ public string Email { get; set; }
+ public string MegrendelesSzamStr { get; set; }
+ public string MegrendelesIdopontStr { get; set; }
+ public bool Felretett { get; set; }
+ public bool Proforma { get; set; }
+ public bool AutomatikusAr { get; set; }
+ public bool Eloleg { get; set; }
+ public bool Sendmail { get; set; }
+ public string MailSubject { get; set; }
+ public string MailBody { get; set; }
+ public string Eredetiszamla { get; set; }
+
+ public List Items { get; set; } = new List();
+
+ public void AddItem(InvoiceItem item)
+ {
+ Items.Add(item);
+ }
+
+ public string ToXml()
+ {
+ var invoices = new XElement("invoices");
+ var invoice = new XElement("invoice");
+
+ if (VevoID > 0)
+ invoice.Add(new XElement("VevoID", new XCData(VevoID.ToString())));
+
+ invoice.Add(new XElement("VevoNev", new XCData(VevoNev ?? "")));
+ invoice.Add(new XElement("VevoIrsz", new XCData(VevoIrsz ?? "")));
+ invoice.Add(new XElement("VevoTelep", new XCData(VevoTelep ?? "")));
+ invoice.Add(new XElement("VevoOrszag", new XCData(VevoOrszag ?? "")));
+ invoice.Add(new XElement("VevoUtcaHsz", new XCData(VevoUtcaHsz ?? "")));
+
+ if (!string.IsNullOrEmpty(VevoEPNev))
+ invoice.Add(new XElement("VevoEPNev", new XCData(VevoEPNev)));
+ if (!string.IsNullOrEmpty(VevoEPKod))
+ invoice.Add(new XElement("VevoEPKod", new XCData(VevoEPKod)));
+ if (!string.IsNullOrEmpty(SzallNev))
+ invoice.Add(new XElement("SzallNev", new XCData(SzallNev)));
+ if (!string.IsNullOrEmpty(SzallIrsz))
+ invoice.Add(new XElement("SzallIrsz", new XCData(SzallIrsz)));
+ if (!string.IsNullOrEmpty(SzallTelep))
+ invoice.Add(new XElement("SzallTelep", new XCData(SzallTelep)));
+ if (!string.IsNullOrEmpty(SzallUtcaHsz))
+ invoice.Add(new XElement("SzallUtcaHsz", new XCData(SzallUtcaHsz)));
+ if (!string.IsNullOrEmpty(SzallOrszag))
+ invoice.Add(new XElement("SzallOrszag", new XCData(SzallOrszag)));
+
+ invoice.Add(new XElement("SzamlatombID", new XCData(SzamlatombID.ToString())));
+ invoice.Add(new XElement("SzamlaKelte", new XCData(SzamlaKelte.ToString("yyyy.MM.dd."))));
+ invoice.Add(new XElement("TeljesitesKelte", new XCData(TeljesitesKelte.ToString("yyyy.MM.dd."))));
+ invoice.Add(new XElement("Hatarido", new XCData(Hatarido.ToString("yyyy.MM.dd."))));
+ invoice.Add(new XElement("Devizanem", new XCData(Devizanem ?? "")));
+ invoice.Add(new XElement("FizetesiMod", new XCData(FizetesiMod ?? "")));
+
+ if (!string.IsNullOrEmpty(Megjegyzes))
+ invoice.Add(new XElement("Megjegyzes", new XCData(Megjegyzes)));
+ if (!string.IsNullOrEmpty(Nyelv1))
+ invoice.Add(new XElement("Nyelv1", new XCData(Nyelv1)));
+ if (!string.IsNullOrEmpty(Nyelv2))
+ invoice.Add(new XElement("Nyelv2", new XCData(Nyelv2)));
+ if (Arfolyam.HasValue)
+ invoice.Add(new XElement("Arfolyam", new XCData(Arfolyam.Value.ToString())));
+ if (!string.IsNullOrEmpty(ArfolyamDeviza))
+ invoice.Add(new XElement("ArfolyamDeviza", new XCData(ArfolyamDeviza)));
+
+ invoice.Add(new XElement("Fizetve", Fizetve ? "1" : "0"));
+ invoice.Add(new XElement("Eszamla", Eszamla ? "1" : "0"));
+
+ if (!string.IsNullOrEmpty(VevoAdoszam))
+ invoice.Add(new XElement("VevoAdoszam", new XCData(VevoAdoszam)));
+ if (!string.IsNullOrEmpty(VevoCsAdoszam))
+ invoice.Add(new XElement("VevoCsAdoszam", new XCData(VevoCsAdoszam)));
+ if (!string.IsNullOrEmpty(Telefon))
+ invoice.Add(new XElement("Telefon", new XCData(Telefon)));
+ if (!string.IsNullOrEmpty(Email))
+ invoice.Add(new XElement("Email", new XCData(Email)));
+ if (!string.IsNullOrEmpty(MegrendelesSzamStr))
+ invoice.Add(new XElement("MegrendelesSzamStr", new XCData(MegrendelesSzamStr)));
+ if (!string.IsNullOrEmpty(MegrendelesIdopontStr))
+ invoice.Add(new XElement("MegrendelesIdopontStr", new XCData(MegrendelesIdopontStr)));
+
+ invoice.Add(new XElement("Felretett", Felretett ? "1" : "0"));
+ invoice.Add(new XElement("Proforma", Proforma ? "1" : "0"));
+
+ if (AutomatikusAr)
+ invoice.Add(new XElement("AutomatikusAr", "1"));
+ if (Eloleg)
+ invoice.Add(new XElement("Eloleg", "1"));
+ if (Sendmail)
+ {
+ invoice.Add(new XElement("Sendmail", "1"));
+ if (!string.IsNullOrEmpty(MailSubject))
+ invoice.Add(new XElement("MailSubject", new XCData(MailSubject)));
+ if (!string.IsNullOrEmpty(MailBody))
+ invoice.Add(new XElement("MailBody", new XCData(MailBody)));
+ }
+ if (!string.IsNullOrEmpty(Eredetiszamla))
+ invoice.Add(new XElement("Eredetiszamla", new XCData(Eredetiszamla)));
+
+ // Add items
+ foreach (var item in Items)
+ {
+ var tetel = new XElement("tetel");
+ tetel.Add(new XElement("TetelNev", new XCData(item.TetelNev ?? "")));
+ tetel.Add(new XElement("AfaSzoveg", item.AfaSzoveg ?? ""));
+ tetel.Add(new XElement("Brutto", item.Brutto ? "1" : "0"));
+ tetel.Add(new XElement("EgysegAr", item.EgysegAr.ToString()));
+ tetel.Add(new XElement("Mennyiseg", item.Mennyiseg.ToString()));
+ tetel.Add(new XElement("MennyisegEgyseg", new XCData(item.MennyisegEgyseg ?? "")));
+
+ if (item.KedvezmenyOsszeg.HasValue)
+ tetel.Add(new XElement("KedvezmenyOsszeg", item.KedvezmenyOsszeg.Value.ToString()));
+ if (item.TermekID.HasValue)
+ tetel.Add(new XElement("TermekID", item.TermekID.Value.ToString()));
+ if (!string.IsNullOrEmpty(item.Megjegyzes))
+ tetel.Add(new XElement("Megjegyzes", new XCData(item.Megjegyzes)));
+ if (!string.IsNullOrEmpty(item.CikkSzam))
+ tetel.Add(new XElement("CikkSzam", new XCData(item.CikkSzam)));
+ if (!string.IsNullOrEmpty(item.VTSZSZJ))
+ tetel.Add(new XElement("VTSZSZJ", new XCData(item.VTSZSZJ)));
+ if (item.ElolegSzamlaTABLE_ID.HasValue)
+ tetel.Add(new XElement("ElolegSzamlaTABLE_ID", item.ElolegSzamlaTABLE_ID.Value.ToString()));
+ if (!string.IsNullOrEmpty(item.ElolegSzamlaSorszam))
+ tetel.Add(new XElement("ElolegSzamlaSorszam", new XCData(item.ElolegSzamlaSorszam)));
+
+ invoice.Add(tetel);
+ }
+
+ invoices.Add(invoice);
+ return new XDeclaration("1.0", "UTF-8", null).ToString() + "\n" + invoices.ToString();
+ }
+ }
+
+ public class InvoiceItem
+ {
+ public string TetelNev { get; set; }
+ public string AfaSzoveg { get; set; }
+ public bool Brutto { get; set; }
+ public decimal EgysegAr { get; set; }
+ public decimal Mennyiseg { get; set; }
+ public string MennyisegEgyseg { get; set; }
+ public decimal? KedvezmenyOsszeg { get; set; }
+ public int? TermekID { get; set; }
+ public string Megjegyzes { get; set; }
+ public string CikkSzam { get; set; }
+ public string VTSZSZJ { get; set; }
+ public int? ElolegSzamlaTABLE_ID { get; set; }
+ public string ElolegSzamlaSorszam { get; set; }
+ }
+
+ public class InvoiceCreateResponse
+ {
+ public string ErrorCode { get; set; }
+ public string Message { get; set; }
+ public int? TableId { get; set; }
+ public int? VevoID { get; set; }
+ public string TechId { get; set; }
+ public string Sorszam { get; set; }
+ public string PrintUrl { get; set; }
+
+ public bool IsSuccess => ErrorCode == "200";
+
+ public static InvoiceCreateResponse FromXml(string xml)
+ {
+ var doc = XDocument.Parse(xml);
+ var invoice = doc.Descendants("invoice").FirstOrDefault();
+
+ if (invoice == null)
+ {
+ throw new InnVoiceApiException("Invalid XML response format");
+ }
+
+ return new InvoiceCreateResponse
+ {
+ ErrorCode = invoice.Element("error")?.Value,
+ Message = invoice.Element("message")?.Value?.Trim(),
+ TableId = int.TryParse(invoice.Element("TABLE_ID")?.Value?.Trim(), out var tid) ? tid : (int?)null,
+ VevoID = int.TryParse(invoice.Element("VevoID")?.Value?.Trim(), out var vid) ? vid : (int?)null,
+ TechId = invoice.Element("techid")?.Value?.Trim(),
+ Sorszam = invoice.Element("Sorszam")?.Value?.Trim(),
+ PrintUrl = invoice.Element("PrintUrl")?.Value?.Trim()
+ };
+ }
+ }
+
+ ///
+ /// Create a new invoice
+ ///
+ public async Task CreateInvoiceAsync(InvoiceCreateRequest request)
+ {
+ var url = $"{_baseUrl}/{_companyName}/invoice";
+
+ var xml = request.ToXml();
+ var content = new FormUrlEncodedContent(new[]
+ {
+ new KeyValuePair("data", xml)
+ });
+
+ try
+ {
+ var response = await _httpClient.PostAsync(url, content);
+ response.EnsureSuccessStatusCode();
+
+ var responseContent = await response.Content.ReadAsStringAsync();
+ return InvoiceCreateResponse.FromXml(responseContent);
+ }
+ catch (HttpRequestException ex)
+ {
+ throw new InnVoiceApiException($"Error creating invoice: {ex.Message}", ex);
+ }
+ }
+
+ ///
+ /// Update an existing invoice
+ ///
+ public async Task UpdateInvoiceAsync(int tableId, InvoiceCreateRequest request)
+ {
+ // Set the VevoID if updating customer information
+ var url = $"{_baseUrl}/{_companyName}/invoice";
+
+ var xml = request.ToXml();
+ var content = new FormUrlEncodedContent(new[]
+ {
+ new KeyValuePair("data", xml),
+ new KeyValuePair("id", tableId.ToString())
+ });
+
+ try
+ {
+ var response = await _httpClient.PostAsync(url, content);
+ response.EnsureSuccessStatusCode();
+
+ var responseContent = await response.Content.ReadAsStringAsync();
+ return InvoiceCreateResponse.FromXml(responseContent);
+ }
+ catch (HttpRequestException ex)
+ {
+ throw new InnVoiceApiException($"Error updating invoice: {ex.Message}", ex);
+ }
+ }
\ No newline at end of file
diff --git a/Nop.Plugin.Misc.AIPlugin/Services/OpenAIApiService.cs b/Nop.Plugin.Misc.AIPlugin/Services/OpenAIApiService.cs
index bc12f6b..7e01266 100644
--- a/Nop.Plugin.Misc.AIPlugin/Services/OpenAIApiService.cs
+++ b/Nop.Plugin.Misc.AIPlugin/Services/OpenAIApiService.cs
@@ -21,6 +21,10 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services
private const string OpenAiEndpoint = "https://api.openai.com/v1/chat/completions";
private const string OpenAiImageEndpoint = "https://api.openai.com/v1/images/generations";
private const string OpenAiFileEndpoint = "https://api.openai.com/v1/files";
+ private const string BaseUrl = "https://api.openai.com/v1";
+
+ private string? _assistantId;
+ private string? _vectorStoreId;
public OpenAIApiService(ISettingService settingService, HttpClient httpClient)
{
@@ -286,66 +290,371 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services
#endregion
#region === PDF ANALYSIS (NEW) ===
- public async Task AnalyzePdfAsync(string filePath, string userPrompt)
- {
- // Step 1: Upload PDF
- using var form = new MultipartFormDataContent();
- using var fileStream = File.OpenRead(filePath);
- var fileContent = new StreamContent(fileStream);
- fileContent.Headers.ContentType = new MediaTypeHeaderValue("application/pdf");
- form.Add(fileContent, "file", Path.GetFileName(filePath));
- form.Add(new StringContent("assistants"), "purpose");
- var uploadResponse = await _httpClient.PostAsync(OpenAiFileEndpoint, form);
- if (!uploadResponse.IsSuccessStatusCode)
+ private async Task EnsureAssistantAndVectorStoreAsync()
+ {
+ // Find or create vector store
+ if (_vectorStoreId == null)
{
- var error = await uploadResponse.Content.ReadAsStringAsync();
- throw new Exception($"File upload failed: {error}");
+ _vectorStoreId = await FindOrCreateVectorStoreAsync("pdf-analysis-store");
}
- using var uploadJson = await JsonDocument.ParseAsync(await uploadResponse.Content.ReadAsStreamAsync());
- var fileId = uploadJson.RootElement.GetProperty("id").GetString();
-
- // Step 2: Ask model with file reference
- var requestBody = new
+ // Find or create assistant
+ if (_assistantId == null)
{
- model = "gpt-4.1", // must support file_search
- messages = new[]
+ _assistantId = await FindOrCreateAssistantAsync("PDF and Image Analyzer Assistant");
+ }
+ }
+
+ //TEMPORARY: Cleanup all assistants (for testing purposes) - A.
+ public async Task CleanupAllAssistantsAsync()
+ {
+ Console.WriteLine("Cleaning up all existing assistants...");
+
+ var listRequest = new HttpRequestMessage(HttpMethod.Get, $"{BaseUrl}/assistants");
+ listRequest.Headers.Add("OpenAI-Beta", "assistants=v2");
+ var response = await _httpClient.SendAsync(listRequest);
+
+ if (response.IsSuccessStatusCode)
+ {
+ using var json = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
+ var assistants = json.RootElement.GetProperty("data");
+
+ foreach (var assistant in assistants.EnumerateArray())
{
- new { role = "system", content = "You are an assistant that analyzes uploaded PDF files." },
- new { role = "user", content = userPrompt }
- },
- tools = new[]
+ var id = assistant.GetProperty("id").GetString();
+ var name = assistant.GetProperty("name").GetString();
+
+ var deleteRequest = new HttpRequestMessage(HttpMethod.Delete, $"{BaseUrl}/assistants/{id}");
+ deleteRequest.Headers.Add("OpenAI-Beta", "assistants=v2");
+ await _httpClient.SendAsync(deleteRequest);
+
+ Console.WriteLine($"Deleted assistant: {name} ({id})");
+ }
+
+ Console.WriteLine("Cleanup complete!");
+ }
+
+ // Reset local cache
+ _assistantId = null;
+ }
+
+ public async Task AnalyzePdfAsync(Stream file, string fileName, string userPrompt)
+ {
+
+ await EnsureAssistantAndVectorStoreAsync();
+ var fileId = await UploadFileAsync(file, fileName);
+ var isImage = IsImageFile(fileName);
+
+ if (!isImage)
+ {
+ await AttachFileToVectorStoreAsync(fileId);
+ }
+
+ var threadId = await CreateThreadAsync();
+
+ if (isImage)
+ {
+ await AddUserMessageWithImageAsync(threadId, userPrompt, fileId);
+ }
+ else
+ {
+ await AddUserMessageAsync(threadId, userPrompt);
+ }
+
+ var runId = await CreateRunAsync(threadId);
+ await WaitForRunCompletionAsync(threadId, runId);
+
+ return await GetAssistantResponseAsync(threadId);
+ }
+
+ private bool IsImageFile(string fileName)
+ {
+ var extension = Path.GetExtension(fileName).ToLowerInvariant();
+ return extension == ".jpg" || extension == ".jpeg" || extension == ".png" || extension == ".gif" || extension == ".webp";
+ }
+
+ private async Task UploadFileAsync(Stream file, string fileName)
+ {
+ using var form = new MultipartFormDataContent();
+ var fileContent = new StreamContent(file);
+
+ // Determine MIME type based on file extension
+ var extension = Path.GetExtension(fileName).ToLowerInvariant();
+ fileContent.Headers.ContentType = extension switch
+ {
+ ".pdf" => new MediaTypeHeaderValue("application/pdf"),
+ ".jpg" or ".jpeg" => new MediaTypeHeaderValue("image/jpeg"),
+ ".png" => new MediaTypeHeaderValue("image/png"),
+ ".gif" => new MediaTypeHeaderValue("image/gif"),
+ ".webp" => new MediaTypeHeaderValue("image/webp"),
+ _ => new MediaTypeHeaderValue("application/octet-stream")
+ };
+
+ form.Add(fileContent, "file", fileName);
+ form.Add(new StringContent("assistants"), "purpose");
+
+ var response = await _httpClient.PostAsync($"{BaseUrl}/files", form);
+ await EnsureSuccessAsync(response, "upload file");
+
+ using var json = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
+ return json.RootElement.GetProperty("id").GetString()!;
+ }
+
+ private async Task AttachFileToVectorStoreAsync(string fileId)
+ {
+ var body = new { file_id = fileId };
+ var request = CreateAssistantRequest(
+ HttpMethod.Post,
+ $"{BaseUrl}/vector_stores/{_vectorStoreId}/files",
+ body
+ );
+
+ var response = await _httpClient.SendAsync(request);
+ await EnsureSuccessAsync(response, "attach file to vector store");
+ }
+
+ private async Task CreateThreadAsync()
+ {
+ var request = CreateAssistantRequest(
+ HttpMethod.Post,
+ $"{BaseUrl}/threads",
+ new { }
+ );
+
+ var response = await _httpClient.SendAsync(request);
+ await EnsureSuccessAsync(response, "create thread");
+
+ using var json = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
+ return json.RootElement.GetProperty("id").GetString()!;
+ }
+
+ private async Task AddUserMessageAsync(string threadId, string userPrompt)
+ {
+ var body = new
+ {
+ role = "user",
+ content = userPrompt
+ };
+
+ var request = CreateAssistantRequest(
+ HttpMethod.Post,
+ $"{BaseUrl}/threads/{threadId}/messages",
+ body
+ );
+
+ var response = await _httpClient.SendAsync(request);
+ await EnsureSuccessAsync(response, "add user message");
+ }
+
+ private async Task AddUserMessageWithImageAsync(string threadId, string userPrompt, string fileId)
+ {
+ var body = new
+ {
+ role = "user",
+ content = new object[]
{
- new { type = "file_search" }
- },
+ new { type = "text", text = userPrompt },
+ new { type = "image_file", image_file = new { file_id = fileId } }
+ }
+ };
+
+ var request = CreateAssistantRequest(
+ HttpMethod.Post,
+ $"{BaseUrl}/threads/{threadId}/messages",
+ body
+ );
+
+ var response = await _httpClient.SendAsync(request);
+ await EnsureSuccessAsync(response, "add user message with image");
+ }
+
+ private async Task CreateRunAsync(string threadId)
+ {
+ var body = new
+ {
+ assistant_id = _assistantId,
tool_resources = new
{
file_search = new
{
- vector_store_ids = new string[] { fileId! }
+ vector_store_ids = new[] { _vectorStoreId }
}
}
};
- var requestJson = JsonSerializer.Serialize(requestBody, new JsonSerializerOptions
- {
- PropertyNamingPolicy = JsonNamingPolicy.CamelCase
- });
+ var request = CreateAssistantRequest(
+ HttpMethod.Post,
+ $"{BaseUrl}/threads/{threadId}/runs",
+ body
+ );
- var requestContent = new StringContent(requestJson, Encoding.UTF8, "application/json");
- var chatResponse = await _httpClient.PostAsync(OpenAiEndpoint, requestContent);
- chatResponse.EnsureSuccessStatusCode();
+ var response = await _httpClient.SendAsync(request);
+ await EnsureSuccessAsync(response, "create run");
- using var responseJson = await JsonDocument.ParseAsync(await chatResponse.Content.ReadAsStreamAsync());
- var result = responseJson.RootElement
- .GetProperty("choices")[0]
- .GetProperty("message")
- .GetProperty("content")
- .GetString();
-
- return result ?? "No response from model";
+ using var json = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
+ return json.RootElement.GetProperty("id").GetString()!;
}
- #endregion
+
+ private async Task WaitForRunCompletionAsync(string threadId, string runId)
+ {
+ const int pollIntervalMs = 1000;
+ const int maxAttempts = 60; // 1 minute timeout
+ int attempts = 0;
+
+ while (attempts < maxAttempts)
+ {
+ var request = CreateAssistantRequest(
+ HttpMethod.Get,
+ $"{BaseUrl}/threads/{threadId}/runs/{runId}"
+ );
+
+ var response = await _httpClient.SendAsync(request);
+ await EnsureSuccessAsync(response, "check run status");
+
+ using var json = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
+ var status = json.RootElement.GetProperty("status").GetString()!;
+
+ if (status == "completed")
+ return;
+
+ if (status != "in_progress" && status != "queued")
+ throw new Exception($"Run failed with status: {status}");
+
+ await Task.Delay(pollIntervalMs);
+ attempts++;
+ }
+
+ throw new TimeoutException("Run did not complete within the expected time");
+ }
+
+ private async Task GetAssistantResponseAsync(string threadId)
+ {
+ var request = CreateAssistantRequest(
+ HttpMethod.Get,
+ $"{BaseUrl}/threads/{threadId}/messages"
+ );
+
+ var response = await _httpClient.SendAsync(request);
+ await EnsureSuccessAsync(response, "retrieve messages");
+
+ using var json = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
+ var messages = json.RootElement.GetProperty("data");
+
+ if (messages.GetArrayLength() == 0)
+ return "No response";
+
+ var firstMessage = messages[0]
+ .GetProperty("content")[0]
+ .GetProperty("text")
+ .GetProperty("value")
+ .GetString();
+
+ return firstMessage ?? "No response";
+ }
+
+ private HttpRequestMessage CreateAssistantRequest(HttpMethod method, string url, object? body = null)
+ {
+ var request = new HttpRequestMessage(method, url);
+ request.Headers.Add("OpenAI-Beta", "assistants=v2");
+
+ if (body != null)
+ {
+ var json = JsonSerializer.Serialize(body, new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ });
+ request.Content = new StringContent(json, Encoding.UTF8, "application/json");
+ }
+
+ return request;
+ }
+
+ private async Task EnsureSuccessAsync(HttpResponseMessage response, string operation)
+ {
+ if (!response.IsSuccessStatusCode)
+ {
+ var errorBody = await response.Content.ReadAsStringAsync();
+ Console.WriteLine($"Error Status: {response.StatusCode}");
+ Console.WriteLine($"Error Body: {errorBody}");
+ throw new Exception($"Failed to {operation}: {errorBody}");
+ }
+ }
+
+
+
+ private async Task FindOrCreateVectorStoreAsync(string name)
+ {
+ // List existing vector stores
+ var listRequest = new HttpRequestMessage(HttpMethod.Get, $"{BaseUrl}/vector_stores");
+ listRequest.Headers.Add("OpenAI-Beta", "assistants=v2");
+ var response = await _httpClient.SendAsync(listRequest);
+
+ if (response.IsSuccessStatusCode)
+ {
+ using var json = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
+ var stores = json.RootElement.GetProperty("data");
+
+ foreach (var store in stores.EnumerateArray())
+ {
+ if (store.GetProperty("name").GetString() == name)
+ {
+ return store.GetProperty("id").GetString()!;
+ }
+ }
+ }
+
+ // Create new if not found
+ var createBody = new { name = name };
+ var createRequest = CreateAssistantRequest(HttpMethod.Post, $"{BaseUrl}/vector_stores", createBody);
+ var createResponse = await _httpClient.SendAsync(createRequest);
+ await EnsureSuccessAsync(createResponse, "create vector store");
+
+ using var createJson = await JsonDocument.ParseAsync(await createResponse.Content.ReadAsStreamAsync());
+ return createJson.RootElement.GetProperty("id").GetString()!;
+ }
+
+ private async Task FindOrCreateAssistantAsync(string name)
+ {
+ // List existing assistants
+ var listRequest = new HttpRequestMessage(HttpMethod.Get, $"{BaseUrl}/assistants");
+ listRequest.Headers.Add("OpenAI-Beta", "assistants=v2");
+ var response = await _httpClient.SendAsync(listRequest);
+
+ if (response.IsSuccessStatusCode)
+ {
+ using var json = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
+ var assistants = json.RootElement.GetProperty("data");
+
+ foreach (var assistant in assistants.EnumerateArray())
+ {
+ if (assistant.GetProperty("name").GetString() == name)
+ {
+ return assistant.GetProperty("id").GetString()!;
+ }
+ }
+ }
+
+ // Create new if not found
+ var assistantBody = new
+ {
+ name = name,
+ instructions = "You are an assistant that analyzes uploaded files. When you receive an image, analyze and describe what you see in the image in detail. When you receive a PDF or text document, use the file_search tool to find and analyze relevant information. Always respond directly to the user's question about the file they uploaded.",
+ model = "gpt-4o",
+ tools = new[] { new { type = "file_search" } }
+ };
+
+ var request = CreateAssistantRequest(HttpMethod.Post, $"{BaseUrl}/assistants", assistantBody);
+ var createResponse = await _httpClient.SendAsync(request);
+ await EnsureSuccessAsync(createResponse, "create assistant");
+
+ using var createJson = await JsonDocument.ParseAsync(await createResponse.Content.ReadAsStreamAsync());
+ return createJson.RootElement.GetProperty("id").GetString()!;
+ }
+
+ #endregion
+
}
+
}
+
diff --git a/Nop.Plugin.Misc.AIPlugin/Views/OrderAttributes.cshtml b/Nop.Plugin.Misc.AIPlugin/Views/OrderAttributes.cshtml
new file mode 100644
index 0000000..96e8e4e
--- /dev/null
+++ b/Nop.Plugin.Misc.AIPlugin/Views/OrderAttributes.cshtml
@@ -0,0 +1,61 @@
+@model Nop.Plugin.Misc.FruitBankPlugin.Models.OrderAttributesModel
+
+
+
+
+
diff --git a/Nop.Plugin.Misc.AIPlugin/Views/ProductAttributes.cshtml b/Nop.Plugin.Misc.AIPlugin/Views/ProductAttributes.cshtml
index 221ea78..b144f6b 100644
--- a/Nop.Plugin.Misc.AIPlugin/Views/ProductAttributes.cshtml
+++ b/Nop.Plugin.Misc.AIPlugin/Views/ProductAttributes.cshtml
@@ -1,4 +1,4 @@
-@* File: Plugins/Nop.Plugin.YourCompany.ProductAttributes/Views/ProductCustomAttributes.cshtml *@
+
@model Nop.Plugin.Misc.FruitBankPlugin.Models.ProductAttributesModel