README.md

royalty-calc

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

Без зависимостей. TypeScript, строгий режим. 121 тест, покрытие выше 98 %.

npm install royalty-calc

Зачем

Роялти считают в таблице до тех пор, пока в сети не появляется вторая схема расчёта. Дальше начинается то, ради чего написана эта библиотека:

  • Копейки. 0.1 + 0.2 в JavaScript даёт 0.30000000000000004. На одном начислении это незаметно, на годовом акте — расхождение, которое ищут двое. Здесь деньги — целые копейки, ставки — целые базисные пункты, а округление происходит ровно один раз и по явно заданному правилу.
  • Прогрессивная шкала. «6 % до миллиона, дальше 4 %» в договорах означает две разные вещи, и на границе ступени они расходятся в полтора раза. Режим задаётся явно, умолчания нет.
  • Объяснение. Спор о сумме начинается с вопроса «откуда эта цифра». Ответ приходит вместе с цифрой, а не собирается заново по памяти.

Как считать

import { calculateRoyalty, percent, rubles, formatRubles } from 'royalty-calc';

const result = calculateRoyalty(
  { kind: 'min-guarantee', rate: percent(5), minimum: rubles(30_000) },
  { revenue: rubles(400_000) },
);

formatRubles(result.amount); // '30000,00'
result.explanation;
// [
//   'По ставке: 400000,00 ₽ × 5 % = 20000,00 ₽',
//   'Минимум по договору: 30000,00 ₽',
//   'Ставка ниже минимума, начислен минимум 30000,00 ₽',
// ]

Суммы задаются в рублях через rubles() или в копейках через kopecks(), ставки — в процентах через percent() или в базисных пунктах через basisPoints(). Голое число библиотека не примет: rubles(1000) и kopecks(1000) — это разные деньги, и перепутать их не даст компилятор.

Восемь схем

Схема Что считает
percent-revenue Процент от выручки
fixed Фиксированная сумма за период
fixed-plus-percent Фиксированная часть плюс процент
per-unit Ставка за единицу объёма: заказ, тонна, м²
tiered-revenue Прогрессивная шкала по выручке
min-guarantee Процент, но не меньше фиксированной суммы
formula Произвольная формула по переменным отчёта
none Роялти не платится

Прогрессивная шкала

const scheme = {
  kind: 'tiered-revenue',
  mode: 'marginal', // или 'flat'
  tiers: [
    { from: kopecks(0), rate: percent(6) },
    { from: rubles(1_000_000), rate: percent(4) },
  ],
} as const;

marginal — каждая ступень применяется к своей части выручки, как в подоходном налоге. flat — вся выручка считается по ставке той ступени, в которую попала. На выручке 1 000 000 ₽ первая даёт 60 000 ₽, вторая — 40 000 ₽. Поэтому режим обязателен: угадывать здесь нечего.

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

Произвольная формула

calculateRoyalty(
  { kind: 'formula', expression: '(revenue - returns) * 0.05' },
  { revenue: rubles(1_000_000), variables: { returns: rubles(100_000) } },
);

Формула разбирается собственным парсером — не eval и не сторонний вычислитель. Выражение пишет пользователь, и оно физически не может вызвать ничего, кроме min, max, round, floor, ceil, abs.

Все промежуточные значения — точные дроби на bigint, поэтому порядок скобок не влияет на последнюю копейку. Переменные revenue и units подставляются автоматически. Имена переменных можно писать кириллицей: выручка * 0.05.

Чего не хватает в отчёте, видно до расчёта:

import { missingVariables } from 'royalty-calc';

missingVariables('revenue - returns', { revenue: 1 }); // ['returns']

Округление и потолок

calculateRoyalty(scheme, report, {
  rounding: 'half-even', // 'half-up' (по умолчанию), 'half-even', 'floor', 'ceil'
  cap: rubles(70_000),   // верхняя граница начисления за период
});

floor — округление в пользу франчайзи, ceil — в пользу управляющей компании, half-even меньше перекашивает итог на больших объёмах.

Эффективная ставка

import { effectiveRate } from 'royalty-calc';

effectiveRate(result, report); // 500 базисных пунктов, то есть 5 %

«У вас прогрессивная шкала, по факту это 4,3 % от выручки» — понятнее списка ступеней, и сразу видно, если схема работает не так, как её задумывали.

Разработка

npm install
npm test          # 121 тест
npm run coverage  # пороги: 95 % строк, 90 % ветвей
npm run typecheck
npm run build

Зеркала

Этот репозиторий лежит на трёх площадках сразу. Первоисточник — GitHub, российские площадки принимают те же коммиты:

  • GitHub: https://github.com/franchise-control/royalty-calc
  • GitFlic: https://gitflic.ru/project/franchise-control/royalty-calc
  • GitVerse: https://gitverse.ru/franchise-control/royalty-calc

Зеркало на российской площадке не прихоть: домен github.com в России не заблокирован, но доступность к нему плавает — в мае 2026 доля сбоев выросла с 4 % до 10–16 % при том, что регулятор блокировку отрицал. Если GitHub у вас не открывается, берите код с GitFlic или GitVerse: он тот же, вплоть до хеша коммита.

Откуда это

Библиотека вынесена из Franchise Control — платформы управления франчайзинговой сетью: реестр точек и договоров, начисление роялти, сбор отчётов о выручке от франчайзи, документы и проверки качества.

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

Лицензия

MIT. Пользуйтесь в коммерческих продуктах без оговорок.

Описание
Расчёт роялти во франчайзинговой сети: восемь схем из договоров коммерческой концессии, точная арифметика без плавающей точки, объяснение расчёта. TypeScript, без зависимостей. Зеркало репозитория с GitHub.
Конвейеры
0 успешных
0 с ошибкой
Разработчики