116 lines
13 KiB
Markdown
116 lines
13 KiB
Markdown
# Ядро мобильного приложения 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`.
|