@mf/finance (0.2.0)
Installation
@mf:registry=npm install @mf/finance@0.2.0"@mf/finance": "0.2.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 |