26 дек. 2025 г.·7 мин

Схема биллингового реестра, которая сходится: счёта и платежи

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

Схема биллингового реестра, которая сходится: счёта и платежи

Почему данные биллинга перестают сходиться

Для финансов «сверить» — простое обещание: итоги в отчётах совпадают с исходными записями, и каждую цифру можно проследить. Если в месяце указано, что собрано $12,430, вы должны суметь показать точные платежи (и возвраты), увидеть, к каким счетам они применялись, и объяснить каждую разницу датированной записью.

Данные биллинга обычно перестают сходиться, когда в базе хранят не факты, а результаты. Колонки вроде paid_amount, balance или amount_due обновляются приложением со временем. Одна ошибка, одна повторная попытка или одно ручное «исправление» могут тихо изменить историю. Через недели таблица счетов говорит, что счёт «оплачен», а строки платежей не сходятся или есть возврат без соответствующего кредита.

Ещё одна распространённая причина — смешение разных типов документов. Счёт — это не платёж. Кредит‑мемо — не возврат. Корректировка — не скидка. Когда всё сжимают в одну строку «transactions» с множеством опциональных полей, отчётность превращается в угадывание, а аудит — в споры.

Корень несоответствия прост: приложения часто интересует текущее состояние («доступ активен?»), а финансам нужен след («что произошло, когда и почему?»). Схема биллингового реестра должна поддерживать оба запроса, но прослеживаемость должна быть приоритетом.

Проектируйте под эти цели:

  • Понятные итоги по клиенту, по счёту и по отчётному периоду
  • Каждое изменение записывается новой строкой (не перезаписывается)
  • Полная цепочка от счета к платежам, кредитам, возвратам и корректировкам
  • Возможность пересчитать итоги из сырых записей и получить тот же ответ

Пример: клиент платит $100, затем получает кредит $20 — в отчётах должно быть видно $100 собранных, $20 зачислено и $80 чистыми, без правки исходной суммы счета.

Отдельно: счета, платежи, кредиты и корректировки

Если хотите схему, которая сходится, рассматривайте каждый тип документа как отдельное событие. Смешивание их в одну таблицу "transactions" кажется аккуратным, но стирает смысл.

Счёт — это претензия: «клиент нам должен». Храните его как документ с заголовком (клиент, номер счёта, дата выставления, срок оплаты, валюта, итоги) и отдельными позициями (что продано, количество, цена за единицу, налоговая категория). Допускается хранить итоговые суммы заголовка для скорости, но вы всегда должны уметь объяснить их из позиций.

Платёж — это движение денег: «деньги пошли от клиента к нам». В карточных потоках часто встречаются авторизация (банк разрешил) и capture (деньги списаны). Многие системы хранят авторизации как операционные записи и в реестр помещают только capture, чтобы не завышать отчёт о наличности.

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

  • Invoice: увеличивает дебиторскую задолженность и выручку (или отложенную выручку)
  • Payment: увеличивает наличность и уменьшает дебиторку
  • Credit memo: уменьшает дебиторку
  • Refund: уменьшает наличность

Корректировка — это исправление, которое вносит команда, когда реальность не совпала с записями. Корректировка требует контекста, чтобы финансы доверяли ей. Храните, кто создал, когда опубликована, код причины и краткую заметку. Примеры: «списать 0.03 из‑за округления» или «мигрировать наследственный баланс».

Практическое правило: спросите «существовало бы это, если бы никто не ошибся?». Счета, платежи, кредит‑мемо и возвраты всё равно существовали бы. Корректировки должны быть редкими, явно помеченными и легко проверяемыми.

Выберите модель реестра, удобную для аудита

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

Документы и проводки (храните оба)

Сохраняйте документы (заголовок и позиции счёта, квитанция о платеже, кредит‑мемо), потому что людям нужно их читать. Но не полагайтесь только на итоги документов как на единственный источник правды для сверки.

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

Простая модель, дружелюбная к аудиту, следует нескольким правилам:

  • Неизменяемые записи: не редактируйте опубликованные суммы; изменения — это новые записи.
  • Явное событие проводки: каждый документ создаёт пакет проводок с уникальной ссылкой.
  • Сбалансированность: записи правильно сходятся по сумме (часто дебет равен кредиту на уровне компании).
  • Разные даты: храните дату документа (что видит клиент) и дату проводки (что попадает в отчёты).
  • Стабильные ссылки: храните внешние ссылки (номер счёта, ID процессора платежей) рядом с внутренними ID.

