mf

@mf/finance (0.3.0)

Published 2026-08-21 16:55:00 +00:00 by lucky

Installation

@mf:registry=
npm install @mf/finance@0.3.0
"@mf/finance": "0.3.0"

About this package

@mf/finance — домен и расчёты раздела «Финансы»

Две точки входа:

  • @mf/finance — домен и расчёты. Ни сети, ни криптографии, ни DOM.
  • @mf/finance/store — клиентский индекс: IndexedDB, догрузка по курсору, догоняющее применение планов. Крипта, транспорт и хранилище передаются портами, а не импортируются.

Чистота корневой точки входа проверяется машинно: npm run check:pure (входит в npm test) валится, если в пакете появилась зависимость, импорт извне или обращение к платформе вне ./store. Это тот же приём, что tools/archlint для сервисов — «красная лампа» вместо договорённости на словах.

Экраны собираем в mf/web; сюда не попадает ничего про интерфейс.

Что внутри

Модуль Что делает
day календарная дата без времени и поясов; целая арифметика, прижатие к последнему дню месяца
money деньги как целые минорные единицы; деление без потери копеек; комиссии путей ввода и вывода
recurrence двенадцать правил повторения из исходного проекта, состояние плана, догоняющее применение
amortization графики кредита: аннуитет и дифференцированные платежи по фактическим дням
types расшифрованные формы: запись, счёт, обязательство, метка, позиция чека
receipt состав чека и инвариант «сумма позиций равна сумме операции»
payoff четыре стратегии погашения долга и проверка «план равен остатку»
fx курсы, кросс-курсы, приведение к валюте семьи
settings валютный режим семьи: «все расчёты всегда в одной валюте»
ledger свёртки балансов и остатков, проверка записи перед сохранением
transfer пара операций перевода с комиссиями и курсом
aggregate хронология плана и факта, итоги, метки, очереди, динамика

Откуда взялись правила

Раздел переносится из старого проекта (docs/legacy-budget/), где логика была обкатана на живых людях. Инварианты из инвентаризации — прямо тест-кейсы: каждый значимый тест назван тем правилом, которое он защищает.

Исправленные по ходу дефекты источника:

  • 31 января плюс месяц уезжало в март — теперь прижимается к 28/29 февраля;
  • «ежемесячно 31-го» после февраля навсегда становилось 28-м, потому что вхождения считались прокруткой предыдущего. Теперь — от начала сетки, и 31-е возвращается;
  • «каждые N месяцев M-го» не работало вовсе: цикл инициализации не выполнялся ни разу;
  • квартальный аннуитет делил ставку на 12 вместо 4;
  • подсказка «последний платёж» считалась как платёж·n − платёж·(n−1), то есть всегда показывала сам платёж;
  • копейки терялись: суммы позиций чека складывались через parseInt;
  • итоги по валютам складывались в одно число — рубли с долларами.

Валютный режим

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

Зачем чистота домена

Помимо тестов и песочницы:

  • Аудит крипты не расползается. Ключи живут в @mf/crypto; появление его в зависимостях означало бы, что каждый релиз финансов надо смотреть глазами на предмет обращения с ключами. Проверяется check:pure.
  • Инцидент воспроизводим. Данных пользователя у нас нет и быть не может (ADR-0002), попросить «пришлите базу» нельзя. Единственный способ разобраться с неверной цифрой — повторить расчёт на синтетических данных. Если бы домен умел расшифровывать, для повтора понадобились бы чужие ключи, то есть ничего бы не вышло.
  • Второй клиент достаётся даром. Появится мобильный — переписываются порты, а не расчёты.
  • Релизы развязаны. @mf/crypto версионируется по крипто-причинам, @mf/finance по предметным; связанные, они тянули бы друг друга.

Чем чистота не является: это не граница безопасности. Оба пакета едут в одном бандле, и вредоносный клиент читает ключи независимо от их раскладки по пакетам — этот риск в модели угроз заявлен открытым до доверенного клиента.

Ничего не хранится числом

Баланс счёта и остаток обязательства — свёртки операций (ADR-0021). Отсюда два следствия, которые видны в API:

  • начальный остаток счёта — это операция, а не поле: иначе свёртка никогда не совпадёт с реальностью;
  • «забытый платёж» — это факт без счёта. Отдельного флага нет: две формы одного состояния всегда разъезжаются, а так «нет счёта» и «не двигает баланс» — одно утверждение.

То же и с подтверждением: отсутствие статуса у плана означает, что подтверждение не требуется и план применится догоняющим автопостингом. В источнике это был отдельный флаг рядом со статусом.

Что выяснилось на тестах

Дифференцированные платежи убывают не монотонно. При начислении по фактическим дням после короткого февраля идёт длинный март, и лишние три дня процентов перебивают уменьшение тела: платёж вырастает. Арифметика верна, но привычная формулировка «дальше только меньше» в интерфейсе будет неправдой раз в год. Закреплено тестом, чтобы не выглядело ошибкой.

Разработка

npm install
npm test          # 189 тестов + проверка чистоты
npm run typecheck
npm run build

Публикация в npm-реестр Gitea — release-CI монорепы, идемпотентно по версии. Новая версия = bump в package.json → пуш в main → bump зависимости в mf/web.

Dependencies

Development Dependencies

ID Version
@types/node ^22.0.0
typescript ^5.6.0
vitest ^2.1.0
Details
npm
2026-08-21 16:55:00 +00:00
5
47 KiB
Assets (1)
Versions (8) View all
0.8.0 2026-08-23
0.7.0 2026-08-23
0.6.0 2026-08-23
0.5.0 2026-08-21
0.4.0 2026-08-21