fix(price): read last quote and daily extremes from the active provider

A stray montzar tick landed in the prices table, which stores no source, so
the database_last fallback served a ~168M quote on a shop whose Zaarbox board
was at ~217M. Last price and today's high/low now come from price_sync_logs
filtered by price_provider, the database fallback no longer counts as today's
price, and daily extremes reset when the provider changes.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-31 09:45:37 +03:30
co-authored by Cursor
parent 1b73305d6f
commit 142cfd7661
5 changed files with 98 additions and 37 deletions
+76 -23
View File
@@ -5,9 +5,11 @@ namespace App\Services;
use App\Models\Cnt;
use App\Models\Price;
use App\Models\PriceCandle;
use App\Models\PriceSyncLog;
use App\Models\Product;
use App\Models\Setting;
use Carbon\Carbon;
use Illuminate\Support\Facades\Cache;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
class ConfigService
@@ -37,13 +39,65 @@ class ConfigService
}
/**
* آخرین قیمت ذخیره‌شده در جدول prices.
* آخرین قیمت مرجع ثبت‌شده از همین provider (price_sync_logs).
*
* جدول prices منبع قیمت را نگه نمی‌دارد؛ یک تیک از provider دیگر
* (مثلاً montzar روی فروشگاهی که تابلوی zaarbox دارد) تا روزها
* «آخرین قیمت» می‌ماند و مظنه پرت نشان داده می‌شود.
*/
public function referenceQuotePrice(): float
public function lastProviderQuotePrice(): int
{
$last = Price::query()->orderByDesc('id')->value('price');
$provider = $this->priceProvider();
return $last !== null ? (float) $last : 0.0;
return (int) Cache::remember(
'price:last_provider_quote:' . $provider,
10,
function () use ($provider) {
try {
return (int) PriceSyncLog::query()
->where('source', $provider)
->where('liquid_gold_price', '>', 0)
->orderByDesc('id')
->value('liquid_gold_price');
} catch (\Throwable $e) {
return 0;
}
}
);
}
/**
* کمینه/بیشینهٔ قیمت مرجع امروز — فقط ردیف‌های همین provider.
*
* @return array{min: int, max: int}
*/
public function providerTodayQuoteExtremes(): array
{
$provider = $this->priceProvider();
$row = Cache::remember(
'price:provider_today_extremes:' . $provider,
10,
function () use ($provider) {
try {
$stats = PriceSyncLog::query()
->where('source', $provider)
->where('liquid_gold_price', '>', 0)
->whereDate('created_at', now()->toDateString())
->selectRaw('MIN(liquid_gold_price) as min_price, MAX(liquid_gold_price) as max_price')
->first();
return [
'min' => (int) ($stats->min_price ?? 0),
'max' => (int) ($stats->max_price ?? 0),
];
} catch (\Throwable $e) {
return ['min' => 0, 'max' => 0];
}
}
);
return is_array($row) ? $row : ['min' => 0, 'max' => 0];
}
/** مظنهٔ مثبت قابل استفاده است — محدودیت انحراف نسبت به DB برداشته شد. */
@@ -542,18 +596,24 @@ class ConfigService
$currentUp = (float) ($upSetting->value ?: 0);
$currentDown = (float) ($downSetting->value ?: 0);
$anchor = $currentUp > 0 ? $currentUp : $this->referenceQuotePrice();
if ($anchor > 0 && ! $this->isPlausibleQuotePrice((float) $priceInt, $anchor)) {
return;
}
if ($extremeDate !== $today) {
// سقف و کف روز باید از یک منبع باشند؛ وگرنه تیک provider دیگر
// بازهٔ جعلی می‌سازد (مثلاً سقف تابلو و کف montzar).
$provider = $this->priceProvider();
$extremeSource = trim((string) $this->getString('price_extreme_source', ''));
if ($extremeDate !== $today || ($extremeSource !== '' && $extremeSource !== $provider)) {
$this->UpdateConfig('up_price', $priceInt);
$this->UpdateConfig('down_price', $priceInt);
$this->UpdateConfig('price_extreme_date', $today);
$this->UpdateConfig('price_extreme_source', $provider);
return;
}
if ($extremeSource === '') {
$this->UpdateConfig('price_extreme_source', $provider);
}
if ($currentUp <= 0 || $priceInt > $currentUp) {
$this->UpdateConfig('up_price', $priceInt);
}
@@ -569,6 +629,12 @@ class ConfigService
*/
public function resolvedDailyBaseExtremes(): array
{
// تاریخچهٔ برچسب‌دار همین provider — سقف/کف بدون قاطی شدن با تیک منبع دیگر
$fromLog = $this->providerTodayQuoteExtremes();
if ($fromLog['max'] > 0 && $fromLog['min'] > 0) {
return ['up' => $fromLog['max'], 'down' => $fromLog['min']];
}
$up = $this->getInt('up_price', 0);
$down = $this->getInt('down_price', 0);
@@ -604,21 +670,8 @@ class ConfigService
[$up, $down] = [$down, $up];
}
// کف روز آلوده به مظنهٔ اشتباه provider (مثلاً ۱۶۸M) را دور بریز
if ($up > 0 && $down > 0 && ! $this->isPlausibleQuotePrice((float) $down, (float) $up)) {
$fromTable = $this->filteredTodayMinMax();
if ($fromTable['min'] > 0 && $this->isPlausibleQuotePrice((float) $fromTable['min'], (float) $up)) {
$down = $fromTable['min'];
} else {
$down = $up;
}
if ($this->getInt('down_price', 0) !== $down) {
$this->UpdateConfig('down_price', $down);
}
}
$storedDown = $this->getInt('down_price', 0);
if ($down > 0 && $down < $storedDown && $this->isPlausibleQuotePrice((float) $down, (float) max($up, 1))) {
if ($down > 0 && $down < $storedDown) {
$this->UpdateConfig('down_price', $down);
}