README.md

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 — платформы управления франчайзинговой сетью: реестр точек и договоров, начисление роялти, сбор отчётов о выручке от франчайзи, документы и проверки качества.

Присылали формат, который библиотека не поняла, — заводите issue с примером строки. Живой формат из чужой выгрузки полезнее звезды.

Лицензия

MIT.

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