Files
boursecu_server/app/Http/Controllers/Site/ReportController.php
T
2026-07-24 16:49:42 +03:30

712 lines
26 KiB
PHP

<?php
namespace App\Http\Controllers\Site;
use App\Http\Controllers\Admin\PriceController;
use App\Http\Controllers\Controller;
use App\Mail\YourMailableClass;
use App\Models\Cnt;
use App\Models\Setting;
use App\Models\ConfigStatic;
use App\Models\Customer;
use App\Services\MontzarService;
use App\Services\ZaarboxPriceService;
use App\Models\Price;
use App\Models\PriceCandle;
use App\Models\PriceTrigger;
use App\Models\Product;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Mail;
use Morilog\Jalali\Jalalian;
use App\Services\ConfigService;
use App\Services\LivePriceCacheService;
use App\Services\PriceSnapshotService;
use App\Services\PriceSyncLogService;
use App\Services\ProductDisplayPriceService;
class ReportController extends Controller
{
public function candleChart()
{
$candle = PriceCandle::select(["start",
"high",
"low",
"end",
"date_at"])->whereDate('date_at', '<=', date("Y-m-d"))
->whereDate('date_at', '>', Carbon::now()->subDays(30)->format("Y-m-d"))
->get()->toArray();
$a = [];
foreach ($candle as $c) {
$create['date'] = $c["date_at"];
unset($c["date_at"]);
$create['candle'] = array_values($c);
$a[] = array_values($create);
}
return response()->json($a);
}
public function updatePrise()
{
$difference = (float) (Setting::where('key', 'fee_single')->value('value') ?? config('fee.single', 0));
$lastPrice = Price::latest()->first();
if (!$lastPrice) {
return response()->json();
}
$PriceTriggers = PriceTrigger::where("is_active", true)->get();
foreach ($PriceTriggers as $PriceTrigger) {
$priseGold = $PriceTrigger->dirction == 1
? $lastPrice->price - $difference
: ($PriceTrigger->dirction == 2 ? $lastPrice->price + $difference : $lastPrice->price);
if (eval('return $priseGold ' . $PriceTrigger->price_condition . ' $PriceTrigger->target_price;')) {
$customer = Customer::find($PriceTrigger->customer_id);
if (!$customer) {
continue;
}
$message = "قیمت طلا به " . number_format($priseGold, -1) . " در هر گرم رسید\n" .$this->Config('name_fa');
if ($PriceTrigger->action == 'email') {
Mail::to("$customer->email")->send(new YourMailableClass($message, '', '', '', ''));
} elseif ($PriceTrigger->action == 'sms') {
if (self::isSmsNotifEnabled('sms_notif_price_trigger')) {
$this->send($customer->mobile, $message);
}
}
$PriceTrigger->is_active = false;
$PriceTrigger->triggered_at = Carbon::now();
$PriceTrigger->save();
}
}
return response()->json();
}
public function upDownPrice()
{
return $this->buildUpDownPrices();
}
protected function buildUpDownPrices(): array
{
$config = app(ConfigService::class);
$diffBuy = $config->displayBuyPriceDiffRial();
$diffSell = $config->displaySellPriceDiffRial();
$extremes = $config->resolvedDailyBaseExtremes();
$up = $extremes['up'];
$down = $extremes['down'];
if ($up > 0 && $down > 0) {
return [
'sell' => max(0, (int) ceil($down - $diffSell)),
'buy' => (int) ceil($up + $diffBuy),
];
}
if (!$config->getBool('show_last_price', false)) {
return ['sell' => 0, 'buy' => 0];
}
$price = Price::orderByDesc('id')->first();
$priceValue = $price ? (float) $price->price : 0.0;
if ($priceValue <= 0) {
return ['sell' => 0, 'buy' => 0];
}
return [
'sell' => max(0, (int) ceil($priceValue - $diffSell)),
'buy' => (int) ceil($priceValue + $diffBuy),
];
}
protected function appendUpPrices(array $payload, bool $saveFullCache = false): array
{
// status: open | closed (خارج از ساعت کاری) | no_quote (مظنه در دسترس نیست)
if (!isset($payload['status'])) {
if (!empty($payload['closed'])) {
$payload['status'] = 'closed';
} elseif ((int) ($payload['price'] ?? 0) <= 0 && (int) ($payload['buy'] ?? 0) <= 0) {
$payload['status'] = 'no_quote';
} else {
$payload['status'] = 'open';
}
}
$payload['up'] = $this->buildUpDownPrices();
$payload['last_historical'] = app(ProductDisplayPriceService::class)->resolveHistoricalBasePrice();
$payload['gold_ounce'] = $this->resolveGoldOuncePayload();
app(ProductDisplayPriceService::class)->cacheLivePricePayload($payload);
$isFreshPrice = $saveFullCache || request()->boolean('force_refresh');
// ارسال قیمت به کانال فقط روی «لحظهٔ تغییر قیمت» = مسیر force_refresh (همان چیزی که سرورِ
// حساس به قیمت صدا می‌زند و مبنای برادکست SSE است) — نه هر pulling عادی که کش برمی‌گرداند.
// بعد از پاسخ اجرا می‌شود تا تاخیری برای فراخوان نباشد. در کنسول صدا نمی‌زنیم چون tick()
// خودش مستقیماً broadcastIfEligible را اجرا می‌کند و afterResponse در دیمن اجرا نمی‌شود.
// خودِ سرویس با price_channel_min_seconds و تشخیص تغییر قیمت گیت می‌کند.
if ($isFreshPrice && (int) ($payload['price'] ?? 0) > 0 && ! app()->runningInConsole()) {
dispatch(function () {
app(\App\Services\PriceChannelBroadcastService::class)->broadcastIfEligible();
})->afterResponse();
}
if ($isFreshPrice && (int) ($payload['price'] ?? 0) > 0) {
$pricePayload = $payload;
if (app()->runningInConsole()) {
app(\App\Services\ConditionalOrderService::class)->evaluateFromLastPrice($pricePayload);
} else {
dispatch(function () use ($pricePayload) {
app(\App\Services\ConditionalOrderService::class)->evaluateFromLastPrice($pricePayload);
})->afterResponse();
}
}
if ($isFreshPrice) {
$cache = app(LivePriceCacheService::class);
$cache->markTick();
return $cache->put($payload);
}
return $payload;
}
protected function resolveGoldOuncePayload(): ?array
{
return app(ZaarboxPriceService::class)->clientGoldOuncePayload();
}
public function lastPrice(Request $request)
{
$config = app(ConfigService::class);
$cache = app(LivePriceCacheService::class);
$forceRefresh = $request->boolean('force_refresh');
if (! $forceRefresh) {
$cached = $cache->get();
if ($cached !== null) {
$cached['gold_ounce'] = $this->resolveGoldOuncePayload();
return $cached;
}
// فقط وقتی کران اخیراً tick زده — وگرنه API زنده (جلوگیری از قفل قیمت)
if ($cache->hasRecentTick()) {
$dbBuilt = $cache->buildFromDatabase();
if (($dbBuilt['price'] ?? 0) > 0) {
return $this->appendUpPrices($dbBuilt, false);
}
}
}
if ($config->isStore()) {
$storePayload = array_merge([
'closed' => false,
'time' => now(),
'main_product' => 0,
'zaarbox' => 0,
], $this->storeLastPrice($config));
if ($forceRefresh) {
$this->recordHalfHourPriceSnapshot(
(int) ($storePayload['buy'] ?? 0),
(string) ($storePayload['price_source'] ?? 'store'),
null,
(int) ($storePayload['price'] ?? $storePayload['buy'] ?? 0)
);
}
return $this->appendUpPrices($storePayload, $forceRefresh);
}
$closed = false;
$montzar_price = 0;
$zaarbox_price = 0;
$pr_price = 0;
$montzar_open_buy = false;
$montzar_open_sell = false;
$priceSource = null;
$priceValue = 0;
$toBool = function ($value) {
return filter_var($value, FILTER_VALIDATE_BOOLEAN);
};
$zeroResponse = function () use ($forceRefresh) {
return $this->appendUpPrices([
'sell' => 0,
'buy' => 0,
'price_last' => 0,
'price' => 0,
'main_product' => 0,
'montzar' => 0,
'zaarbox' => 0,
'closed' => true,
'time' => now(),
], $forceRefresh);
};
$priceProvider = $config->priceProvider();
/*
|--------------------------------------------------------------------------
| بستن قیمت طبق منطق قبلی
|--------------------------------------------------------------------------
*/
if ($toBool($config->Config('close_price'))) {
if ($config->isOutsideTradingHours()) {
$config->syncAppClosedFlag();
// نمایش قیمت صفحه اصلی حتی وقتی فروشگاه بسته است — معامله همچنان از سمت اپ بسته می‌ماند
if (! $config->shouldShowHomePriceWhenClosed()) {
return $zeroResponse();
}
} else {
Cnt::where('key', 'app_closed')->update(['value' => 'false']);
}
}
/*
|--------------------------------------------------------------------------
| Settings
|--------------------------------------------------------------------------
*/
$settings = Setting::whereIn('key', [
'main_product_price',
])->get();
$settingsMap = $settings->pluck('value', 'key');
$show_last_price = $config->getBool('show_last_price', false);
$main_product_price = $settingsMap->get('main_product_price');
/*
|--------------------------------------------------------------------------
| Montzar — فقط وقتی price_provider=montzar
|--------------------------------------------------------------------------
*/
if ($priceProvider === 'montzar' && !$closed) {
try {
$montzarBaseUrl = rtrim($config->Config('montzar_base_url') ?: 'https://montpar.ir', '/');
$montzarProductId = (int) ($config->Config('montzar_product_id') ?: 25);
$montzar = app(MontzarService::class)->productList();
if (isset($montzar['message'])) {
throw new \Exception('Provider message: ' . $montzar['message']);
}
$collection = collect($montzar);
$montzarProduct = $collection->firstWhere('id', $montzarProductId);
if (!$montzarProduct) {
throw new \Exception("Provider product not found. product_id={$montzarProductId}");
}
if (!isset($montzarProduct['sub_agent'])) {
throw new \Exception("Provider product has no sub_agent. product_id={$montzarProductId}");
}
$buy = (float) ($montzarProduct['sub_agent']['rate_buy_gram_price'] ?? 0);
$sell = (float) ($montzarProduct['sub_agent']['rate_sell_gram_price'] ?? 0);
if ($buy <= 0 || $sell <= 0) {
throw new \Exception("Provider invalid gram price. buy={$buy}, sell={$sell}");
}
$customPrice = [];
$customPrice['price'] = ($buy + $sell) / 2;
$customPrice['diff'] = ($buy - $sell) / 2;
$config->UpdateConfig('montzar_live_price', (float) $customPrice['price']);
$config->UpdateConfig('montzar_diff', (float) $customPrice['diff']);
$config->UpdateConfig('montzar_mazaneh', ((float) $customPrice['price'] * 4.331802));
$config->UpdateConfig(
'montzar_truly_buy_mazaneh',
$montzarProduct['sub_agent']['rate_buy_price'] ?? 0
);
$config->UpdateConfig(
'montzar_truly_sell_mazaneh',
$montzarProduct['sub_agent']['rate_sell_price'] ?? 0
);
$config->UpdateConfig(
'montzar_last_product_log',
json_encode($montzarProduct, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)
);
$montzarFlags = $config->montzarOpenFlags();
$montzar_open_buy = $montzarFlags['open_buy'];
$montzar_open_sell = $montzarFlags['open_sell'];
$montzar_price = (int) $customPrice['price'];
$config->UpdateConfig('montzar_closed', 'false');
} catch (\Throwable $e) {
$montzar_price = 0;
\Log::error('MONZAR PRICE ERROR', [
'message' => $e->getMessage(),
'montzar_product_id' => $montzarProductId ?? null,
'montzar_response' => $montzar ?? null,
]);
$config->applyMontzarSyncFailure();
if (!$config->getBool('montzar_closed', true)) {
$montzar_price = $config->getInt('montzar_live_price', 0);
}
}
}
/*
|--------------------------------------------------------------------------
| Zaarbox — price_provider=zaarbox + settings.zaarbox_price_key
|--------------------------------------------------------------------------
*/
if ($priceProvider === 'zaarbox' && !$closed) {
$zaarbox = app(ZaarboxPriceService::class)->fetchPrice();
$zaarbox_price = (int) ($zaarbox['order_price'] ?? 0);
}
/*
|--------------------------------------------------------------------------
| Main Product — settings.main_product سپس const
|--------------------------------------------------------------------------
*/
$pr_product = null;
if ($priceProvider === 'main_product' || $toBool($main_product_price)) {
$pr_product = $config->mainProduct();
}
if ($pr_product && !$closed) {
if ($priceProvider === 'main_product') {
if (Product::hasStoredPriceColumns()) {
app(ZaarboxPriceService::class)->syncProductFromApi($pr_product->fresh());
$pr_product->refresh();
$pr_price = (float) $pr_product->price + (float) ($pr_product->price_profit ?? 0);
} else {
$live = app(ZaarboxPriceService::class)->fetchLiveOrderPrice(
$pr_product->reading_method
);
$pr_price = (float) ($live['order_price'] ?? 0);
}
} else {
$pr_price = Product::hasStoredPriceColumns()
? (float) $pr_product->price + (float) ($pr_product->price_profit ?? 0)
: 0.0;
}
}
/*
|--------------------------------------------------------------------------
| انتخاب قیمت اصلی (همان price_provider)
|--------------------------------------------------------------------------
*/
if ($priceProvider === 'zaarbox' && $zaarbox_price > 0 && !$closed) {
$priceValue = $zaarbox_price;
$priceSource = 'zaarbox';
} elseif ($priceProvider === 'montzar' && ($montzar_price ?? 0) !== 0 && !$closed) {
$priceValue = (int) $montzar_price;
$priceSource = 'montzar';
} elseif ($priceProvider === 'main_product' && ($pr_price ?? 0) !== 0 && !$closed) {
$priceValue = $pr_price;
$priceSource = 'main_product';
} else {
$priceValue = 0;
$priceSource = null;
}
if ($priceValue <= 0) {
$lastPrice = Price::latest()->first();
$storedPrice = $lastPrice ? (float) $lastPrice->price : 0.0;
// fallback فقط با show_last_price تصمیم‌گیری می‌شود؛ force_refresh (تیک کران) نباید آن را override کند.
// آستانهٔ ثابت قبلی (۳۰ میلیون) برای طلا بود و محصولات ارزان‌تر مثل کاتد مس/نقره را اشتباهاً «بدون مظنه» می‌کرد.
if ($storedPrice > 0 && $show_last_price) {
$priceValue = $storedPrice;
$priceSource = 'database_last';
} else {
// مظنه زنده نداریم و show_last_price خاموش است → نه «بسته»، بلکه «مظنه در دسترس نیست»
return $this->appendUpPrices([
'sell' => 0,
'buy' => 0,
'price_last' => 0,
'price' => 0,
'main_product' => $pr_price ?? 0,
'montzar' => $montzar_price ?? 0,
'zaarbox' => $zaarbox_price ?? 0,
'closed' => false,
'status' => 'no_quote',
'time' => now(),
], $forceRefresh);
}
}
/*
|--------------------------------------------------------------------------
| محاسبه sell / buy
|--------------------------------------------------------------------------
*/
$display = $config->buildTradeDisplayPrices(
(float) $priceValue,
$priceSource,
[
'montzar_open_buy' => $montzar_open_buy,
'montzar_open_sell' => $montzar_open_sell,
]
);
$sell = $display['sell'];
$buy = $display['buy'];
$priceLast = $display['priceLast'];
if (!$closed && $priceValue > 0) {
$config->trackDailyPriceExtreme((float) $priceValue);
}
if ($forceRefresh) {
$this->recordHalfHourPriceSnapshot(
(int) $buy,
(string) ($priceSource ?: ($priceProvider ?: 'legacy')),
$this->resolvePriceSourceUrl($priceSource, $config),
(int) $priceValue
);
}
return $this->appendUpPrices([
'sell' => $sell,
'buy' => $buy,
'price_last' => $priceLast,
'price' => $priceValue,
'price_source' => $priceSource,
'main_product' => $pr_price ?? 0,
'montzar' => $montzar_price ?? 0,
'zaarbox' => $zaarbox_price ?? 0,
'closed' => false,
'time' => now(),
], $forceRefresh);
}
protected function recordHalfHourPriceSnapshot(
int $buyPrice,
string $source,
?string $sourceUrl,
int $referencePrice
): void {
if ($buyPrice <= 0 && $referencePrice <= 0) {
return;
}
// فقط fallback دیتابیس — مظنهٔ زنده نیست؛ لاگ/رکورد جدید نساز (همان قیمت دیشب تکرار می‌شود).
if ($source === 'database_last') {
return;
}
$reference = $referencePrice > 0 ? $referencePrice : $buyPrice;
app(PriceSyncLogService::class)->recordSnapshotIfDue(
$source,
$sourceUrl,
$reference,
PriceSnapshotService::HALF_HOUR_MINUTES
);
app(PriceSnapshotService::class)->recordHalfHourBuyPrice($buyPrice);
}
protected function resolvePriceSourceUrl(?string $priceSource, ConfigService $config): ?string
{
if ($priceSource === 'zaarbox') {
return app(ZaarboxPriceService::class)->apiUrl();
}
if ($priceSource === 'montzar') {
return rtrim($config->Config('montzar_base_url') ?: 'https://montpar.ir', '/');
}
return null;
}
/**
* فروشگاه — منطق lastPrice بر اساس price_provider
*/
protected function storeLastPrice(ConfigService $config): array
{
$talaIr = 0;
$montzar = 0;
$montzarEnabled = $config->priceProvider() === 'montzar';
if ($montzarEnabled) {
try {
$montzarProductId = (int) ($config->Config('montzar_product_id') ?: 25);
$montzarProducts = app(MontzarService::class)->productList();
if (isset($montzarProducts['message'])) {
throw new \Exception('Montzar message: ' . $montzarProducts['message']);
}
$montzarProduct = collect($montzarProducts)->firstWhere('id', $montzarProductId);
if (! $montzarProduct || ! isset($montzarProduct['sub_agent'])) {
throw new \Exception("Montzar product not found. product_id={$montzarProductId}");
}
$buy = (float) ($montzarProduct['sub_agent']['rate_buy_gram_price'] ?? 0);
$sell = (float) ($montzarProduct['sub_agent']['rate_sell_gram_price'] ?? 0);
if ($buy <= 0 || $sell <= 0) {
throw new \Exception("Montzar invalid gram price. buy={$buy}, sell={$sell}");
}
$customPrice = [
'price' => ($buy + $sell) / 2,
'diff' => ($buy - $sell) / 2,
];
$config->UpdateConfig('montzar_live_price', (float) $customPrice['price']);
$config->UpdateConfig('montzar_diff', (float) $customPrice['diff']);
$config->UpdateConfig('montzar_mazaneh', ((float) $customPrice['price'] * 4.331802));
$config->UpdateConfig('montzar_truly_buy_mazaneh', $montzarProduct['sub_agent']['rate_buy_price'] ?? 0);
$config->UpdateConfig('montzar_truly_sell_mazaneh', $montzarProduct['sub_agent']['rate_sell_price'] ?? 0);
$config->UpdateConfig(
'montzar_last_product_log',
json_encode($montzarProduct, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)
);
$montzarFlags = $config->montzarOpenFlags();
$openBuy = $montzarFlags['open_buy'];
$openSell = $montzarFlags['open_sell'];
$montzar = (int) $customPrice['price'];
$config->UpdateConfig('montzar_closed', 'false');
} catch (\Throwable $e) {
$montzar = 0;
$config->applyMontzarSyncFailure();
if (!$config->getBool('montzar_closed', true)) {
$montzar = $config->getInt('montzar_live_price', 0);
}
\Log::error('STORE MONZAR PRICE ERROR', ['message' => $e->getMessage()]);
}
}
if ($montzarEnabled && $montzar !== 0) {
$priceValue = $montzar;
$priceSource = 'montzar';
} else {
$priceValue = $talaIr;
$priceSource = 'tala_ir';
}
$lastStored = Price::latest()->first();
if ($priceValue <= 0 && $lastStored && (float) $lastStored->price > 0) {
$priceValue = (float) $lastStored->price;
$priceSource = 'database_last';
}
if ($priceValue <= 0) {
return [];
}
$display = $config->buildTradeDisplayPrices(
(float) $priceValue,
$priceSource
);
$response = [
'sell' => $display['sell'],
'buy' => $display['buy'],
'price_last' => $display['priceLast'],
'price' => $priceValue,
'price_source' => $priceSource,
'tala_ir' => $talaIr,
];
if ($montzarEnabled) {
$response['montzar'] = $montzar;
}
return $response;
}
public function check()
{
$service = Http::
withHeaders([
'accept' => 'application/json',
'Authorization' => 'Bearer ohxsrbjagqvbt0glaygt'
])->get('https://studio.persianapi.com/index.php/web-service/common/gold-currency-coin?format=json&limit=30&page=1')
->json();
$list = [];
if (isset($service['result'])) {
foreach ($service['result'] as $item) {
if ($item['key'] == 137120 || $item['key'] == 137119) {
$list[] = $item;
if ($item['key'] == 137120) {
(new PriceController())->store(new Request(["price" => $item['price']]));
}
}
}
Cnt::where('key', 'price')->update(['value_json' => $list]);
}
}
public function test()
{
$a = '{{12, 23, {4, 0, {1}, {1}}}, 0, {1}}';
$b = str_split($a);
$c = [];
$str = "";
foreach ($b as $item) {
if ($item != " ") {
print_r($item);
print_r('++');
if ($item == ',' || $item == '}') {
if (!in_array($c[count($c) - 1], ['{', '}'])) {
$c[count($c) - 1] += (int)$str;
print_r((int)$str);
print_r('^^');
} else {
$c[] = $str;
print_r($c);
print_r('&&');
}
$str = "";
} elseif (in_array($item, ['{', '}'])) {
$c[] = $item;
print_r($c);
print_r('**');
} else {
$str .= $item;
print_r($str);
print_r('@@');
}
}
}
dd($c);
$d = [];
$e = [];
foreach ($c as $item) {
if ($item == '}') {
print_r($d[count($d) - 1]);
print_r('***');
$e[] = $d[count($d) - 1];
} else {
$d[] = $item;
print_r($d);
print_r('-----');
}
}
dd($e);
}
}