B2C: платёжный контур 150 ₽ (CloudPayments) + серверный пейволл #2896

Open
opened 2026-08-15 16:04:22 +00:00 by lekss361 · 1 comment
Owner

Владелец выбрал CloudPayments (13.08.2026) — касса CloudKassir уже подключена, значит одна интеграция и автофискализация оплаты и возврата. В коде сегодня только интеграционный слой Т-Банка (payments/{token,notification,receipt,tbank_client}.py), роутера и статус-машины нет, PAYMENTS_ENABLED=false.

Как лучше сделать

  • Двухстадийка Auth → Confirm. Списываем только после машинного гейта качества: insufficient_data == False И n_analogs >= 5 И confidence != 'low'. Третье условие обязательно: deals-fallback (estimator.py:3243-3277) даёт ненулевую медиану при n_analogs = 0, и гейт только по флагу спишет деньги за «ориентировочно по сделкам».
  • Спекулятивный старт расчёта в момент Init, пока человек проходит 3DS — пайплайн p50 ~9 с успевает.
  • Точка принятия решения — вебхук, не возврат браузера: закрытая вкладка не должна терять заказ.
  • Общий бюджет заказа 45 с → Cancel. Предусловие: _with_budget на Avito IMV (estimator.py:3718) — единственный внешний вызов без потолка.
  • Пейволл обязан быть серверным. В макете рублёвая цифра замылена CSS (filter: blur(7px)), но лежит в разметке обычным текстом — достаётся через view-source, Ctrl+F, reader-mode. До оплаты бэкенд не должен отдавать цену клиенту вообще.
  • Автовозврат при несформированном расчёте обещан в опубликованной Политике возврата (разделы 2.1 и 4), значит обязан появиться вместе с кнопкой, а не после.
  • СБП прямым платежом в Т-Банк — отдельная тема: комиссия 0,4-0,7% против ~3%, но СБП не поддерживает холд на уровне протокола НСПК, то есть ломает схему «не спишем, если расчёт плохой».

Зависит от анонимного периметра.

Владелец выбрал CloudPayments (13.08.2026) — касса CloudKassir уже подключена, значит одна интеграция и автофискализация оплаты и возврата. В коде сегодня только интеграционный слой Т-Банка (`payments/{token,notification,receipt,tbank_client}.py`), роутера и статус-машины нет, `PAYMENTS_ENABLED=false`. ## Как лучше сделать - **Двухстадийка Auth → Confirm.** Списываем только после машинного гейта качества: `insufficient_data == False` И `n_analogs >= 5` И `confidence != 'low'`. Третье условие обязательно: deals-fallback (`estimator.py:3243-3277`) даёт ненулевую медиану при `n_analogs = 0`, и гейт только по флагу спишет деньги за «ориентировочно по сделкам». - Спекулятивный старт расчёта в момент `Init`, пока человек проходит 3DS — пайплайн p50 ~9 с успевает. - Точка принятия решения — **вебхук**, не возврат браузера: закрытая вкладка не должна терять заказ. - Общий бюджет заказа 45 с → `Cancel`. Предусловие: `_with_budget` на Avito IMV (`estimator.py:3718`) — единственный внешний вызов без потолка. - **Пейволл обязан быть серверным.** В макете рублёвая цифра замылена CSS (`filter: blur(7px)`), но лежит в разметке обычным текстом — достаётся через view-source, Ctrl+F, reader-mode. До оплаты бэкенд не должен отдавать цену клиенту вообще. - Автовозврат при несформированном расчёте обещан в опубликованной Политике возврата (разделы 2.1 и 4), значит обязан появиться **вместе** с кнопкой, а не после. - СБП прямым платежом в Т-Банк — отдельная тема: комиссия 0,4-0,7% против ~3%, но СБП **не поддерживает холд** на уровне протокола НСПК, то есть ломает схему «не спишем, если расчёт плохой». Зависит от анонимного периметра.
lekss361 added the
business
priority/p1
scope/backend
labels 2026-08-15 16:04:22 +00:00
lekss361 added the
enhancement
security
tradein
labels 2026-08-16 10:25:30 +00:00
Collaborator

Заметка для реализации платного шага (из PR #3327 / issue #3321, 02.09.2026).

Этаж и состояние с бесплатной формы теперь доживают до платного шага: takeDraft() перекладывает непрочитанные floor/condition в sessionStorage-ключ mera:estimate-draft-extras вместе с адресом; читать — readDraftExtras(expectedAddress) (mera-public/estimate-draft.ts). Сверка адреса обязательна и встроена в сигнатуру: несовпадение → null.

Ловушка, подтверждённая живой приёмкой на проде: в extras адрес лежит КАК ВВЕДЁН пользователем («Екатеринбург, Ленина 5»), а флоу /estimate нормализует адрес в полную форму («Свердловская обл, г. Екатеринбург, пр-кт Ленина, д. 5»). Если платный шаг передаст в readDraftExtras нормализованный адрес — получит null и молча потеряет поля. При реализации: либо сверять по сырому введённому адресу (сохранять его рядом), либо перекладывать extras на нормализованный ключ в момент, когда флоу узнаёт соответствие сырой↔нормализованный.

Заметка для реализации платного шага (из PR #3327 / issue #3321, 02.09.2026). Этаж и состояние с бесплатной формы теперь доживают до платного шага: `takeDraft()` перекладывает непрочитанные `floor`/`condition` в sessionStorage-ключ `mera:estimate-draft-extras` вместе с адресом; читать — `readDraftExtras(expectedAddress)` (`mera-public/estimate-draft.ts`). Сверка адреса обязательна и встроена в сигнатуру: несовпадение → `null`. **Ловушка, подтверждённая живой приёмкой на проде:** в extras адрес лежит КАК ВВЕДЁН пользователем («Екатеринбург, Ленина 5»), а флоу `/estimate` нормализует адрес в полную форму («Свердловская обл, г. Екатеринбург, пр-кт Ленина, д. 5»). Если платный шаг передаст в `readDraftExtras` нормализованный адрес — получит `null` и молча потеряет поля. При реализации: либо сверять по сырому введённому адресу (сохранять его рядом), либо перекладывать extras на нормализованный ключ в момент, когда флоу узнаёт соответствие сырой↔нормализованный.
Sign in to join this conversation.
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#2896
No description provided.