Статья не про 1С, можно позволить себе расслабиться и поесть. Удалены все ссылки на проект, кроме исходников на Github, чтобы особенно самоотверженные модераторы в очередной раз не приняли годную техническую статью за рекламу… но кто хочет, тот всегда найдёт. Приятного чтения.
А помните, было время…
Трансграничные переводы USDT в 2026 году в российских реалиях — уже давно не новость для микро- и малого бизнеса нашей страны. Сотни и тысячи автосервисов, сервисно-производственных предприятий и в целом любой бизнес, имеющий дело с закупкой сырья из-за границы, рано или поздно сталкивается с необходимостью оплаты закупок в чём угодно (хоть натурой), но не в рублях. В ход идут казахские тенге, tether (USDT), Solana и прочие экстравагантные монетки.
Помимо бизнеса этих сегментов, трансграничные переводы в криптовалюте всё чаще применяются для оплаты услуг аутсорса — привет, разработчик на Rust, попивающий чаёк на Кипре (в то время как я гнию в московской панельке). Кроме этого, не будем забывать про граждан, намеренно укрывающихся от уплаты налогов и привыкших принимать и оплачивать 80% оборота, например, тем же USDT.
Так получилось, что я родился скромным Goфером и год назад начал работу над самописной ERP-подобной учётной системой для микробизнеса РФ [Go + pgx | Next.js + TS]. Пару месяцев назад я столкнулся с задачей адаптировать финансовый модуль Kroncl (название платформы) для мультивалютного учёта. Проблема практически любого предприятия малого масштаба, ведущего бизнес более чем в двух рабочих валютах, очень проста — деньги уходят, деньги приходят, курсы меняются, а мы учитываем только рублёвые активы (в лучшем случае). При этом все наши операции по криптокошелькам представляют собой мистические артефакты, которые вроде как есть, но не попадают даже во внутреннюю отчётность для владельца бизнеса.
В результате пары бессонных ночей фин. модуль начал поддерживать 16 валют для создания операций — как фиатных, так и крипто — и научился рассчитывать рублёвый баланс организации, исходя из операций во всех валютах по курсу на момент их создания. Посему в этой статье речь пойдёт о реализации мультивалютности на Go, курсах ЦБ и CoinGecko и разных подходах к решению одной и той же проблемы.
Github | Apache 2.0
Для начала
…для начала поймём, с чем мы работаем. Основа фин. ядра в большинстве учётных систем — транзакции двух направлений: трата или доход. В сухом остатке движение средств в любой организации определяется именно операциями этих двух видов. Каждая операция, в свою очередь, что и логично, помимо направления имеет как минимум сумму. В случае с мультивалютностью мы не можем обойтись только этим значением — нам необходимо понимать, в какой валюте была произведена операция. По этой причине мы просто добавляем ячейку типа CURRENCY_CODE для операций и храним в ней ISO-код валюты.
Всё, на этом можно было бы заканчивать и прощаться, ведь, по большому счёту, мультивалютностью можно назвать саму возможность указывать для операций код валюты. Так, к слову, выглядит мультивалютность в представлении систем вроде Битрикс24 и …
Но на основании только кодов и сумм операций можно сделать разве что свод балансов в каждой конкретной валюте. Мы же не так просты и, что ещё более важно, любой здравый человек с большой вероятностью захочет иметь представление о всех своих активах в одной конкретной валюте, например рублях. Тогда у нас возникнет необходимость в возможности конвертации сумм всех операций во все нужные нам валюты, чтобы на их основании можно было рассчитывать единый баланс.
Конвертация
Мы скажем: хорошо, при запросах к сводке будем рассчитывать баланс организации на основании операций и курса всех валют на текущий момент. Подобное решение стало бы по меньшей мере роковой ошибкой, грозящей возможными исками владельцев компаний-клиентов. Ведь достаточно взглянуть на динамику изменения курса доллара (или, что ещё лучше, BTC) и понять, что расчёт баланса организации по текущим курсам приведёт к огромным кассовым разрывам.
Поэтому для проведения корректной конвертации номиналов (сумм) операций нам необходимо брать курс её валюты на момент создания записи, а не на сегодня. Но как лучше? Конвертировать сумму во все нужные валюты непосредственно в момент создания или рассчитывать динамически, опираясь на историю изменения курсов?
Конвертируем сразу
На момент создания операции берём курс выбранной валюты и пересчитываем сумму во все нужные нам валюты. Таким образом, если нужно поддерживать 8 валют — плюс 8 дополнительных ячеек, для 40 — 40 и так далее. Вместо простой связи AMOUNT + CURRENCY_CODE у нас будут добавляться ячейки по типу: IN_RUB, IN_BTC, IN_USDT и т.д.
В какой-то момент такое ведение проекта приведёт к неконтролируемому кашмару, в котором для добавления новой валюты придётся лезть в миграции. Помимо неудобства, не будем забывать про конечный ресурс нашей базы данных, который будет улетать по мере накопления истории операций. Да, используя такой подход, мы получаем лучшую скорость на этапе расчёта баланса, ведь всё необходимое уже посчитано, но минусы данного подхода всё же перевешивают.
Конвертируем при расчёте баланса
Гораздо более аккуратным решением будет динамический расчёт баланса на основании истории операций и истории изменения курсов всех валют (кэш в оперативке). Работает это по следующему принципу:
-
отдельная таблица вида
currency_rates, хранящая историю изменения курсов необходимых нам валют. Структура таблицы крайне проста:id(ISO-код),rateNUMERIC(18, 8)(курс валюты в базовой, мы русские, значит, православный RUB),timestamp(время обновления),source(источник: ваши провайдеры курсов); -
из провайдеров (о них ещё пойдёт речь) регулярно по указанному периоду подгружаются курсы интересующих нас валют в рублях, полученные значения сохраняются в таблицу (для каждой валюты отдельная запись) и помечаются датой + временем обновления;
-
при запросе баланса организации в рублях отдельный сервис получает историю изменения курсов из памяти в оперативке и «накладывает» её на историю операций организации, находя для каждой операции самый ближайший курс её валюты; после пересчёта мы получаем операции с суммами в базовой валюте;
-
полученные суммы операций просто суммируются, и — пам-пам-пам — мы имеем баланс на основе истории операций, пересчитанных в базовую валюту по курсам, соответствующим курсам валют на момент создания каждой конкретной операции.
Полная реализация пакета currency, отвечающего за конвертацию валют, представлена здесь, его методы вызываются из пакета модуля fm, представленного здесь. Интересным (для меня) моментом во всём этом деле является работа с кэшем, представленным в виде USD_2026-06-23:73.44 для фиатных валют и BTC_2026-06-23_15:4712189 для крипто (для фиата период обновления меньше — один день). Кроме того, если возьмётесь за это дело, не накосячьте с часовыми поясами операций и записей кэша или получите рассинхрон в несколько часов (ближайшим курсом для отдельной валюты будет считаться не самый близкий курс).
Теперь о провайдерах
Допустим, но откуда брать курсы валют и как, собственно, обновлять? Для фиата ответ очевиден — ЦБ РФ: https://www.cbr-xml-daily.ru методом /daily_utf8.xml. Не забываем про поле <Nominal> в каждой валюте, означающее, на сколько нужно делить <Value> (ну, или просто берём <VunitRate>). Лимиты у сайта большие, пользуем.
Для криптовалют по типу USDT, USDC, ETH, TON… ответ тоже очевиден — CoinGecko. API имеет лимит в 10 000 запросов в месяц на бесплатном плане, чего хватает для разумного обновления курсов каждые 15–30 минут, в нашем случае — 60 минут (остаётся запас на тестовые стенды).
Как обновлять
Обновление курсов валют и последующее сохранение в currency_rates реализовывается простыми воркерами, гетающими актуальные курсы и кладущими их в базу. Просто и оптимально. Для каждого провайдера — свой cron-воркер со своим периодом обновления.
Реализация представлена в пакете currency, поддиректория workers.
В итоге…
В конечном итоге фин. модуль понимает 16 валют, включая как фиат, так и крипто, и умеет сводить баланс в рубли со всех операций вне зависимости от её валюты.
Kroncl — операционная учётная система малого бизнеса. Физическая изоляция, 5 модулей, ролевая модель на 70+ разрешениях. Go + pgx | Next.js + TS.
Github | Apache 2.0
Благодарю за чтение, хорошего дня.
ссылка на оригинал статьи https://habr.com/ru/articles/1052090/