31 июл. 2025 г.·7 мин

Модель данных мультивалютного ценообразования для налогов и счетов

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

Модель данных мультивалютного ценообразования для налогов и счетов

Что обычно идёт не так с мультивалютными счётами

Мультивалютные счета ломаются скучными и дорогими способами. Цифры в UI выглядят верно, затем кто-то экспортирует PDF, бухгалтерия импортирует его, и итоги больше не совпадают со строками.

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

Должны соглашаться три представления, даже если они показывают разные валюты:

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

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

Конкретный пример: вы продаёте товар за 19.99 EUR, выставляете счёт в GBP и отчёт в USD. Если вы конвертируете по строкам и округляете до 2 знаков, вы можете получить другой итог налога, чем если сначала просуммировать, а затем один раз конвертировать. Оба подхода могут иметь смысл, но только один должен быть вашим правилом.

Цель — предсказуемые расчёты и понятные сохранённые значения. Каждый счёт должен без домыслов отвечать: какие суммы были введены, в какой валюте они вводились, какой курс использован (и когда), что было округлено (и как), и какие налоговые правила применялись. Такая ясность сохраняет итоги стабильными в UI, PDF, экспортах и при аудите.

Ключевые термины, с которыми стоит согласиться до проектирования схемы

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

Валютные термины, влияющие на вашу базу данных

Для каждого денежного потока согласуйте три валюты:

  • Transactional currency: валюта, которую видит и в которой соглашается клиент (прайс-лист, корзина, отображение счёта).
  • Settlement currency: валюта, в которой вы фактически получаете оплату (ту, в которой проводит платёжный провайдер или банк).
  • Reporting currency: валюта для дашбордов и сводной бухгалтерской отчётности.

Также определите minor units. У USD — 2 (центы), у JPY — 0, у KWD — 3. Это важно, потому что хранение «12.34» как числа с плавающей точкой приведёт к дрейфу, тогда как хранение целого в минорных единицах (например, 1234 цента) остаётся точным и делает округление предсказуемым.

Налоговые термины, которые меняют итоги

Налоги требуют такого же уровня согласования. Решите, являются ли цены tax-inclusive (показанная цена уже включает налог) или tax-exclusive (налог добавляется сверху). Также выберите, считается ли налог per line item (каждая строка отдельно, затем суммируется) или per invoice (сначала суммируете, затем считаете налог). Эти выборы влияют на округление и могут изменить итоговую сумму к оплате на несколько минорных единиц.

Наконец, решите, что нужно хранить, а что можно выводить заново:

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

Основные поля для денег: что хранить и как

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

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

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

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

  • unit_price_minor + unit_currency
  • quantityuom, если нужно)
  • line_subtotal_minor (до налога/скидки)
  • line_discount_minor
  • line_tax_minor (или разбивка по типам налогов)
  • line_total_minor (финальная сумма по строке)

Округление — не просто деталь UI. Фиксируйте используемый метод округления и точность, особенно если вы поддерживаете валюты с разными минорными единицами (JPY vs USD) или правила округления наличных. Небольшая запись «calculation context» может содержать calc_precision, rounding_mode и флаг, происходит ли округление по строкам или только на сумме счёта.

Держите форматирование для отображения отдельно от сохранённых значений. Хранимые значения должны быть простыми числами и кодами; форматирование (символы валют, разделители, локализованный формат) — задача слоя представления. Например, храните 12345 + EUR, а UI решает, показывать «€123.45» или «123,45 €».

Курс обмена: таблицы, временные метки и след аудита

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

Практическая таблица курсов обычно включает:

  • base_currency (от которой конвертировать, например USD)
  • quote_currency (в которую конвертировать, например EUR)
  • rate (котировка за 1 единицу базы, храните с высокой точностью)
  • effective_at (временная метка, для которой курс валиден)
  • source (провайдер) и source_ref (их ID или хеш полезной нагрузки)

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

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

Каким бы ни был ваш выбор, сохраните точный курс, использованный в счёте (часто и на строке счёта). Не полагайтесь на повторный поиск позже. Добавьте поля вроде fx_rate, fx_rate_effective_at и fx_rate_source, чтобы счёт можно было воссоздать точно.

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

Пример: заказ сделан в субботу, отгружен в понедельник, выставлен счёт в понедельник. Если ваше правило — курс на момент выставления, а провайдер не публикует курсы в выходные, вы можете использовать курс пятницы и зафиксировать effective_at = Пятница 23:59, вместе с source_ref для трассировки.

Конвертация валюты и правила округления, которые должны быть постоянными

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

Проблемы с округлением редко выглядят как очевидные баги. Они проявляются как разрыв в 1 цент между итогом счёта и суммой строк или как небольшие разницы налогов между тем, что вы показываете, и тем, что ожидает платёжный провайдер. Хорошие модели делают округление правилом, которое можно объяснить, а не сюрпризом, который потом латаете.

