fix(tradein/payments): отказ банка на Init не блокирует покупателя на 30 минут — мёртвая попытка уходит из предиката живого платежа #3361

Merged
bot-backend merged 2 commits from fix/3323-payment-init-fail-409 into main 2026-09-05 19:19:01 +00:00
Collaborator

Refs #3323 п.2 [medium] (НЕ auto-close — в сборнике другие пункты). Чинится ДО включения payments_enabled.

Дефект

Init-fail оставлял строку NEW с payment_url=NULL; она попадает в предикат uidx 279, _find_live_payment её не видит (фильтр payment_url IS NOT NULL), повторный INSERT ловит конфликт → ложный 409 «retry shortly» до истечения окна.

Фикс — вариант (а) без нового статуса

Строка после Init-fail переводится в терминальный DEADLINE_EXPIRED тем же UPDATE, что уже пишет error_code/error_message. Это существующая конвенция «попытка мертва, пара освобождена» (_ABANDONABLE_STATUSES → :300), а не вторая параллельная; INIT_FAILED потребовал бы правки CHECK в миграции 233 ради ярлыка, при том что причина лежит в error_*.

Предикат 279 (цитата): WHERE estimate_id IS NOT NULL AND status IN ('NEW','FORM_SHOWED','PREAUTHORIZING','AUTHORIZING','AUTHORIZED','3DS_CHECKING','3DS_CHECKED','CHECKING','CHECKED','PROCESSING','CONFIRMING')DEADLINE_EXPIRED вне него, миграций нет.

Читатели статусов: REJECTED|AUTH_FAIL|DEADLINE_EXPIRED только в payments.py + тестах (фронт/админка не читают); вебхук защищает лишь CONFIRMED от отката — терминальный статус не блокирует последующий CONFIRMED.

Тесты

142 passed, 1 skipped (-k payment). Новые: поведенческий test_bank_refusal_on_init_does_not_lock_the_buyer_out + test_init_failed_status_is_outside_the_live_predicate (сверяет константу с ТЕКСТОМ миграций 279 и 233). Регресс успешного Init держат существующие test_repeat_checkout_reuses_live_link / test_parallel_checkout_does_not_create_second_payment. Фальсификация (откат только SET status = :status): AssertionError: мёртвая строка осталась в предикате 279 — assert 'NEW' not in frozenset({...}).

Refs #3323 п.2 [medium] (НЕ auto-close — в сборнике другие пункты). Чинится ДО включения `payments_enabled`. ## Дефект Init-fail оставлял строку `NEW` с `payment_url=NULL`; она попадает в предикат uidx 279, `_find_live_payment` её не видит (фильтр `payment_url IS NOT NULL`), повторный INSERT ловит конфликт → ложный 409 «retry shortly» до истечения окна. ## Фикс — вариант (а) без нового статуса Строка после Init-fail переводится в терминальный `DEADLINE_EXPIRED` тем же UPDATE, что уже пишет `error_code/error_message`. Это существующая конвенция «попытка мертва, пара освобождена» (`_ABANDONABLE_STATUSES` → :300), а не вторая параллельная; `INIT_FAILED` потребовал бы правки CHECK в миграции 233 ради ярлыка, при том что причина лежит в `error_*`. Предикат 279 (цитата): `WHERE estimate_id IS NOT NULL AND status IN ('NEW','FORM_SHOWED','PREAUTHORIZING','AUTHORIZING','AUTHORIZED','3DS_CHECKING','3DS_CHECKED','CHECKING','CHECKED','PROCESSING','CONFIRMING')` — `DEADLINE_EXPIRED` вне него, миграций нет. Читатели статусов: `REJECTED|AUTH_FAIL|DEADLINE_EXPIRED` только в `payments.py` + тестах (фронт/админка не читают); вебхук защищает лишь `CONFIRMED` от отката — терминальный статус не блокирует последующий `CONFIRMED`. ## Тесты `142 passed, 1 skipped` (-k payment). Новые: поведенческий `test_bank_refusal_on_init_does_not_lock_the_buyer_out` + `test_init_failed_status_is_outside_the_live_predicate` (сверяет константу с ТЕКСТОМ миграций 279 и 233). Регресс успешного Init держат существующие `test_repeat_checkout_reuses_live_link` / `test_parallel_checkout_does_not_create_second_payment`. Фальсификация (откат только `SET status = :status`): `AssertionError: мёртвая строка осталась в предикате 279 — assert 'NEW' not in frozenset({...})`.
bot-backend added 1 commit 2026-09-05 18:55:24 +00:00
fix(payments): отказ банка на Init больше не запирает покупателя на 30 минут
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI / changes (pull_request) Successful in 10s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m52s
5bd9c586b7
Строка после провального Init оставалась в NEW и без payment_url: для
_find_live_payment (фильтр payment_url IS NOT NULL) её нет, а для предиката
UNIQUE миграции 279 — есть. Следующий checkout ловил конфликт и получал 409
'retry shortly' до истечения _ABANDONED_AFTER_MINUTES — из-за сбоя банка, а
не своего действия.

Переводим такую строку в терминальный DEADLINE_EXPIRED (тот же, которым
checkout уже помечает брошенные попытки) в том же UPDATE, что пишет
error_code/error_message: статус вне предиката 279, пара
(estimate_id, product_code) освобождается сразу. Новый статус в CHECK 233 не
заводим — миграция ради ярлыка не нужна, причина и так в error_*.

Refs #3323
Light1YT added 1 commit 2026-09-05 19:11:44 +00:00
fix(payments): обрыв соединения и Init без PaymentURL перестают запирать покупателя
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / changes (pull_request) Successful in 11s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 5m11s
ec8213c269
Три дыры в одном замке (строка без payment_url невидима для _find_live_payment,
но видима предикату UNIQUE 279 → ложный 409 на 30 минут):

- tbank_client ловил пару (TimeoutException, NetworkError): RemoteProtocolError,
  ProxyError и UnsupportedProtocol летели наружу голым httpx-типом мимо
  `except TBankApiError` в checkout. Ловим родителя — httpx.TransportError.
- Ветка «Success:true без PaymentURL» отвечала 502, не трогая статус, — здесь
  банк заказ ПРИНЯЛ. Тот же терминальный статус, error_code=no_payment_url;
  UPDATE вынесен в общий _mark_init_failed.
- _FakeDb в тестах применял статус по наличию ключа в params, а не по тексту
  SQL: мутант без `SET status = :status` оставался зелёным. Гейт по SQL —
  мутант краснит все три теста про замок.

Комментарий про «возможный холд» на ветке отказа Init поправлен: Init холд не
создаёт, авторизация идёт с оплаты формы, а форму покупателю не выдавали.
bot-backend merged commit 1120f9d430 into main 2026-09-05 19:19:01 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#3361
No description provided.