Mango.Nop.Plugins/Nop.Plugin.Misc.AIPlugin/Services/AICalculationService.cs

245 lines
11 KiB
C#

using Nop.Core;
using Nop.Core.Domain.Customers;
using Nop.Core.Domain.Orders;
using Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Nop.Plugin.Misc.FruitBankPlugin.Services
{
public class AICalculationService
{
private readonly CerebrasAPIService _cerebrasApiService;
private readonly OpenAIApiService _openAIApiService;
private readonly IStoreContext _storeContext;
private readonly FruitBankDbContext _fruitBankDbContext;
private readonly StockTakingDbContext _stockTakingDbContext;
public AICalculationService(
CerebrasAPIService cerebrasApiService,
IStoreContext storeContext,
OpenAIApiService openAIApiService,
FruitBankDbContext fruitBankDbContext,
StockTakingDbContext stockTakingDbContext)
{
_cerebrasApiService = cerebrasApiService;
_storeContext = storeContext;
_openAIApiService = openAIApiService;
_fruitBankDbContext = fruitBankDbContext;
_stockTakingDbContext = stockTakingDbContext;
}
private record OrderSummary(
string Partner,
decimal Total,
bool Audited,
[property: JsonPropertyName("weight_ok")] bool? WeightOk,
[property: JsonPropertyName("invoice_created")] bool? InvoiceCreated,
bool? Completed);
private record StockSummary(
string Product,
int Diff,
bool Consistent);
public async Task<string> GetWelcomeMessageAsync(Customer customer)
{
// ── Parallel data fetching ────────────────────────────────────────────────
var storeTask = _storeContext.GetCurrentStoreAsync();
var lastStockTakingTask = _stockTakingDbContext.StockTakings.GetAll()
.OrderByDescending(st => st.Created)
.FirstOrDefaultAsync();
var secondLastStockTakingTask = _stockTakingDbContext.StockTakings.GetAll(true)
.OrderByDescending(st => st.Created)
.Skip(1)
.FirstOrDefaultAsync();
var allOrdersTask = _fruitBankDbContext.OrderDtos.GetAll(true).ToListAsync();
var weatherTask = GetWeatherAsync(); // see method below
await Task.WhenAll(
storeTask,
lastStockTakingTask,
secondLastStockTakingTask,
allOrdersTask,
weatherTask);
var store = await storeTask;
var lastStockTaking = await lastStockTakingTask;
var secondLastStockTaking = await secondLastStockTakingTask;
var allOrders = await allOrdersTask;
var weatherInfo = await weatherTask;
// ── Null guards ───────────────────────────────────────────────────────────
if (lastStockTaking == null || secondLastStockTaking == null)
return await _openAIApiService.GetSimpleResponseAsync(
$"You are a helpful assistant. Greet {customer.FirstName} warmly in Hungarian and let them know there is no stock taking data available yet.",
"Hello") ?? string.Empty;
// ── Today's orders ────────────────────────────────────────────────────────
var today = DateTime.Now.Date;
var allTodaysOrders = allOrders
.Where(order => order.DateOfReceiptOrCreated.Date == today)
.ToList();
int todaysOrderNumber = allTodaysOrders.Count;
// ── Stock taking summary ──────────────────────────────────────────────────
var lastStockTakingList = await _stockTakingDbContext.StockTakingItems
.GetAll()
.Where(item => item.StockTakingId == lastStockTaking.Id)
.ToListAsync();
var problematicStockItems = lastStockTakingList
.Where(item => item.QuantityDiff != 0)
.ToList();
var productIds = problematicStockItems.Select(i => i.ProductId).ToList();
var allProductHistories = await _fruitBankDbContext.StockQuantityHistories
.GetAll()
.Where(ph => productIds.Contains(ph.ProductId)
&& ph.CreatedOnUtc > secondLastStockTaking.Created.ToUniversalTime())
.ToListAsync();
var historiesByProduct = allProductHistories
.GroupBy(ph => ph.ProductId)
.ToDictionary(g => g.Key, g => g.Sum(ph => ph.QuantityAdjustment));
var productDtos = await _fruitBankDbContext.ProductDtos.GetByIdsAsync(productIds);
var fy = productDtos.ToAsyncEnumerable();
var productDtosDictionary = await fy.ToDictionaryAsync(p => p.Id, p => p.Name);
var stockSummaries = new List<StockSummary>();
foreach (var stockItem in problematicStockItems)
{
var formerDetails = secondLastStockTaking.StockTakingItems
.FirstOrDefault(sti => sti.ProductId == stockItem.ProductId);
if (formerDetails == null) continue;
int salesAdjustmentSum = historiesByProduct.GetValueOrDefault(stockItem.ProductId, 0);
string productName = productDtosDictionary.GetValueOrDefault(stockItem.ProductId, "sfgh");
int adjustedFormerValue = formerDetails.OriginalStockQuantity + formerDetails.QuantityDiff;
int expectedCurrent = adjustedFormerValue - salesAdjustmentSum;
int actualCurrent = stockItem.OriginalStockQuantity + stockItem.QuantityDiff;
bool isConsistent = expectedCurrent == actualCurrent;
var productDto = _fruitBankDbContext.ProductDtos.GetById(stockItem.ProductId);
stockSummaries.Add(new StockSummary(
Product: productName,
Diff: stockItem.QuantityDiff,
Consistent: isConsistent));
}
// ── Order summary ─────────────────────────────────────────────────────────
var orderSummaries = allTodaysOrders.Select(order =>
{
var audited = order.IsAllOrderItemAudited;
return new OrderSummary(
Partner: order.Customer.Company,
Total: order.OrderTotal,
Audited: audited,
WeightOk: audited ? order.IsValidMeasuringValues() : null,
InvoiceCreated: audited ? order.GenericAttributes.Any(a => a.Key == "InnVoiceOrderTableId") : null,
Completed: audited ? order.OrderStatus == OrderStatus.Complete : null);
}).ToList();
// ── System message ────────────────────────────────────────────────────────
var serializerOptions = new JsonSerializerOptions { WriteIndented = false, PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
var systemMessage = $"""
You are a helpful assistant of a webshop called {store.Name}, of the company {store.CompanyName}.
You work in the administration area with the ADMIN user whose name is {customer.FirstName}.
Current date and time: {DateTime.Now}.
When the user greets you, respond with a warm HUNGARIAN welcome message. Reference the time of day
naturally and make it pleasant and motivating to start the workday.
Confirm that the AI connection is established and you are ready to assist.
After the greeting, give a concise Hungarian morning briefing based on the data below.
Keep it brief — only highlight things that need attention or are noteworthy.
Do not list every order one by one; summarize and flag issues.
--- CURRENT WEATHER ---
{weatherInfo}
--- ORDER DATA FIELD GUIDE ---
audited: all items have been physically measured/checked
weight_ok: measured weights within acceptable thresholds (null if not audited)
invoice_created: order sent to InnVoice invoicing system (null if not audited)
completed: order status is Complete (null if not audited)
⚠ Flag: audited=true but completed=false → needs attention
⚠ Flag: audited=true but invoice_created=false → needs attention
--- STOCK DATA FIELD GUIDE ---
diff: quantity difference found during stock taking (non-zero = discrepancy)
consistent: discrepancy explainable by sales history since last stock taking
⚠ Flag: consistent=false → serious unexplained inventory discrepancy, highlight clearly
--- TODAY'S ORDERS ({todaysOrderNumber} total) ---
{JsonSerializer.Serialize(orderSummaries, serializerOptions)}
--- LAST STOCK TAKING DISCREPANCIES ({stockSummaries.Count} items) ---
{JsonSerializer.Serialize(stockSummaries, serializerOptions)}
""";
const string userMessage = "Hello";
var response = await _openAIApiService.GetSimpleResponseAsync(systemMessage, userMessage) ?? string.Empty;
return response;
}
// ── Weather helper ────────────────────────────────────────────────────────────
private async Task<string> GetWeatherAsync()
{
try
{
// TODO: move these to config/appsettings
const string apiKey = "de87827f73de5a204c61e1412a0e0b4e";
const string city = "Budapest"; // change to your city
const string units = "metric";
const string lang = "hu";
using var http = new HttpClient();
var url = $"https://api.openweathermap.org/data/2.5/weather?q={city}&appid={apiKey}&units={units}&lang={lang}";
var json = await http.GetStringAsync(url);
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
var description = root.GetProperty("weather")[0].GetProperty("description").GetString();
var temp = root.GetProperty("main").GetProperty("temp").GetDouble();
var feelsLike = root.GetProperty("main").GetProperty("feels_like").GetDouble();
var humidity = root.GetProperty("main").GetProperty("humidity").GetInt32();
return $"{description}, {temp:F1}°C (feels like {feelsLike:F1}°C), humidity: {humidity}%";
}
catch (Exception ex)
{
Console.WriteLine(ex + "Weather fetch failed, skipping weather data.");
return "Weather data unavailable.";
}
}
}
}