feat(tradein/payments): схема БД, конфиг и kill-switch платёжного контура #2732

Merged
lekss361 merged 5 commits from feat/tradein-payments-schema into main 2026-08-06 16:33:28 +00:00
Collaborator

Summary

PR-B из серии A..F платёжного контура МЕРЫ (Т-Банк эквайринг) — см. mera-tbank-acquiring-recon.md в корне репо, §9 «Разбивка на PR». Только схема БД + конфиг + kill-switch, бизнес-логики нет.

Входит

  • tradein-mvp/backend/data/sql/228_payments.sql — три таблицы:
    • payments — одна строка на попытку оплаты (Init → notify → Confirm/Cancel), CHECK по статусам T-Bank Acquiring API.
    • payment_notifications — append-only лог входящих вебхуков, идемпотентность через UNIQUE(tbank_payment_id, status, amount_kopecks, token).
    • payment_entitlements — что выдано за платёж (доступ/кредит), UNIQUE(payment_id, kind, ref_id).
    • FK на trade_in_estimates(id) / trade_in_leads(id) (оба уже uuid PK, проверено по факту миграций 001/172) — ON DELETE SET NULL, по образцу trade_in_leads.estimate_id.
  • _manifest_applied.txt — добавлено имя новой миграции (контракт #2216, tests/test_migrations_manifest.py).
  • app/core/config.pyTBANK_TERMINAL_KEY, TBANK_PASSWORD (SecretStr), TBANK_API_BASE_URL, TBANK_NOTIFICATION_URL, TBANK_SUCCESS_URL, TBANK_FAIL_URL, TBANK_PAY_TYPE (Literal["O","T"], дефолт "T" — двухстадийная схема с холдом), TBANK_RECEIPT_ENABLED, TBANK_TAXATION, TBANK_FFD_VERSION, PAYMENTS_ENABLED (kill-switch, дефолт False).
  • app/main.py (lifespan) — fail-fast: PAYMENTS_ENABLED=true при пустых TBANK_TERMINAL_KEY/TBANK_PASSWORD роняет старт контейнера с внятным сообщением (по образцу существующей проверки identity_store=auth).

НЕ входит (следующие PR серии)

  • Роутеров, эндпоинтов, httpx-клиента T-Bank — нет.
  • Подписи/проверки Token, сборки Receipt — нет.
  • Статус-машины, обработчика нотификаций, идемпотентной обработки — нет.
  • Изменений rbac.py / roles.yaml / Caddyfile / docker-compose — нет.
  • Ничего не задеплоено вручную, ничего не продано.

Контур выключен по умолчанию

PAYMENTS_ENABLED=false (дефолт) — все три таблицы создаются, но никакой код их не пишет и не читает до появления PR-D/E. Fail-fast сработает только если кто-то явно включит PAYMENTS_ENABLED=true без секретов терминала.

Test plan

  • ruff check + ruff format --check (line-length 100) на изменённые .py
  • python -c "import ast; ast.parse(...)" на изменённые .py
  • Полный pytest (uv run --directory backend pytest) — 3755 passed, 9 skipped, 0 failed
  • tests/test_migrations_manifest.py — зелёный (новый файл + manifest-запись согласованы)
  • Реальное применение 228_payments.sql на прод-БД (auto-apply на деплое) — вне контроля этого PR, но миграция идемпотентна (CREATE TABLE IF NOT EXISTS, DROP CONSTRAINT IF EXISTS перед ADD CONSTRAINT)
## Summary PR-B из серии A..F платёжного контура МЕРЫ (Т-Банк эквайринг) — см. `mera-tbank-acquiring-recon.md` в корне репо, §9 «Разбивка на PR». Только схема БД + конфиг + kill-switch, **бизнес-логики нет**. ### Входит - `tradein-mvp/backend/data/sql/228_payments.sql` — три таблицы: - `payments` — одна строка на попытку оплаты (Init → notify → Confirm/Cancel), CHECK по статусам T-Bank Acquiring API. - `payment_notifications` — append-only лог входящих вебхуков, идемпотентность через `UNIQUE(tbank_payment_id, status, amount_kopecks, token)`. - `payment_entitlements` — что выдано за платёж (доступ/кредит), `UNIQUE(payment_id, kind, ref_id)`. - FK на `trade_in_estimates(id)` / `trade_in_leads(id)` (оба уже `uuid PK`, проверено по факту миграций 001/172) — `ON DELETE SET NULL`, по образцу `trade_in_leads.estimate_id`. - `_manifest_applied.txt` — добавлено имя новой миграции (контракт #2216, `tests/test_migrations_manifest.py`). - `app/core/config.py` — `TBANK_TERMINAL_KEY`, `TBANK_PASSWORD` (`SecretStr`), `TBANK_API_BASE_URL`, `TBANK_NOTIFICATION_URL`, `TBANK_SUCCESS_URL`, `TBANK_FAIL_URL`, `TBANK_PAY_TYPE` (`Literal["O","T"]`, дефолт `"T"` — двухстадийная схема с холдом), `TBANK_RECEIPT_ENABLED`, `TBANK_TAXATION`, `TBANK_FFD_VERSION`, `PAYMENTS_ENABLED` (kill-switch, дефолт `False`). - `app/main.py` (`lifespan`) — fail-fast: `PAYMENTS_ENABLED=true` при пустых `TBANK_TERMINAL_KEY`/`TBANK_PASSWORD` роняет старт контейнера с внятным сообщением (по образцу существующей проверки `identity_store=auth`). ### НЕ входит (следующие PR серии) - Роутеров, эндпоинтов, httpx-клиента T-Bank — нет. - Подписи/проверки `Token`, сборки `Receipt` — нет. - Статус-машины, обработчика нотификаций, идемпотентной обработки — нет. - Изменений `rbac.py` / `roles.yaml` / Caddyfile / docker-compose — нет. - Ничего не задеплоено вручную, ничего не продано. ### Контур выключен по умолчанию `PAYMENTS_ENABLED=false` (дефолт) — все три таблицы создаются, но никакой код их не пишет и не читает до появления PR-D/E. Fail-fast сработает только если кто-то явно включит `PAYMENTS_ENABLED=true` без секретов терминала. ## Test plan - [x] `ruff check` + `ruff format --check` (line-length 100) на изменённые `.py` - [x] `python -c "import ast; ast.parse(...)"` на изменённые `.py` - [x] Полный `pytest` (`uv run --directory backend pytest`) — 3755 passed, 9 skipped, 0 failed - [x] `tests/test_migrations_manifest.py` — зелёный (новый файл + manifest-запись согласованы) - [ ] Реальное применение `228_payments.sql` на прод-БД (auto-apply на деплое) — вне контроля этого PR, но миграция идемпотентна (`CREATE TABLE IF NOT EXISTS`, `DROP CONSTRAINT IF EXISTS` перед `ADD CONSTRAINT`)
bot-backend added 1 commit 2026-08-06 12:18:38 +00:00
feat(tradein/payments): схема БД, конфиг и kill-switch платёжного контура
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 8s
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 3m5s
277d7e6030
bot-backend added 1 commit 2026-08-06 12:52:56 +00:00
fix(tradein/payments): NULLS NOT DISTINCT дедуп, pd_erased_at, статусы T-Bank
All checks were successful
CI / changes (pull_request) Successful in 8s
CI Trade-In / changes (pull_request) Successful in 10s
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 / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 3m9s
b8f225bf86
HOLD-правки ревью PR #2732 (миграция ни разу не применялась на проде,
manifest не трогаем):

