FinGuide / «Финансовый капитал» — контракт бэкенд ↔ фронтенд¶
Источники анализа:
- Сайт Figma
https://smooth-try-70453479.figma.site/; пакет Figma Make разобран статически. - Excel-файл
Модель_P---56630d2a-6465-4036-bd42-9117c7dc9bd6.xlsx— расчётная модель FinGuide; подробный разбор:docs/model-analytics.md.
В прототипе все данные живут в localStorage; для боевого продукта источником истины должен быть бэкенд.
Экраны из макета¶
/Дашборд: доходы/расходы за год, чистый баланс, норма сбережений, цели, пенсионный капитал, прогноз и рекомендации./foundationОбщие данные: профиль, возраст, валюта, стартовый капитал, доходность/инфляция./incomeCRUD источников дохода./expensesCRUD категорий расходов./goalsфинансовые цели + приоритет waterfall-распределения./goal-trackingфактические взносы к целям./pensionпенсионный расчёт и стратегииpreserve/spend./summaryсводный отчёт./analyticsграфики, оценка финансового здоровья, план/факт, тренды, капитал./calendarежемесячный трекер накоплений./budget50/30/20 и конверты бюджета./scenarios/compareсценарии и сравнение./settings,/profile,/faq.
Базовые правила API¶
- Базовый URL внутри сервиса:
/api/v1. - Публичный URL реального бэкенда на текущем сервере:
https://finguide.les13.tech/finguide-api/api/v1. GET /api/v1возвращает JSON-индекс сервиса со ссылками на Swagger/OpenAPI и основные endpoints.- Swagger UI реального бэкенда:
https://finguide.les13.tech/finguide-api/swagger-ui.html. - Legacy mock Swagger больше не входит в текущий публичный deployment contract; mock artifacts остаются только для локального/исторического сравнения.
Текущая real-реализация покрывает public registration/password reset facades поверх Keycloak, чтение и управление планами, dashboard/health/cashflow/monthly-cashflow, persisted scenario CRUD/compare, persisted analytics assumptions/current balance/yearly projection/pension settings/pension projection, CRUD для IncomeSource, ExpenseItem, Goal, включая goals/reorder, legacy persisted Contribution ledger, persisted BudgetSettings, monthly tracker и operation journal. Локально это работает на H2 demo mode, production-like стенд — на PostgreSQL через prod profile.
OpenAPI guardrail #16 включён в тестовый набор: checked-in openapi/openapi.json сейчас содержит 58 операций, а известный target-only gap в 8 операций явно зафиксирован. Новые endpoints должны одновременно добавляться в real Springdoc и уменьшать этот gap; случайное исчезновение уже реализованной операции из /v3/api-docs ломает тесты.
- Авторизация:
Authorization: Bearer <JWT>с access token из Keycloak realmfinguide. Бэкенд валидирует JWT как OAuth2 Resource Server. Login/refresh/logout остаются Keycloak OIDC endpoints; FinGuide API владеет только public facadesPOST /auth/registerиPOST /auth/password/forgot, которые вызывают Keycloak admin/client flows. В demo/H2 режиме (FINGUIDE_DEMO_MODE=true)/api/v1/**временно открыт для интеграции фронтенда с real backend без Keycloak. - Все даты: ISO-8601 (
YYYY-MM-DD,date-timeUTC). - Деньги: число в валюте записи +
currency; агрегаты возвращаются в базовой валюте плана. - Ответы:
{ "data": ... }; ошибки:{ "error": { "code", "message", "details", "requestId" } }. - Все запросы на запись возвращают актуальный ресурс; фронтенд может делать оптимистичное обновление, но затем обязан принять ответ бэкенда.
- Расчёты (
dashboard,analytics,pension,budget spent) делает бэкенд, чтобы фронтенд не дублировал финансовую логику. - Денежные входные суммы для расходов и целей передаются положительными числами; бэкенд сам нормализует знак исходящих платежей. В рассчитанных ответах расходы и цели тоже положительные, а
netSavings = income - expenses - goalExpenses. - Поля
*Pctв API — процентные пункты (6означает 6%). Внутри расчётов бэкенд переводит их в десятичную ставку (0.06).
Общие конвенции API¶
Пагинация¶
Целевой contract для методов API со списками, которые могут вырасти неограниченно (notifications, monthly-tracker, export jobs; legacy contributions при сохранении совместимости), использует курсорную пагинацию:
limitпо умолчанию50, максимум200.cursor— непрозрачная строка, выдаваемая сервером; клиент не интерпретирует её.- Ответ:
Короткие справочные коллекции (incomes, expenses, goals, envelopes, пользовательские scenarios) возвращаются целиком без пагинации — они ограничены по бизнес-смыслу; пользовательских сценариев максимум 10.
Идемпотентность запросов на запись¶
Все небезопасные POST-запросы, повторное выполнение которых создаёт дубликаты, поддерживают заголовок:
Применимо к:
- legacy
POST /plans/{planId}/contributions; POST /plans/{planId}/incomes,POST /plans/{planId}/expenses,POST /plans/{planId}/goals;POST /import,POST /export;POST /scenariosиPOST /scenarios/compareсейчас реализованы; idempotency storage остаётся целевым follow-up.
Бэкенд хранит (user_id, idempotency_key) минимум 24 часа. Повтор с тем же ключом и тем же телом возвращает ранее сохранённый ответ; повтор с другим телом — 409 CONFLICT.
PATCH/PUT/DELETE идемпотентны по семантике HTTP и не требуют заголовка Idempotency-Key.
Оптимистическая конкуренция¶
Сущности, редактируемые из нескольких вкладок/устройств (Goal, IncomeSource, ExpenseItem, PensionSettings, BudgetSettings, UserProfile), включают поле version: int в теле ответа и заголовок ETag.
Клиент при PATCH/PUT присылает:
Если версия в БД новее — бэкенд отвечает 412 PRECONDITION_FAILED с актуальным состоянием в error.details.current. Фронтенд обязан показать конфликт пользователю и не перезаписывать изменения молча.
Contribution не требует версионирования — это журнал только на добавление.
Коды ошибок¶
Стандартный формат ошибки:
{
"error": {
"code": "PLAN_NOT_FOUND",
"message": "Plan not found or access denied",
"details": { ... },
"requestId": "01HF..."
}
}
Перечень code расширяется, но существующие коды не переименовываются:
| code | HTTP | Когда |
|---|---|---|
VALIDATION_FAILED |
400 | Невалидное тело запроса; details.fields[] содержит ошибки по полям |
UNAUTHENTICATED |
401 | Нет JWT или JWT невалиден |
FORBIDDEN |
403 | JWT валиден, но нет доступа к ресурсу |
PLAN_NOT_FOUND |
404 | План не найден или скрыт по правам |
RESOURCE_NOT_FOUND |
404 | Общий 404 для goal/income/expense/contribution |
CONFLICT |
409 | Бизнес-конфликт, например несовпадение идемпотентного запроса или дубликат |
PRECONDITION_FAILED |
412 | If-Match не совпал с текущей версией |
RATE_LIMITED |
429 | Превышен лимит; details.retryAfterSec |
INTERNAL |
500 | Непредвиденная ошибка; всегда логируется с requestId |
DEPENDENCY_UNAVAILABLE |
503 | Keycloak / БД / внешний сервис недоступны |
Фронтенд маппит code на UX: уведомление, диалог конфликта, перенаправление на экран входа. На message завязываться нельзя — текст может локализоваться.
Главные сущности¶
Профиль пользователя (UserProfile)¶
id, name, email, phone, avatarUrl, age, gender, initialBalance, createdAt, updatedAt.
Доходы и расходы (IncomeSource / ExpenseItem)¶
id, name, amount, currency, frequency: monthly|yearly|one_time, growthType: manual|inflation|none|schedule, growthPct, growthSchedule[], startDate, endDate, опционально startYear, endYear.
Для POST id и timestamps генерирует сервер; PATCH принимает частичное тело и меняет только переданные поля. В текущей H2-реализации null в nullable-полях (endDate, growthLabel, icon, indexLabel) трактуется как «оставить текущее значение», поэтому очистка таких полей будет оформлена отдельной merge-patch/replace семантикой позже. Валидация: суммы неотрицательные, growthPct от 0 до 100, endDate >= startDate, currency — ISO-4217 alpha-3.
Для расходов дополнительно: budgetClass: needs|wants|savings, growthLabel.
growthSchedule[] нужен для Excel-модели: по каждой строке дохода/расхода могут быть разные ставки роста по годам, а не один постоянный процент.
Цель (Goal)¶
id, name, icon, currentCost, savedAmount, currency, targetYear, targetMonth, type: one_time|recurring, growthType, growthPct, growthSchedule[], priority. targetYear должен быть не меньше 2024; targetMonth — 1..12, при отсутствии в request backend использует декабрь (12).
Логика waterfall-распределения: цель с меньшим priority получает свободные накопления первой; порядок сортировки priority, targetYear, targetMonth, id, а /goals/reorder управляет priority.
Для Excel-модели Goal также должен поддерживать плановые расходы на цели: plannedAmount, frequency, startDate/endDate или startYear/endYear. Это покрывает лист Цели: ежемесячные и ежегодные расходы на цели как отдельный денежный поток.
Взнос (Contribution, legacy)¶
id, goalId, amount, currency, date, note.
Contribution ledger оставлен как legacy compatibility API и помечен deprecated в backend/controller/OpenAPI. В текущем backend create/update отключены и возвращают goal contribution ledger is disabled; use goal savedAmount and savings tracker instead. Фактический прогресс цели задаётся через Goal.savedAmount, а план-факт накоплений — через monthly tracker.
В текущем path Goal.savedAmount — persisted поле цели и редактируется через goal create/patch. Старые contribution rows остаются доступными для чтения/удаления, но не являются текущим write-path.
Пенсионные настройки (PensionSettings)¶
currentAge, retirementAge, monthlyExpenses, desiredMonthlyExpensesCurrentPrices, currency, expectedReturnPct, inflationPct, withdrawalStrategy: preserve_capital|spend_down_30y, statePensionEnabled, statePensionMonthly.
Excel-модель требует два пенсионных расчёта:
preserve_capital: тратить только реальную доходность от капитала;spend_down: тратить желаемый уровень расходов и считать возраст исчерпания капитала.
Предположения модели (ModelAssumptions)¶
startYear, projectionEndYear или horizonYears, birthYear, monthsPerYear, currency, initialCapital, investmentReturnPct, inflationSchedule[], sourceModel.
Это слой, который переносит лист Вводные в бэкенд и делает расчёты воспроизводимыми без Excel.
Настройки бюджета (BudgetSettings)¶
method: 503020|envelope, envelopes[], classifications{expenseId: needs|wants|savings}.
Конверт бюджета: id, name, limit, icon, color, рассчитанные spent, remaining, pct, isOver.
Сценарий (Scenario)¶
id, name, emoji, description, isBase (canonical), base (temporary compatibility alias), snapshot или adjustments.
Стандартные сценарии из макета: базовый, оптимистичный, пессимистичный. Пользовательские сценарии — до 10 штук.
Карта методов API¶
Авторизация и профиль¶
Keycloak владеет входом, refresh/logout, MFA и пользовательскими сессиями. Эти endpoints доступны под публичным route /auth/realms/finguide/protocol/openid-connect/...; frontend использует Authorization Code + PKCE и передаёт access token в API.
POST /auth/register— реализовано в real backend; public facade создаёт пользователя в Keycloak через admin client. Request:firstName,lastName,email,password; response201с{ "data": { "email": "..." } }.POST /auth/password/forgot— реализовано в real backend; public facade принимаетemail, rate-limited, и запрашивает Keycloak execute-actions email. Response202, без раскрытия существования пользователя.GET /me— реализовано в real backend; возвращает локальный бизнес-профиль, связанный сJWT.sub, с синхронизациейemail/ФИО из JWT.PATCH /me,PUT/DELETE /me/avatar— отдельные задачи профиля.- Смена пароля — вне зоны ответственности backend; пароль меняется через Keycloak account/password reset screens.
План и CRUD¶
GET /plans/current— реализовано. Для anonymous demo возвращает seeded plan22222222-2222-4222-8222-222222222222; для authenticated пользователя первыйGETтранзакционно и идемпотентно создаёт его собственный current plan, клонируя persisted demo seed, и дальше возвращает только пользовательский план.GET /plans— реализовано; возвращает summaries планов текущего пользователя.POST /plans— реализовано; создаёт пустой план и делает его current. Request:{ "name": "..." }.POST /plans/{planId}/copy— реализовано; копирует модель плана в новый current plan без фактической историиmonthly_tracker,operation_journalи legacycontributions.PUT /plans/current— реализовано; переключает current plan. Request:{ "planId": "..." }.GET/POST /plans/{planId}/incomes,GET/PATCH/DELETE /plans/{planId}/incomes/{id}— реализовано в real backend.GET/POST /plans/{planId}/expenses,GET/PATCH/DELETE /plans/{planId}/expenses/{id}— реализовано в real backend.GET/POST /plans/{planId}/goals,GET/PATCH/DELETE /plans/{planId}/goals/{id}— реализовано в real backend.POST /plans/{planId}/goals/reorder— реализовано в real backend; тело{ "goalIds": ["..."] }должно содержать все текущие id целей ровно по одному разу.GET/POST /plans/{planId}/contributions,GET/PATCH/DELETE /plans/{planId}/contributions/{id}— legacy/deprecated compatibility endpoints; read/delete требуют доступ к плану, create/update сейчас отключены и возвращают ошибкуgoal contribution ledger is disabled; use goal savedAmount and savings tracker instead. Удаление цели удаляет связанные с ней старые взносы, чтобы не оставлять orphan ledger records. Текущая H2-реализация возвращает полный список без pagination/idempotency.GET/PATCH /plans/{planId}/pension— реализовано в real backend;PATCHделает full replace persistedPensionSettingsи требует writable plan access, поэтому общий anonymous seed read-only.GET/PATCH /plans/{planId}/budget,POST /plans/{planId}/budget/envelopes/autogenerate— реализовано в real backend.GET /plans/{planId}/calendar/monthly-tracker?year=2026иPOST /plans/{planId}/calendar/monthly-tracker— реализовано в real backend; хранит статус месяца (completed|partial|missed) и фактическую сумму накопления. В analytics влияет именноamount.GET/POST /plans/{planId}/tracker/entries,PATCH/DELETE /plans/{planId}/tracker/entries/{entryId}— реализовано в real backend; хранит журнал операций страницы/tracking(date,title,amount,type,status) и требует writable plan access для mutations.type=goalсейчас отклоняется backend'ом; использоватьGoal.savedAmountи monthly tracker.
Аналитика и производные данные¶
GET /plans/{planId}/dashboard— реализовано в real backend.GET /plans/{planId}/analytics/projection?years=30— реализовано в real backend; строит годовую проекцию из persistedPlanState;yearsвалидируется в диапазоне1..60.GET/PATCH /plans/{planId}/analytics/assumptions— реализовано в real backend;PATCHтребует writable plan access, поэтому общий anonymous seed read-only.GET /plans/{planId}/analytics/balance/current— реализовано в real backend.GET /plans/{planId}/analytics/cashflow?years=12— реализовано в real backend;yearsопционален и ограничен диапазоном1..80.GET /plans/{planId}/analytics/cashflow/monthly— реализовано в real backend; источник для помесячных графиков/tracker, где tracker facts могут замещать плановые monthly savings.GET /plans/{planId}/analytics/health— реализовано в real backend.GET /plans/{planId}/pension/projection— реализовано в real backend; строится из persisted state и текущих pension settings. Response включает накопленныйcapitalAtRetirementи strategy-specificrequiredCapitalAtRetirement: вpreserveCapitalвместе со статусомcalculated|non_positive_real_return, вspendDownдля фиксированного 30-летнего funding period.
analytics/cashflow — главный метод API в стиле Excel-модели. Он возвращает годовые строки: возраст, номер периода, доходы, расходы, расходы на цели, чистые сбережения, капитал на начало/конец года.
Сценарии¶
GET/POST /scenarios— implementedGET/PATCH/DELETE /scenarios/{scenarioId}— implementedPOST /scenarios/compare— implemented
Уведомления, импорт и экспорт¶
GET /notifications?filter=all|unreadPOST /notifications/readPOST /importPOST /export,GET /export/{jobId}
Интеграция фронтенда¶
- Если включён OIDC: пройти
/login→ Keycloak Authorization Code + PKCE →/auth/callback, сохранить access token и подставлятьAuthorization. Затем:GET /me+GET /plans/current+GET /plans/{id}/dashboard. Frontend должен разделять cache для anonymous demo и authenticated user session и показывать neutral loader до завершения восстановления токена/current plan, чтобы не мигал seeded demo/default профиль. - CRUD-экраны используют локальное оптимистичное состояние, но инвалидируют
dashboard,analytics,budgetпосле записи. - Дашборд и графики не считают деньги сами — только отображают расчётные методы API.
- Для миграции из прототипа можно один раз прочитать ключи localStorage
finguide-data,finguide-user-profile,finguide-budgets,finguide-scenariosи отправитьPUT /plans/current/POST /import. - Импорт/экспорт должен поддерживать JSON для полного плана,
excel_modelдля исходной модели и CSV/XLSX/PDF для отчётов.