From 5537cd7086d605953331c21d8d67c183b0072a7c Mon Sep 17 00:00:00 2001 From: mohammad sedighi Date: Mon, 31 Aug 2026 16:14:02 +0330 Subject: [PATCH] Retry Montzar orders after refreshing a stale quote. When the partner rejects with a mazaneh-changed error, pull the live quote and submit once more so the customer buy can succeed without a page refresh. Co-authored-by: Cursor --- app/Services/MontzarService.php | 272 ++++++++++++++++++++++--- tests/Unit/MontzarQuoteChangedTest.php | 25 +++ 2 files changed, 271 insertions(+), 26 deletions(-) create mode 100644 tests/Unit/MontzarQuoteChangedTest.php diff --git a/app/Services/MontzarService.php b/app/Services/MontzarService.php index 2d563e1..73d40e4 100644 --- a/app/Services/MontzarService.php +++ b/app/Services/MontzarService.php @@ -315,6 +315,41 @@ class MontzarService return $pending->post($url, $payload ?? []); } + /** + * آیا پاسخ همکار به‌خاطر کهنه بودن مظنه رد شده؟ + */ + 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')); + } + /** * ثبت سفارش در منتظر — قبل از ایجاد سفارش/تراکنش محلی صدا زده می‌شود. * @@ -322,49 +357,60 @@ class MontzarService */ public function submitOrder(array &$data): array { - $payload = $this->makeOrderPayload($data); - $token = $this->getToken(); + $attempt = $this->submitOrderOnce($data); - $httpResponse = $this->postToMontzar( - $this->endpoint('/server/customer/order'), - $token, - $payload - ); + 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, + ]); - $body = $this->decodeMontzarHttpBody($httpResponse); + $refreshed = false; + try { + $refreshed = $this->syncQuoteSettingsFromProvider(); + } catch (\Throwable $e) { + Log::warning('Montzar quote refresh failed', [ + 'error' => $e->getMessage(), + ]); + } - if (is_array($body) && ($body['message'] ?? null) === 'no authorised') { - $token = $this->montzarLogin(); + if (!$refreshed) { + $refreshed = $this->applyQuoteFromErrorBody( + is_array($attempt['body'] ?? null) ? $attempt['body'] : null, + (int) ($attempt['payload']['mazaneh'] ?? 0) + ); + } - $httpResponse = $this->postToMontzar( - $this->endpoint('/server/customer/order'), - $token, - $payload - ); - - $body = $this->decodeMontzarHttpBody($httpResponse); + if ($refreshed) { + $attempt = $this->submitOrderOnce($data); + } } - $this->logMontzarOrderResponse($httpResponse, $body); - - $parsed = $this->parseMontzarOrderResponse($body, (int) $data['direction']); + $parsed = $attempt['parsed']; if (!$parsed['ok']) { Log::warning('Montzar order rejected', [ 'base_url' => $this->baseUrl(), - 'status' => $httpResponse->status(), - 'body' => $body, - 'raw' => $httpResponse->body(), - 'payload' => $payload, + 'status' => $attempt['http']->status(), + 'body' => $attempt['body'], + 'raw' => $attempt['http']->body(), + 'payload' => $attempt['payload'], 'reason' => $parsed['error'], ]); throw new UnprocessableEntityHttpException( - $this->formatMontzarErrorMessage($parsed['error'], $httpResponse) + $this->formatMontzarErrorMessage($parsed['error'], $attempt['http']) ); } - return $parsed; + return [ + 'ok' => true, + 'provider_id' => $parsed['provider_id'] ?? null, + 'error' => null, + ]; } public function applyParsedOrder(array &$data, array $parsed): void @@ -380,6 +426,180 @@ class MontzarService return null; } + /** + * @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|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 $data + * @param list $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; + } + private function makeOrderPayload(array $data): array { $mazaneh = ((int) $data['direction'] === 1) diff --git a/tests/Unit/MontzarQuoteChangedTest.php b/tests/Unit/MontzarQuoteChangedTest.php new file mode 100644 index 0000000..5d705f3 --- /dev/null +++ b/tests/Unit/MontzarQuoteChangedTest.php @@ -0,0 +1,25 @@ +assertTrue(MontzarService::isQuoteChangedMessage('تغییر در مظنه !')); + $this->assertTrue(MontzarService::isQuoteChangedMessage('تغيير در مظنه')); + $this->assertTrue(MontzarService::isQuoteChangedMessage('مظنه تغییر کرده است')); + $this->assertTrue(MontzarService::isQuoteChangedMessage('Quote changed')); + } + + public function test_ignores_unrelated_messages(): void + { + $this->assertFalse(MontzarService::isQuoteChangedMessage(null)); + $this->assertFalse(MontzarService::isQuoteChangedMessage('')); + $this->assertFalse(MontzarService::isQuoteChangedMessage('موجودی شما کمتر از میزان سفارش است')); + $this->assertFalse(MontzarService::isQuoteChangedMessage('سفارشات توسط ارائه دهنده بسته شده است.')); + } +}