- Блокер: UNIQUE(payment_id, kind, ref_id) и UNIQUE(tbank_payment_id, status,
  amount_kopecks, token) не защищали при NULL (NULL != NULL в Postgres) —
  на проде воспроизведено 3 одинаковых INSERT -> 3 строки. Оба ключа теперь
  UNIQUE NULLS NOT DISTINCT (PG15+, прод на 16.4).
- payment_notifications.processed_at — контракт "выдача состоялась" для PR-D,
  закрывает окно at-most-once (падение между INSERT нотификации и выдачей
  товара при захолдированных деньгах).
- payments_lead_idx — FK lead_id ON DELETE SET NULL без индекса это seq scan
  на каждый DELETE FROM trade_in_leads, а #2547 вводит пакетное удаление.
- payment_entitlements: убраны amount/consumed — модель кредитов/пакетов
  явно отвергнута в mera-b2c-paid-flow-decision.md §1 (capability-URL вместо
  кошелька).
- payments.pd_erased_at — покрытие purge-контура #2547, который не знает про
  ПДн в payments (customer_email/customer_phone), появляющиеся в PR-D.
- payments.order_id CHECK char_length <= 50 — лимит T-Bank OrderId, падать
  у себя, а не на /v2/Init.
- payments.status CHECK сверен с github.com/nikita-vanyasin/tinkoff/status.go
  (developer.tbank.ru рендерит enum клиентским JS, прямого доступа нет):
  убраны неподтверждённые AUTHORIZED_AND_CHARGED/RECEIPT_REGISTERED, добавлены
  подтверждённые 3DS_CHECKING/3DS_CHECKED. ATTEMPTS_EXPIRED/PAY_CHECKING НЕ
  добавлены — не подтверждены ни одним источником.
