Pull to refresh

Мультивалютная бухгалтерия для NodeJS

Reading time7 min
Views4.5K

Весной 2022 г. я начал свой проект Flanker - бот, который может выпустить для вас карту Visa USA с пополнением криптовалютой.

Мне понадобился простой модуль бухгалтерии, с легким API и без GUI. Довольно быстро я нашел medici - модуль работающий на mongoDB, очень легкий. Ему не хватало поддержки мультивалютности, но я решил, что на первое время сойдет, и внедрил его у себя.

Однако прошло несколько месяцев, проект вырос, и я решил запилить свою бухгалтерию с блекджеком, мультивалютностью, и работающую на родной для меня связке Sequelize + Postgres.

Ссылка на модуль: Fledger

Зачем

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

Правильное решение - это со старта внедрять бухгалтерию с двойной записью (double ledger).

Людям, не знакомым с принципами бухгалтерии, кажется, что бухгалтерия это сложно. На самом деле это не так, и я покажу вам, почему.

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

Двойная запись

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

Он отражает простую истину: деньги не берутся из ниоткуда, и никуда не деваются, они лишь перетекают из кармана в карман (со счёта на счёт). Поэтому любое движение денег должно быть отражено (минимум) на двух счетах - с какого ушло, и куда пришло. Если по-простому, то приход называется дебет, а уход - кредит. Дебет должен быть равен кредиту как на уровне одной транзакции, так и на уровне всей бухгалтерии в целом. Это и есть "основной закон бухгалтерии", или баланс. Именно это простое правило делает бухгалтерию с двойной записью устойчивой к ошибкам.

Использование

Перед тем как начать пользоваться Fledger, советую изучить README, тк здесь не покрыты мелкие технические детали. Здесь я сосредоточился больше на общих принципах бухгалтерии.

Я буду давать примеры использования, основанные на работе Flanker, тк данный кейс в этом плане показательный.

Допустим, юзер пополняет свой счёт на 100 долларов - закидывает 100 USDT на наш криптовалютный кошелек. Вот как это можно отразить:

Account

Debit

Credit

Assets:usdt

100

UserBalances:1

100

Дебит - по счёту Assets:usdt, который отражает состояние нашего криптокошелька. Кредит - по счёту UserBalances:1, который отражает состояние счёта клиента с id=1.

Вот как эта запись будет выглядеть в API Fledger:

await book.entry('User 1 deposit')
  .debit('Assets:usdt', 10000, {type: 'userDeposit'})
  .credit('UserBalances:1', 10000, {type: 'userDeposit'})
  .commit()

На что здесь нужно обратить внимание:

  • суммы вносятся не в долларах, а в центах. Вся бухгалтерия работает в целых числах, финансовые данные никогда не хранятся в вещественных значениях. Я вообще храню не в центах, а в сатоши (10^-8);

  • каждой транзакции мы передаем объект meta-info {type: 'userDeposit'}. С помощью мета-инфы удобно фильтровать записи. Например, теперь мы сможем со счета Assets:usdt вытащить все записи пользовательских депозитов.

Счета и субсчета

Fledger использует обычную для бухгалтерии концепцию субсчетов. Например, у нас есть счёт UserBalances, у него есть субсчета UserBalances:1 и UserBalances:2.

При запросе баланса или истории счета UserBalances, Fledger покажет баланс или историю этого счета И всех его субсчетов. Так можно узнать суммарный баланс всех юзеров на площадке, или, например суммарный баланс всех наших кошельков/банковских счетов под счётом Assets

Комиссия

Усложним пример. Скажем, мы хотим списать с юзера комиссию сразу при пополнении. Вот как это будет выглядеть:

Accounts

Debit

Credit

Assets:usdt

100

UserBalances:1

95

Income:fees

5

Или в API Fledger:

await book.entry('User 1 deposit')
  .debit('Assets:usdt', 10000, {type: 'userDeposit'})
  .credit('UserBalances:1', 9500, {type: 'userDeposit'})
  .credit('Income:fees', 500, {type: 'userDeposit'})
  .commit()

Одна запись может содержать больше двух транзакций (в теории - неограниченное количество). Но они должны быть в балансе!

Как узнать, куда дебет, а куда кредит?

В примере выше мы проводим дебет по счету Assets а кредит по счету UserBalances. Почему так, а не наоборот?

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

  • Активы (Assets)

  • Пассивы или обязательства (Liabilities)

  • Приходы (Incomes)

  • Расходы (Expenses)

Постараюсь по-простому объяснить, что это значит.

Активы - это ништяки, которыми владеет наша фирма. Кошелек с криптой - это точно актив, ведь лучше когда он есть, чем когда его нет. Если запись показывает рост активов, то это дебет по счёту актива (уменьшение активов - кредит)

Обязательства или пассивы - это отстой. Это то, что мы должны окружающему миру (юзерам, другим компаниям, государству). Если запись показывает рост пассивов, то это кредит по счёту пассива (уменьшение пассивов - дебет).

Теперь разберемся с этой записью:

await book.entry('User 1 deposit')
  .debit('Assets:usdt', 10000, {type: 'userDeposit'})
  .credit('UserBalances:1', 9500, {type: 'userDeposit'})
  .credit('Income:fees', 500, {type: 'userDeposit'})
  .commit()

Юзер пополнил наш криптокошелек. Это рост нашего актива? Да. Значит это дебет по счету Assets:usdt.

Одновременно юзер пополнил баланс своего аккаунта в нашей системе (UserBalances:1). Счёт UserBalances - это наши обязательства перед юзерами. Поэтому мы проводим кредит по счёту UserBalances:1.

Эти обязательства возникли как раз вследствие того, что юзер пополнил наш актив. Чувствуете? Здесь кроется еще одна гениальность метода двойной записи. Активы всегда равны пассивам! Активы фирмы не могут вырасти просто так сами по себе, это всегда сопровождается ростом пассивов. При этом баланс остается нулевым!

Приходы (Incomes). Теперь разберемся с последней транзакцией записи: credit('Income:fees', 500, {type: 'userDeposit'}). Счёт Income:fees это счёт Прихода. Мы списали с юзера комиссию 5 долларов, у нашей фирмы в связи с этим обязательств не возникает, но мы должны куда-то записать этот кредит. Для этого и нужны счета, на которых аккумулируется Income. В конце месяца мы глянем кредиты по этому счету, и увидим, сколько нам удалось заработать (грязными, то есть пока что без вычета расходов). Рост приходов - это кредит по счету приходов.

Расходы (Expenses). Допустим, нам нужно заплатить за хостинг. Как это выглядит с точки зрения бухгалтерии? Мы платим деньги (например, с нашего счёта Assets:bank). Это уменьшение актива, то есть кредит по счёту Assets:bank. А взамен получаем некий нематериальный актив, то есть месяц хостинга. Чтобы отразить этот факт, существуют счета Expenses. Получается, что в данном случае мы должны провести дебет по счету Expenses. Рост расходов - это дебет по счету расходов.

Мультивалютность

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

Account

Debit

Credit

Exchange rate

Assets:AlfaBank (RUB)

6000

60.0

UserBalances:1 (USD)

100

1.0

Или в API Fledger:

await book.entry('User 1 deposit')
  .debit('Assets:AlfaBank ', 6000, {type: 'userDeposit'}, 60.0)
  // курс обмена -----------------------------------------^
  .credit('UserBalances:1', 100, {type: 'userDeposit'})
  .commit()

На что здесь обратить внимание:

  • курс обмена для счетов номинированных в валюте - вещественное число;

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

  • дебит и кредит сбалансированы здесь через курс обмена.

Счет торгового баланса (trading balance account)

Допустим, мы создали запись с несколькими валютами, как в предыдущем разделе:

Account

Debit

Credit

Exchange rate

Assets:AlfaBank (RUB)

6000

60.0

UserBalances:1

100

1.0

Эта запись сбалансирована на момент ее появления в бухгалтерии, так и вся бухгалтерия сбалансирована (активы равны пассивам).

Прошло 10 минут, курс RUBUSD изменился, стал, скажем 80.0 (не имеет отношения к реальному курсу!)

Бухгалтерия больше не сбалансирована, рубли немного обесценились, и активы больше не равны пассивам. Что делать?

На помощь приходит Trading Balance Account, или Счет торгового баланса. Тема довольно обширная, поэтому для полноценного ознакомления предлагаю пройти в туториал Питера Селинджера

Но если вкратце, то Счет торгового баланса - это такой особый тип счета, с которым нельзя делать операции непосредственно. Он является мультивалютным, и автоматом меняет свое состояние каждый раз, когда мы создаем мультивалютную операцию. Он накапливает несбалансированные части разных валют, которые участвовали в мультивалютных операциях. Таким образом, если в любой момент времени пересчитать его оценку в базовой валюте (с учетом актуальных курсов валют), он покажет положительный или отрицательный баланс. Положительный баланс будет означать "выигрыш от обмена валют", отрицательный - "потери от обмена валют".

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

Чтобы запросить у Fledger изменение Счета торгового баланса за период времени, есть метод:

// query change of Trading Account state for last month:
let tb = await book.tradingBalance({
  startDate: moment().subtract(1, 'month').toDate(),
  endDate: new Date()
})

// query current state of Trading Account (for all time):
let tb = await book.tradingBalance()

// returns object:
// {
//   base: <change of trading balance converted to base currency, in string>,
//   currency: {
//     USD: <USD trading balance in string>,
//     THB: <THB trading balance in string>
//   }
// }

Баланс этого счета не кешируется, поэтому такой запрос book.tradingBalance() равносилен запросу всех транзакций бухгалтерии за всё время. Избегайте этого, лучше запрашивайте за ограниченные промежутки времени.

Итог

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

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

  • разобраться, как записать пополнение баланса юзера (куда дебит, куда кредит), и как записать его расход;

  • разобраться как получить баланс счета юзера через API Fledger (изи);

  • разобраться, какую метаинфу лучше дописывать к транзакциям, чтобы потом было удобно с ними работать (слайсить выдачу запроса book.ledger)

Tags:
Hubs:
+8
Comments3

Articles