Натуральные ключи vs суррогатные ID

Используйте суррогатные ID для джоинов и производительности, но также храните стабильный натуральный ключ, который переживёт миграции и реимпорты. Финансы будут просить «Invoice INV-10483» задолго до смены внутренних ID. Рассматривайте номера счётов и внешние ID провайдеров как первоклассные поля.

Сторнирования без удаления истории

Когда что‑то нужно отменить, не удаляйте и не перезаписывайте. Постьте сторнирование: новые записи, зеркально отражающие оригинал с противоположными знаками, связанные с оригинальной проводкой.

Пример: платёж $100, применённый не к тому счёту, превращается в два шага: сторнируете неправильно применённую проводку, затем проводите новое применение к правильному счёту.

Пошаговый план схемы (таблицы и ключи)

Схема реестра надёжнее, когда каждый тип документа имеет собственную таблицу, и вы связываете их явными записями распределений (вместо догадок позже).

Начните с небольшого набора основных таблиц, каждая с понятным первичным ключом (UUID или bigserial) и обязательными внешними ключами:

  • customers: customer_id (PK), плюс стабильные идентификаторы вроде external_ref (unique)
  • invoices: invoice_id (PK), customer_id (FK), invoice_number (unique), issue_date, due_date, currency
  • invoice_lines: invoice_line_id (PK), invoice_id (FK), line_type, description, qty, unit_price, tax_code, amount
  • payments: payment_id (PK), customer_id (FK), payment_date, method, currency, gross_amount
  • credits: credit_id (PK), customer_id (FK), credit_number (unique), credit_date, currency, amount

Затем добавьте таблицы, которые делают итоги проверяемыми: allocations. Платёж или кредит могут покрывать несколько счетов, а счёт может быть оплачен несколькими платежами.

Используйте таблицы‑связки с собственными ключами (не только составные):

  • payment_allocations: payment_allocation_id (PK), payment_id (FK), invoice_id (FK), allocated_amount, posted_at
  • credit_allocations: credit_allocation_id (PK), credit_id (FK), invoice_id (FK), allocated_amount, posted_at

Наконец, держите корректировки отдельно, чтобы финансы видели, что и почему изменилось. Таблица adjustments может ссылаться на целевой документ через invoice_id (nullable) и хранить дельту суммы, не переписывая историю.

Добавьте поля аудита везде, где вы публикуете деньги:

  • created_at, created_by
  • reason_code (write-off, rounding, goodwill, chargeback)
  • source_system (manual, import, Stripe, support tool)

Кредиты, возвраты и списания без поломанных итогов

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

Большинство проблем со сверкой начинается, когда кредиты и возвраты записывают как «отрицательные платежи», или когда списания смешивают в позициях счета. Чистая схема хранит каждый тип документа отдельно, и их взаимодействие происходит только через явные распределения.

Кредит должен показывать, почему вы уменьшили задолженность клиента. Если он применяется к одному счёту — заведите одно кредит‑мемо и выделите его на тот счёт. Если охватывает несколько счетов — распределите один кредит‑мемо на несколько счетов. Кредит остаётся одним документом с множеством распределений.

Возвраты — это события, похожие на платежи, а не «отрицательные платежи». Возврат — это уход наличности, поэтому обрабатывайте его как отдельную запись (часто связанную с оригинальным платёжным ID для справки), а затем распределяйте как платёж. Это сохраняет ясность аудита, когда банковская выписка показывает и приход, и исходящий возврат.

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

Правила проводки, которые предотвращают двойной учёт

Эти правила убирают большинство «таинственных разниц»:

  • Никогда не храните отрицательный платёж. Используйте запись возврата.
  • Никогда не уменьшайте итог счёта после публикации. Используйте кредит‑мемо или корректировку.
  • Публикуйте документы один раз (с posted_at) и не редактируйте суммы после публикации.
  • Единственное, что меняет баланс счёта — это сумма опубликованных распределений.
  • Списание — это корректировка с кодом причины, распределяемая по счёту как кредит.

