Fix: сбой платёжного API навсегда терял реальную оплату юзера
Build & Push Image / Build and push Caddy (Cloudflare DNS) image (push) Successful in 26s
Build & Push Image / Build and push to Gitea Registry (push) Successful in 3m32s
CI / fmt + clippy + test (push) Failing after 21m21s

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 аудита критичных путей оплаты.
This commit is contained in:
2026-07-11 03:54:16 +07:00
parent dca8d067f3
commit f034de43cf
2 changed files with 22 additions and 2 deletions
+10 -1
View File
@@ -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 {
+12 -1
View File
@@ -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