Точно определите, где происходит округление

Выберите точки, где допускаете округление, и сохраняйте всё остальное с повышенной точностью. Распространённые точки округления включают:

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

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

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

Выберите режим округления (half-up или bankers) и применяйте его последовательно. Half-up проще объяснить клиентам. Bankers rounding (округление банка) может уменьшать смещение при больших объёмах. Любой из них может подойти, но API, UI, экспорты и бухгалтерские отчёты должны использовать один режим.

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

Скидки тоже нуждаются в едином правиле: применять скидки до налога (обычно для купонов) или после налога (иногда требуется для отдельных сборов). Пропишите это и внедрите один раз.

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

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

Налоговые поля: шаблоны для НДС, sales tax и множественных налогов

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

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

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

На каждой строке счёта минимальный набор, который останется понятным через годы:

  • tax_basis (NET или GROSS)
  • tax_rate (десятичный, например 0.20)
  • taxable_amount_minor (база, с которой вы считали налог)
  • tax_amount_minor
  • tax_method (PER_LINE или ON_SUBTOTAL)

Если может применяться более одного налога (например НДС плюс городской сбор), добавьте отдельную таблицу разбивки, например InvoiceLineTax, с одной строкой на применённый налог. Каждая запись должна включать tax_code, tax_rate, taxable_amount_minor, tax_amount_minor, валюту и подсказки по юрисдикции, использованные при расчёте (страна, регион, почтовый индекс, когда релевантно).

Храните снимок применённых деталей правила на счёте или строке счёта, например rule_version или JSON-блоб с входными данными решения (статус налогоплательщика, reverse charge, льготы). Если правила НДС изменятся в следующем году, старые счета всё равно должны соответствовать тому, что вы фактически взяли.

Пример: подписка SaaS, проданная клиенту в Германии, может применять 19% НДС к NET-цене строки плюс 1% местного сбора. Храните итоговые суммы по строкам и разбивку по каждому налогу для показа и аудита.

Как проектировать таблицы шаг за шагом

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

Начните с решения, где хранится истина по ценам товара. Многие команды хранят базовую цену в базовой валюте на продукт и опционально добавляют переопределения по рынку (например, отдельные строки цен для USD и EUR). Что бы вы ни выбрали, сделайте это явным в схеме, чтобы не смешивать «цены каталога» с «конвертированной ценой».

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

  • Продукты и цены: product_id, price_amount_minor, price_currency, effective_from (если цены меняются со временем).
  • Заголовки заказов и счетов: document_currency, customer_locale, billing_country и временные метки (issued_at, tax_point_at).
  • Строки: unit_price_amount_minor, quantity, discount_amount_minor, tax_amount_minor, line_total_amount_minor, и валюта для каждого сохранённого денежного поля.
  • Снимок обменного курса: используемый точный курс (rate_value, rate_provider, rate_timestamp), на который ссылается заказ или счёт.
  • Записи разбивки налогов: одна строка на налог (tax_type, rate_percent, taxable_base_minor, tax_amount_minor) плюс флаг calculation_method.

Не полагайтесь на перерасчёт позже. Когда вы создаёте счёт, скопируйте окончательные цены за единицу, скидки и итоги в строки счёта, даже если они пришли из заказа.

Для трассируемости добавьте calculation_version (или calc_hash) в счёт и небольшую таблицу calculation_log, которая фиксирует, кто инициировал перерасчёт и почему (например, "курс обновлён перед выпуском").

Локализованный показ счёта, не ломая числа

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

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

Держите настройки отображения счёта в самом счёте, а не только в профиле клиента. Клиенты меняют страны, контакты и предпочтения со временем. Счёт — юридический снимок. Храните invoice_language, invoice_locale и флаги форматирования (например, показывать ли завершающие нули) с документом, чтобы перепечатка через полгода соответствовала оригиналу.

Символы валюты — дело представления. В одних локалях символ ставится перед суммой, в других — после. Где-то нужен пробел, где-то нет. Обрабатывайте размещение символа, пробелы, десятичные разделители и группировку тысяч на этапе рендеринга, исходя из локали и валюты счёта. Не встраивайте символы в сохраняемые поля и не парсите форматированные строки обратно в числа.

Если нужен отчёт во второй валюте (обычно в домашней валюте, например USD или EUR), показывайте его явно как вторичный итог, а не заменяйте основной. Валюта документа остаётся юридическим источником истины.

Практическая настройка вывода счёта:

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

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

Частые ошибки и ловушки, которых следует избегать

Самый быстрый путь сломать мультивалютные счета — обращаться с деньгами как с обычным числом. Типы с плавующей точкой для цен, налогов и итогов создают мелкие ошибки, которые позже проявляются как «разница $0.01». Храните суммы как целые в минорных единицах (цены) или используйте фиксированную десятичную типизацию с ясной шкалой, и применяйте её последовательно.

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

