feat(mera/b2c): платёжный роутер, статус-машина и доставка купленного (за флагом) #3231
No reviewers
Labels
No labels
Fable 5 ревью
GG-форсайт
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
feedback/max
generative
needs-discussion
needs-human
observability
pause-bots
performance
priority/p0
priority/p1
priority/p2
priority/p3
scope/backend
scope/db
scope/devops
scope/frontend
scope/qa
scrapers
security
site-finder
stage/1
stage/2
status/blocked
status/done
status/needs-analysis
status/needs-fix
status/qa
status/ready
status/review
status/wip
tech-debt
tradein
ux
week ревью 1
wontfix
ИРД
вторичка
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#3231
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "feat/b2c-payments-router"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Недостающая середина платёжного контура. Тяжёлая половина была сделана раньше и лежала без проводки: клиент Т-Банка, подпись токена, разбор нотификаций, чеки 54-ФЗ (
app/services/payments/**, 699 строк с тестами), схема233_payments.sqlприменена на проде, периметр под вебхук подготовлен (свой лимитер, скрабинг Sentry, пропуск тела из аудита). Не было роутера, статус-машины и доставки товара.Всё за
payments_enabled(на продеFalse) → при выключенном флаге ручки отвечают 503. Миграция не понадобилась.Что внутри
POST /payments/checkout— цена берётся из серверного каталога поproduct_code, а не из тела запроса. Свойorder_idпишется ДО похода в банк. Идемпотентность: живой платёж по(estimate_id, product_code)переиспользуется вместе с уже полученнымPaymentURL— второго Init и второго холда не создаётся. Ошибка банка оставляет запись сerror_code/error_messageи отдаёт 502; слепой повтор Init запрещён контрактом самогоinit_payment.POST /payments/notify— вебхук. Сырой INSERT вpayment_notificationsидёт до разбора и пишется даже при невалидной подписи (token_valid=false) — иначе расследовать нечего. Защита от дублей —ON CONFLICT DO NOTHINGна UNIQUE из миграции, а не «проверить-потом-вставить» (это гонка). Сумма сверяется сpayments.amount_kopecks: это единственная защита от перекладывания символов внутри подписи. Незнакомый статус →'UNKNOWN', и предварительные статусы не имеют права затереть уже наступившийCONFIRMED.processed_atставится ПОСЛЕ выдачи, ответ"OK"— тоже после. Отказ (403/400) только там, где принять означало бы соврать про деньги.GET /trade-in/r/{token}— доставка. Право доступа — сам токен, RBAC не участвует. 404 одинаковый на «нет», «протух» и «удалено».GET /payments/status/{order_id}— экран после оплаты;report_url = null, пока выдачи нет, вместо правдоподобной ссылки в 404.Одно место, где легко было сломать инвариант
Токен доставки положен в
payment_entitlements.subject, аref_idосталсяestimate_id. На(payment_id, kind, ref_id)держится UNIQUE «выдали ровно один раз» — случайный токен вref_idсделал бы каждую строку уникальной и молча отменил эту защиту. Выдача продлеваетtrade_in_estimates.retain_untilчерезGREATEST, иначе purge удалил бы строку через сутки и оплаченная ссылка указывала бы в пустоту.Чек не выписывается, и это осознанно
TBANK_TAXATIONпуст,TBANK_RECEIPT_ENABLED=false. Вместо того чтобы угадать систему налогообложения, код отдаёт жёсткий отказ «receipt is not configured»: подставленный наугад режим означает неверный фискальный документ. Ставка НДС в позиции сейчасnoneи требует подтверждения бухгалтером вместе сTaxation.Приёмка на проде (флаг выключен)
Ожидается 503: роут есть, приём выключен. 404 означало бы старый образ, 401 — что путь не попал в
_PUBLIC_PATHSи вебхук банка получил бы отказ.Таблицы
payments/payment_notifications/payment_entitlementsобязаны остаться пустыми при выключенном флаге.За владельцем — без этого включать нельзя
TBANK_TERMINAL_KEY/TBANK_PASSWORDна проде отсутствуют вовсе,app/main.pyроняет старт при включении флага без них.TBANK_NOTIFICATION_URLпуст — без него банк вебхук не пришлёт в принципе.pay_type="T", холд) деньги останутся в холде, пока не появится реконсиляция:Confirmвнутри вебхука запрещён бюджетом времени — банк ждёт ответа около 10 секунд, а подтверждение занимает заметно дольше. Это осознанный разрез, а не недосмотр, но автовозврат уже обещан в опубликованной политике возврата — значит он обязан появиться вместе с кнопкой оплаты.Не хватало ровно проводки: сервисный слой Т-Банка (PR-C) и схема (PR-B, 233) уже были, HTTP-ручек и статус-машины — нет, как и доставки купленного. Всё за kill-switch PAYMENTS_ENABLED (дефолт false): при выключенном контуре каждая ручка отвечает 503 и не трогает ни банк, ни платёжные таблицы, поэтому merge на проде не меняет поведения. Идемпотентность целиком отдана БД (UNIQUE миграции 233 + ON CONFLICT DO NOTHING), а не паре «проверить-потом-вставить»: между проверкой и вставкой проходит параллельный ретрай банка, и товар выдаётся дважды. Признаком «выдача состоялась» служит payment_notifications.processed_at, а не сам факт строки — иначе падение процесса между записью нотификации и выдачей оставило бы клиента без отчёта при списанных деньгах. Доставка — capability-ссылка /api/v1/trade-in/r/<token>: токен лежит в payment_entitlements.subject (ref_id остаётся estimate_id, на нём держится UNIQUE «выдали один раз»), режется из GlitchTip-событий и открыт в rbac отдельным узким префиксом. Тело GET /estimate/{id} вынесено в load_estimate, чтобы у второго права доступа был тот же загрузчик, а не третья копия гейта читаемости.93da29ce81toa48070dd89