Как я проектировал оплату и чеки для частного репетитора: где одно «оплачено» ломает учёт

Самое опасное слово в финансовом интерфейсе — «оплачено». Оно кажется точным, пока не задашь простой вопрос: что именно произошло? Урок провели, ученик нажал кнопку, перевод пришёл на карту или ФНС уже сформировала чек?

Я основатель UrokDesk, и при проектировании этого сценария мы довольно быстро упёрлись в неприятный вывод: если хранить всё одним статусом, система начинает уверенно показывать неправду. На простом уроке это ещё можно не заметить. На авансе, частичной оплате, абонементе или потерянном ответе внешнего сервиса ошибка уже превращается в дубль, неверный баланс или лишний чек.

Ниже — модель, к которой я пришёл. Она полезна не только для кабинета репетитора: те же границы нужны в любом небольшом сервисе, где работа, деньги и внешний документ связаны, но происходят не одновременно.

Урок, отметка ученика и платёж — три разных события

Сначала я пытался привязать оплату прямо к уроку. Провели занятие — появилось начисление. Поставили «оплачено» — начисление закрылось. Для одного урока и одного перевода схема выглядит естественно, но в ней смешаны три факта.

Первый факт — учебный: урок состоялся, был отменён или перенесён. Он может создать начисление или списать занятие из пакета, но сам по себе не доказывает, что деньги пришли.

Второй факт — сообщение ученика. Кнопка «Я оплатил» полезна как сигнал репетитору, но это всё ещё заявление, а не банковская операция. Ученик мог ошибиться в сумме, приложить старую квитанцию или отправить перевод позже.

Третий факт — полученный платёж. Его подтверждает репетитор после того, как действительно увидел поступление. Именно здесь появляются фактическая сумма, дата, способ оплаты и плательщик.

Поэтому отметка ученика в нашей модели переходит в состояние «на проверке». Баланс меняется только после человеческого подтверждения. Если ученик отметил 2 500 рублей, а пришло 2 000, в журнал попадают полученные 2 000, а не значение из формы ученика.

Счета учеников показывают аванс и долг отдельно от кнопки подтверждения оплаты. Демонстрационные данные.
Счета учеников показывают аванс и долг отдельно от кнопки подтверждения оплаты. Демонстрационные данные.
Четыре факта нельзя сводить к одному статусу «оплачено»: урок, сигнал ученика, фактический платёж и чек имеют разные источники подтверждения.
Четыре факта нельзя сводить к одному статусу «оплачено»: урок, сигнал ученика, фактический платёж и чек имеют разные источники подтверждения.

Главный объект — поступление денег, а не урок

Следующая ошибка — считать, что одному уроку обязательно соответствует один платёж и один чек. В жизни один перевод часто закрывает сразу несколько занятий. Иногда он закрывает старый долг и оставляет остаток авансом. Иногда это оплата пакета из восьми уроков.

Если выпускать документ «по урокам», один перевод легко превращается в несколько чеков. Если выпускать документ «по кнопкам», повторное нажатие создаёт дубль. Поэтому главным объектом я сделал одно фактическое поступление денег.

У платежа может быть несколько распределений: на один урок, на три урока или частично на уроки и частично в аванс. Состав будущего чека берётся из этих позиций, но сам чек остаётся один. Жёсткий инвариант звучит так: у одного платежа может быть не больше одного действующего чека.

Это правило должно работать не только в интерфейсе. Повторная команда после обновления страницы обязана вернуть уже существующий результат, а не начать выпуск заново.

Аванс и абонемент не должны выпускать чек при каждом списании

Аванс — это уже полученные деньги, назначение которых может уточняться позже. Если репетитор получил 6 000 рублей вперёд, финансовое событие произошло в день поступления всей суммы. Последующие уроки лишь уменьшают аванс. Они не создают новые поступления и поэтому не должны заново запускать выпуск чека.

С абонементом похожая развилка. Денежный платёж отвечает на вопрос «сколько и когда получили», а счётчик пакета — «сколько занятий осталось». Ученик может оплатить восемь уроков одним переводом: тогда фиксируется один платёж на всю полученную сумму и один связанный чек, а каждое проведённое занятие уменьшает только остаток пакета.

Частичная оплата тоже не требует особой магии. В журнале остаётся ровно та сумма, которая пришла, а непокрытая часть остаётся долгом. Важно не подменять фактический перевод ожидаемой стоимостью урока.

