From f034de43cf38ec946b8d28ab233cb9b3fa0892dd Mon Sep 17 00:00:00 2001 From: nineap Date: Sat, 11 Jul 2026 03:54:16 +0700 Subject: [PATCH] =?UTF-8?q?Fix:=20=D1=81=D0=B1=D0=BE=D0=B9=20=D0=BF=D0=BB?= =?UTF-8?q?=D0=B0=D1=82=D1=91=D0=B6=D0=BD=D0=BE=D0=B3=D0=BE=20API=20=D0=BD?= =?UTF-8?q?=D0=B0=D0=B2=D1=81=D0=B5=D0=B3=D0=B4=D0=B0=20=D1=82=D0=B5=D1=80?= =?UTF-8?q?=D1=8F=D0=BB=20=D1=80=D0=B5=D0=B0=D0=BB=D1=8C=D0=BD=D1=83=D1=8E?= =?UTF-8?q?=20=D0=BE=D0=BF=D0=BB=D0=B0=D1=82=D1=83=20=D1=8E=D0=B7=D0=B5?= =?UTF-8?q?=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TonProvider::verify_payment и CryptoBotProvider::verify_payment на любой неуспешный HTTP-ответ (rate-limit/5xx/недоступность провайдера) возвращали Ok(false) — confirm_payment на это необратимо помечает инвойс `failed`. reconcile_pending_payments подбирает только `pending`, так что реальный платёж, наткнувшийся на временный сбой TonAPI/CryptoBot именно в момент подтверждения, терялся навсегда без пути восстановления. Теперь неуспешный HTTP-статус/ответ "не ok" от провайдера — это Err (не трогаем статус инвойса, можно повторить), а не Ok(false) (окончательное "не оплачено"). Найдено в ходе pre-launch аудита критичных путей оплаты. --- src/modules/billing/providers/cryptobot.rs | 11 ++++++++++- src/modules/billing/providers/ton.rs | 13 ++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/modules/billing/providers/cryptobot.rs b/src/modules/billing/providers/cryptobot.rs index 1961b47..c484a0c 100644 --- a/src/modules/billing/providers/cryptobot.rs +++ b/src/modules/billing/providers/cryptobot.rs @@ -107,7 +107,16 @@ impl PaymentProvider for CryptoBotProvider { .map_err(|e| AppError::Internal(format!("Ошибка парсинга ответа CryptoBot: {e}")))?; if res["ok"].as_bool() != Some(true) { - return Ok(false); + // Ответ API "не ok" (сбой/рейт-лимит/недоступность CryptoBot) — не то + // же самое, что "инвойс не найден/не оплачен". Раньше это тоже + // схлопывалось в `Ok(false)`, а confirm_payment необратимо помечает + // инвойс `failed` на любое `false` — реальная оплата терялась + // навсегда при простом временном сбое CryptoBot API. `Err` оставляет + // инвойс `pending`, юзер может нажать "Я оплатил" ещё раз. + return Err(AppError::Internal(format!( + "CryptoBot getInvoices вернул ошибку: {}", + res["error"] + ))); } let Some(item) = res["result"]["items"].as_array().and_then(|a| a.first()) else { diff --git a/src/modules/billing/providers/ton.rs b/src/modules/billing/providers/ton.rs index 1a87391..9e5f34f 100644 --- a/src/modules/billing/providers/ton.rs +++ b/src/modules/billing/providers/ton.rs @@ -62,7 +62,18 @@ impl PaymentProvider for TonProvider { .map_err(|e| AppError::Internal(format!("Ошибка запроса к TON API: {}", e)))?; if !res.status().is_success() { - return Ok(false); // Транзакция не найдена или еще не подтверждена + // Неуспешный HTTP-статус (429/5xx/недоступность TonAPI) — это НЕ то + // же самое, что "транзакция не найдена/невалидна". Раньше оба случая + // схлопывались в `Ok(false)`, а confirm_payment на любое `false` + // необратимо помечает инвойс `failed` — реальный платёж, наткнувшийся + // на временный сбой TonAPI именно в момент подтверждения, терялся + // навсегда: `reconcile_pending_payments` подбирает только `pending`, + // не `failed`. Возвращаем `Err`, чтобы `confirm_payment` НЕ трогал + // статус инвойса и юзер/сверка могли повторить попытку позже. + return Err(AppError::Internal(format!( + "TonAPI недоступен или транзакция ещё не проиндексирована: HTTP {}", + res.status() + ))); } let data: Value = res