fix: centralize price totals server-side; batch API call; 5-min total cache
This commit is contained in:
+111
-48
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user