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 — платформы управления франчайзинговой сетью: реестр точек и договоров, начисление роялти, сбор отчётов о выручке от франчайзи, документы и проверки качества.
- Учёт и расчёт роялти — разбор восьми схем и того, когда какая уместна
- Калькулятор роялти — те же схемы в браузере, без установки
- Чек-лист проверки договора коммерческой концессии — PDF, без оплаты
Нашли ошибку в расчёте или схему, которой здесь не хватает, — заводите issue. Схема, которую мы не учли, интереснее для нас, чем звезда.
Лицензия
MIT. Пользуйтесь в коммерческих продуктах без оговорок.