revenue-report
Отчёт франчайзи о выручке: разбор периода и денежных сумм в том виде, в котором их присылают, проверка отчёта со списком всех ошибок сразу и разбор CSV-выгрузки.
Без зависимостей. TypeScript, строгий режим. 122 теста, покрытие выше 99 %.
npm install revenue-report
Зачем
Отчёт о выручке приходит не объектом с полем revenue: number. Он приходит так:
точка;период;выручка;возвраты
MSK-014;сентябрь 2026;"1 480 000,00 ₽";"12 000,00"
MSK-021;01.09.2026-30.09.2026;980 500.50;(0,00)
Каждая строка здесь — отдельная ловушка. Неразрывные пробелы из 1С. Запятая как разделитель копеек и она же как разделитель полей. 1.234 — это тысяча или одна целая? Бухгалтерские скобки вместо минуса. Месяц словом. BOM, из-за которого первый столбец не находится по имени.
Библиотека делает ровно это и ничего больше: приводит присланное к виду, с которым можно считать.
Разбор суммы
import { parseMoney, formatMoney } from 'revenue-report';
parseMoney('1 480 000,00 ₽'); // 148000000 (копейки)
parseMoney('1 234 567.89'); // 123456789
parseMoney('(1 234,00)', { allowNegative: true }); // -123400
formatMoney(148_000_000); // '1 480 000,00'
Результат — целые копейки: тот же смысл, что в royalty-calc, суммы передаются между библиотеками без преобразований.
Неоднозначное библиотека не угадывает, а отвергает:
| Строка | Результат |
|---|---|
1.234,56 |
123456 — дробная часть та, что правее |
1.234.567 |
123456700 — один вид разделителя несколько раз, это разряды |
1.234 |
123400 — точка и полная группа из трёх цифр |
1,234 |
ошибка — в российской записи запятая отделяет копейки, а трёх знаков там не бывает |
12.34.56 |
ошибка — прочитать однозначно нельзя |
Период
Период — полуинтервал [start, end). Соседние месяцы стыкуются без зазора и без нахлёста, и не надо каждый раз решать, входит ли последний день.
import { Period } from 'revenue-report';
Period.parse('сентябрь 2026'); // [2026-09-01, 2026-10-01)
Period.parse('2026-09');
Period.parse('09.2026');
Period.parse('01.09.2026 — 30.09.2026'); // диапазон включительно
Period.month(2026, 9);
Period.inclusive('2026-09-05', '2026-09-20');
const september = Period.month(2026, 9);
september.days; // 30
september.lastDay; // '2026-09-30'
september.contains('2026-10-01'); // false — конец в период не входит
String(september); // 'сентябрь 2026'
Даты — строки ГГГГ-ММ-ДД, а не Date: Date несёт время и часовой пояс, а у отчётного месяца ни того ни другого нет. 2026-02-30 отвергается — формат верный, дня не существует.
Проверка отчёта
import { validateReport } from 'revenue-report';
const result = validateReport(
{
outletId: 'MSK-014',
period: 'сентябрь 2026',
revenue: '1 480 000,00 ₽',
metrics: { возвраты: '12 000,00' },
},
{ requiredMetrics: ['возвраты'] },
);
if (result.ok) {
result.report.revenue; // 148000000
} else {
result.issues; // [{ field: 'metrics.возвраты', message: '...' }]
}
Проверка возвращает все ошибки сразу, а не падает на первой. Франчайзи заполняет отчёт раз в месяц, и переписка «а теперь исправьте следующее поле» растягивает закрытие периода на неделю.
Имена полей принимаются и по-русски: точка, период, выручка, объём, показатели, комментарий.
Стыковка с расчётом роялти
import { validateReport, toVariables } from 'revenue-report';
import { calculateRoyalty, percent } from 'royalty-calc';
const result = validateReport(raw);
if (!result.ok) return result.issues;
calculateRoyalty(
{ kind: 'formula', expression: '(revenue - возвраты) * 0.05' },
{ revenue: result.report.revenue, variables: toVariables(result.report) },
);
toVariables отдаёт revenue, units, days и все показатели отчёта.
CSV
import { parseCsvWithHeader } from 'revenue-report';
parseCsvWithHeader(fileContents);
// [{ точка: 'MSK-014', период: 'сентябрь 2026', выручка: '1 480 000,00' }, …]
Разделитель определяется по первой строке: русский Excel пишет точку с запятой, потому что запятая занята под копейки. BOM снимается. Кавычки, разделитель и перевод строки внутри поля разбираются по правилам RFC 4180. Заголовки приводятся к нижнему регистру. Пустые строки в конце выгрузки пропускаются.
Ошибки называют номер строки: Строка 2: Столбцов 3, а в заголовке 2.
Сопоставление заголовков с полями отчёта библиотека не делает намеренно — у каждой сети своя форма, и угадывать здесь хуже, чем написать три строки явно.
Разработка
npm install
npm test # 122 теста
npm run coverage # пороги: 95 % строк, 90 % ветвей
npm run typecheck
npm run build
Зеркала
Этот репозиторий лежит на трёх площадках сразу. Первоисточник — GitHub, российские площадки принимают те же коммиты:
- GitHub: https://github.com/franchise-control/revenue-report
- GitFlic: https://gitflic.ru/project/franchise-control/revenue-report
- GitVerse: https://gitverse.ru/franchise-control/revenue-report
Зеркало на российской площадке не прихоть: домен github.com в России не заблокирован, но доступность к нему плавает — в мае 2026 доля сбоев выросла с 4 % до 10–16 % при том, что регулятор блокировку отрицал. Если GitHub у вас не открывается, берите код с GitFlic или GitVerse: он тот же, вплоть до хеша коммита.
Откуда это
Библиотека вынесена из Franchise Control — платформы управления франчайзинговой сетью: реестр точек и договоров, начисление роялти, сбор отчётов о выручке от франчайзи, документы и проверки качества.
- Управление франшизами
- Регламент сбора отчётов о выручке — PDF, без оплаты
- royalty-calc — парная библиотека: считает роялти по тому, что собрала эта
Присылали формат, который библиотека не поняла, — заводите issue с примером строки. Живой формат из чужой выгрузки полезнее звезды.
Лицензия
MIT.