Налоги, сборы, валюты и правила округления

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

Налоги и сборы: храните на уровне позиции

Храните суммы налогов и сборов по каждой позиции, а не только суммарно по счёту. Разные товары имеют разные ставки, сборы могут облагаться налогом или нет, и освобождения часто применяются к части счёта. Если хранить только tax_total на уровне счета, рано или поздно появится случай, который вы не сможете объяснить.

Храните:

  • Сырые позиции (что продано, qty, unit_price, скидка)
  • Вычисленные итоги позиции (line_subtotal, line_tax, line_total)
  • Итоги счёта (subtotal, tax_total, total)
  • Ставку налога и тип налога, использованные для расчёта
  • Сборы как отдельные позиции (например, «комиссия за обработку платежа»)

Это позволяет финансам восстановить итоги и подтвердить, что налоги считались одинаково каждый раз.

Мультивалюта: храните и то, что произошло, и то, как вы это отражаете

Если поддерживаете несколько валют, записывайте и валюту транзакции, и значения в валюте отчётности. Практический минимум: currency_code на каждом денежном документе, fx_rate, использованный при проводке, и отдельные суммы для отчётности (например, amount_reporting), если книги ведутся в одной валюте.

Пример: клиент выставлен на 100.00 EUR плюс 20.00 EUR НДС. Сохраните эти EUR‑позиции и итоги, плюс fx_rate, применённый при публикации, и конвертированные итоги для отчётности.

Округление заслуживает отдельного обращения. Выберите одно правило округления (по строке или по счёту) и придерживайтесь его. Когда округление даёт разницу, фиксируйте её явно как строку корректировки округления (или маленькую корректировку), а не молча меняйте итоги.

Статусы, даты проводок и что не стоит хранить

Создавайте API реестра быстро
Откройте конечные точки реестра, на которые могут полагаться ваши веб‑ и мобильные приложения, без ручной серверной логики.
Сгенерировать API

Сверка портится, когда «статус» используется как сокращение бухгалтерской правды. Рассматривайте статус как метку процесса, а опубликованные проводки — как источник истины.

Сделайте статусы строгими и скучными. Каждый должен отвечать: может ли этот документ уже влиять на итоги?

  • Draft: внутренний, не опубликован, не должен попадать в отчёты
  • Issued: финализирован и отправлен, готов к публикации (или уже опубликован)
  • Void: отменён; если был опубликован, должен быть сторнирован
  • Paid: полностью погашен опубликованными платежами и кредитами
  • Refunded: деньги возвращены через опубликованный возврат

Даты важнее, чем многие команды думают. Финансы спросят: «В какой месяц это относится?» — и ваш ответ не должен зависеть от логов UI.

  • issued_at: когда счёт стал финальным
  • posted_at: когда он учитывается в отчётности
  • settled_at: когда средства прошли клиринг или платёж подтверждён
  • voided_at / refunded_at: когда сторнирование вступило в силу

Что не стоит хранить как правду: выводимые числа, которые нельзя восстановить из реестра. Поля вроде balance_due, is_overdue или customer_lifetime_value подходят как кэшированные представления только если вы всегда можете пересчитать их из invoices, payments, credits, allocations и adjustments.

Небольшой пример: повторная попытка платежа дошла до шлюза дважды. Без idempotency key вы запишете два платежа, пометите счёт «paid», а финансы увидят лишние $100 в наличности. Храните уникальный idempotency_key для внешней попытки списания и отвергайте дубли на уровне БД.

Отчёты, которые финансы ждут с первого дня

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

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

Большинство команд начинают с:

  • Aging дебиторки: суммы в разрезе клиентов и по возрастным корзинам (0‑30, 31‑60 и т.д.)
  • Полученные деньги: наличность по дням, неделям и месяцам, по датам публикации платежей
  • Выручка vs наличность: выставленные счета по датам публикации vs поступления по датам публикации
  • Трассируемость для экспорта: путь от строки GL‑экспорта до точных документов и строк распределений, которые её создали

В aging распределения играют ключевую роль. Aging — это не просто «итог счёта минус итоги платежей». Это «что осталось открытым по каждому счёту на указанную дату». Для этого нужно хранить, как каждый платёж, кредит или корректировка была применена к конкретным счетам и когда эти распределения были опубликованы.

