# Ядро мобильного приложения 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//… 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:///api/v1/`. Для Staff — `/api/v1/staff`. - Заголовки каждого запроса: `Authorization: Bearer `, `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`.