config()->Config('montzar_base_url') ?: 'https://montpar.ir', '/' ); } private function endpoint(string $path): string { return $this->baseUrl() . '/' . ltrim($path, '/'); } /** Pending HTTP با همان گزینه‌های پایدار (بدون پروکسی / IPv4 / HTTP1.1) */ private function http(float $timeout = 10) { $extraCurl = defined('CURLOPT_TCP_FASTOPEN') ? [(int) constant('CURLOPT_TCP_FASTOPEN') => 1] : []; return Http::timeout($timeout) ->acceptJson() ->withOptions(Controller::httpClientOptions($extraCurl, $timeout)); } public function montzarLogin(): string { try { $response = $this->http(10) ->post($this->endpoint('/server/auth/login'), [ 'mobile' => $this->config()->Config('montzar_mobile'), 'password' => $this->config()->Config('montzar_password'), 'provider' => 'customer', 'type_app' => 'client', ]) ->json(); } catch (\Throwable $e) { Log::error('Montzar login request failed', [ 'base_url' => $this->baseUrl(), 'error' => $e->getMessage(), ]); throw new UnprocessableEntityHttpException( 'سرویس موقتا در دسترس نیست، لطفاً با پشتیبانی تماس بگیرید.' ); } $token = is_array($response) ? ($response['access_token'] ?? $response['token'] ?? null) : null; if (!$token) { Log::error('Montzar login failed', [ 'base_url' => $this->baseUrl(), 'mobile' => $this->config()->Config('montzar_mobile'), 'response' => $response, ]); // 422 تا فرانت toast نشان دهد؛ جزئیات فنی فقط در لاگ می‌ماند throw new UnprocessableEntityHttpException( 'سرویس موقتا در دسترس نیست، لطفاً با پشتیبانی تماس بگیرید.' ); } Setting::updateOrCreate( ['key' => 'montzar_token'], ['value' => $token, 'is_active' => 1, 'can_edit' => 1] ); return $token; } public function getToken(): string { $token = trim((string) $this->config()->Config('montzar_token')); if (!$token) { $token = $this->montzarLogin(); } return $token; } public function productList(): array { $attempts = 3; $lastError = null; for ($attempt = 1; $attempt <= $attempts; $attempt++) { try { return $this->productListOnce(); } catch (\Throwable $e) { $lastError = $e; Log::warning('Montzar productList attempt failed', [ 'attempt' => $attempt, 'max' => $attempts, 'base_url' => $this->baseUrl(), 'error' => $e->getMessage(), ]); if ($attempt < $attempts) { usleep(250_000 * $attempt); } } } throw $lastError ?? new \RuntimeException('Montzar product list failed'); } private function productListOnce(): array { $token = $this->getToken(); try { $response = $this->http(7) ->withToken($token) ->get($this->endpoint('/server/customer/product/list')) ->throw() ->json(); } catch (\Throwable $e) { // توکن مرده / قطع شبکه — یک بار لاگین مجدد $token = $this->montzarLogin(); $response = $this->http(7) ->withToken($token) ->get($this->endpoint('/server/customer/product/list')) ->throw() ->json(); } if ((isset($response['message']) && $response['message'] === 'no authorised') || $response === null) { $token = $this->montzarLogin(); $response = $this->http(7) ->withToken($token) ->get($this->endpoint('/server/customer/product/list')) ->throw() ->json(); } if (!is_array($response)) { throw new \Exception('Montzar product list response is not array'); } return $response; } /** * @return array */ public function listProductOptions(): array { try { $response = $this->productList(); } catch (\Throwable $e) { Log::warning('Montzar product options failed', ['error' => $e->getMessage()]); return []; } if (! is_array($response) || (isset($response['message']) && ! array_is_list($response))) { return []; } return collect($response) ->filter(fn ($row) => is_array($row) && array_key_exists('id', $row)) ->map(function (array $row) { $sub = is_array($row['sub_agent'] ?? null) ? $row['sub_agent'] : []; $buy = (float) ($sub['rate_buy_gram_price'] ?? 0); $sell = (float) ($sub['rate_sell_gram_price'] ?? 0); $price = ($buy > 0 && $sell > 0) ? (int) round(($buy + $sell) / 2) : (int) round(max($buy, $sell)); $id = (int) ($row['id'] ?? 0); $name = trim((string) ( $row['name_fa'] ?? $row['name'] ?? $row['title'] ?? $row['product_name'] ?? '' )); return [ 'id' => (string) $id, 'name' => $name !== '' ? $name : ('محصول #' . $id), 'price' => $price, 'buy' => (int) round($buy), 'sell' => (int) round($sell), ]; }) ->filter(fn (array $row) => $row['id'] !== '' && $row['id'] !== '0') ->sortBy('name', SORT_NATURAL | SORT_FLAG_CASE) ->values() ->all(); } /** * مانده طلا/ریال اکانت اپ روی نود همکار (دفتر پنل همکار — نه ته‌حساب اپ). * * @return list */ public function fetchColleagueLedgerMandeh(): array { $body = $this->requestColleagueLedger('GET', '/server/customer/fin_tahesab/mandeh'); if (!is_array($body)) { throw new UnprocessableEntityHttpException('پاسخ نامعتبر مانده دفتر همکار'); } // بعضی نودها آرایهٔ مستقیم برمی‌گردانند؛ بعضی داخل data if (isset($body[0]) || $body === []) { return array_values($body); } if (isset($body['data']) && is_array($body['data'])) { return array_values($body['data']); } return [$body]; } /** * ریز اسناد دفتر اکانت روی نود همکار در بازهٔ تاریخ میلادی Y-m-d. * * @return list> */ public function fetchColleagueLedgerTable(string $startDate, string $endDate): array { $body = $this->requestColleagueLedger('POST', '/server/customer/fin_tahesab/table', [ 'startDate' => $startDate, 'endDate' => $endDate, ]); if (!is_array($body)) { throw new UnprocessableEntityHttpException('پاسخ نامعتبر ریز دفتر همکار'); } if (isset($body[0]) || $body === []) { return array_values($body); } if (isset($body['data']) && is_array($body['data'])) { return array_values($body['data']); } if (isset($body['rows']) && is_array($body['rows'])) { return array_values($body['rows']); } return []; } /** * @param array|null $payload * @return array|null */ private function requestColleagueLedger(string $method, string $path, ?array $payload = null): ?array { $token = $this->getToken(); $url = $this->endpoint($path); $httpResponse = $this->sendColleagueLedgerHttp($method, $url, $token, $payload); $body = $this->decodeMontzarHttpBody($httpResponse); if (is_array($body) && ($body['message'] ?? null) === 'no authorised') { $token = $this->montzarLogin(); $httpResponse = $this->sendColleagueLedgerHttp($method, $url, $token, $payload); $body = $this->decodeMontzarHttpBody($httpResponse); } if ($httpResponse->failed() && !is_array($body)) { Log::warning('Colleague ledger request failed', [ 'method' => $method, 'url' => $url, 'status' => $httpResponse->status(), 'raw' => $httpResponse->body(), ]); throw new UnprocessableEntityHttpException( $this->formatMontzarErrorMessage(null, $httpResponse) ); } if (is_array($body) && !empty($body['message']) && is_string($body['message']) && !isset($body[0])) { throw new UnprocessableEntityHttpException($body['message']); } return $body; } private function sendColleagueLedgerHttp(string $method, string $url, string $token, ?array $payload) { $pending = $this->http(30)->withToken($token); if (strtoupper($method) === 'GET') { return $pending->get($url, $payload ?? []); } return $pending->post($url, $payload ?? []); } /** * ثبت سفارش در منتظر — قبل از ایجاد سفارش/تراکنش محلی صدا زده می‌شود. * * @return array{ok: bool, provider_id: int|null, error: string|null} */ public function submitOrder(array &$data): array { $payload = $this->makeOrderPayload($data); $token = $this->getToken(); $httpResponse = $this->postToMontzar( $this->endpoint('/server/customer/order'), $token, $payload ); $body = $this->decodeMontzarHttpBody($httpResponse); if (is_array($body) && ($body['message'] ?? null) === 'no authorised') { $token = $this->montzarLogin(); $httpResponse = $this->postToMontzar( $this->endpoint('/server/customer/order'), $token, $payload ); $body = $this->decodeMontzarHttpBody($httpResponse); } $this->logMontzarOrderResponse($httpResponse, $body); $parsed = $this->parseMontzarOrderResponse($body, (int) $data['direction']); if (!$parsed['ok']) { Log::warning('Montzar order rejected', [ 'base_url' => $this->baseUrl(), 'status' => $httpResponse->status(), 'body' => $body, 'raw' => $httpResponse->body(), 'payload' => $payload, 'reason' => $parsed['error'], ]); throw new UnprocessableEntityHttpException( $this->formatMontzarErrorMessage($parsed['error'], $httpResponse) ); } return $parsed; } public function applyParsedOrder(array &$data, array $parsed): void { $this->syncOrderWithMontzarResponse($data, $parsed); } public function sendOrder(array &$data) { $parsed = $this->submitOrder($data); $this->applyParsedOrder($data, $parsed); return null; } private function makeOrderPayload(array $data): array { $mazaneh = ((int) $data['direction'] === 1) ? $this->config()->Config('montzar_truly_buy_mazaneh') : $this->config()->Config('montzar_truly_sell_mazaneh'); $montzarPrice = (float) $this->config()->Config('montzar_live_price'); $cash = (int) round( $data['total_price'] ?? ($montzarPrice * (float) $data['amount']) ); $customerName = auth('customer')->check() ? auth('customer')->user()->name : ($data['customer']['name'] ?? ''); return [ 'mazaneh' => (int) $mazaneh, 'amount' => (float) $data['amount'], 'description' => $this->config()->Config('name_fa') . ' - ' . $customerName, 'direction' => (int) $data['direction'], 'product_id' => (int) ($this->config()->Config('montzar_product_id') ?: 25), 'cash' => $cash, 'input_mode' => 'gold', ]; } private function postToMontzar(string $url, string $token, array $payload) { return $this->http(15) ->withToken($token) ->post($url, $payload); } private function decodeMontzarHttpBody($httpResponse): ?array { $body = $httpResponse->json(); if (is_array($body)) { return $body; } $raw = trim((string) $httpResponse->body()); if ($raw === '') { return null; } $decoded = json_decode($raw, true); if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) { return $decoded; } return null; } private function logMontzarOrderResponse($httpResponse, $body): void { try { $value = $body === null ? $httpResponse->body() : json_encode($body, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); // مربوط به منتزر: settings $this->config()->UpdateConfig('montzar_order_response', $value); // عمومی: اگر در const/Cnt داری همان‌جا بماند Cnt::where('key', 'last_order_log')->update(['value' => $value]); } catch (\Exception $e) { Log::warning('Could not persist montzar order response', [ 'error' => $e->getMessage(), ]); } } private function formatMontzarErrorMessage(?string $error, $httpResponse): string { if ($error && $error !== 'پاسخ نامعتبر از سرور منتظر (اتصال یا فرمت JSON)') { return $error; } $status = $httpResponse->status(); $raw = trim((string) $httpResponse->body()); if ($raw === '') { return "خطا در ارتباط با منتظر (HTTP {$status})"; } if (stripos($raw, ' false, 'provider_id' => null, 'error' => 'پاسخ نامعتبر از سرور منتظر (اتصال یا فرمت JSON)', ]; } if (!empty($body['message']) && is_string($body['message'])) { return [ 'ok' => false, 'provider_id' => null, 'error' => $body['message'], ]; } $providerId = $this->extractProviderOrderId($body); if ($providerId !== null) { return [ 'ok' => true, 'provider_id' => $providerId, 'error' => null, ]; } $sideKey = $direction === 1 ? 'buy' : 'sell'; if (($body[$sideKey] ?? null) === true) { return [ 'ok' => true, 'provider_id' => null, 'error' => null, ]; } foreach ($body as $item) { if (!is_array($item)) { continue; } if (!empty($item['id']) && is_numeric($item['id'])) { return [ 'ok' => true, 'provider_id' => (int) $item['id'], 'error' => null, ]; } if (!empty($item['order']['id'])) { return [ 'ok' => true, 'provider_id' => (int) $item['order']['id'], 'error' => null, ]; } if (isset($item['sts']) && in_array((int) $item['sts'], [1, 2], true)) { return [ 'ok' => true, 'provider_id' => !empty($item['id']) ? (int) $item['id'] : null, 'error' => null, ]; } } return [ 'ok' => false, 'provider_id' => null, 'error' => 'سفارشات توسط ارائه دهنده بسته شده است.', ]; } private function extractProviderOrderId(array $body): ?int { if (!empty($body['id'])) { return (int) $body['id']; } if (!empty($body['order']['id'])) { return (int) $body['order']['id']; } foreach ([1, 2] as $index) { if (isset($body[$index]) && is_array($body[$index])) { if (!empty($body[$index]['id'])) { return (int) $body[$index]['id']; } if (!empty($body[$index]['order']['id'])) { return (int) $body[$index]['order']['id']; } } } foreach (['buy', 'sell'] as $sideKey) { if (!isset($body[$sideKey]) || !is_array($body[$sideKey])) { continue; } if (!empty($body[$sideKey]['id'])) { return (int) $body[$sideKey]['id']; } if (!empty($body[$sideKey]['order']['id'])) { return (int) $body[$sideKey]['order']['id']; } } return null; } private function syncOrderWithMontzarResponse(array &$data, array $parsed): void { $montzarDiff = (float) $this->config()->Config('montzar_diff'); $montzarPrice = (float) $this->config()->Config('montzar_live_price'); $totalPriceMontzar = ((int) $data['direction'] === 1) ? ($montzarPrice + $montzarDiff) : ($montzarPrice - $montzarDiff); $providerOrderId = $parsed['provider_id'] ?? null; if (isset($data['order']) && $data['order'] instanceof Order) { if ($providerOrderId !== null && Schema::hasColumn('orders', 'provider_id')) { $data['order']->provider_id = $providerOrderId; } if (Schema::hasColumn('orders', 'total_price_montzar')) { $data['order']->total_price_montzar = $totalPriceMontzar; } $data['order']->save(); } $data['total_price_montzar'] = $totalPriceMontzar; if ($providerOrderId !== null) { $data['provider_id'] = $providerOrderId; } } }