245 lines
11 KiB
C#
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.";
|
|
}
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|