2026-05-24 13:56:33 +03:30
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
|
|
namespace App\Services;
|
|
|
|
|
|
|
2026-06-27 13:07:15 +03:30
|
|
|
|
use App\Http\Controllers\Controller;
|
2026-05-24 22:29:43 +03:30
|
|
|
|
use App\Models\Cnt;
|
|
|
|
|
|
use App\Models\Order;
|
2026-06-14 12:01:20 +03:30
|
|
|
|
use App\Models\Setting;
|
2026-05-24 13:56:33 +03:30
|
|
|
|
use Illuminate\Support\Facades\Http;
|
2026-05-24 22:29:43 +03:30
|
|
|
|
use Illuminate\Support\Facades\Log;
|
2026-05-25 12:47:04 +03:30
|
|
|
|
use Illuminate\Support\Facades\Schema;
|
2026-05-24 22:29:43 +03:30
|
|
|
|
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
|
2026-05-24 13:56:33 +03:30
|
|
|
|
|
2026-05-25 12:47:04 +03:30
|
|
|
|
class MontzarService
|
2026-05-24 13:56:33 +03:30
|
|
|
|
{
|
2026-05-25 12:47:04 +03:30
|
|
|
|
private function config(): ConfigService
|
|
|
|
|
|
{
|
|
|
|
|
|
return app(ConfigService::class);
|
|
|
|
|
|
}
|
2026-05-24 22:29:43 +03:30
|
|
|
|
|
|
|
|
|
|
private function baseUrl(): string
|
|
|
|
|
|
{
|
|
|
|
|
|
return rtrim(
|
2026-05-25 12:47:04 +03:30
|
|
|
|
$this->config()->Config('montzar_base_url') ?: 'https://montpar.ir',
|
2026-05-24 22:29:43 +03:30
|
|
|
|
'/'
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private function endpoint(string $path): string
|
|
|
|
|
|
{
|
|
|
|
|
|
return $this->baseUrl() . '/' . ltrim($path, '/');
|
|
|
|
|
|
}
|
2026-05-25 12:47:04 +03:30
|
|
|
|
|
2026-08-26 21:47:09 +03:30
|
|
|
|
/** 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));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 12:47:04 +03:30
|
|
|
|
public function montzarLogin(): string
|
2026-05-24 19:22:09 +03:30
|
|
|
|
{
|
2026-08-01 14:04:01 +03:30
|
|
|
|
try {
|
2026-08-26 21:47:09 +03:30
|
|
|
|
$response = $this->http(10)
|
2026-08-01 14:04:01 +03:30
|
|
|
|
->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(
|
|
|
|
|
|
'سرویس موقتا در دسترس نیست، لطفاً با پشتیبانی تماس بگیرید.'
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-05-24 13:56:33 +03:30
|
|
|
|
|
2026-08-01 14:04:01 +03:30
|
|
|
|
$token = is_array($response)
|
|
|
|
|
|
? ($response['access_token'] ?? $response['token'] ?? null)
|
|
|
|
|
|
: null;
|
2026-05-24 19:22:09 +03:30
|
|
|
|
|
|
|
|
|
|
if (!$token) {
|
2026-06-14 12:01:20 +03:30
|
|
|
|
Log::error('Montzar login failed', [
|
|
|
|
|
|
'base_url' => $this->baseUrl(),
|
|
|
|
|
|
'mobile' => $this->config()->Config('montzar_mobile'),
|
|
|
|
|
|
'response' => $response,
|
|
|
|
|
|
]);
|
2026-08-01 14:04:01 +03:30
|
|
|
|
// 422 تا فرانت toast نشان دهد؛ جزئیات فنی فقط در لاگ میماند
|
|
|
|
|
|
throw new UnprocessableEntityHttpException(
|
|
|
|
|
|
'سرویس موقتا در دسترس نیست، لطفاً با پشتیبانی تماس بگیرید.'
|
|
|
|
|
|
);
|
2026-05-24 19:22:09 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-14 12:01:20 +03:30
|
|
|
|
Setting::updateOrCreate(
|
|
|
|
|
|
['key' => 'montzar_token'],
|
2026-08-19 20:15:28 +03:30
|
|
|
|
['value' => $token, 'is_active' => 1, 'can_edit' => 1]
|
2026-06-14 12:01:20 +03:30
|
|
|
|
);
|
2026-05-24 19:22:09 +03:30
|
|
|
|
|
|
|
|
|
|
return $token;
|
2026-05-24 13:56:33 +03:30
|
|
|
|
}
|
2026-05-24 22:29:43 +03:30
|
|
|
|
|
|
|
|
|
|
public function getToken(): string
|
|
|
|
|
|
{
|
2026-05-25 12:47:04 +03:30
|
|
|
|
$token = trim((string) $this->config()->Config('montzar_token'));
|
2026-05-24 22:29:43 +03:30
|
|
|
|
|
|
|
|
|
|
if (!$token) {
|
2026-05-25 12:47:04 +03:30
|
|
|
|
$token = $this->montzarLogin();
|
2026-05-24 22:29:43 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return $token;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public function productList(): array
|
2026-08-26 21:47:09 +03:30
|
|
|
|
{
|
|
|
|
|
|
$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
|
2026-05-24 22:29:43 +03:30
|
|
|
|
{
|
|
|
|
|
|
$token = $this->getToken();
|
|
|
|
|
|
|
2026-08-26 21:47:09 +03:30
|
|
|
|
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();
|
|
|
|
|
|
}
|
2026-05-24 22:29:43 +03:30
|
|
|
|
|
|
|
|
|
|
if ((isset($response['message']) && $response['message'] === 'no authorised') || $response === null) {
|
2026-05-25 12:47:04 +03:30
|
|
|
|
$token = $this->montzarLogin();
|
2026-05-24 22:29:43 +03:30
|
|
|
|
|
2026-08-26 21:47:09 +03:30
|
|
|
|
$response = $this->http(7)
|
2026-05-24 22:29:43 +03:30
|
|
|
|
->withToken($token)
|
|
|
|
|
|
->get($this->endpoint('/server/customer/product/list'))
|
2026-08-26 21:47:09 +03:30
|
|
|
|
->throw()
|
2026-05-24 22:29:43 +03:30
|
|
|
|
->json();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!is_array($response)) {
|
2026-05-25 12:47:04 +03:30
|
|
|
|
throw new \Exception('Montzar product list response is not array');
|
2026-05-24 22:29:43 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return $response;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-19 14:02:48 +03:30
|
|
|
|
/**
|
|
|
|
|
|
* @return array<int, array{id: string, name: string, price: int, buy: int, sell: int}>
|
|
|
|
|
|
*/
|
|
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-03 18:44:27 +03:30
|
|
|
|
/**
|
|
|
|
|
|
* مانده طلا/ریال اکانت اپ روی نود همکار (دفتر پنل همکار — نه تهحساب اپ).
|
|
|
|
|
|
*
|
|
|
|
|
|
* @return list<array{name_eng?: string, name_fa?: string, value?: float|int, unit?: string, isKol?: bool}>
|
|
|
|
|
|
*/
|
|
|
|
|
|
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<array<string, mixed>>
|
|
|
|
|
|
*/
|
|
|
|
|
|
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<string, mixed>|null $payload
|
|
|
|
|
|
* @return array<mixed>|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)
|
|
|
|
|
|
{
|
2026-08-26 21:47:09 +03:30
|
|
|
|
$pending = $this->http(30)->withToken($token);
|
2026-08-03 18:44:27 +03:30
|
|
|
|
|
|
|
|
|
|
if (strtoupper($method) === 'GET') {
|
|
|
|
|
|
return $pending->get($url, $payload ?? []);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return $pending->post($url, $payload ?? []);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-31 16:14:02 +03:30
|
|
|
|
/**
|
|
|
|
|
|
* آیا پاسخ همکار بهخاطر کهنه بودن مظنه رد شده؟
|
|
|
|
|
|
*/
|
|
|
|
|
|
public static function isQuoteChangedMessage(?string $message): bool
|
|
|
|
|
|
{
|
|
|
|
|
|
if ($message === null || trim($message) === '') {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$normalized = str_replace(
|
|
|
|
|
|
['ي', 'ك', 'ة', '!', '?', '؟', '.'],
|
|
|
|
|
|
['ی', 'ک', 'ه', '', '', '', ''],
|
|
|
|
|
|
$message
|
|
|
|
|
|
);
|
|
|
|
|
|
$normalized = preg_replace('/\s+/u', ' ', $normalized) ?? $normalized;
|
|
|
|
|
|
$normalized = trim($normalized);
|
|
|
|
|
|
|
|
|
|
|
|
if ($normalized === '') {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (mb_stripos($normalized, 'تغییر در مظنه') !== false) {
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (mb_stripos($normalized, 'مظنه') !== false && mb_stripos($normalized, 'تغییر') !== false) {
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$latin = mb_strtolower($normalized);
|
|
|
|
|
|
|
|
|
|
|
|
return str_contains($latin, 'quote')
|
|
|
|
|
|
&& (str_contains($latin, 'change') || str_contains($latin, 'changed'));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-02 21:24:26 +03:30
|
|
|
|
/**
|
|
|
|
|
|
* ثبت سفارش در منتظر — قبل از ایجاد سفارش/تراکنش محلی صدا زده میشود.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @return array{ok: bool, provider_id: int|null, error: string|null}
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function submitOrder(array &$data): array
|
2026-05-24 22:29:43 +03:30
|
|
|
|
{
|
2026-08-31 16:14:02 +03:30
|
|
|
|
$attempt = $this->submitOrderOnce($data);
|
2026-05-24 22:29:43 +03:30
|
|
|
|
|
2026-08-31 16:14:02 +03:30
|
|
|
|
if (
|
|
|
|
|
|
!($attempt['parsed']['ok'] ?? false)
|
|
|
|
|
|
&& self::isQuoteChangedMessage($attempt['parsed']['error'] ?? null)
|
|
|
|
|
|
) {
|
|
|
|
|
|
Log::info('Montzar quote changed — refreshing mazaneh and retrying', [
|
|
|
|
|
|
'error' => $attempt['parsed']['error'] ?? null,
|
|
|
|
|
|
'old_mazaneh' => $attempt['payload']['mazaneh'] ?? null,
|
|
|
|
|
|
]);
|
2026-05-24 22:29:43 +03:30
|
|
|
|
|
2026-08-31 16:14:02 +03:30
|
|
|
|
$refreshed = false;
|
|
|
|
|
|
try {
|
|
|
|
|
|
$refreshed = $this->syncQuoteSettingsFromProvider();
|
|
|
|
|
|
} catch (\Throwable $e) {
|
|
|
|
|
|
Log::warning('Montzar quote refresh failed', [
|
|
|
|
|
|
'error' => $e->getMessage(),
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
2026-05-24 22:29:43 +03:30
|
|
|
|
|
2026-08-31 16:14:02 +03:30
|
|
|
|
if (!$refreshed) {
|
|
|
|
|
|
$refreshed = $this->applyQuoteFromErrorBody(
|
|
|
|
|
|
is_array($attempt['body'] ?? null) ? $attempt['body'] : null,
|
|
|
|
|
|
(int) ($attempt['payload']['mazaneh'] ?? 0)
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-05-24 22:29:43 +03:30
|
|
|
|
|
2026-08-31 16:14:02 +03:30
|
|
|
|
if ($refreshed) {
|
|
|
|
|
|
$attempt = $this->submitOrderOnce($data);
|
|
|
|
|
|
}
|
2026-05-24 22:29:43 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-31 16:14:02 +03:30
|
|
|
|
$parsed = $attempt['parsed'];
|
2026-05-24 22:29:43 +03:30
|
|
|
|
|
|
|
|
|
|
if (!$parsed['ok']) {
|
2026-05-25 12:47:04 +03:30
|
|
|
|
Log::warning('Montzar order rejected', [
|
2026-05-24 22:29:43 +03:30
|
|
|
|
'base_url' => $this->baseUrl(),
|
2026-08-31 16:14:02 +03:30
|
|
|
|
'status' => $attempt['http']->status(),
|
|
|
|
|
|
'body' => $attempt['body'],
|
|
|
|
|
|
'raw' => $attempt['http']->body(),
|
|
|
|
|
|
'payload' => $attempt['payload'],
|
2026-05-24 22:29:43 +03:30
|
|
|
|
'reason' => $parsed['error'],
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
throw new UnprocessableEntityHttpException(
|
2026-08-31 16:14:02 +03:30
|
|
|
|
$this->formatMontzarErrorMessage($parsed['error'], $attempt['http'])
|
2026-05-24 22:29:43 +03:30
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-31 16:14:02 +03:30
|
|
|
|
return [
|
|
|
|
|
|
'ok' => true,
|
|
|
|
|
|
'provider_id' => $parsed['provider_id'] ?? null,
|
|
|
|
|
|
'error' => null,
|
|
|
|
|
|
];
|
2026-07-02 21:24:26 +03:30
|
|
|
|
}
|
2026-05-24 22:29:43 +03:30
|
|
|
|
|
2026-07-02 21:24:26 +03:30
|
|
|
|
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;
|
2026-05-24 22:29:43 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-31 16:14:02 +03:30
|
|
|
|
/**
|
|
|
|
|
|
* @return array{parsed: array, body: mixed, payload: array, http: \Illuminate\Http\Client\Response}
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function submitOrderOnce(array &$data): array
|
|
|
|
|
|
{
|
|
|
|
|
|
$payload = $this->makeOrderPayload($data);
|
|
|
|
|
|
$token = $this->getToken();
|
|
|
|
|
|
$url = $this->endpoint('/server/customer/order');
|
|
|
|
|
|
|
|
|
|
|
|
$httpResponse = $this->postToMontzar($url, $token, $payload);
|
|
|
|
|
|
$body = $this->decodeMontzarHttpBody($httpResponse);
|
|
|
|
|
|
|
|
|
|
|
|
if (is_array($body) && ($body['message'] ?? null) === 'no authorised') {
|
|
|
|
|
|
$token = $this->montzarLogin();
|
|
|
|
|
|
$httpResponse = $this->postToMontzar($url, $token, $payload);
|
|
|
|
|
|
$body = $this->decodeMontzarHttpBody($httpResponse);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$this->logMontzarOrderResponse($httpResponse, $body);
|
|
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
|
'parsed' => $this->parseMontzarOrderResponse($body, (int) $data['direction']),
|
|
|
|
|
|
'body' => $body,
|
|
|
|
|
|
'payload' => $payload,
|
|
|
|
|
|
'http' => $httpResponse,
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* مظنهٔ تازهٔ همکار را از لیست محصول روی settings مینویسد.
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function syncQuoteSettingsFromProvider(): bool
|
|
|
|
|
|
{
|
|
|
|
|
|
$products = $this->productList();
|
|
|
|
|
|
|
|
|
|
|
|
if (isset($products['message']) && !array_is_list($products)) {
|
|
|
|
|
|
Log::warning('Montzar quote refresh got message payload', [
|
|
|
|
|
|
'message' => $products['message'],
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$productId = (int) ($this->config()->Config('montzar_product_id') ?: 25);
|
|
|
|
|
|
$product = collect($products)->firstWhere('id', $productId);
|
|
|
|
|
|
|
|
|
|
|
|
if (!is_array($product) || !isset($product['sub_agent']) || !is_array($product['sub_agent'])) {
|
|
|
|
|
|
Log::warning('Montzar quote refresh: product not found', [
|
|
|
|
|
|
'product_id' => $productId,
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$sub = $product['sub_agent'];
|
|
|
|
|
|
$buyGram = (float) ($sub['rate_buy_gram_price'] ?? 0);
|
|
|
|
|
|
$sellGram = (float) ($sub['rate_sell_gram_price'] ?? 0);
|
|
|
|
|
|
$buyMazaneh = $sub['rate_buy_price'] ?? 0;
|
|
|
|
|
|
$sellMazaneh = $sub['rate_sell_price'] ?? 0;
|
|
|
|
|
|
|
|
|
|
|
|
if ((int) $buyMazaneh <= 0 && (int) $sellMazaneh <= 0) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$mid = ($buyGram > 0 && $sellGram > 0) ? (($buyGram + $sellGram) / 2) : 0;
|
|
|
|
|
|
$diff = ($buyGram > 0 && $sellGram > 0) ? (($buyGram - $sellGram) / 2) : 0;
|
|
|
|
|
|
|
|
|
|
|
|
if ($mid > 0) {
|
|
|
|
|
|
$this->config()->UpdateConfig('montzar_live_price', (float) $mid);
|
|
|
|
|
|
$this->config()->UpdateConfig('montzar_diff', (float) $diff);
|
|
|
|
|
|
$this->config()->UpdateConfig('montzar_mazaneh', ((float) $mid * 4.331802));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if ((int) $buyMazaneh > 0) {
|
|
|
|
|
|
$this->config()->UpdateConfig('montzar_truly_buy_mazaneh', $buyMazaneh);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if ((int) $sellMazaneh > 0) {
|
|
|
|
|
|
$this->config()->UpdateConfig('montzar_truly_sell_mazaneh', $sellMazaneh);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$this->config()->UpdateConfig(
|
|
|
|
|
|
'montzar_last_product_log',
|
|
|
|
|
|
json_encode($product, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
Log::info('Montzar mazaneh refreshed before order retry', [
|
|
|
|
|
|
'buy_mazaneh' => $buyMazaneh,
|
|
|
|
|
|
'sell_mazaneh' => $sellMazaneh,
|
|
|
|
|
|
'live_price' => $mid,
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* اگر خودِ خطای همکار مظنهٔ جدید را برگرداند، همان را ذخیره کن.
|
|
|
|
|
|
* مظنهٔ اکوشدهٔ همان درخواست قبلی را دوباره ذخیره نکن.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param array<string, mixed>|null $body
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function applyQuoteFromErrorBody(?array $body, int $previousMazaneh): bool
|
|
|
|
|
|
{
|
|
|
|
|
|
if ($body === null) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$data = is_array($body['data'] ?? null) ? $body['data'] : $body;
|
|
|
|
|
|
if (!is_array($data)) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$buyMazaneh = $this->firstPositiveNumber($data, [
|
|
|
|
|
|
'rate_buy_price',
|
|
|
|
|
|
'buy_mazaneh',
|
|
|
|
|
|
'mazaneh_buy',
|
|
|
|
|
|
'truly_buy_mazaneh',
|
|
|
|
|
|
]);
|
|
|
|
|
|
$sellMazaneh = $this->firstPositiveNumber($data, [
|
|
|
|
|
|
'rate_sell_price',
|
|
|
|
|
|
'sell_mazaneh',
|
|
|
|
|
|
'mazaneh_sell',
|
|
|
|
|
|
'truly_sell_mazaneh',
|
|
|
|
|
|
]);
|
|
|
|
|
|
$mazaneh = $this->firstPositiveNumber($data, ['new_mazaneh', 'new_quote']);
|
|
|
|
|
|
|
|
|
|
|
|
$updated = false;
|
|
|
|
|
|
|
|
|
|
|
|
if ($buyMazaneh !== null && $buyMazaneh !== $previousMazaneh) {
|
|
|
|
|
|
$this->config()->UpdateConfig('montzar_truly_buy_mazaneh', $buyMazaneh);
|
|
|
|
|
|
$updated = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if ($sellMazaneh !== null && $sellMazaneh !== $previousMazaneh) {
|
|
|
|
|
|
$this->config()->UpdateConfig('montzar_truly_sell_mazaneh', $sellMazaneh);
|
|
|
|
|
|
$updated = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!$updated && $mazaneh !== null && $mazaneh !== $previousMazaneh) {
|
|
|
|
|
|
$this->config()->UpdateConfig('montzar_truly_buy_mazaneh', $mazaneh);
|
|
|
|
|
|
$this->config()->UpdateConfig('montzar_truly_sell_mazaneh', $mazaneh);
|
|
|
|
|
|
$updated = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if ($updated) {
|
|
|
|
|
|
Log::info('Montzar mazaneh taken from error body', [
|
|
|
|
|
|
'buy_mazaneh' => $buyMazaneh,
|
|
|
|
|
|
'sell_mazaneh' => $sellMazaneh,
|
|
|
|
|
|
'mazaneh' => $mazaneh,
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return $updated;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @param array<string, mixed> $data
|
|
|
|
|
|
* @param list<string> $keys
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function firstPositiveNumber(array $data, array $keys): ?int
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach ($keys as $key) {
|
|
|
|
|
|
if (!isset($data[$key]) || !is_numeric($data[$key])) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
$value = (int) $data[$key];
|
|
|
|
|
|
if ($value > 0) {
|
|
|
|
|
|
return $value;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-24 22:29:43 +03:30
|
|
|
|
private function makeOrderPayload(array $data): array
|
|
|
|
|
|
{
|
|
|
|
|
|
$mazaneh = ((int) $data['direction'] === 1)
|
2026-05-25 12:47:04 +03:30
|
|
|
|
? $this->config()->Config('montzar_truly_buy_mazaneh')
|
|
|
|
|
|
: $this->config()->Config('montzar_truly_sell_mazaneh');
|
2026-05-24 22:29:43 +03:30
|
|
|
|
|
2026-05-25 16:17:24 +03:30
|
|
|
|
$montzarPrice = (float) $this->config()->Config('montzar_live_price');
|
2026-05-24 22:29:43 +03:30
|
|
|
|
|
|
|
|
|
|
$cash = (int) round(
|
2026-05-25 12:47:04 +03:30
|
|
|
|
$data['total_price'] ?? ($montzarPrice * (float) $data['amount'])
|
2026-05-24 22:29:43 +03:30
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
$customerName = auth('customer')->check()
|
|
|
|
|
|
? auth('customer')->user()->name
|
|
|
|
|
|
: ($data['customer']['name'] ?? '');
|
|
|
|
|
|
|
2026-09-02 15:55:03 +03:30
|
|
|
|
$payload = [
|
2026-05-24 22:29:43 +03:30
|
|
|
|
'mazaneh' => (int) $mazaneh,
|
|
|
|
|
|
'amount' => (float) $data['amount'],
|
2026-05-25 12:47:04 +03:30
|
|
|
|
'description' => $this->config()->Config('name_fa') . ' - ' . $customerName,
|
2026-05-24 22:29:43 +03:30
|
|
|
|
'direction' => (int) $data['direction'],
|
2026-05-25 12:47:04 +03:30
|
|
|
|
'product_id' => (int) ($this->config()->Config('montzar_product_id') ?: 25),
|
2026-05-24 22:29:43 +03:30
|
|
|
|
'cash' => $cash,
|
|
|
|
|
|
'input_mode' => 'gold',
|
|
|
|
|
|
];
|
2026-09-02 15:55:03 +03:30
|
|
|
|
|
|
|
|
|
|
if (
|
2026-09-05 22:30:56 +03:30
|
|
|
|
isset($data['order'])
|
2026-09-02 15:55:03 +03:30
|
|
|
|
&& $data['order'] instanceof Order
|
|
|
|
|
|
) {
|
|
|
|
|
|
$payload['callback'] = app(ColleagueOrderCallbackService::class)->callbackUrl((int) $data['order']->id);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return $payload;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-09-02 18:14:32 +03:30
|
|
|
|
* وضعیت سفارش روی نود همکار — اول GET مسیر+شناسه، اگر sts نبود POST جدول همان مسیر.
|
2026-09-02 15:55:03 +03:30
|
|
|
|
*
|
|
|
|
|
|
* @return array<string, mixed>|null
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function fetchColleagueOrder(int $providerOrderId): ?array
|
|
|
|
|
|
{
|
|
|
|
|
|
if ($providerOrderId <= 0) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$path = trim($this->config()->getString('montzar_order_status_path', '/server/customer/order'));
|
|
|
|
|
|
if ($path === '') {
|
|
|
|
|
|
$path = '/server/customer/order';
|
|
|
|
|
|
}
|
2026-09-02 18:14:32 +03:30
|
|
|
|
$path = rtrim($path, '/');
|
2026-09-02 15:55:03 +03:30
|
|
|
|
|
2026-09-02 18:14:32 +03:30
|
|
|
|
$got = $this->montzarJson('GET', $this->endpoint($path . '/' . $providerOrderId));
|
|
|
|
|
|
$row = $this->colleagueOrderRowFromBody($got, $providerOrderId);
|
|
|
|
|
|
if ($row) {
|
|
|
|
|
|
return $row;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
foreach ($this->colleagueOrderTableUrls($path) as $url) {
|
|
|
|
|
|
$body = $this->montzarJson('POST', $url, [
|
|
|
|
|
|
'filters' => [[
|
|
|
|
|
|
'field' => 'id',
|
|
|
|
|
|
'cr' => 'and',
|
|
|
|
|
|
'type' => 'text',
|
|
|
|
|
|
'op' => 'eq',
|
|
|
|
|
|
'value' => (string) $providerOrderId,
|
|
|
|
|
|
]],
|
|
|
|
|
|
'limit' => 12,
|
|
|
|
|
|
'page' => 1,
|
|
|
|
|
|
'search' => '',
|
|
|
|
|
|
]);
|
|
|
|
|
|
$row = $this->colleagueOrderRowFromBody($body, $providerOrderId);
|
|
|
|
|
|
if ($row) {
|
|
|
|
|
|
return $row;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return is_array($got) ? $got : null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @return list<string>
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function colleagueOrderTableUrls(string $path): array
|
|
|
|
|
|
{
|
|
|
|
|
|
$urls = [$this->endpoint($path . '/table')];
|
|
|
|
|
|
if (str_ends_with($path, '/order') && !str_contains($path, '/single/')) {
|
|
|
|
|
|
$urls[] = $this->endpoint(str_replace('/customer/order', '/customer/single/order', $path) . '/table');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return array_values(array_unique($urls));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @param array<string, mixed>|null $body
|
|
|
|
|
|
* @return array<string, mixed>|null
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function colleagueOrderRowFromBody(?array $body, int $providerOrderId): ?array
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!is_array($body)) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$candidates = [$body, $body['data'] ?? null, $body['order'] ?? null];
|
|
|
|
|
|
if (isset($body['rows']) && is_array($body['rows'])) {
|
|
|
|
|
|
foreach ($body['rows'] as $row) {
|
|
|
|
|
|
if (is_array($row)) {
|
|
|
|
|
|
$candidates[] = $row;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$matched = null;
|
|
|
|
|
|
foreach ($candidates as $row) {
|
|
|
|
|
|
if (!is_array($row) || !array_key_exists('sts', $row)) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
$id = (int) ($row['id'] ?? $row['order_id'] ?? 0);
|
|
|
|
|
|
if ($id === $providerOrderId) {
|
|
|
|
|
|
return $row;
|
|
|
|
|
|
}
|
|
|
|
|
|
if ($id === 0 && $matched === null) {
|
|
|
|
|
|
$matched = $row;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return $matched;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @param array<string, mixed> $payload
|
|
|
|
|
|
* @return array<string, mixed>|null
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function montzarJson(string $method, string $url, array $payload = []): ?array
|
|
|
|
|
|
{
|
2026-09-02 15:55:03 +03:30
|
|
|
|
$token = $this->getToken();
|
|
|
|
|
|
try {
|
2026-09-02 18:14:32 +03:30
|
|
|
|
$http = $this->http(15)->withToken($token);
|
|
|
|
|
|
$httpResponse = strtoupper($method) === 'POST'
|
|
|
|
|
|
? $http->post($url, $payload)
|
|
|
|
|
|
: $http->get($url);
|
2026-09-02 15:55:03 +03:30
|
|
|
|
} catch (\Throwable $e) {
|
|
|
|
|
|
Log::warning('Montzar order status request failed', [
|
|
|
|
|
|
'url' => $url,
|
|
|
|
|
|
'error' => $e->getMessage(),
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$body = $this->decodeMontzarHttpBody($httpResponse);
|
|
|
|
|
|
if (is_array($body) && ($body['message'] ?? null) === 'no authorised') {
|
|
|
|
|
|
$token = $this->montzarLogin();
|
2026-09-02 18:14:32 +03:30
|
|
|
|
$http = $this->http(15)->withToken($token);
|
|
|
|
|
|
$httpResponse = strtoupper($method) === 'POST'
|
|
|
|
|
|
? $http->post($url, $payload)
|
|
|
|
|
|
: $http->get($url);
|
2026-09-02 15:55:03 +03:30
|
|
|
|
$body = $this->decodeMontzarHttpBody($httpResponse);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return is_array($body) ? $body : null;
|
2026-05-24 22:29:43 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 12:47:04 +03:30
|
|
|
|
private function postToMontzar(string $url, string $token, array $payload)
|
2026-05-24 22:29:43 +03:30
|
|
|
|
{
|
2026-08-26 21:47:09 +03:30
|
|
|
|
return $this->http(15)
|
2026-05-24 22:29:43 +03:30
|
|
|
|
->withToken($token)
|
|
|
|
|
|
->post($url, $payload);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 12:47:04 +03:30
|
|
|
|
private function decodeMontzarHttpBody($httpResponse): ?array
|
2026-05-24 22:29:43 +03:30
|
|
|
|
{
|
|
|
|
|
|
$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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 12:47:04 +03:30
|
|
|
|
private function logMontzarOrderResponse($httpResponse, $body): void
|
2026-05-24 22:29:43 +03:30
|
|
|
|
{
|
|
|
|
|
|
try {
|
|
|
|
|
|
$value = $body === null
|
|
|
|
|
|
? $httpResponse->body()
|
|
|
|
|
|
: json_encode($body, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
|
|
|
|
|
|
2026-05-25 12:47:04 +03:30
|
|
|
|
// مربوط به منتزر: settings
|
|
|
|
|
|
$this->config()->UpdateConfig('montzar_order_response', $value);
|
|
|
|
|
|
|
|
|
|
|
|
// عمومی: اگر در const/Cnt داری همانجا بماند
|
2026-05-24 22:29:43 +03:30
|
|
|
|
Cnt::where('key', 'last_order_log')->update(['value' => $value]);
|
|
|
|
|
|
} catch (\Exception $e) {
|
2026-05-25 12:47:04 +03:30
|
|
|
|
Log::warning('Could not persist montzar order response', [
|
2026-05-24 22:29:43 +03:30
|
|
|
|
'error' => $e->getMessage(),
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 12:47:04 +03:30
|
|
|
|
private function formatMontzarErrorMessage(?string $error, $httpResponse): string
|
2026-05-24 22:29:43 +03:30
|
|
|
|
{
|
2026-05-25 12:47:04 +03:30
|
|
|
|
if ($error && $error !== 'پاسخ نامعتبر از سرور منتظر (اتصال یا فرمت JSON)') {
|
2026-05-24 22:29:43 +03:30
|
|
|
|
return $error;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$status = $httpResponse->status();
|
|
|
|
|
|
$raw = trim((string) $httpResponse->body());
|
|
|
|
|
|
|
|
|
|
|
|
if ($raw === '') {
|
2026-05-25 12:47:04 +03:30
|
|
|
|
return "خطا در ارتباط با منتظر (HTTP {$status})";
|
2026-05-24 22:29:43 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (stripos($raw, '<!DOCTYPE') !== false || stripos($raw, '<html') !== false) {
|
2026-05-25 12:47:04 +03:30
|
|
|
|
return "خطا از سرویس منتظر (HTTP {$status}). پاسخ JSON دریافت نشد.";
|
2026-05-24 22:29:43 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return mb_substr($raw, 0, 300);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 12:47:04 +03:30
|
|
|
|
private function parseMontzarOrderResponse($body, int $direction): array
|
2026-05-24 22:29:43 +03:30
|
|
|
|
{
|
|
|
|
|
|
if (!is_array($body)) {
|
|
|
|
|
|
return [
|
|
|
|
|
|
'ok' => false,
|
|
|
|
|
|
'provider_id' => null,
|
2026-05-25 12:47:04 +03:30
|
|
|
|
'error' => 'پاسخ نامعتبر از سرور منتظر (اتصال یا فرمت JSON)',
|
2026-05-24 22:29:43 +03:30
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-02 15:55:03 +03:30
|
|
|
|
if (isset($item['sts']) && in_array((int) $item['sts'], [1, 2, 4], true)) {
|
2026-05-24 22:29:43 +03:30
|
|
|
|
return [
|
|
|
|
|
|
'ok' => true,
|
|
|
|
|
|
'provider_id' => !empty($item['id']) ? (int) $item['id'] : null,
|
|
|
|
|
|
'error' => null,
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-02 15:55:03 +03:30
|
|
|
|
if (!empty($body['pending']) || !empty($body[0]['pending'])) {
|
|
|
|
|
|
return [
|
|
|
|
|
|
'ok' => true,
|
|
|
|
|
|
'provider_id' => $this->extractProviderOrderId($body),
|
|
|
|
|
|
'error' => null,
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-24 22:29:43 +03:30
|
|
|
|
return [
|
|
|
|
|
|
'ok' => false,
|
|
|
|
|
|
'provider_id' => null,
|
|
|
|
|
|
'error' => 'سفارشات توسط ارائه دهنده بسته شده است.',
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private function extractProviderOrderId(array $body): ?int
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!empty($body['id'])) {
|
|
|
|
|
|
return (int) $body['id'];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-02 15:55:03 +03:30
|
|
|
|
if (!empty($body['order_id']) && is_numeric($body['order_id'])) {
|
|
|
|
|
|
return (int) $body['order_id'];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-24 22:29:43 +03:30
|
|
|
|
if (!empty($body['order']['id'])) {
|
|
|
|
|
|
return (int) $body['order']['id'];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-02 15:55:03 +03:30
|
|
|
|
foreach ([0, 1, 2] as $index) {
|
2026-05-25 12:47:04 +03:30
|
|
|
|
if (isset($body[$index]) && is_array($body[$index])) {
|
|
|
|
|
|
if (!empty($body[$index]['id'])) {
|
|
|
|
|
|
return (int) $body[$index]['id'];
|
|
|
|
|
|
}
|
2026-05-24 22:29:43 +03:30
|
|
|
|
|
2026-09-02 15:55:03 +03:30
|
|
|
|
if (!empty($body[$index]['order_id']) && is_numeric($body[$index]['order_id'])) {
|
|
|
|
|
|
return (int) $body[$index]['order_id'];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 12:47:04 +03:30
|
|
|
|
if (!empty($body[$index]['order']['id'])) {
|
|
|
|
|
|
return (int) $body[$index]['order']['id'];
|
|
|
|
|
|
}
|
2026-05-24 22:29:43 +03:30
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 12:47:04 +03:30
|
|
|
|
private function syncOrderWithMontzarResponse(array &$data, array $parsed): void
|
2026-05-24 22:29:43 +03:30
|
|
|
|
{
|
2026-05-25 12:47:04 +03:30
|
|
|
|
$montzarDiff = (float) $this->config()->Config('montzar_diff');
|
2026-05-25 16:17:24 +03:30
|
|
|
|
$montzarPrice = (float) $this->config()->Config('montzar_live_price');
|
2026-05-24 22:29:43 +03:30
|
|
|
|
|
2026-05-25 12:47:04 +03:30
|
|
|
|
$totalPriceMontzar = ((int) $data['direction'] === 1)
|
|
|
|
|
|
? ($montzarPrice + $montzarDiff)
|
|
|
|
|
|
: ($montzarPrice - $montzarDiff);
|
2026-05-24 22:29:43 +03:30
|
|
|
|
|
|
|
|
|
|
$providerOrderId = $parsed['provider_id'] ?? null;
|
|
|
|
|
|
|
|
|
|
|
|
if (isset($data['order']) && $data['order'] instanceof Order) {
|
2026-05-28 13:13:06 +03:30
|
|
|
|
if ($providerOrderId !== null && Schema::hasColumn('orders', 'provider_id')) {
|
2026-05-24 22:29:43 +03:30
|
|
|
|
$data['order']->provider_id = $providerOrderId;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 12:47:04 +03:30
|
|
|
|
if (Schema::hasColumn('orders', 'total_price_montzar')) {
|
|
|
|
|
|
$data['order']->total_price_montzar = $totalPriceMontzar;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-24 22:29:43 +03:30
|
|
|
|
$data['order']->save();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 12:47:04 +03:30
|
|
|
|
$data['total_price_montzar'] = $totalPriceMontzar;
|
2026-05-24 22:29:43 +03:30
|
|
|
|
|
|
|
|
|
|
if ($providerOrderId !== null) {
|
|
|
|
|
|
$data['provider_id'] = $providerOrderId;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-24 19:22:09 +03:30
|
|
|
|
}
|