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 <cursoragent@cursor.com>
This commit is contained in:
+246
-26
@@ -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<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;
|
||||
}
|
||||
|
||||
private function makeOrderPayload(array $data): array
|
||||
{
|
||||
$mazaneh = ((int) $data['direction'] === 1)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Services\MontzarService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class MontzarQuoteChangedTest extends TestCase
|
||||
{
|
||||
public function test_detects_persian_quote_changed_toast(): void
|
||||
{
|
||||
$this->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('سفارشات توسط ارائه دهنده بسته شده است.'));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user