Сервис не должен притворяться платёжным оператором

В этом сценарии кабинет не принимает и не удерживает деньги. Репетитор получает их привычным способом, а затем фиксирует уже состоявшийся платёж. Это ограничение оказалось полезным для интерфейса: нельзя написать «оплатить в сервисе», если сервис лишь ведёт учёт.

По той же причине автоматический выпуск без проверки человека был бы плохой идеей. Даже если ученик приложил банковскую квитанцию, репетитор должен подтвердить реальное получение, сумму и дату. Автоматизация начинается после точки, в которой человек принял ответственность за факт поступления.

Самый опасный ответ провайдера — «неизвестно»

Внешний сервис может принять запрос, сформировать чек и не успеть вернуть ответ из-за тайм-аута. Для интерфейса это не обычная ошибка. Если сразу повторить тот же запрос, можно получить два документа на одно поступление.

Поэтому между «успешно» и «неуспешно» нужен отдельный статус: результат неизвестен. В нём система сначала сверяет внешние операции по времени, сумме и точному составу позиций. Если подходящий чек найден, он связывается с исходным платежом. Если совпадений несколько, решение возвращается человеку: угадывать здесь опаснее, чем попросить сверку.

Только после подтверждения, что первая операция не состоялась, допустима новая попытка. Для меня это стало общим правилом интеграций: потерянный ответ нельзя автоматически приравнивать к невыполненному действию.

Ошибка в платеже и ошибка в чеке исправляются по-разному

У одной кнопки «Отменить» здесь тоже оказалось слишком много смыслов.

Если неверны сумма или дата поступления, ошибочен сам платёж. Нужно исправить или отменить финансовую запись, пересчитать счёт и аннулировать связанный чек.

Если платёж верен, но в чеке ошиблись в названии услуги или реквизитах плательщика, финансовую запись менять нельзя. Аннулируется только чек, после чего создаётся исправленный документ на тот же платёж. Его сумма и дата остаются зафиксированы исходным поступлением.

Если деньги действительно вернули, сначала происходит возврат клиенту, а уже затем платёж и чек получают соответствующую отмену. Нельзя превращать кнопку в интерфейсе в доказательство того, что деньги физически ушли обратно.

Во всех трёх случаях история сохраняется. Ошибочный документ не исчезает, а остаётся связан с причиной отмены и заменой. При этом правило «не больше одного действующего чека на платёж» продолжает выполняться.

История показывает действующий и аннулированный чеки и отдельное действие для исправленного документа. Демонстрационные данные.
История показывает действующий и аннулированный чеки и отдельное действие для исправленного документа. Демонстрационные данные.

Ученику нужна прозрачность, но не внутренняя кухня

Финансовый экран ученика тоже легко перегрузить. Ему важно видеть текущий аванс или долг, отправленную отметку на проверке, подтверждённые платежи и официальный чек, если он сформирован.

При этом внутренние ошибки провайдера, служебные комментарии репетитора и реквизиты плательщика ученику не нужны. Публичный статус должен честно отвечать на его вопрос, не раскрывая технические и лишние персональные данные.

Ученик различает ожидаемую оплату, свою отметку на проверке и подтверждённый результат. Демонстрационные данные.
Ученик различает ожидаемую оплату, свою отметку на проверке и подтверждённый результат. Демонстрационные данные.

Инварианты, которые я бы зафиксировал до отрисовки экранов

В итоге у меня получился короткий набор правил:

1. Проведённый урок, отметка ученика и полученный платёж хранятся как разные события.

2. Финансовый счёт меняется только после подтверждения фактического поступления человеком.

3. У одного платежа может быть несколько уроков или позиций, но не больше одного действующего чека.

4. Аванс и оплата абонемента создают финансовое событие один раз; последующее списание занятий не изображает новый доход.

5. Неопределённый ответ внешнего сервиса сначала сверяется, а не повторяется вслепую.

6. Отмена и исправление дополняют историю, а не переписывают её задним числом.

Самой сложной частью оказалась не форма с суммой и не кнопка «Сформировать чек». Сложнее было запретить системе делать удобные, но неверные выводы. Одно слово «оплачено» пришлось разложить на несколько состояний — зато после этого баланс, чек и история перестали спорить друг с другом.

Это разбор продуктовой модели, а не налоговая консультация. Для реальных операций репетитор всё равно отвечает за корректность суммы, даты, плательщика и состава услуги.

1