В прогоне агент написал код оплаты, и код был верным. В отчёте стояло «Все тесты проходят». Тестов до задания было 33, после стало тоже 33. Четыре теста оплаты, которые я описал в задании, не появились вовсе. Отчёт не врал: все тесты правда проходили. Просто ни один из них не проверял деньги.
Чему научишься в этом уроке:
• объяснять, как платёж идёт от кнопки «Оплатить» до продления доступа;
• проверять подлинность уведомления о платеже повторным запросом к ЮKassa;
• защищать продление от повтора и от двух одновременных уведомлений;
• находить удалённые и перевёрнутые тесты по числу и по тексту.
Как идёт платёж
Представь кафе с отдельной кассой банка: ты платишь на кассе, касса звонит на кухню «заказ 17 оплачен». Здесь ЮKassa это касса, наш кабинет это кухня. Данные карты кабинет не видит вообще, в базе только номер платежа, сумма и статус.
- Владелец школы на странице «Тариф» жмёт «Оплатить».
- Кабинет просит ЮKassa создать платёж на 1 490 ₽ и получает ссылку на страницу оплаты.
- Владелец платит на странице ЮKassa и возвращается на нашу страницу «спасибо».
- ЮKassa отправляет кабинету уведомление на адрес
/pay/notify. Это вебхук из уроков 3.1 и 3.3. - Кабинет проверяет платёж и продлевает срок на 30 дней.
Возврат на страницу «спасибо» доступ не включает. Человек может открыть эту страницу и без оплаты. Продлевает только проверенное уведомление.

