krona-staff/docs/APP-CORE.md
Ochenstarik 3464998168
Some checks are pending
ci / verify (push) Waiting to run
feat: add secure attendance portal and payroll foundation
2026-09-24 15:59:38 +07:00

116 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Ядро мобильного приложения Krona Business Platform
Документ для команды, которая делает приложение (в том числе для Codex). Описывает каркас, в который модули платформы добавляют свои экраны. Staff — первый модуль; касса, финансы и остальное подключаются тем же способом.
Правило номер один: **приложение не считает деньги**. Все суммы, смены и остатки приходят с сервера уже рассчитанными, вместе с объяснением, из чего они сложились. Клиент их только показывает. Это условие проверяемости расчёта и требование [ADR-002](adr/ADR-002-payroll-domain-and-platform-alignment.md).
## 1. Стек
| Что | Решение | Почему |
| --------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ |
| Каркас | React Native + Expo (managed), TypeScript strict | тот же язык, что и модули платформы; типы API переиспользуются, сборка Android и iOS без Mac через EAS |
| Навигация | Expo Router | файловая маршрутизация, удобна для подключения экранов модулями |
| Серверное состояние | TanStack Query + persist | кэш, повторы, офлайн-чтение |
| Локальное состояние | Zustand | как во фронтенде платформы |
| Формы и валидация | React Hook Form + Zod | Zod уже используется в модулях |
| Секреты на устройстве | expo-secure-store | refresh-токен и PIN-хеш только здесь |
| Биометрия | expo-local-authentication | разблокировка, не аутентификация на сервере |
| Уведомления | expo-notifications | push; пока Core не отдаёт push, канал Telegram остаётся запасным |
| Тесты | Vitest + React Native Testing Library, Maestro для сценариев | — |
Совместимость: Android 9+, iOS 15+. Публикация: RuStore и Google Play, TestFlight/App Store — по решению владельца.
## 2. Структура
```
app/ экраны Expo Router: (auth), (tabs), modules/<module>/…
src/core/
api/ http-клиент, конверт ответа, ошибки, Idempotency-Key
auth/ сессия, refresh, PIN, биометрия, приглашения
modules/ реестр модулей: вкладки, экраны, права
ui/ дизайн-система: токены, компоненты, типографика
i18n/ ru по умолчанию
telemetry/ логи без персональных данных
src/modules/staff/ экраны и запросы модуля Staff
```
Модуль регистрируется в реестре декларативно и не правит ядро:
```ts
registerModule({
id: 'ru.krona.staff',
title: 'Работа',
requiredPermissions: ['staff.payslip.read_own'],
tabs: [{ name: 'today', icon: 'clock', screen: TodayScreen }],
routes: [{ path: 'staff/payroll/:runId', screen: PayrollRunScreen }],
});
```
Ядро показывает вкладку, только если у пользователя есть право и модуль включён у арендатора. Это зеркалит слоты платформы (`34-MODULE-SYSTEM`), чтобы приложение не расходилось с вебом.
## 3. Аутентификация
Учётными записями владеет ядро платформы, приложение не заводит своих пользователей и не хранит пароли.
**Приглашение (решение владельца).** Администратор создаёт учётку сотрудника и отправляет приглашение одним из трёх способов: письмо на почту, ссылка в мессенджер, QR-код на экране. Все три ведут на один одноразовый токен приглашения.
**Первый вход:**
1. переход по приглашению (или сканирование QR в приложении);
2. сотрудник задаёт пароль — уходит в Core, на устройстве не остаётся;
3. сотрудник задаёт PIN из 6 цифр — хранится только как хеш в secure-store, сервер о нём не знает;
4. если на телефоне есть биометрия, предлагается включить её вместо PIN.
**Дальнейшие входы:** PIN или биометрия разблокируют refresh-токен в secure-store, ядро меняет его на access-токен. Пароль нужен только при смене устройства и после отзыва сессии.
**Токены:** access-JWT живёт 15 минут и хранится только в памяти; refresh — непрозрачный, в secure-store, с привязкой к Device ID. Три неверных PIN подряд — пауза, десять — сброс локальной сессии и вход по паролю.
**Чего сегодня не хватает в ядре платформы** (проверено по коду `ochenstarik/business-platform`): поток приглашения реализован как каркас, восстановление пароля отсутствует, права модулей нельзя выдать роли. До их готовности модуль Staff в режиме `standalone` выдаёт собственные токены для пилота — это временный режим, который выключается переключением на `platform`. Собственную модель пользователей в приложении заводить нельзя ни при каких условиях.
## 4. Работа с API
- База: `https://<host>/api/v1/<module>`. Для Staff — `/api/v1/staff`.
- Заголовки каждого запроса: `Authorization: Bearer <access>`, `X-Tenant-ID`, `X-Request-Id` (ULID, генерирует клиент).
- Успешный ответ: `{"data": …, "meta": …}`. Ошибка: `{"errors": [{"code": "…", "message": "…", "field": "…"}], "meta": {"request_id": "…"}}`. Показывать пользователю `message`, в лог писать `code` и `request_id`.
- Любая мутация несёт `Idempotency-Key` (UUID v4, генерирует клиент и сохраняет до подтверждения). Повтор с тем же ключом и другим телом получает `409`.
- `401` → однократный refresh и повтор запроса; второй `401` → выход на экран входа.
- Чужой ресурс возвращает `404`, а не `403` — не показывать это как «нет прав».
- Деньги приходят как `{"amount_minor": "623100", "currency_code": "RUB"}`, строкой. Клиент **не** приводит к числу с плавающей точкой: форматирование только через хелпер `formatMoney`, который работает со строкой и делит на 100.
- Типы генерируются из `api/openapi.yaml` модуля через `openapi-typescript`; рукописные интерфейсы ответов запрещены.
## 5. Экраны Staff, версия 1
| Экран | Содержание |
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| Сегодня | текущая смена: точка, время прихода, идёт ли смена, предварительное начисление за день с пометкой «до подтверждения управляющим» |
| Мои смены | список за период: дата, точка, часы, статус (`ожидает подтверждения`, `подтверждена`, `отклонена`) |
| Моя зарплата | неделя: начислено, выдано, остаток. Внутри — по дням: выручка точки не показывается, показываются часы, доля фонда, почасовая, надбавка, итог |
| Детализация дня | объяснение суммы: «фонд точки ÷ 3 участника + 12 ч × 100 ₽ + надбавка 500 ₽» |
| Профиль | имя, табельный номер, точка, выход, смена PIN, включение биометрии |
Сотрудник видит только свои данные. Ни один экран не должен запрашивать данные других сотрудников; при попытке сервер ответит `404`.
## 6. Офлайн
Приложение работает в кафе с плохой связью, поэтому:
- чтение — из кэша TanStack Query с сохранением на диск, с явной пометкой «данные на такое-то время»;
- мутации (их мало: подтверждение прочтения, смена PIN) — очередь с `Idempotency-Key` и повтором;
- денежные суммы никогда не считаются на клиенте, даже офлайн: если данных нет, показывается прочерк, а не оценка.
## 7. Что делает сервер, а не приложение
- расчёт начислений, фонда, долей, гарантий, округлений;
- определение, какие смены попали в расчёт;
- проверка прав: приложение прячет кнопки, но решает всегда сервер;
- агрегаты за неделю и остаток.
## 8. Порядок работ
1. Ядро: навигация, дизайн-токены, http-клиент, обработка ошибок, реестр модулей.
2. Аутентификация: приглашение, пароль, PIN, биометрия, refresh, Device ID.
3. Модуль Staff: «Сегодня», «Мои смены», «Моя зарплата», «Детализация», «Профиль».
4. Push-уведомления: отметка о приходе, подтверждение смены, утверждение ведомости.
5. Сборка: EAS profiles (`development`, `preview`, `production`), публикация в RuStore.
Серверная часть (расчёт, ведомость, подтверждение смен, API) делается в репозитории `krona-staff` и к моменту пункта 3 отдаёт готовый контракт в `api/openapi.yaml`.