improvements, fixes

This commit is contained in:
Loretta 2025-11-01 00:42:59 +01:00
parent 9b0afb2d2c
commit d84276e3ac
2 changed files with 427 additions and 312 deletions

View File

@ -483,8 +483,8 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
await _orderService.UpdateOrderAsync(order); await _orderService.UpdateOrderAsync(order);
var orderDto = await _dbContext.OrderDtos.GetByIdAsync(order.Id, true); //var orderDto = await _dbContext.OrderDtos.GetByIdAsync(order.Id, true);
await _sendToClient.SendMeasuringNotification("Módosult a rendelés, mérjétek újra!", orderDto); //await _sendToClient.SendMeasuringNotification("Módosult a rendelés, mérjétek újra!", orderDto);
return true; return true;
}); });

View File

@ -15,7 +15,9 @@ using Nop.Web.Framework;
using Nop.Web.Framework.Mvc.Filters; using Nop.Web.Framework.Mvc.Filters;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using AyCode.Core.Consts;
using UglyToad.PdfPig.DocumentLayoutAnalysis.TextExtractor; using UglyToad.PdfPig.DocumentLayoutAnalysis.TextExtractor;
using AyCode.Core.Extensions;
namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
{ {
@ -68,6 +70,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
testGridModel2.Configuration = new GridConfiguration(); testGridModel2.Configuration = new GridConfiguration();
testGridModel2.Configuration.ShowChildGridsAsTabs = true; testGridModel2.Configuration.ShowChildGridsAsTabs = true;
testGridModel2.ChildGrids = new List<TestGridModel>(); testGridModel2.ChildGrids = new List<TestGridModel>();
var childGrid1 = new TestGridModel var childGrid1 = new TestGridModel
{ {
GridName = "TestGrid", GridName = "TestGrid",
@ -88,7 +91,6 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
}; };
testGridModel2.ChildGrids.Add(childGrid2); testGridModel2.ChildGrids.Add(childGrid2);
testPageModel.Grids.Add(testGridModel2); testPageModel.Grids.Add(testGridModel2);
var testGridModel = new TestGridModel(); var testGridModel = new TestGridModel();
@ -124,8 +126,8 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
// Request model for deserialization // Request model for deserialization
public class LoadChildGridRequest public class LoadChildGridRequest
{ {
public int ContextId { get; set; } // The actual row ID from data.Id public int ContextId { get; set; } // The actual row ID from data.Id
public TestGridModel ChildModel { get; set; } // The full child grid model public TestGridModel ChildModel { get; set; } // The full child grid model
} }
@ -226,24 +228,22 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
} }
[HttpPost] [HttpPost]
[RequestSizeLimit(10485760)] // 10MB [RequestSizeLimit(10485760)] // 10MB
[RequestFormLimits(MultipartBodyLengthLimit = 10485760)] [RequestFormLimits(MultipartBodyLengthLimit = 10485760)]
public async Task<IActionResult> UploadFile(List<IFormFile> files, int shippingDocumentId, int? partnerId) public async Task<IActionResult> UploadFile(List<IFormFile> files, int shippingDocumentId, int? partnerId)
{ {
//an empty shippingdocument created by the user //an empty shippingdocument created by the user
var shippingDocument = await _dbContext.ShippingDocuments.GetByIdAsync(shippingDocumentId); var shippingDocument = await _dbContext.ShippingDocuments.GetByIdAsync(shippingDocumentId);
var shippingDocumentAnalysisResult = new ShippingDocumentAnalysisResult
{
Partner = new Partner(),
ShippingDocument = shippingDocument,
ShippingItems = []
};
shippingDocumentAnalysisResult.ShippingDocument.ShippingDocumentToFiles = [];
ShippingDocumentAnalysisResult shippingDocumentAnalysisResult = new ShippingDocumentAnalysisResult();
shippingDocumentAnalysisResult.Partner = new Partner();
shippingDocumentAnalysisResult.ShippingDocument = shippingDocument;
shippingDocumentAnalysisResult.ShippingItems = new List<ShippingItem>();
shippingDocumentAnalysisResult.ShippingDocument.ShippingDocumentToFiles = new List<ShippingDocumentToFiles>();
//checks //checks
// - files exist // - files exist
@ -264,73 +264,67 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
} }
var filesList = new List<Files>(); var filesList = new List<Files>();
//iteratation 1: iterate documents to determine their type by AI //iteratation 1: iterate documents to determine their type by AI
var responseRawPdfTexts = string.Empty;
for (int i = 0; i < files.Count; i++) foreach (var currentFile in files)
{ {
var fileName = currentFile.FileName;
var fileName = files[i].FileName; var fileSize = currentFile.Length;
var fileSize = files[i].Length;
var dbFile = new Files(); var dbFile = new Files();
string pdfText = ""; var pdfText = "";
_logger.Detail($"Received file: {fileName} for Document ID: {shippingDocumentId}, content type: {files[i].ContentType}"); _logger.Detail($"Received file: {fileName} for Document ID: {shippingDocumentId}, content type: {currentFile.ContentType}");
if (!files[i].ContentType.Equals("application/pdf", StringComparison.OrdinalIgnoreCase) && !files[i].ContentType.Equals("image/jpeg", StringComparison.OrdinalIgnoreCase)) if (!currentFile.ContentType.Equals("application/pdf", StringComparison.OrdinalIgnoreCase) && !currentFile.ContentType.Equals("image/jpeg", StringComparison.OrdinalIgnoreCase))
return Json(new { success = false, errorMessage = "Only PDF or jpg files are allowed" }); return Json(new { success = false, errorMessage = "Only PDF or jpg files are allowed" });
// Validate file size (max 20MB) // Validate file size (max 20MB)
if (files[i].Length > 20 * 1024 * 1024) if (currentFile.Length > 20 * 1024 * 1024)
return Json(new { success = false, errorMessage = "File size must be less than 10MB" }); return Json(new { success = false, errorMessage = "File size must be less than 10MB" });
// - get text extracted from pdf // - get text extracted from pdf
// Validate file type (PDF only) // Validate file type (PDF only)
//if (file.Length > 0 && file.ContentType == "application/pdf") //if (file.Length > 0 && file.ContentType == "application/pdf")
if (files[i].Length > 0)
if (currentFile.Length > 0)
{ {
if (files[i].ContentType.Equals("application/pdf", StringComparison.OrdinalIgnoreCase)) if (currentFile.ContentType.Equals("application/pdf", StringComparison.OrdinalIgnoreCase))
{ {
try try
{ {
// Open the PDF from the IFormFile's stream directly in memory // Open the PDF from the IFormFile's stream directly in memory
using (var stream = files[i].OpenReadStream()) await using var stream = currentFile.OpenReadStream();
using (var pdf = UglyToad.PdfPig.PdfDocument.Open(stream)) using var pdf = UglyToad.PdfPig.PdfDocument.Open(stream);
// Now you can analyze the PDF content
foreach (var page in pdf.GetPages())
pdfText += ContentOrderTextExtractor.GetText(page);
//still nothing? let's send it to AI
if (string.IsNullOrWhiteSpace(pdfText))
{ {
// Now you can analyze the PDF content try
foreach (var page in pdf.GetPages())
{ {
// Extract text from each page // ✅ Use the service we implemented earlier
pdfText += ContentOrderTextExtractor.GetText(page); //pdfText = await _openAIApiService.AnalyzePdfAsync(stream, currentFile.FileName, "Please extract all readable text from this PDF.");
pdfText = await _openAIApiService.AnalyzePdfAsync(stream, currentFile.FileName, DefaultFullPrompt);
} }
//still nothing? let's send it to AI catch (Exception aiEx)
if (string.IsNullOrWhiteSpace(pdfText))
{ {
try Console.Error.WriteLine($"OpenAI Assistants API failed: {aiEx.Message}");
{ return StatusCode(500, $"Failed to process PDF: {aiEx.Message}");
// ✅ Use the service we implemented earlier
pdfText = await _openAIApiService.AnalyzePdfAsync(stream, files[i].FileName, "Please extract all readable text from this PDF.");
}
catch (Exception aiEx)
{
Console.Error.WriteLine($"OpenAI Assistants API failed: {aiEx.Message}");
return StatusCode(500, $"Failed to process PDF: {aiEx.Message}");
}
} }
// For demonstration, let's just log the extracted text
_logger.Detail($"Extracted text from {files[i].FileName}: {pdfText}");
} }
// For demonstration, let's just log the extracted text
_logger.Detail($"Extracted text from {currentFile.FileName}: {pdfText}");
} }
catch (Exception ex) catch (Exception ex)
{ {
// Handle potential exceptions during PDF processing // Handle potential exceptions during PDF processing
Console.Error.WriteLine($"Error processing PDF file {files[i].FileName}: {ex.Message}"); Console.Error.WriteLine($"Error processing PDF file {currentFile.FileName}: {ex.Message}");
return StatusCode(500, $"Error processing PDF file: {ex.Message}"); return StatusCode(500, $"Error processing PDF file: {ex.Message}");
} }
} }
@ -339,80 +333,107 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
try try
{ {
// Open the Image from the IFormFile's stream directly in memory // Open the Image from the IFormFile's stream directly in memory
using (var stream = files[i].OpenReadStream()) await using var stream = currentFile.OpenReadStream();
try
{ {
// ✅ Use the service we implemented earlier
try pdfText = await _openAIApiService.AnalyzePdfAsync(stream, currentFile.FileName, "Please extract all readable text from this image.");
{ }
// ✅ Use the service we implemented earlier catch (Exception aiEx)
pdfText = await _openAIApiService.AnalyzePdfAsync(stream, files[i].FileName, "Please extract all readable text from this image."); {
} Console.Error.WriteLine($"OpenAI Assistants API failed: {aiEx.Message}");
catch (Exception aiEx) return StatusCode(500, $"Failed to process PDF: {aiEx.Message}");
{
Console.Error.WriteLine($"OpenAI Assistants API failed: {aiEx.Message}");
return StatusCode(500, $"Failed to process PDF: {aiEx.Message}");
}
// For demonstration, let's just log the extracted text
_logger.Detail($"Extracted text from {files[i].FileName}: {pdfText}");
} }
}
// For demonstration, let's just log the extracted text
_logger.Detail($"Extracted text from {currentFile.FileName}: {pdfText}");
}
catch (Exception ex) catch (Exception ex)
{ {
// Handle potential exceptions during PDF processing // Handle potential exceptions during PDF processing
Console.Error.WriteLine($"Error processing PDF file {files[i].FileName}: {ex.Message}"); Console.Error.WriteLine($"Error processing PDF file {currentFile.FileName}: {ex.Message}");
return StatusCode(500, $"Error processing PDF file: {ex.Message}"); return StatusCode(500, $"Error processing PDF file: {ex.Message}");
} }
} }
//we should have some kind of text now //we should have some kind of text now
_logger.Detail(pdfText); _logger.Detail(pdfText);
} }
string analysisPrompt = "Extract the document identification number from this document, determine the type of the " + //ORIGINAL!!! - J.
"document IN ENGLISH from the available list, and return them as JSON: documentNumber, documentType. " + shippingDocumentAnalysisResult = await ProcessRawText(shippingDocumentId, partnerId, pdfText, shippingDocumentAnalysisResult, filesList, dbFile);
$"Available filetypes: {nameof(DocumentType.Invoice)}, {nameof(DocumentType.ShippingDocument)} , {nameof(DocumentType.OrderConfirmation)}, {nameof(DocumentType.Unknown)}" + await SaveDocumentToFileSystem(currentFile, shippingDocumentId, $"{Guid.NewGuid()}_{currentFile.FileName}");//shippingDocumentAnalysisResult.Partner.Name);
"If you can't find information of any of these, return null value for that field.";
//here I can start preparing the file entity //Ez rész a catch végéig nekem teszt! - J
var metaAnalyzis = await _aiCalculationService.GetOpenAIPDFAnalysisFromText(pdfText.ToString(), analysisPrompt); //try
//{
// responseRawPdfTexts += $"{AcEnv.NL}{AcEnv.NL}{currentFile.Name}:{AcEnv.NL}{pdfText}";
var extractedMetaData = ParseMetaDataAIResponse(metaAnalyzis); // var transactionSuccess = await SaveFileInfoToDb(shippingDocumentId, shippingDocumentAnalysisResult, filesList, dbFile, pdfText);
bool transactionSuccess = await SaveFileInfoToDB(shippingDocumentId, shippingDocumentAnalysisResult, filesList, dbFile, pdfText, extractedMetaData); // if (!transactionSuccess)
// {
// _logger.Error($"(transactionSuccess == false)");
// return BadRequest($"Error saving file! RawText:{AcEnv.NL}{pdfText}");
// }
if (!transactionSuccess) _logger.Error($"(transactionSuccess == false)"); // if (await SaveDocumentToFileSystem(currentFile, shippingDocumentId, $"{Guid.NewGuid()}_{currentFile.FileName}"))
// {
// //var partnerResponse = await _aiCalculationService.GetOpenAIPDFAnalysisFromText(
// // pdfText,
// // $"{DefaultFullPrompt}"
// //);
// //responseRawPdfTexts += $"{AcEnv.NL}{AcEnv.NL}{AcEnv.NL}Response json:{AcEnv.NL}{partnerResponse}";
// }
// //return Ok(shippingDocumentAnalysisResult);
//}
//catch (Exception ex)
//{
// _logger.Error($"Error saving file: {ex.Message}", ex);
// //return Json(new { success = false, errorMessage = ex.Message });
// return BadRequest("No files were uploaded.");
//}
}
//iteration 2: iterate documents again
// - determine the items listed in the documents by AI
// try to fill all shippingitems information from the iteration
return Ok($"Files for Shipping Document ID {shippingDocumentId} were uploaded successfully!{AcEnv.NL}{AcEnv.NL}{responseRawPdfTexts}");
}
private async Task<ShippingDocumentAnalysisResult> ProcessRawText(int shippingDocumentId, int? partnerId, string pdfText, ShippingDocumentAnalysisResult shippingDocumentAnalysisResult, List<Files> filesList, Files dbFile)
{
var transactionSuccess = await SaveFileInfoToDb(shippingDocumentId, shippingDocumentAnalysisResult, filesList, dbFile, pdfText);
if (!transactionSuccess) _logger.Error($"(transactionSuccess == false)");
// - IF WE DON'T HAVE PARTNERID ALREADY: read partner information
// (check if all 3 refers to the same partner)
// save partner information to partners table { Id, Name, TaxId, CertificationNumber, PostalCode, Country, State, County, City, Street }
if (partnerId == null)
{
//find partner in DB... there is a serious chance that the partner Name and Taxid determines the partner
var partners = await _dbContext.Partners.GetAll().ToListAsync();
foreach (var dbpartner in partners.Where(dbpartner => pdfText.Contains(dbpartner.Name) || (dbpartner.TaxId != null && pdfText.Contains(dbpartner.TaxId))))
{
partnerId = dbpartner.Id;
_logger.Detail($"Found existing partner in DB: {dbpartner.Name} (ID: {dbpartner.Id})");
break;
}
// - IF WE DON'T HAVE PARTNERID ALREADY: read partner information
// (check if all 3 refers to the same partner)
// save partner information to partners table { Id, Name, TaxId, CertificationNumber, PostalCode, Country, State, County, City, Street }
if (partnerId == null) if (partnerId == null)
{ {
_logger.Detail("No existing partner found in DB, proceeding to extract partner info via AI.");
//find partner in DB... there is a serious chance that the partner Name and Taxid determines the partner // string partnerAnalysisPrompt = "You are an agent of Fruitbank, helping to analyze pdf content. Determine which partner of Fruitbank sent this document, extract their data and return them as JSON: name, taxId, certificationNumber, postalCode, country, state, county, city, street. " +
//"If you can't find information of any of these, return null value for that field.";
var partners = await _dbContext.Partners.GetAll().ToListAsync(); // //here I can start preparing the file entity
foreach (var dbpartner in partners) // var partnerAnalyzis = await _aiCalculationService.GetOpenAIPDFAnalysisFromText(pdfText.ToString(), partnerAnalysisPrompt);
{
if (pdfText.Contains(dbpartner.Name) || (dbpartner.TaxId != null && pdfText.Contains(dbpartner.TaxId)))
{
partnerId = dbpartner.Id;
_logger.Detail($"Found existing partner in DB: {dbpartner.Name} (ID: {dbpartner.Id})");
break;
}
}
if (partnerId == null) var partnerPrompt = @"Extract the partner/company information from the following text and return ONLY a valid JSON object with these exact fields:
{
_logger.Detail("No existing partner found in DB, proceeding to extract partner info via AI.");
// string partnerAnalysisPrompt = "You are an agent of Fruitbank, helping to analyze pdf content. Determine which partner of Fruitbank sent this document, extract their data and return them as JSON: name, taxId, certificationNumber, postalCode, country, state, county, city, street. " +
//"If you can't find information of any of these, return null value for that field.";
// //here I can start preparing the file entity
// var partnerAnalyzis = await _aiCalculationService.GetOpenAIPDFAnalysisFromText(pdfText.ToString(), partnerAnalysisPrompt);
var partnerPrompt = @"Extract the partner/company information from the following text and return ONLY a valid JSON object with these exact fields:
{ {
""Name"": ""company name"", ""Name"": ""company name"",
""TaxId"": ""tax identification number"", ""TaxId"": ""tax identification number"",
@ -430,57 +451,49 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
Text to analyze: Text to analyze:
" + pdfText; " + pdfText;
var partnerResponse = await _aiCalculationService.GetOpenAIPDFAnalysisFromText( var partnerResponse = await _aiCalculationService.GetOpenAIPDFAnalysisFromText(
pdfText, pdfText,
partnerPrompt partnerPrompt
); );
shippingDocumentAnalysisResult.Partner = JsonSerializer.Deserialize<Partner>(CleanJsonResponse(partnerResponse), shippingDocumentAnalysisResult.Partner = JsonSerializer.Deserialize<Partner>(CleanJsonResponse(partnerResponse),
new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
if (shippingDocumentAnalysisResult.Partner.Name != null)
{
_logger.Detail("AI Analysis Partner Result:");
_logger.Detail(shippingDocumentAnalysisResult.Partner.Name);
}
if (shippingDocumentAnalysisResult.Partner.TaxId != null)
{
_logger.Detail(shippingDocumentAnalysisResult.Partner.TaxId);
}
if (shippingDocumentAnalysisResult.Partner.Country != null)
{
_logger.Detail(shippingDocumentAnalysisResult.Partner.Country);
}
if (shippingDocumentAnalysisResult.Partner.State != null)
{
_logger.Detail(shippingDocumentAnalysisResult.Partner.State);
}
if (shippingDocumentAnalysisResult.Partner != null)
{
shippingDocumentAnalysisResult.Partner = shippingDocumentAnalysisResult.Partner;
}
if (shippingDocumentAnalysisResult.Partner.Name != null)
{
_logger.Detail("AI Analysis Partner Result:");
_logger.Detail(shippingDocumentAnalysisResult.Partner.Name);
} }
if (shippingDocumentAnalysisResult.Partner.TaxId != null)
{
_logger.Detail(shippingDocumentAnalysisResult.Partner.TaxId);
}
if (shippingDocumentAnalysisResult.Partner.Country != null)
{
_logger.Detail(shippingDocumentAnalysisResult.Partner.Country);
}
if (shippingDocumentAnalysisResult.Partner.State != null)
{
_logger.Detail(shippingDocumentAnalysisResult.Partner.State);
}
if (shippingDocumentAnalysisResult.Partner != null)
{
shippingDocumentAnalysisResult.Partner = shippingDocumentAnalysisResult.Partner;
}
} }
}
//shortcut
//shortcut try
{
// Step 2: Extract Shipping Document Information
try var shippingDocPrompt = @"Extract the shipping document information from the following text and return ONLY a valid JSON object with these exact fields:
{
// Step 2: Extract Shipping Document Information
var shippingDocPrompt = @"Extract the shipping document information from the following text and return ONLY a valid JSON object with these exact fields:
{ {
""DocumentIdNumber"": ""document ID or reference number if exists, otherwise empty string"", ""DocumentIdNumber"": ""document ID or reference number if exists, otherwise empty string"",
""ShippingDate"": ""shipping date in ISO format (yyyy-MM-dd)"", ""ShippingDate"": ""shipping date in ISO format (yyyy-MM-dd)"",
@ -493,16 +506,16 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
Text to analyze: Text to analyze:
" + pdfText; " + pdfText;
var shippingDocResponse = await _aiCalculationService.GetOpenAIPDFAnalysisFromText( var shippingDocResponse = await _aiCalculationService.GetOpenAIPDFAnalysisFromText(
pdfText, pdfText,
shippingDocPrompt shippingDocPrompt
); );
var shippingDocData = JsonSerializer.Deserialize<ShippingDocumentDto>(CleanJsonResponse(shippingDocResponse), var shippingDocData = JsonSerializer.Deserialize<ShippingDocumentDto>(CleanJsonResponse(shippingDocResponse),
new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
// Step 3: Extract Shipping Items // Step 3: Extract Shipping Items
var itemsPrompt = @"Extract all shipping items (fruits, vegetables, or products) from the following text and return ONLY a valid JSON array with objects having these exact fields: var itemsPrompt = @"Extract all shipping items (fruits, vegetables, or products) from the following text and return ONLY a valid JSON array with objects having these exact fields:
[ [
{ {
""Name"": ""product name"", ""Name"": ""product name"",
@ -518,152 +531,170 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
Text to analyze: Text to analyze:
" + pdfText; " + pdfText;
var itemsResponse = await _aiCalculationService.GetOpenAIPDFAnalysisFromText( var itemsResponse = await _aiCalculationService.GetOpenAIPDFAnalysisFromText(
pdfText, pdfText,
itemsPrompt itemsPrompt
); );
var items = JsonSerializer.Deserialize<List<ShippingItem>>(CleanJsonResponse(itemsResponse), var items = JsonSerializer.Deserialize<List<ShippingItem>>(CleanJsonResponse(itemsResponse),
new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) ?? new List<ShippingItem>(); new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) ?? new List<ShippingItem>();
// Prepare result // Prepare result
var result = new ShippingDocumentAnalysisResult var result = new ShippingDocumentAnalysisResult
{
Partner = shippingDocumentAnalysisResult.Partner,
ShippingDocument = new ShippingDocument
{ {
Partner = shippingDocumentAnalysisResult.Partner, DocumentIdNumber = shippingDocData.DocumentIdNumber,
ShippingDocument = new ShippingDocument ShippingDate = shippingDocData.ShippingDate,
{ Country = shippingDocData.Country,
DocumentIdNumber = shippingDocData.DocumentIdNumber, TotalPallets = shippingDocData.TotalPallets
ShippingDate = shippingDocData.ShippingDate, },
Country = shippingDocData.Country, ShippingItems = items
TotalPallets = shippingDocData.TotalPallets };
},
ShippingItems = items
};
//return Ok(result); result.ShippingDocument.ShippingDocumentToFiles = shippingDocumentAnalysisResult.ShippingDocument.ShippingDocumentToFiles;
}
catch (JsonException ex)
{
return BadRequest($"Failed to parse AI response: {ex.Message}");
}
// - save the documents to file system - wwwroot/uploads/orders/order-{orderId}/fileId-documentId.pdf
// where documentId is the number or id IN the document
try
{
// Create upload directory if it doesn't exist
var uploadsPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "uploads", "orders", "order" + shippingDocumentId.ToString(), "documents");
Directory.CreateDirectory(uploadsPath);
// Generate unique filename
fileName = $"{Guid.NewGuid()}_{files[i].FileName}";
var filePath = Path.Combine(uploadsPath, fileName);
// Save file
using (var stream = new FileStream(filePath, FileMode.Create))
{
await files[i].CopyToAsync(stream);
}
return Ok(shippingDocumentAnalysisResult);
}
catch (Exception ex)
{
_logger.Error($"Error saving file: {ex.Message}", ex);
//return Json(new { success = false, errorMessage = ex.Message });
return BadRequest("No files were uploaded.");
}
// - create a list of documents to read information and to save the document's information to DB
// - INSIDE ITERATION:
// - IF SHIPPINGDOCUMENT: read shipping information
// save document information into DB {Id, PartnerId, ShippingId, DocumentIdNumber, ShippingDate, Country, TotalPallets, IsAllMeasured}
// - IF Orderdocument: try reading shipping information and financial information
// save document information into DB {Id, PartnerId, ShippingId, DocumentIdNumber, ShippingDate, Country, TotalPallets, IsAllMeasured}
// - IF Invoice: try reading financial information
// save document information into DB {financial db table not created yet}
// - save the documents to Files Table - { name, extension, type, rawtext }
//try
//{
// // Analyze PDF with AI to extract structured data
// var lastPrompt = "You work for FruitBank. Extract the following information from this shipping document and return as JSON: documentDate, recipientName, senderName, invoiceNumber, totalAmount, itemCount, notes. If a field is not found, return null for that field.";
// var aiAnalysis = await _aiCalculationService.GetOpenAIPDFAnalysisFromText(
// pdfText,
// lastPrompt
// );
// // Parse AI response (assuming it returns JSON)
// var extractedData = ParseShippingDocumentAIResponse(aiAnalysis);
// //TODO: Save document record to database
// _logger.Detail("AI Analysis Result:");
// _logger.Detail(extractedData.RecipientName);
// _logger.Detail(extractedData.SenderName);
// _logger.Detail(extractedData.InvoiceNumber);
// _logger.Detail(extractedData.TotalAmount.ToString());
// _logger.Detail(extractedData.ItemCount.ToString());
// _logger.Detail(extractedData.Notes);
// var documentId = 1; // Replace with: savedDocument.Id
// // Return structured document model
// var documentModel = new ShippingDocumentModel
// {
// Id = documentId,
// ShippingId = shippingDocumentId,
// FileName = file.FileName,
// FilePath = $"/uploads/shippingDocuments/{fileName}",
// FileSize = (int)(file.Length / 1024),
// DocumentDate = extractedData.DocumentDate,
// RecipientName = extractedData.RecipientName,
// SenderName = extractedData.SenderName,
// InvoiceNumber = extractedData.InvoiceNumber,
// TotalAmount = extractedData.TotalAmount,
// ItemCount = extractedData.ItemCount,
// Notes = extractedData.Notes,
// RawAIAnalysis = aiAnalysis // Store the raw AI response for debugging
// };
// // var savedDocument = await _documentService.InsertDocumentAsync(document);
// // Mock saved document ID
// //return Json(new
// //{
// // success = true,
// // document = documentModel
// //});
//}
//catch (Exception ex)
//{
// _logger.Error($"Error uploading file: {ex.Message}", ex);
// //return Json(new { success = false, errorMessage = ex.Message });
// return BadRequest("No files were uploaded.");
//}
return result;
//return Ok(result);
}
catch (JsonException ex)
{
_logger.Error($"ProcessRawText ERROR; {ex.Message}", ex);
} }
return null;
// - create a list of documents to read information and to save the document's information to DB
// - INSIDE ITERATION:
// - IF SHIPPINGDOCUMENT: read shipping information
// save document information into DB {Id, PartnerId, ShippingId, DocumentIdNumber, ShippingDate, Country, TotalPallets, IsAllMeasured}
// - IF Orderdocument: try reading shipping information and financial information
// save document information into DB {Id, PartnerId, ShippingId, DocumentIdNumber, ShippingDate, Country, TotalPallets, IsAllMeasured}
// - IF Invoice: try reading financial information
// save document information into DB {financial db table not created yet}
// - save the documents to Files Table - { name, extension, type, rawtext }
//try
//{
// // Analyze PDF with AI to extract structured data
// var lastPrompt = "You work for FruitBank. Extract the following information from this shipping document and return as JSON: documentDate, recipientName, senderName, invoiceNumber, totalAmount, itemCount, notes. If a field is not found, return null for that field.";
// var aiAnalysis = await _aiCalculationService.GetOpenAIPDFAnalysisFromText(
// pdfText,
// lastPrompt
// );
// // Parse AI response (assuming it returns JSON)
// var extractedData = ParseShippingDocumentAIResponse(aiAnalysis);
// //TODO: Save document record to database
// _logger.Detail("AI Analysis Result:");
// _logger.Detail(extractedData.RecipientName);
// _logger.Detail(extractedData.SenderName);
// _logger.Detail(extractedData.InvoiceNumber);
// _logger.Detail(extractedData.TotalAmount.ToString());
// _logger.Detail(extractedData.ItemCount.ToString());
// _logger.Detail(extractedData.Notes);
//iteration 2: iterate documents again // var documentId = 1; // Replace with: savedDocument.Id
// - determine the items listed in the documents by AI
// try to fill all shippingitems information from the iteration // // Return structured document model
// var documentModel = new ShippingDocumentModel
// {
// Id = documentId,
// ShippingId = shippingDocumentId,
// FileName = file.FileName,
// FilePath = $"/uploads/shippingDocuments/{fileName}",
// FileSize = (int)(file.Length / 1024),
// DocumentDate = extractedData.DocumentDate,
// RecipientName = extractedData.RecipientName,
// SenderName = extractedData.SenderName,
// InvoiceNumber = extractedData.InvoiceNumber,
// TotalAmount = extractedData.TotalAmount,
// ItemCount = extractedData.ItemCount,
// Notes = extractedData.Notes,
// RawAIAnalysis = aiAnalysis // Store the raw AI response for debugging
// };
return Ok($"Files for Shipping Document ID {shippingDocumentId} were uploaded successfully!");
// // var savedDocument = await _documentService.InsertDocumentAsync(document);
// // Mock saved document ID
// //return Json(new
// //{
// // success = true,
// // document = documentModel
// //});
//}
//catch (Exception ex)
//{
// _logger.Error($"Error uploading file: {ex.Message}", ex);
// //return Json(new { success = false, errorMessage = ex.Message });
// return BadRequest("No files were uploaded.");
//}
} }
private async Task<bool> SaveFileInfoToDB(int shippingDocumentId, ShippingDocumentAnalysisResult shippingDocumentAnalysisResult, List<Files> filesList, Files dbFile, string pdfText, ExtractedDocumentMetaData extractedMetaData) /// <summary>
/// Save the documents to file system - wwwroot/uploads/orders/order-{orderId}/fileId-documentId.pdf where documentId is the number or id IN the document
/// </summary>
/// <param name="file"></param>
/// <param name="shippingDocumentId"></param>
/// <param name="fileName"></param>
/// <returns></returns>
private async Task<bool> SaveDocumentToFileSystem(IFormFile file, int shippingDocumentId, string fileName)
{
try
{
// Create upload directory if it doesn't exist
var uploadsPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "uploads", "orders", "order" + shippingDocumentId, "documents");
Directory.CreateDirectory(uploadsPath);
// Generate unique filename
//var fileName = $"{Guid.NewGuid()}_{file.FileName}";
var filePath = Path.Combine(uploadsPath, fileName);
// Save file
await using var stream = new FileStream(filePath, FileMode.Create);
await file.CopyToAsync(stream);
return true;
}
catch (Exception ex)
{
_logger.Error($"Error saving file: {ex.Message}", ex);
//return Json(new { success = false, errorMessage = ex.Message });
}
return false;
}
private async Task<ExtractedDocumentMetaData> GetDocumentTypeFromRawText(string pdfText)
{
const string analysisPrompt = "Extract the document identification number from this document, determine the type of the " +
"document IN ENGLISH from the available list, and return them as JSON: documentNumber, documentType. " +
$"Available filetypes: {nameof(DocumentType.Invoice)}, {nameof(DocumentType.ShippingDocument)} , {nameof(DocumentType.OrderConfirmation)}, {nameof(DocumentType.Unknown)}" +
"If you can't find information of any of these, return null value for that field.";
//here I can start preparing the file entity
var metaAnalyzis = await _aiCalculationService.GetOpenAIPDFAnalysisFromText(pdfText, analysisPrompt);
return ParseMetaDataAIResponse(metaAnalyzis);
}
private async Task<bool> SaveFileInfoToDb(int shippingDocumentId, ShippingDocumentAnalysisResult shippingDocumentAnalysisResult, List<Files> filesList, Files dbFile, string pdfText)
{
var extractedMetaData = await GetDocumentTypeFromRawText(pdfText);
return await SaveFileInfoToDb(shippingDocumentId, shippingDocumentAnalysisResult, filesList, dbFile, pdfText, extractedMetaData);
}
private async Task<bool> SaveFileInfoToDb(int shippingDocumentId, ShippingDocumentAnalysisResult shippingDocumentAnalysisResult, List<Files> filesList, Files dbFile, string pdfText, ExtractedDocumentMetaData extractedMetaData)
{ {
if (extractedMetaData.DocumentNumber != null) if (extractedMetaData.DocumentNumber != null)
{ {
@ -672,7 +703,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
dbFile.FileName = extractedMetaData.DocumentNumber; dbFile.FileName = extractedMetaData.DocumentNumber;
} }
var transactionSuccess = await _dbContext.TransactionSafeAsync(async _ => return await _dbContext.TransactionSafeAsync(async _ =>
{ {
await _dbContext.Files.InsertAsync(dbFile); await _dbContext.Files.InsertAsync(dbFile);
filesList.Add(dbFile); filesList.Add(dbFile);
@ -689,7 +720,6 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
await _dbContext.ShippingDocumentToFiles.InsertAsync(shippingDocumentToFiles); await _dbContext.ShippingDocumentToFiles.InsertAsync(shippingDocumentToFiles);
return true; return true;
}); });
return transactionSuccess;
} }
@ -746,7 +776,6 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
{ {
public string RawText { get; set; } public string RawText { get; set; }
} }
public class ShippingDocumentAnalysisResult public class ShippingDocumentAnalysisResult
{ {
public Partner Partner { get; set; } public Partner Partner { get; set; }
@ -762,15 +791,14 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
public int TotalPallets { get; set; } public int TotalPallets { get; set; }
} }
private static ExtractedDocumentData ParseShippingDocumentAIResponse(string aiResponse)
private ExtractedDocumentData ParseShippingDocumentAIResponse(string aiResponse)
{ {
try try
{ {
// Try to parse as JSON first // Try to parse as JSON first
var data = System.Text.Json.JsonSerializer.Deserialize<ExtractedDocumentData>( var data = JsonSerializer.Deserialize<ExtractedDocumentData>(
aiResponse, aiResponse,
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true } new JsonSerializerOptions { PropertyNameCaseInsensitive = true }
); );
return data ?? new ExtractedDocumentData(); return data ?? new ExtractedDocumentData();
} }
@ -784,15 +812,12 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
} }
} }
private ExtractedDocumentMetaData ParseMetaDataAIResponse(string aiResponse) private static ExtractedDocumentMetaData ParseMetaDataAIResponse(string aiResponse)
{ {
try try
{ {
// Try to parse as JSON first // Try to parse as JSON first
var data = System.Text.Json.JsonSerializer.Deserialize<ExtractedDocumentMetaData>( var data = JsonSerializer.Deserialize<ExtractedDocumentMetaData>(aiResponse, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
aiResponse,
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true }
);
return data ?? new ExtractedDocumentMetaData(); return data ?? new ExtractedDocumentMetaData();
} }
catch catch
@ -806,14 +831,14 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
} }
} }
private ExtractedPartnerData ParsePartnerDataAIResponse(string aiResponse) private static ExtractedPartnerData ParsePartnerDataAIResponse(string aiResponse)
{ {
try try
{ {
// Try to parse as JSON first // Try to parse as JSON first
var data = System.Text.Json.JsonSerializer.Deserialize<ExtractedPartnerData>( var data = JsonSerializer.Deserialize<ExtractedPartnerData>(
aiResponse, aiResponse,
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true } new JsonSerializerOptions { PropertyNameCaseInsensitive = true }
); );
return data ?? new ExtractedPartnerData(); return data ?? new ExtractedPartnerData();
} }
@ -887,13 +912,11 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
var fullPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", filePath.TrimStart('/')); var fullPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", filePath.TrimStart('/'));
if (System.IO.File.Exists(fullPath)) if (!System.IO.File.Exists(fullPath)) return Json(new { success = false, message = "File not found" });
{
System.IO.File.Delete(fullPath); System.IO.File.Delete(fullPath);
return Json(new { success = true }); return Json(new { success = true });
}
return Json(new { success = false, message = "File not found" });
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -901,6 +924,98 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
} }
} }
public void GetJson()
{
var partner = new Partner();
var jsonResolver = new IgnoreAndRenamePropertySerializerContractResolver();
jsonResolver.IgnoreProperty(typeof(Partner), nameof(partner.Name));
jsonResolver.IgnoreProperty(typeof(Partner), "Title");
jsonResolver.IgnoreProperty(typeof(Partner), "Title");
jsonResolver.IncludesProperty(typeof(Partner), "Title");
jsonResolver.RenameProperty(typeof(Partner), "FirstName", "firstName");
var serializerSettings = new Newtonsoft.Json.JsonSerializerSettings();
serializerSettings.ContractResolver = jsonResolver;
var json = Newtonsoft.Json.JsonConvert.SerializeObject(partner, serializerSettings);
}
private const string DefaultFullPrompt = $"Role:\r\nYou are an AI data extraction assistant for Fruitbank, a " +
$"fruit and vegetable wholesale company. Your task is to analyze a list of extracted pdf " +
$"documents (delivery notes, invoices, or order confirmations) and extract structured information about " +
$"the shipment and its items.\r\n\r\n🎯 Goal:\r\nRead the provided extracted pdf text and extract all shipment " +
$"details and items according to the data model below.\r\n Generate the complete JSON output following this " +
$"structure.\r\n\r\n🧩 Data Models:\r\n\r\npublic " +
$"class Partner\r\n{{\r\n " +
$"/// <summary>\r\n /// Partner entity primary key\r\n /// </summary>\r\n " +
$"public int Id {{ get; set; }}\r\n " +
$"/// <summary>\r\n /// Partner company name\r\n /// </summary>\r\n " +
$"public string Name {{ get; set; }}\r\n " +
$"/// <summary>\r\n /// Partner company TaxId\r\n /// </summary>\r\n " +
$"public string TaxId {{ get; set; }}\r\n /// <summary>\r\n " +
$"/// Partner company Certification if exists\r\n /// </summary>\r\n " +
$"public string CertificationNumber {{ get; set; }}\r\n /// <summary>\r\n " +
$"/// Partner company address PostalCode\r\n /// </summary>\r\n " +
$"public string PostalCode {{ get; set; }}\r\n /// <summary>\r\n " +
$"/// Partner company address Country\r\n /// </summary>\r\n " +
$"public string Country {{ get; set; }}\r\n /// <summary>\r\n " +
$"/// Partner company address State if exists\r\n /// </summary>\r\n " +
$"public string State {{ get; set; }}\r\n /// <summary>\r\n " +
$"/// Partner company address County if exists\r\n /// </summary>\r\n " +
$"public string County {{ get; set; }}\r\n /// <summary>\r\n " +
$"/// Partner company address City\r\n /// </summary>\r\n " +
$"public string City {{ get; set; }}\r\n /// <summary>\r\n " +
$"/// Partner company address Street\r\n /// </summary>\r\n " +
$"public string Street {{ get; set; }}\r\n\t/// <summary>\r\n " +
$"/// Entities of ShippingDocument\r\n /// </summary>\r\n\tpublic List<ShippingDocument> " +
$"ShippingDocuments {{ get; set; }}\t\r\n}}\r\n\r\npublic class ShippingDocument\r\n{{\r\n " +
$"/// <summary>\r\n /// ShippingItem entity primary key\r\n /// </summary>\r\n " +
$"public int Id {{ get; set; }}\r\n /// <summary>\r\n /// Partner entity primary key\r\n " +
$"/// </summary>\r\n public int PartnerId {{ get; set; }}\t\r\n\t/// <summary>\r\n " +
$"/// Entities of ShippingItem\r\n /// </summary>\r\n\t" +
$"public List<ShippingItem> ShippingItems {{ get; set; }}\r\n /// <summary>\r\n " +
$"/// DocumentIdNumber if exists\r\n /// </summary>\r\n public string DocumentIdNumber {{ get; set; }}\r\n " +
$"/// <summary>\r\n /// \r\n /// </summary>\r\n public DateTime ShippingDate {{ get; set; }}\r\n " +
$"/// <summary>\r\n /// Shipping pickup Contry of origin\r\n /// </summary>\r\n " +
$"public string Country {{ get; set; }}\r\n\t/// <summary>\r\n /// Sum of ShippingItem pallets\r\n " +
$"/// </summary>\r\n public int TotalPallets {{ get; set; }}\r\n\t/// <summary>\r\n " +
$"/// Filename of pdf\r\n /// </summary>\r\n\tpublic string PdfFileName {{ get; set; }}\r\n}}\r\n\r\n" +
$"public class ShippingItem\r\n{{\r\n /// <summary>\r\n /// ShippingItem entity primary key\r\n /// " +
$"</summary>\r\n public int Id {{ get; set; }}\r\n /// <summary>\r\n /// " +
$"ShippingDocument entity primary key\r\n /// </summary>\r\n " +
$"public int ShippingDocumentId {{ get; set; }}\r\n /// " +
$"<summary>\r\n /// Name of the fruit or vegitable\r\n /// </summary>\r\n " +
$"public string Name {{ get; set; }}\r\n\t/// <summary>\r\n /// Translated Name to Hungarian\r\n " +
$"/// </summary>\r\n public string HungarianName {{ get; set; }}\r\n /// <summary>\r\n " +
$"/// Pallets of fruit or vegitable item\r\n /// </summary>\r\n " +
$"public int PalletsOnDocument {{ get; set; }}\r\n /// <summary>\r\n " +
$"/// Quantity of fruit or vegitable item\r\n /// </summary>\r\n " +
$"public int QuantityOnDocument {{ get; set; }}\r\n /// <summary>\r\n " +
$"/// Net weight in kg. of fruit or vegitable item\r\n /// </summary>\r\n " +
$"public double NetWeightOnDocument {{ get; set; }}\r\n /// <summary>\r\n " +
$"/// Gross weight in kg. of fruit or vegitable item\r\n /// </summary>\r\n " +
$"public double GrossWeightOnDocument {{ get; set; }}\r\n}}\r\n\r\n🧾 Output Requirements\r\n- " +
$"Output must be a single valid JSON object containing:\r\n- One Partner object\r\n- " +
$"One ShippingDocument object\r\n- A list of all related ShippingItem objects\r\n\r\n- " +
$"Primary keys (Partner.Id, ShippingDocument.Id, ShippingItem.Id) should be auto-generated integers " +
$"(e.g. sequential: 1, 2, 3…).\r\n\r\n- When a field is missing or unclear, return it as an empty " +
$"string or 0 (depending on type).\r\nDo not omit any fields.\r\n\r\n- " +
$"All dates must be in ISO 8601 format (yyyy-MM-dd).\r\n\r\n🧭 Instructions to the AI\r\n" +
$"1. Analyze the provided PDF files one by one carefully.\r\n" +
$"2. Identify the Partner/Company details, " +
$"document identifiers, and each shipment item.\r\n" +
$"3. FruitBank is not a partner! Always look for THE OTHER partner on the document. \r\n " +
$"4. Generate a complete hierarchical JSON of ALL received documents in ONE JSON structure according to the " +
$"data model above.\r\n5. Do not include any explanations or text outside the JSON output. " +
$"Only return the structured JSON.\r\n" +
$"6. A teljes ShippingItem.Name-et tedd bele a ShippingItem.HungarianName-be " +
$"és a zöldség vagy gyümölcs nevét fordítsd le magyarra!\r\n" +
$"7. A ShippingDocument-et tedd bele a Partner entitásba!\r\n" +
$"8. ShippingItem-eket tedd bele a ShippingDocument-be!\r\n" +
$"9. Do not assume or modify any data, if you don't find a value, return null, if you find a value, keep it unmodified.";
} }
public class UploadModel public class UploadModel