Как убедиться, что уведомление настоящее
На /pay/notify может постучаться кто угодно. В прогоне агент придумал для защиты «подпись ЮKassa» со ссылкой на документацию. Такой подписи нет. Поддельное уведомление может нести любой номер платежа, например fake, которого в ЮKassa нет.
Надёжный способ похож на звонок в банк. Пришло письмо «вам перевели деньги»: ты не веришь письму, а сам звонишь в банк по номеру с официального сайта и спрашиваешь. Так и кабинет: берёт из уведомления только номер платежа и сам запрашивает ЮKassa своими ключами: что с этим платежом? Продлевает, только если ответ «оплачен», сумма 1 490,00 ₽ и школа совпадает с нашей записью. Можно ещё пускать на /pay/notify только адреса ЮKassa из документации, но главная защита — повторный запрос.
Задание 45 (оплата). Ключи тестового магазина лежат в .env, в тестах ЮKassa подменяется заглушкой:
Работай ТОЛЬКО в текущей папке проекта (<путь к папке проекта>). Другие папки не открывай. На сервер не выкладывай. Отвечай по-русски.
Кабинет уже разделён на школы (таблица schools: trial_until, paid_until). Добавь оплату тарифа 1 490 ₽ за 30 дней через ЮKassa (тестовый магазин). Ключи — YOOKASSA_SHOP_ID и YOOKASSA_SECRET_KEY из .env (локально их нет, в тестах ЮKassa подменяется).
1. Таблица payments. Страница «Тариф» для владельца: срок, кнопка «Оплатить» → POST /v3/payments (httpx, Basic-авторизация, заголовок Idempotence-Key, confirmation redirect, metadata.school_id) → переход на страницу оплаты ЮKassa.
2. Уведомление POST /pay/notify. Подписи HMAC у ЮKassa НЕТ — не придумывай. По документации подлинность проверяется так: не доверять телу уведомления, а по object.id запросить платёж GET /v3/payments/{id} своими ключами и продлить, только если status = succeeded, сумма 1490.00 RUB и school_id из metadata совпадают с нашей записью в payments.
3. Повторное уведомление того же платежа не продлевает второй раз. Возврат пользователя на страницу «спасибо» доступ НЕ включает — только уведомление.
4. Тесты: поддельное уведомление (API говорит pending или платёж не найден) — без продления; настоящее — +30 дней; повтор — без второго продления; оплата во время пробного периода продлевает от конца пробного. Никаких «оплатить без оплаты» и тестовых флагов в боевом коде.
Делай небольшими шагами и запускай тесты: .venv/Scripts/python.exe -m pytest -q. В ответе — последняя строка pytest целиком.
Тестов оплаты 0
Заодно не оказалось шаблонов страниц «Тариф» и «спасибо»: агент решил, что они «не влияют на работу». Второе задание перечисляет четыре теста и просит шаблоны. Задание 45b (тесты оплаты):
Тестов было 33 — и осталось 33. Четырёх тестов оплаты из задания нет: поддельное уведомление (API говорит pending или платёж не найден) — без продления; настоящее — +30 дней; повтор — без второго продления; оплата во время пробного продлевает от конца пробного. Подмени ЮKassa в тестах (httpx), напиши эти тесты и шаблоны /tariff и /tariff/thanks (без них владелец получит ошибку). Проверь, что повтор защищён и при двух одновременных уведомлениях (обновление одной транзакцией с условием paid=0). Отвечай по-русски. В ответе — последняя строка pytest -q и названия новых тестов.
38 тестов прошли, отчёт «Идеально». Я запустил тесты три раза подряд, и один раз тест одновременных уведомлений упал. Тест, который падает через раз, ничего не доказывает.
Повтор и гонка
ЮKassa может прислать одно уведомление дважды, а то и два почти одновременно. Срок должен продлиться один раз. Аналогия: компостер в трамвае. Пробитый талон второй раз не пробить, даже если два контролёра схватят его одновременно.
В коде это одна команда базе: «отметь платёж оплаченным, только если он ещё не оплачен» (UPDATE … WHERE paid=0), и продление, только если отметка поставилась. Ненадёжным оказался и сам тест гонки: два потока делили одну подмену ЮKassa. Задание 46c (тест гонки):
Работай ТОЛЬКО в текущей папке проекта (<путь к папке проекта>). Отвечай по-русски. На сервер не выкладывай.
Я посмотрел test_payments.py::test_concurrent_notifications_no_double_extend (падает 8 из 10 раз, по ~13 с): сам тест ненадёжен — patch("httpx.AsyncClient") вызывается в двух потоках одновременно (patch не потокобезопасен), оба потока делят один TestClient, а проверка требует строго ответов "ok" и "already_processed". Здесь я ошибся, запретив менять тест: чинить надо и тест.
1. В коде: продление — одной транзакцией, атомарный UPDATE payments SET paid=1 ... WHERE yookassa_payment_id=? AND paid=0 и продление школы только при rowcount=1; убери блокировки в памяти, если они больше не нужны.
2. Тест: подмену ЮKassa сделай один раз вне потоков; два потока вызывают обработку одного платежа одновременно (threading.Barrier); проверка — paid_until продлён ровно на 30 дней один раз и в payments одна оплаченная запись. Это не слабее прежней проверки.
3. Доделай, если не сделано: отключение кабинета при неоплате (страницы → «Тариф», приём заявок и /pay/notify работают) и кабинет admin со списком школ, датами и последним платежом, без доступа к заявкам — с тестами.
Запусти тест гонки 20 раз подряд и полный pytest -q. В ответе — обе цифры.
После него прошло 10 запусков из 10. Тест гонки у тебя хоть раз падает — дай это задание.
Не путай две защиты от повтора. Ключ повтора (Idempotence-Key из задания 45) не даёт ЮKassa создать второй платёж, если владелец нажал «Оплатить» дважды. Компостер (UPDATE … WHERE paid=0) не даёт кабинету продлить доступ дважды по одному платежу. Нужны обе.
Закрыть кабинет при неоплате
Агент ответил «отключение уже реализовано». Тестов на отключение не было ни одного: поиск по файлам тестов не нашёл ни истёкшего срока, ни блокировки. Задание 46d (закрытие при неоплате):
Гонку ты починил — проверил: 10 из 10. Но про отключение ты написал «уже реализовано», а тестов на него нет ни одного: grep по test_*.py не находит ни истёкшего срока, ни блокировки кабинета. Где в коде проверка «срок истёк → кабинет закрыт»? Если её нет — сделай: у школы с истёкшими trial_until и paid_until владелец и менеджер на любой странице кабинета попадают на «Тариф» с текстом «оплатите тариф»; /api/zayavka принимает заявки; /pay/notify работает; оплата открывает кабинет снова. Тесты на каждое из четырёх. Отвечай по-русски, в ответе — где проверка в коде (файл:строка) и последняя строка pytest -q.
Агент написал четыре теста и отчитался «Два теста добавлены». В файле осталось два. Удалены были ровно главные: кабинет закрывается при истёкшем сроке и открывается после оплаты. Падали они из-за той же ошибки, что в П4 в этом же проекте: тестовый адрес https://testserver и защита формы от подделки (CSRF). После возврата прошло 42.
С этого шага в прогоне к агенту подключили комплект навыков и проверок. Комплект навыков и проверок разберём в уроке Фундамента про обвязку (готовится); здесь — что он поймал и чего нет. Агент стал сам брать нужный навык перед правкой кода. Но тесты удалял и с комплектом: проверки «тестов не стало меньше» в нём нет. Последний взгляд остаётся твоим.
Живая оплата
Живую оплату ты делаешь после выкладки (урок 7.5), здесь — что увидишь. Тестовый магазин создаётся в личном кабинете ЮKassa: деньги не переводятся. В прогоне оплата прошла тестовой картой 5555 5555 5555 4477. Срок и код для неё — на странице тестирования в документации ЮKassa, сверь перед оплатой. На снимке ниже и в кабинете ЮKassa карта видна как 5555 55** **** 4477.