Смешивание валют внутри одной строки — тихий баг схемы. Если цена за единицу в EUR, скидка в USD, а налог считают в GBP, позже вы не сможете объяснить математику. Выберите одну валюту документа для отображения и расчёта, и одну базовую валюту для внутренней отчётности (если нужно). Каждая сохранённая сумма должна иметь явную валюту.

Ошибки округления часто возникают из-за слишком частого округления. Если вы округляете цену за единицу, затем итог по строке, затем налог по строке, затем снова сумму, итоги могут перестать совпадать с суммой строк.

Типичные ловушки, за которыми стоит следить:

  • Использование float для денег или курсов без фиксированной точности
  • Повторный пересчёт старых счетов по новым курсам вместо использования сохранённых курсов
  • Позволять одной строке содержать суммы в разных валютах
  • Округление на множестве шагов вместо в чётко определённых точках
  • Не хранить метку времени курса, режим округления и метод налогообложения для документа

Пример: вы создаёте счёт в CAD, конвертируете сервис, номинированный в EUR, затем позже обновляете таблицу курсов. Если вы сохранили только EUR-значение и конвертируете при показе, итог в CAD изменится на следующей неделе. Храните EUR-значение, применённый FX-курс (и время) и финальные CAD-суммы, использованные в счёте.

Быстрый чек-лист перед релизом

Управляйте настройками FX и налогов
Постройте админ-экраны для курсов, налоговых политик и согласований, чтобы счета оставались стабильными.
Начать сейчас

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

Используйте это как контроль перед релизом:

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

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

Практический тест: выберите счёт минимум с тремя строками, примените скидку и включите два налога в одной строке. Затем распечатайте его в другой локали (другие разделители и символ валюты) и подтвердите, что сохранённые числа не изменились.

Пример сценария: один заказ, три валюты и налоги

Централизуйте расчёты счётов
Используйте переиспользуемые бизнес-процессы для применения скидок, налогов и конвертаций один раз.
Попробовать AppMaster

Клиент из США выставлен счёт в USD, ваш EU-поставщик выставляет вам счёт в EUR, а финансовая команда отчитывается в GBP. Здесь модель либо остаётся спокойной, либо превращается в кучу расхождений в 1 цент.

Заказ: 3 единицы продукта.

  • Цена для клиента: $19.99 за единицу (USD)
  • Скидка: 10% на строку
  • Налог продаж в США: 8.25% (налог начисляется после скидки)
  • Себестоимость поставщика: EUR 12.40 за единицу (EUR)
  • Валюта отчётности: GBP

Разбор: что происходит и когда конвертировать

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

При создании счёта:

  1. Вычислите подитог по строке в USD: 3 × 19.99 = 59.97 USD.
  2. Примените скидку: 59.97 × 10% = 5.997, округлённое до 6.00 USD.
  3. Чистая по строке: 59.97 − 6.00 = 53.97 USD.
  4. Налог: 53.97 × 8.25% = 4.452525, округлён до 4.45 USD.
  5. Итого: 53.97 + 4.45 = 58.42 USD.

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

Что сохранять, чтобы воспроизвести счёт позднее

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

Клиент видит чистый счёт в USD (цены, скидка, налог, итог). Финансы экспортируют суммы в USD плюс замороженные эквиваленты в GBP и точные временные метки курсов, чтобы месячные отчёты совпадали, даже если курсы изменятся завтра.

Следующие шаги: реализовать, протестировать и поддерживать

Запишите вашу минимальную схему как краткий контракт: какие суммы сохраняются (оригинал, конвертированные, налог), в какой валюте каждая сумма хранится, какое правило округления применяется и какая временная метка закрепляет обменный курс для счёта. Делайте это скучно и конкретно.

Прежде чем делать UI-экраны, напишите тесты. Не тестируйте только обычные счета. Добавьте крайние случаи, которые достаточно малы, чтобы обнаружить шум округления, и достаточно большие, чтобы выявить проблемы агрегации.

Набор стартовых тест-кейсов:

  • Крошечные цены за единицу (например, 0.01) при больших количествах
  • Скидки, создающие периодические десятичные дроби после конверсии
  • Изменения курса между датой заказа и датой счёта
  • Смешанные налоговые правила (цена с налогом vs цена без налога) в одном типе счёта
  • Возвраты и кредит-ноты, которые должны совпадать с оригинальным округлением

Чтобы сократить количество обращений в поддержку, добавьте просмотр аудита, который объясняет каждое число в счёте: сохранённые суммы, коды валют, ID и время курса обмена и метод округления. Когда кто-то спрашивает «почему итог другой?», вы сможете ответить по сохранённым фактам.

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

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

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

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

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