- config.py: комментарий TBANK_PAY_TYPE ссылался на recon-док (выбирает "O"),
  переставлен на mera-b2c-paid-flow-decision.md §1 (выбирает "T", источник
  реального дефолта).

Полный pytest: 3755 passed, 9 skipped, 0 failed.
bot-backend added 2 commits 2026-08-06 16:07:23 +00:00
fix(tradein/payments): разрешить коллизию номера миграции 228 -> 232
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 7s
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 / openapi-codegen-check (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 3m7s
04b268ad3a
Пока PR был на ревью, в main влились 228_scrape_proxies_browser_health.sql
и 230_house_merge_log.sql — номер 228 занят, 229/231 параллельно занимает
PR #2547. Переименовано в 232_payments.sql (git mv, история файла сохранена),
manifest обновлён (228_payments.sql -> 232_payments.sql), шапка миграции
объясняет причину переименования и актуализирован "Apply after".

Смержен свежий forgejo/main (обычный merge, без rebase/force-push) —
конфликтов не было, config.py не задет. test_new_files_do_not_reuse_prefix
теперь зелёный. Полный pytest после merge: 3858 passed, 10 skipped, 0 failed.
bot-backend added 1 commit 2026-08-06 16:28:55 +00:00
fix(tradein/payments): номер 232->233 + статусы T-Bank по официальной спеке
All checks were successful
CI / changes (pull_request) Successful in 9s
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 / 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 3m10s
95ed9c7811
Четыре правки HOLD-ревью PR #2732, форма таблиц не меняется:

1. Коллизия номера: 232_listings_observation_time_meaning.sql несёт открытый
   PR #2742 (не main, поэтому пропущено в прошлый раз). Переименовано 232 ->
   233 (git mv, история сохранена), manifest обновлён. Урок задокументирован
   в шапке файла: сверять номер нужно по main И по всем открытым PR-веткам.
   Перепроверено дважды (main + 5 остальных открытых веток) — 233 свободен.

2. payments_status_check дополнен пятью реальными in-flight статусами из
   канонического источника (OpenAPI-спека developer.tbank.ru/schemas/eacq/
   openapi.yaml, схема Confirm-2, v1.24): CHECKING, CHECKED, PROCESSING,
   COMPLETING, COMPLETED. Это статусы, которые GetState/CheckOrder вернёт по
   зависшему платежу — их читает реконсиляция PR-E; пропуск реального
   значения был единственным опасным направлением ошибки CHECK.

3. Блок про источник статусов переписан: убрана ссылка на сторонний Go-клиент
   и формулировка "enum рендерится клиентским JS" — источник истины теперь
   openapi.yaml. Отдельно зафиксировано: GetState/CheckOrder объявляют Status
   свободной строкой (maxLength: 20, без enum) — наш CHECK строже контракта
   поставщика, осознанно.

4. Контракт 'UNKNOWN' переадресован: теперь явно на обработчик нотификаций
   И задачу реконсиляции (GetState/CheckOrder, PR-E), а не только на вебхуки.

Побочно подтверждено спекой: AUTHORIZED_AND_CHARGED/RECEIPT_REGISTERED
отсутствуют (плюс maxLength:20 делает первое физически невозможным);
3DS_CHECKING/3DS_CHECKED и PREAUTHORIZING есть; ATTEMPTS_EXPIRED/PAY_CHECKING
нет; PARTIAL_REVERSED/REFUND_FAILED тоже нет в спеке — оставлены как
безвредный запас, комментарий перестал ложно утверждать обратное.

Полный pytest: 3858 passed, 10 skipped, 0 failed.
lekss361 merged commit 2d62b87cf3 into main 2026-08-06 16:33:28 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
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#2732
No description provided.