Полученная наличность должна основываться на таблице payments, а не на статусе счета. Клиенты платят заранее, с опозданием или частями.

Выручка vs наличность — причина, почему счета и платежи должны быть разными. Пример: вы выписываете счёт $1,000 30 марта, получаете $600 5 апреля и выдаёте кредит $100 20 апреля. Выручка относится к марту (публикация счёта), наличность — к апрелю (публикация платежа), а кредит снижает дебиторку в момент публикации. Рубежи связывают всё вместе.

Пример: один клиент, четыре типа документов

Один клиент, один месяц, четыре типа документов. Каждый документ хранится один раз, а деньги перемещаются через таблицу распределений (иногда называемую "applications"). Это делает итог по балансу лёгким для пересчёта и аудита.

Предположим клиента C-1001 (Acme Co.).

Записи, которые вы создаёте

invoices

invoice_idcustomer_idinvoice_dateposted_atcurrencytotal
INV-10C-10012026-01-052026-01-05USD120.00

payments

payment_idcustomer_idreceived_atposted_atmethodamount
PAY-77C-10012026-01-102026-01-10card70.00

credits (кредит‑мемо, goodwill и т.д.)

credit_idcustomer_idcredit_dateposted_atreasonamount
CR-5C-10012026-01-122026-01-12service issue20.00

adjustments (коррекция после факта, не новая продажа)

adjustment_idcustomer_idadjustment_dateposted_atnoteamount
ADJ-3C-10012026-01-152026-01-15underbilled fee5.00

allocations (именно это фактически сверяет баланс)

allocation_iddoc_type_fromdoc_id_fromdoc_type_todoc_id_toposted_atamount
AL-900paymentPAY-77invoiceINV-102026-01-1070.00
AL-901creditCR-5invoiceINV-102026-01-1220.00

Как считается баланс по счёту

Для INV-10 аудитор может пересчитать открытый баланс из исходных строк:

open_balance = invoice.total + sum(adjustments) - sum(allocations)

Итого: 120.00 + 5.00 - (70.00 + 20.00) = 35.00 к оплате.

Для трассировки суммы 35.00:

  • Начните с итоговой суммы счета (INV-10)
  • Прибавьте опубликованные корректировки, связанные с этим счётом (ADJ-3)
  • Вычтите каждое опубликованное распределение, применённое к счёту (AL-900, AL-901)
  • Убедитесь, что каждое распределение ссылается на реальный исходный документ (PAY-77, CR-5)
  • Проверьте даты и posted_at, чтобы объяснить хронологию

Частые ошибки, разрушающие сверку

Преобразуйте схему в софт
Сгенерируйте API и админ‑панели из единого источника, чтобы счёт‑фактуры и распределения оставались согласованными.
Попробовать AppMaster

Большинство проблем со сверкой не «математические баги». Это отсутствие правил, из‑за чего одно и то же событие записывают по-разному в зависимости от участника процесса.

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

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

Шаблоны, которые обычно ломают итоги:

  • Использование отрицательных строк без строгого правила сторнирования и ссылки на оригинал
  • Редактирование старых счетов вместо публикации корректировок или кредит‑нотов
  • Смешение внешних ID шлюзов транзакций и внутренних ID без таблицы соответствия и явного источника правды
  • Позволять коду приложения вычислять итоги при отсутствии поддерживающих строк (налоги, сборы, округление, распределения)
  • Не разделять «движение денег» (cash movement) и «распределение денег» (какому счёту применено)

Последний пункт вызывает наибольшую путаницу. Пример: клиент платит $100, затем вы списываете $60 на счёт A и $40 на счёт B. Платёж — одно движение наличности, но создаёт два распределения. Если хранить только «платёж = счёт», вы не сможете поддержать частичные платежи, переплаты или перераспределения.

Чек‑лист и следующие шаги

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

Быстрые проверки сверки

