Ранняя версия HiBill умела записывать движение денег четырьмя разными способами: расход, заём, возврат и расчёт. У каждого была своя таблица, свой экран и свои правила.
Сегодня способов два. Рассказываем почему — и чему мы научились, пока их разбирали.
Три сущности, которые по сути были одной
Посмотрите, что на самом деле хранили три из четырёх.
Заём: деньги перешли от A к B, и теперь B должен A.
Возврат: деньги перешли от B к A и погасили часть того, что B был должен.
Расчёт: деньги перешли от B к A и погасили часть того, что B был должен, — и мы помечали его как окончательный.
Три названия. Одно событие. Деньги перешли ровно от одного человека ровно к одному. Все различия были смысловыми — в том, что пользователь имел в виду, — а не структурными. А мы зашили этот смысл в схему, где он застыл тремя ветками кода, которые пришлось бы вечно держать согласованными.
Выдало всё поле статуса у расчёта. У расчётов были состояния pending, paid и cancelled. Интерфейс каждый раз ставил paid. Мы построили конечный автомат и ни разу им не воспользовались, потому что в жизни расчёт записывают после того, как деньги уже ушли. Мёртвое состояние в схеме — верный признак того, что такой сущности быть не должно.
Чем они действительно отличаются
Проверку реальным использованием выдержало только одно различие: сколько людей на каждом конце.
Расход — «один ко многим». Кто-то (или несколько человек) заплатил, а стоимость делят между собой несколько человек. Самое интересное здесь — разделение.

Перевод — «один к одному». Деньги ушли от одного человека и пришли к другому, целиком. Делить нечего: вся сумма достаётся получателю.
Это настоящее структурное различие, и оно единственное. Поэтому видов два.
Всё, что умела выразить старая модель из четырёх сущностей, выражает и эта:
Что вы хотите записать | Как |
|---|---|
Ужин, разделённый на пятерых | Расход |
Одолжить другу €50 | Перевод |
Вернуть эти €50 | Перевод в обратную сторону |
Довести баланс до нуля | Перевод |
Расчёт перестал быть особой церемонией. У него нет отдельного экрана и отдельного статуса. Это перевод, который просто доводит баланс до нуля, и он виден в истории рядом со всем остальным, потому что ничем другим он и не является. Записать его — то же самое, что записать что угодно: напишите отдал ане 15, и проверка покажет перевод — Вы отдали €15.00 кому: Anna Lopes, — и никакого разделения проверять не нужно.

Правила переехали в одно место
Когда видов стало меньше, правила можно было сформулировать один раз и проверять в одном месте. Есть единственный файл, который отвечает на вопрос «корректна ли эта транзакция?», и говорит он примерно следующее:
Для расхода: суммы плательщиков, если плательщики есть, должны сложиться в итог — расход может начаться и пустым, а людей добавят позже или они сами заявят свою часть по ссылке. Участники должны свои доли.
Для перевода: ровно один плательщик, ровно один получатель, и доля получателя — вся сумма.
Вот и весь свод правил по видам. Когда сущностей было четыре, равнозначные правила были раскиданы по четырём сервисам и со временем разошлись: в ветке с несколькими плательщиками были проверки, которых не было в ветке расчётов. Теперь дверь одна, и всё проходит через неё.
Единственное исключение
В базе данных есть и третий вид, который вы никогда не создаёте сами: Упрощение.
Когда система замечает круг долгов — вы должны Marco, Marco должен Aya, а Aya — вам, — она может сократить этот круг, не меняя итога ни у кого из вас. Такие поправки должны оставаться настоящими записями, иначе балансы сдвигались бы без объяснения.
Поэтому это транзакции с пометкой Упрощение, и они видны в истории. Правило, которого мы держались: ни один баланс не меняется без видимой причины. Число, которое тихо уплывает, хуже числа, которое чуть больше, чем нужно.
Стоило ли оно того?
Переписав модель, мы удалили больше кода, чем добавили, — обычно это хороший знак. Но главная выгода оказалась в том, что стало проще потом.
Открытое разделение, распределение позиций чека, балансы в нескольких валютах, добавление людей, которые ещё не зарегистрировались, — раньше каждой из этих функций понадобилось бы четыре реализации, по одной на сущность, поэтому все они тихо откладывались. С двумя видами и общими правилами каждая оказалась небольшой задачей.
Вот довод в пользу того, чтобы сжимать модель, и он вовсе не про изящество. Каждая лишняя сущность в схеме умножает стоимость каждой функции, которую вы когда-либо добавите. Мы платили 4× за всё, чтобы сохранить различия, которые существовали только у нас в головах.
Оба вида уже работают в HiBill. Что строится поверх них, подробно разобрано в статье о том, как устроены балансы.
Читайте также
Как работает HiBill: простой учёт денег между друзьями — вся модель за одно прочтение: люди, балансы и два вида транзакций.
Упрощение долгов простыми словами: как гасятся долги по кругу — откуда берётся третий вид, который создаёт сама система.
Мы отказываемся конвертировать ваши валюты — ещё одно решение в модели данных, которое держит балансы честными.