fix: centralize price totals server-side; batch API call; 5-min total cache

This commit is contained in:
dadaloop82
2026-05-07 18:55:37 +00:00
parent a01ca583ea
commit 6c342a412b
3 changed files with 179 additions and 167 deletions
+111 -48
View File
@@ -6607,50 +6607,103 @@ function getShoppingPrice(PDO $db): void {
/**
* GET /api/?action=get_all_shopping_prices
* POST body: { items: [{name, quantity, unit, default_quantity, package_unit}], country, currency, lang, force_refresh }
* POST body: { items: [{name}], country, currency, lang, force_refresh }
* qty/unit are resolved SERVER-SIDE from smart_shopping_cache not trusted from client.
*
* Returns: { success, prices: { name priceEntry }, total, total_label }
* Returns: { success, prices: { name priceEntry }, total, total_label, from_total_cache }
*/
function getAllShoppingPrices(PDO $db): void {
$input = json_decode(file_get_contents('php://input'), true) ?? [];
$items = $input['items'] ?? [];
$country = trim($input['country'] ?? env('PRICE_COUNTRY', 'Italia'));
$currency= trim($input['currency'] ?? env('PRICE_CURRENCY', 'EUR'));
$lang = trim($input['lang'] ?? 'it');
$input = json_decode(file_get_contents('php://input'), true) ?? [];
$clientItems = $input['items'] ?? [];
$country = trim($input['country'] ?? env('PRICE_COUNTRY', 'Italia'));
$currency = trim($input['currency'] ?? env('PRICE_CURRENCY', 'EUR'));
$lang = trim($input['lang'] ?? 'it');
$forceRefresh = !empty($input['force_refresh']);
$updateMonths = (int)env('PRICE_UPDATE_MONTHS', '3');
if (empty($items)) {
if (empty($clientItems)) {
echo json_encode(['success' => true, 'prices' => [], 'total' => 0, 'total_label' => _formatPrice(0, $currency)]);
return;
}
$cache = _loadPriceCache();
$now = time();
$maxAge = $updateMonths * 30 * 86400;
$prices = [];
$total = 0.0;
$missing = [];
// ── Resolve qty/unit from server-side smart cache (source of truth) ──────
$smartItems = [];
$smartCacheFile = __DIR__ . '/../data/smart_shopping_cache.json';
if (file_exists($smartCacheFile)) {
$raw = file_get_contents($smartCacheFile);
if ($raw) {
$sc = json_decode($raw, true);
if ($sc && isset($sc['items'])) $smartItems = $sc['items'];
}
}
// Build lookup: lowercase name/shopping_name → smart item
$smartByName = [];
foreach ($smartItems as $si) {
$smartByName[mb_strtolower($si['name'] ?? '')] = $si;
if (!empty($si['shopping_name'])) {
$smartByName[mb_strtolower($si['shopping_name'])] = $si;
}
}
// Build canonical items array using server-side qty/unit
$items = [];
foreach ($clientItems as $ci) {
$name = trim($ci['name'] ?? '');
if ($name === '') continue;
$si = $smartByName[mb_strtolower($name)] ?? null;
$items[] = [
'name' => $name,
'quantity' => (float)(($si['suggested_qty'] ?? $si['buy_qty'] ?? null) ?? ($ci['quantity'] ?? 1)),
'unit' => trim(($si['suggested_unit'] ?? $si['unit'] ?? null) ?? ($ci['unit'] ?? 'conf')),
'default_quantity' => (float)(($si['default_qty'] ?? null) ?? ($ci['default_quantity'] ?? 0)),
'package_unit' => trim(($si['package_unit'] ?? null) ?? ($ci['package_unit'] ?? '')),
];
}
// ── 5-minute server-side total cache ──────────────────────────────────────
// Key = hash of item names + resolved qty/unit + country (not force_refresh)
$totalCachePath = __DIR__ . '/../data/shopping_total_cache.json';
$totalCacheKey = md5(json_encode(array_map(
fn($i) => [$i['name'], $i['quantity'], $i['unit']],
$items
)) . $country . $currency);
if (!$forceRefresh && file_exists($totalCachePath)) {
$tc = json_decode(file_get_contents($totalCachePath), true) ?? [];
if (isset($tc[$totalCacheKey]) && (time() - ($tc[$totalCacheKey]['ts'] ?? 0)) < 300) {
$cached = $tc[$totalCacheKey]['result'];
$cached['from_total_cache'] = true;
echo json_encode($cached, JSON_UNESCAPED_UNICODE);
return;
}
}
// ── Price computation ─────────────────────────────────────────────────────
$priceCache = _loadPriceCache();
$now = time();
$maxAge = $updateMonths * 30 * 86400;
$prices = [];
$total = 0.0;
$missing = [];
// First pass: serve from cache
foreach ($items as $item) {
$name = trim($item['name'] ?? '');
$qty = (float)($item['quantity'] ?? 1);
$unit = trim($item['unit'] ?? 'pz');
$defQty = (float)($item['default_quantity'] ?? 0);
$pkgUnit = trim($item['package_unit'] ?? '');
if (empty($name)) continue;
$name = $item['name'];
$qty = $item['quantity'];
$unit = $item['unit'];
$defQty = $item['default_quantity'];
$pkgUnit = $item['package_unit'];
$key = _priceKey($name, $country);
if (!$forceRefresh && isset($cache[$key])) {
$age = $now - ($cache[$key]['cached_at'] ?? 0);
if (!$forceRefresh && isset($priceCache[$key])) {
$age = $now - ($priceCache[$key]['cached_at'] ?? 0);
if ($age < $maxAge) {
$entry = $cache[$key];
$entry = $priceCache[$key];
$est = _calcEstimatedTotal($entry['price_per_unit'], $entry['unit_label'] ?? '', $qty, $unit, $defQty, $pkgUnit);
$prices[$name] = array_merge($entry, [
'estimated_total' => $est,
'estimated_total' => $est,
'estimated_total_label' => _formatPrice($est, $currency),
'from_cache' => true,
'from_cache' => true,
]);
$total += $est ?? 0;
continue;
@@ -6661,30 +6714,30 @@ function getAllShoppingPrices(PDO $db): void {
// Second pass: fetch missing from AI (sequential to avoid rate limits)
foreach ($missing as $item) {
$name = trim($item['name'] ?? '');
$qty = (float)($item['quantity'] ?? 1);
$unit = trim($item['unit'] ?? 'pz');
$defQty = (float)($item['default_quantity'] ?? 0);
$pkgUnit = trim($item['package_unit'] ?? '');
$name = $item['name'];
$qty = $item['quantity'];
$unit = $item['unit'];
$defQty = $item['default_quantity'];
$pkgUnit = $item['package_unit'];
$key = _priceKey($name, $country);
$priceData = _fetchPriceFromAI($name, $country, $currency, $lang);
if ($priceData && $priceData['price_per_unit'] !== null) {
$entry = [
'name' => $name,
'price_per_unit'=> (float)$priceData['price_per_unit'],
'unit_label' => $priceData['unit_label'] ?? 'kg',
'currency' => $currency,
'source_note' => $priceData['source_note'] ?? '',
'country' => $country,
'cached_at' => $now,
'name' => $name,
'price_per_unit' => (float)$priceData['price_per_unit'],
'unit_label' => $priceData['unit_label'] ?? 'pz',
'currency' => $currency,
'source_note' => $priceData['source_note'] ?? '',
'country' => $country,
'cached_at' => $now,
];
$cache[$key] = $entry;
$priceCache[$key] = $entry;
$est = _calcEstimatedTotal($entry['price_per_unit'], $entry['unit_label'], $qty, $unit, $defQty, $pkgUnit);
$prices[$name] = array_merge($entry, [
'estimated_total' => $est,
'estimated_total' => $est,
'estimated_total_label' => _formatPrice($est, $currency),
'from_cache' => false,
'from_cache' => false,
]);
$total += $est ?? 0;
} else {
@@ -6692,15 +6745,25 @@ function getAllShoppingPrices(PDO $db): void {
}
}
_savePriceCache($cache);
_savePriceCache($priceCache);
$total = round($total, 2);
echo json_encode([
'success' => true,
'prices' => $prices,
'total' => $total,
'total_label' => _formatPrice($total, $currency),
]);
$total = round($total, 2);
$result = [
'success' => true,
'prices' => $prices,
'total' => $total,
'total_label' => _formatPrice($total, $currency),
'from_total_cache' => false,
];
// Persist to total cache
$tc = file_exists($totalCachePath) ? (json_decode(file_get_contents($totalCachePath), true) ?? []) : [];
// Keep cache small: max 10 keys (different list configurations)
if (count($tc) >= 10) $tc = array_slice($tc, -9, null, true);
$tc[$totalCacheKey] = ['ts' => $now, 'result' => $result];
file_put_contents($totalCachePath, json_encode($tc, JSON_UNESCAPED_UNICODE));
echo json_encode($result, JSON_UNESCAPED_UNICODE);
}
/**