Запустите эти проверки на небольшой выборке (один клиент, один месяц), затем на полном наборе:

  • Каждая опубликованная сумма в отчёте трассируется до исходных строк (позиция счёта, платёж, кредит‑мемо, корректировка) с датой публикации и валютой.
  • Суммы распределений не превышают документ, к которому они применяются (сумма распределений платежа <= сумма платежа; то же для кредитов).
  • Ничего не удаляется. Ошибочные записи сторнируются с указанием причины, затем корректируются новой опубликованной строкой.
  • Открытый баланс выводится, а не хранится (open amount = invoice total - опубликованные распределения и кредиты).
  • Итоги документа соответствуют его позициям (заголовок счёта = сумма позиций, налогов и сборов с учётом выбранного правила округления).

Следующие шаги для запуска рабочего решения

Когда схема устоялась, постройте вокруг неё операционные рабочие процессы:

  • Админ‑экраны для создания, публикации и сторнирования счетов, платежей, кредитов и корректировок с обязательными заметками
  • Вид сверки, показывающий документы и распределения рядом, включая кто и когда публиковал
  • Экспорты, которые ждут финансы (по дате проводки, по клиенту, по GL‑маппингу, если он есть)
  • Процесс закрытия периода: блокировка дат проводок для закрытых месяцев и требование сторнирований для поздних исправлений
  • Тестовые сценарии (возвраты, частичные платежи, списания), которые должны давать ожидаемые итоги

Если нужен быстрый путь к рабочему внутреннему финансовому порталу, AppMaster (appmaster.io) помогает моделировать PostgreSQL‑схему, генерировать API и собирать админ‑экраны из единого источника, чтобы правила публикации и распределений оставались согласованными по мере развития приложения.

Вопросы и ответы

Что на самом деле значит «сверить» биллинг‑данные?

Реконcиляция означает, что каждая сумма в отчёте может быть восстановлена из исходных записей и прослежена до датированных проводок. Если отчёт показывает, что вы собрали $12,430, вы должны уметь указать точные опубликованные платежи и возвраты, которые в сумме дают эту цифру, без опоры на перезаписываемые поля.

Почему со временем суммы в биллинге перестают сходиться?

Чаще всего проблема возникает из‑за хранения изменяющихся «результатов», таких как paid_amount или balance_due, как если бы это были факты. Когда эти поля обновляют повторные попытки, баги или ручные правки, теряется исторический след и суммы перестают соответствовать реальным событиям.

Почему не стоит класть счета, платежи, кредиты и возвраты в одну таблицу «transactions»?

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

В чём разница между кредит‑мемо и возвратом?

Кредит‑мемо уменьшает задолженность клиента, но не предполагает выдачу наличных. Возврат — это исходящий поток денег. Как правило, они связаны, но это разные события. Обозначая их одинаково (например, как отрицательные платежи), вы усложняете сопоставление банка и отчётность по денежным потокам.

Как исправить ошибку в биллинге, не переписывая историю?

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

Как обрабатывать частичные платежи или один платёж на несколько счетов?

Используйте явные записи распределений (applications), которые связывают платеж или кредит с одним или несколькими счетами с указанием суммы и даты проводки. Открытый остаток по счёту должен вычисляться из сумм счёта плюс корректировки минус опубликованные распределения.

Какие даты стоит хранить, чтобы отчётность по месяцам оставалась консистентной?

Храните и дату документа, и дату проводки. Дата документа — то, что видит клиент; дата проводки (posted_at) — когда это учитывается в финансовых отчётах и закрытиях периодов. Так месяц‑конец не будет сдвигаться из‑за правок позже.

Налоги и сборы: хранить на уровне счёта или позиции?

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

Как хранить мультивалютные суммы и округление, чтобы можно было воспроизвести итоги?

Храните суммы в валюте транзакции и также сохраняйте значения в валюте отчётности с курсом, использованным при проводке. Выберите правило округления (по строке или по счёту) и придерживайтесь его; любые разницы фиксируйте отдельной строкой корректировки округления.

Можно ли полагаться на статус счета (Paid/Void) для отчётности?

Статусы — это часть рабочей логики (Draft, Issued, Void, Paid), но учётной правдой являются опубликованные записи в реестре и распределения. Статус может быть неправильным; неизменяемые опубликованные записи позволяют финансам пересчитать суммы одинаково в любой момент.

Легко начать
Создай что-то невероятное

Экспериментируйте с AppMaster с бесплатной подпиской.
Как только вы будете готовы, вы сможете выбрать подходящий платный план.

Попробовать AppMaster