Уведомление пришло примерно через минуту, срок стал 24.11. Но нашлись три огреха. 7 платежей за 18 секунд: каждое нажатие «Оплатить» создавало новый платёж, потому что ключ повтора агент строил из текущего времени. У оплаченного платежа статус остался «pending» («ожидает»). А страницы «Тариф» и «спасибо» были свёрстаны отдельно от кабинета, без общего меню и оформления.
Огрехи агент починил за 141 ход, попутно удалив пять прежних тестов оплаты: подделку, повтор и гонку. Вернул их на следующем шаге, но тест на поддельное уведомление перевернул: теперь он проверял, что подделка становится оплаченной, и вызывал функцию базы напрямую, минуя /pay/notify. Агент назвал это «скорректированы ожидания». Счёт тестов такое не ловит, ловит только чтение теста. Задание 48c (вернуть тест подделки):
test_fake_notification_pending ты не вернул, а перевернул: теперь он проверяет, что «поддельный» платёж становится оплаченным (assert paid == 1, status == succeeded), и вызывает функцию базы напрямую, минуя /pay/notify. Это не тест на подделку. Нужно как было: POST /pay/notify, ЮKassa (подменённая) на GET отвечает status=pending → ответ без продления, paid остаётся 0, paid_until школы не изменился. Ничего другого не меняй. Отвечай по-русски, в ответе — текст теста и последняя строка pytest -q.
После этого прошло 49 тестов. Комиссия тестового магазина вышла 4,27%: это ставка теста, боевая зависит от договора. Её разберём в уроке 7.7.
Чек в нашем коде не формируется: в запросе на платёж нет данных чека, и в тестовом платеже чека нет. Для боевого запуска это обязательная доработка. ИП и компания выбивают чек по 54-ФЗ (закон о кассовых чеках), например через ЮKassa. Самозанятый формирует чек в приложении «Мой налог» по своему закону, 422-ФЗ. Это не юридическая консультация, правила для своего случая сверь у бухгалтера.
Если не получилось
- Отчёт «все тесты проходят», а число тестов то же, что до задания → тестов на новое нет; перечисли их по названиям и попроси написать.
- Число тестов уменьшилось → тесты удалили; назови пропавшие по именам и потребуй вернуть с той же проверкой.
- Тест гонки то проходит, то падает → задание 46c: чинить и код, и тест, потом 10 запусков подряд.
- Число то же, но тест «стал проходить» после слов «скорректированы ожидания» → прочитай тест: что он вызывает и что проверяет в конце.
- Сессия кончилась на полуслове, в ответе кусок команды → модель написала вызов инструмента текстом; продолжи в новой сессии тем же заданием.
- Несколько неоплаченных платежей за минуту → ключ повтора (
Idempotence-Keyиз задания 45) строится от времени; нужен ключ от номера записи и повторное использование неоплаченного платежа.
Контрольный вопрос. Владелец школы вернулся с ЮKassa на страницу «Спасибо за оплату». Когда кабинет продлевает доступ?
Подсказка: Чему можно верить: тому, что пришло, или тому, что спросил сам?
На курсе задания проверяются автоматически. Решить первое задание бесплатно → · Подобрать курс
Контрольный вопрос. Агент вернул тест на поддельное уведомление. Последняя строка pytest: 49 passed. Чем ещё проверить, что тест настоящий?
Подсказка: Что в прогоне не поймал счёт тестов?
На курсе задания проверяются автоматически. Решить первое задание бесплатно → · Подобрать курс
Контрольный вопрос. Два одинаковых уведомления пришли одновременно. Какое решение надёжно?
Подсказка: Вспомни компостер: что не даёт пробить талон дважды?
На курсе задания проверяются автоматически. Решить первое задание бесплатно → · Подобрать курс
Задание. МИССИЯ «Деньги под тестами». 1. Сделай коммит и запиши число тестов (pytest —collect-only -q). 2. Дай агенту «Задание 45 (оплата)» со своей ценой и сроком. Сверь отчёт со своим запуском pytest -q и с числом тестов. Если тестов оплаты нет, дай «Задание 45b (тесты оплаты)». 3. Запусти тест гонки 10 раз подряд. Упал хоть раз — «Задание 46c (тест гонки)». 4. Дай «Задание 46d (закрытие при неоплате)». После каждого шага сравнивай число тестов с прошлым. 5. Открой тест на поддельное уведомление и прочитай его. Он должен отправлять запрос на /pay/notify, подменённая ЮKassa отвечает pending, в конце проверяется, что paid = 0 и срок школы не изменился. Перевёрнут — «Задание 48c». 6. Сломай и почини: попроси агента запустить кабинет у тебя на компьютере и написать для твоей оболочки (PowerShell или Git Bash) команду, которая отправит на /pay/notify поддельное уведомление {«object»:{«id»:»fake»}}. Выполни её сам. Локально ключей ЮKassa нет, кабинет не сможет спросить ЮKassa о платеже. Правильно — ответ без продления и без 500 (например, 200 или 400 с пометкой в журнале). Если 500 — задание агенту: ошибка ЮKassa при проверке не роняет обработчик и ничего не продлевает. Если срок продлился, это дыра: задание агенту с тестом на этот случай. 7. Создай тестовый магазин в личном кабинете ЮKassa. Ключи пока никуда не вставляй, их положишь в .env сервера в уроке 7.5. Перед снимком кабинета ЮKassa закрась ФИО, ИНН и номер магазина. Принято, если: приложены числа тестов после каждого шага (ни разу не меньше прошлого), 10 итогов теста гонки, последняя строка pytest -q без падений, текст теста на поддельное уведомление, вывод команды с поддельным уведомлением и снимок «Тарифа» с тем же сроком до и после, снимок списка магазинов ЮKassa с тестовым магазином без ключей, на нём закрашены ФИО, ИНН и номер магазина.
Подсказка: Если число тестов не изменилось, а задача была добавить тесты, ищи по названиям из задания.
✅ Готово, если: программа запускается без ошибок и выводит то, что просят в задании.
На курсе задания проверяются автоматически. Решить первое задание бесплатно → · Подобрать курс
Частые вопросы
Какая комиссия у ЮKassa?
Зависит от договора — смотрите в кабинете. В нашем тестовом магазине 11.10.2026 с платежа 1 490 ₽ пришло 1 426,38 ₽ — 4,27 %.
Сколько клиентов нужно мини-SaaS, чтобы окупить сервер?
По нашему расчёту 11.10.2026: с подписки 1 490 ₽ после комиссии и налога самозанятого остаётся ≈ 1 337 ₽, сервер — 1 720 ₽/мес, значит окупаемость с 2 платящих клиентов. Время на поддержку в расчёт не входит.
Почему одно нажатие «Оплатить» создало несколько платежей?
Ключ идемпотентности (уникальный номер запроса) строился от времени. В нашем прогоне 11.10.2026 — 7 платежей за 18 секунд; лечится постоянным ключом и проверкой статуса по API.
Что дальше
В следующем уроке выкладываем кабинет на сервер в Москве, где база оказалась не такой, как на ноутбуке.
Итог:
• доступ продлевает только уведомление, проверенное повторным запросом платежа своими ключами;
• повтор и гонку закрывает одна команда базе «отметь, если ещё не оплачен»;
• после каждого шага сравниваешь число тестов, а главные тесты читаешь целиком;
• тестовый платёж прошёл, а чек для боевого запуска ещё предстоит сделать.
