<p align="center">
  <img src="app/assets/brand/readme-logo.png" alt="PicPac" width="420" />
</p>

<p align="center">
  <strong>От фотографий в телефоне — до готового заказа фотокниги.</strong><br/>
  Мобильное приложение для заказа фотопродукции
  <a href="https://picpac.by/">picPac.by</a> (ООО «Фотопринт»)
</p>

<p align="center">
  <a href="https://picpac.jaraslau.dev"><img src="https://img.shields.io/badge/demo-picpac.jaraslau.dev-DD5757?style=flat-square&labelColor=1A2422" alt="demo" /></a>
  <a href=".github/workflows/deploy.yml"><img src="https://img.shields.io/github/actions/workflow/status/jaraslaukunin-team/greatstone-hackathon/deploy.yml?branch=main&style=flat-square&label=deploy&color=7AA45A&labelColor=1A2422" alt="deploy" /></a>
  <img src="https://img.shields.io/badge/Node.js-20%2B-DD5757?style=flat-square&labelColor=1A2422&logo=nodedotjs&logoColor=white" alt="Node.js 20+" />
  <img src="https://img.shields.io/badge/TypeScript-5-DD5757?style=flat-square&labelColor=1A2422&logo=typescript&logoColor=white" alt="TypeScript" />
  <img src="https://img.shields.io/badge/Expo-SDK%2058-DD5757?style=flat-square&labelColor=1A2422&logo=expo&logoColor=white" alt="Expo SDK 58" />
  <img src="https://img.shields.io/badge/Android%20%7C%20iOS%20%7C%20Web-7AA45A?style=flat-square&labelColor=1A2422" alt="Android | iOS | Web" />
  <a href="https://github.com/jaraslaukunin-team/greatstone-hackathon/stargazers"><img src="https://img.shields.io/github/stars/jaraslaukunin-team/greatstone-hackathon?style=flat-square&label=stars&color=DD5757&labelColor=1A2422&logo=github&logoColor=white" alt="stars" /></a>
</p>

**PicPac Mobile** — клиент и API для заказа фотокниг и печати: каталог, загрузка фото, автоматическая вёрстка, редактор, проверка качества, оформление заказа и отслеживание статуса. Рядом — совместные альбомы по ссылке и AI-помощник. Цены и права всегда считает сервер.

> **[Открыть демо →](https://picpac.jaraslau.dev)**  
> Вход: **«Продолжить как гость»** или любой контакт и код **`000000`** (`DEMO_MODE`).  
> Оплата демонстрационная — реальные деньги не списываются.

**Быстрые ссылки:** [демо](https://picpac.jaraslau.dev) · [возможности](#возможности) · [сценарий](docs/demo-script.md) · [быстрый старт](#быстрый-старт) · [API](#api) · [тесты](#тесты)

## Содержание

**Продукт**

- [Возможности](#возможности)
- [Модули](#модули)
- [Роли и права](#роли-и-права)
- [Пользовательский путь](#пользовательский-путь)

**Архитектура и запуск**

- [Архитектура](#архитектура)
- [Стек](#стек)
- [Структура репозитория](#структура-репозитория)
- [Быстрый старт](#быстрый-старт)
- [Приложение: Expo, веб, APK](#приложение-expo-веб-apk)
- [Развёртывание](#развёртывание)

**API и данные**

- [Тестовые учётные записи и данные](#тестовые-учётные-записи-и-данные)
- [Переменные окружения](#переменные-окружения)
- [API](#api)
- [Цена, доставка и деньги](#цена-доставка-и-деньги)

**Качество**

- [Тесты](#тесты)
- [Документация](#документация)
- [Ограничения](#ограничения)

## Возможности

**Клиентский сценарий:** вход (гость или код) → каталог и цены из API → проект из фото → автомакет или ручная раскладка → проверка качества → корзина и доставка → заказ с идемпотентностью → статусы и поддержка.

**Редакторы:** обложка и темы книги, шаблоны разворотов, свободные слои, фоторедактор (кадр, фильтры, текст, кисть), печать фото (формат → лента → кадрирование).

**Совместная работа:**

- выпускной альбом — общий макет у организатора, фото и пожелания учеников, QR на видео, именная копия после заказа;
- общий альбом — пул фото по ссылке, у каждого своя книга (`fork` / `sync`), заказывается личная книга.

**AI:** согласие на обработку, генеративная правка фото, чат по каталогу и заказам; отзыв согласия в профиле.

**Сотрудник:** очередь заказов, смена статуса, обращения и журнал операций.

**Инженерия:** npm workspaces, `@picpac/shared`, Fastify + PostgreSQL, Expo (Android / iOS / веб). Права, цена и защита от дубля заказа — на сервере.

## Модули

| Часть | Содержание |
|---|---|
| `packages/shared` | DTO и zod-схемы, деньги, цена, доставка, автомакет, качество, темы, сборка выпускного и общего альбомов |
| `api` | Вход, каталог, quote, проекты, фото, заказы, оператор, поддержка, альбомы, privacy, AI |
| Приложение (`app/`) | Клиентский путь, редактор книги, фоторедактор, альбомы, заказы, поддержка, AI |
| Развёртывание | Docker Compose + Caddy (HTTPS), веб на том же домене, GitHub Actions |
| Платежи | Имитация в тестовом режиме; реального списания нет |

## Архитектура

```
┌───────────────────────────────────────────────┐
│ Приложение Expo (React Native, TypeScript)    │
│ Android · iOS · веб                           │
│ экраны · редактор · черновик · очередь фото   │
└──────────────────────┬────────────────────────┘
                       │ HTTPS, JWT в Authorization
┌──────────────────────▼────────────────────────┐
│ Caddy (HTTPS, раздача веб-сборки)             │
│ → API (Fastify 5)                             │
│ auth · catalog · quote · projects · photos    │
│ orders · operator · support · albums · ai     │
│ privacy · audit; права на каждом запросе      │
└────────────┬─────────────────────┬────────────┘
             │ SQL (Drizzle)       │ файлы
┌────────────▼──────────┐    ┌─────▼────────────┐
│ PostgreSQL 16         │    │ Диск: uploads/   │
│ 16 таблиц, миграции   │    │ подписанные      │
│ 0000–0012             │    │ ссылки, AES-GCM  │
└───────────────────────┘    └──────────────────┘

        @picpac/shared — общие типы, схемы и чистая логика
        (цена, автомакет, качество, темы, сборка альбомов)
```

Права, цена и идемпотентность проверяются в коде API (а не через RLS) и покрыты тестами на реальной базе.
Приложение границей безопасности не является: на каждый запрос сервер проверяет роль и доступ к объекту.

Альбомы устроены двумя способами (общая механика: ссылка-приглашение, участники, прогресс):

- **Выпускной альбом** — один общий макет у организатора. Ученики загружают свои фото и оставляют пожелание
  и ссылку на видео; «Собрать альбом» строит личные развороты, на каждом QR на видео. Заказанный альбом участник
  может заказать «своей именной копией» (его имя на обложке, его разворот первым).
- **Общий альбом** — общий пул фото. У каждого участника своя книга (`fork`): фото пула копируются, книга сама
  раскладывается главами по дням, новые фото подтягиваются (`sync`). Заказывается личная книга, сам пул — нет.

## Стек

| Слой | Технологии |
|---|---|
| Монорепозиторий | npm workspaces, TypeScript |
| API | Node.js 20+, Fastify 5, zod, Drizzle ORM, `pg`, `jose` (JWT HS256), `@fastify/multipart`, `exifr`, `image-size`, `sharp` |
| База | PostgreSQL 16, SQL-миграции drizzle-kit |
| Общий пакет | TypeScript-исходники без сборки, zod |
| Приложение | Expo SDK 58, React Native 0.88, Expo Router, TanStack Query, Zustand, AsyncStorage, Reanimated, `@shopify/react-native-skia` (фоторедактор), `react-native-svg`, `qrcode-generator` |
| Тесты | Vitest (shared, API), Jest (приложение) |
| Инфраструктура | Docker Compose (БД, API, Caddy), GitHub Actions, EAS Build для APK |

Лицензии зависимостей перечислены в [`LICENSES.md`](LICENSES.md).

## Структура репозитория

```
.
├── api/
│   ├── src/
│   │   ├── app.ts, server.ts, config.ts, audit.ts, errors.ts
│   │   ├── auth/            вход по коду, гость, JWT, роли
│   │   ├── db/              схема, клиент, мигратор, сид
│   │   ├── storage/         диск, шифрование, подписанные ссылки
│   │   └── modules/         catalog, quote, projects, photos, orders, operator,
│   │                        support, albums, privacy, crypto, ai
│   ├── test/                тесты модулей на реальной базе
│   ├── drizzle/             SQL-миграции 0000–0012
│   └── fixtures/            тестовые фото (в том числе figma/)
├── app/
│   ├── app/                 экраны (Expo Router): (auth), (tabs), catalog, project, album, orders, print, ai…
│   ├── src/                 book (страницы, шаблоны, слои, QR), photoEditor, project (черновик, автосохранение),
│   │                        album, orders, photos, api, auth, theme, ui, i18n
│   ├── assets/              шрифты, логотипы, примеры оформления
│   ├── app.json, eas.json   конфигурация Expo и сборок
│   └── scripts/             копирование canvaskit.wasm для веба
├── packages/shared/src/     types, schemas, money, delivery, pricing, layout, quality, themes, album, event
├── docs/                    сценарий демо, спецификации, заметки по интеграции
├── tools/mock-api           упрощённый мок API для работы без сервера
├── scripts/                 лицензии и вспомогательные скрипты
├── Caddyfile, docker-compose.yml, docker/initdb/
├── SECURITY.md              политика безопасности
└── .env.example             переменные окружения без значений
```

## Роли и права

| Роль | Кто это | Что может |
|---|---|---|
| Гость | Клиент без учётной записи | Каталог, проект; для заказа нужна идентификация |
| Клиент (`customer`) | Частный клиент picPac.by | Свои проекты и заказы, обращения, свои фото, избранные заказы |
| Оператор (`operator`) | Сотрудник «Фотопринт» | Все заказы, смена статуса, журнал, ответы на обращения |
| Организатор альбома | Клиент, создавший альбом | Приглашения, пароль, сборка макета (выпускной); общий пул (общий альбом) |
| Участник альбома | Клиент по приглашению | Выпускной: только свои фото и своя запись; общий: видит все фото пула и ведёт свою книгу |

Организатор и участник — роли внутри альбома, а не глобальные. Доступ к чужому объекту возвращает `404`
(а не `403`), чтобы не раскрывать существование объекта, и пишется в журнал как `access.denied`.
`403` отдаёт только проверка роли.

## Пользовательский путь

| Шаг | Экран | Что происходит |
|---|---|---|
| 1 | Вход | Код на телефон или e-mail либо «Продолжить как гость» |
| 2 | Главная и каталог | Примеры оформления, линейки, цены и сроки из API |
| 3 | Создание проекта | Формат, тема оформления, автоматическая или ручная раскладка |
| 4 | Фото | Загрузка из галереи (JPEG/PNG), очередь в фоне, повтор при обрыве |
| 5 | Книга | Вкладки «Обложка / Разворот / Страницы»: листание, шаблоны разворота, «Расставить фото», свободный режим со слоями, фоторедактор |
| 6 | Проверка | Пустые ячейки блокируют заказ, низкое разрешение — предупреждение с DPI |
| 7 | Корзина | Доставка или пункт выдачи, промокод, цена пересчитывается на сервере |
| 8 | Подтверждение | Заказ создаётся один раз при повторах, деньги не списываются |
| 9 | Статус | `accepted → moderation → production → ready_for_delivery → completed` |
| 10 | История и поддержка | Состав, сумма, статус, обращения (с фото) по заказу, повтор и избранное |

Отдельные сценарии: печать фото (формат → лента кадров → кадрирование), выпускной альбом, общий альбом,
AI-ассистент (классические правки и генеративное редактирование, чат по заказам и каталогу).

### Что показать жюри

1. В каталоге «Фотокниги» введите **100 BYN** в поле «Подбери под бюджет». Варианты появятся автоматически. Цена одной книги без доставки рассчитывается через API.
2. Откройте **PicPac Time Machine** в личной книге или общем альбоме. Выберите год, нажмите фото для перехода к развороту и добавьте разворот с датами.
3. Скачайте **PDF в размере выбранного формата** из предпросмотра книги. Оформите тестовый заказ и войдите как оператор, чтобы скачать PDF из карточки заказа и сменить его статус.

[Пошаговый сценарий демо и подготовка тестовых фото](docs/demo-script.md).

## Быстрый старт

Нужны Docker, Node.js 20+ и npm. Ниже запуск API; приложение — в следующем разделе.

```bash
# 1. Зависимости
npm install

# 2. Конфигурация: заполните JWT_SECRET и FILE_URL_SECRET любыми строками для разработки
cp .env.example .env

# 3. Загрузите переменные в оболочку: API не читает .env сам
set -a; . ./.env; set +a

# 4. База: PostgreSQL на localhost:5433, базы picpac и picpac_test
docker compose up -d --wait db

# 5. Схема и тестовые данные
npm run db:migrate -w api
npm run db:seed -w api

# 6. API
npm run dev -w api
```

`DATABASE_URL`, `JWT_SECRET` и `FILE_URL_SECRET` обязательны: без них API не стартует.

Проверка: `curl http://localhost:3000/health` → `{"status":"ok"}`.

Для демо-входа включите `DEMO_MODE=true` в `.env`: код подтверждения тогда всегда `000000`, письма и SMS
не отправляются. Вне демо-режима код генерируется случайно и пишется только в лог приложения.

```bash
# Токен для customer1@test.local (демо-режим)
curl -s -X POST http://localhost:3000/auth/request-code \
  -H 'content-type: application/json' -d '{"contact":"customer1@test.local"}'
TOKEN=$(curl -s -X POST http://localhost:3000/auth/verify-code \
  -H 'content-type: application/json' \
  -d '{"contact":"customer1@test.local","code":"000000"}' | jq -r .token)
curl -s http://localhost:3000/catalog -H "authorization: Bearer $TOKEN"
```

Если образ `postgres:16` не скачивается, задайте в `.env` `POSTGRES_IMAGE=postgres:18`. Порт базы — `5433`,
чтобы не конфликтовать с локальным PostgreSQL.

## Приложение: Expo, веб, APK

Адрес API берётся из `EXPO_PUBLIC_API_URL`, а если он не задан, из `app/app.json` (`extra.apiUrl`).
По умолчанию это `https://picpac.jaraslau.dev`.

**Разработка на телефоне (Expo Go, если используемые нативные модули поддерживаются):**

```bash
cd app
npx expo start
```

Откройте QR-код в Expo Go (телефон и компьютер в одной сети). Для функций с неподдерживаемыми в Expo Go нативными модулями потребуется development build или готовая сборка. Для локального API:
`EXPO_PUBLIC_API_URL=http://<IP-компьютера>:3000 npx expo start`.

**Веб-версия:**

```bash
cd app
npm run web            # разработка
npm run web:build      # production-сборка в app/dist (копирует canvaskit.wasm для фоторедактора)
```

**APK через EAS (облачная сборка):**

```bash
cd app
npx eas-cli@latest login
npx eas-cli@latest init                                  # один раз: привязать проект на вашем аккаунте expo.dev
npx eas-cli@latest build -p android --profile preview    # APK, API: https://picpac.jaraslau.dev
```

Профили в `app/eas.json`: `development` (dev-клиент), `preview` и `production` (APK с адресом API из `env`).
Если после `eas init` `app.json` изменился, закоммитьте `extra.eas.projectId`, чтобы все сборки шли в один проект.
Версии нативных пакетов должны совпадать с ожидаемыми для Expo SDK 58 (`npx expo install --check`):
при несовпадении сборка Android может упасть на компиляции модулей.

Фоторедактор (Skia) в вебе грузит `canvaskit.wasm` лениво; на телефоне нужен dev-клиент или сборка, если модуль
недоступен в Expo Go.

## Развёртывание

Сервер: Docker Compose (`db`, `api`, `caddy`), Caddy выдаёт HTTPS и раздаёт веб-сборку из `./web`; запросы,
не похожие на переходы по страницам, идут в API на том же домене, поэтому CORS не нужен.

Вручную (из корня репозитория):

```bash
git archive --format=tgz -o src.tgz HEAD                  # код
cd app && EXPO_PUBLIC_API_URL=https://<домен> npx expo export -p web --clear --output-dir dist-prod && cd ..
tar czf web.tgz -C app/dist-prod .                         # веб-сборка
# загрузить src.tgz и web.tgz на сервер, распаковать код (без .env) и веб в ./web, затем:
docker compose up -d --build api && docker compose up -d caddy
```

Миграции применяются автоматически при старте API. После изменения каталога (продукты, тарифы) на сервере
запустите сид: `docker compose exec api node --import tsx src/db/seed.ts` (идемпотентен).

Автовыкладка: `.github/workflows/deploy.yml` на каждый push в `main` прогоняет тесты, собирает веб, заливает код и
перезапускает API. Нужны секреты репозитория `VPS_HOST`, `VPS_USER`, `VPS_SSH_KEY`.

При `AI_PROVIDER=aiai` API требует `FILE_ENCRYPTION_KEY` и `PAYLOAD_ENCRYPTION_KEY` (по 64 hex) и не стартует без них.

## Тестовые учётные записи и данные

Паролей нет: вход по коду, в демо-режиме код `000000`.

| Учётная запись | Роль | Что видно |
|---|---|---|
| `customer1@test.local` | клиент | заказы и обращение из сида |
| `customer2@test.local` | клиент | заказы и обращение из сида |
| `operator@test.local` | оператор | все заказы, журнал, обращения |
| «Продолжить как гость» / `POST /auth/guest` | гость | новый анонимный профиль |

Сид создаёт:

- продукты каталога: фотокниги, выпускные книги, печать и цифровая печать с вариантами, ценами и сроками;
- пункты выдачи Белпочты;
- 3 промокода: `HELLO10` (10%), `SCHOOL15` (15%), `OLD20` (просрочен);
- заказы во всех статусах с лентой статусов, обращения и сообщения;
- тестовые фото в `api/fixtures/` (в том числе низкого разрешения и с EXIF-поворотом, `figma/` для проверки редактора).

Сид идемпотентен: повторный запуск находит те же строки по фиксированным UUID. Все данные синтетические.

## Переменные окружения

Файл `.env` в репозиторий не попадает; `.env.example` содержит только имена.

| Переменная | Обязательна | Назначение |
|---|---|---|
| `DATABASE_URL` | да | PostgreSQL |
| `DATABASE_URL_TEST` | для тестов | База тестов; имя обязано оканчиваться на `_test` |
| `JWT_SECRET` | да | Подпись токенов и кодов входа |
| `FILE_URL_SECRET` | да | HMAC для подписанных ссылок на фото и вложения |
| `FILE_ENCRYPTION_KEY` | нет* | AES-256 (64 hex) для шифрования файлов на диске |
| `PAYLOAD_ENCRYPTION_KEY` | нет* | AES-256 (64 hex) master для конверта клиент↔API |
| `UPLOAD_DIR` | нет | Папка файлов, по умолчанию `./uploads` |
| `DEMO_MODE` | нет | `true` — код входа всегда `000000` (на боевом выключить) |
| `PORT` | нет | Порт API, по умолчанию `3000` |
| `PUBLIC_URL` | нет | Публичный адрес для ссылок-приглашений (алиас `PUBLIC_BASE_URL` устарел) |
| `AI_PROVIDER` | нет | `stub` (по умолчанию) или `aiai` |
| `AIAI_BASE_URL`, `AIAI_API_KEY`, `AIAI_EDIT_MODE` | нет | aiai.by: база (https), ключ только на сервере, режим `images`/`chat` |
| `AIAI_MODEL`, `AIAI_CHAT_MODEL` | нет | Модели фоторедактирования и чата |
| `POSTGRES_IMAGE` | нет | Тег образа базы для compose |
| `API_DOMAIN` | для деплоя | Домен(ы) для Caddy |
| `EXPO_PUBLIC_API_URL` | нет | Адрес API для приложения (приоритет над `app.json`) |
| `EXPO_PUBLIC_DEMO_HINT` | нет | `1` — подсказка с демо-кодом на экране входа |

\* Обязательны при `AI_PROVIDER=aiai`.

Секреты не коммитятся. На сервере они задаются только в `.env` или в секретах окружения.

## API

Ответы — JSON с DTO из `@picpac/shared`. Время — ISO 8601 в UTC с `Z`, суммы — целые копейки.

| Группа | Маршруты |
|---|---|
| Служебные | `GET /health`, `GET /files/:photoId?exp&sig`, `GET /crypto/client-key` |
| Вход | `POST /auth/guest`, `/auth/request-code`, `/auth/verify-code`; `GET /me` |
| Каталог и цена | `GET /catalog`, `GET /catalog/:productId`, `POST /price-quote` |
| Проекты | `POST/GET /projects`, `GET/PUT/PATCH/DELETE /projects/:id`, `POST /projects/:id/duplicate` |
| Фото | `POST/GET /projects/:id/photos`, `POST /projects/:id/photos/copy`, `DELETE /photos/:id` |
| Альбомы | `POST /projects/:id/invites`, `POST /invites/accept`, `GET /projects/:id/progress`, `PUT /projects/:id/password`, `PATCH /projects/:id/members/me`, `POST /projects/:id/member-spread` |
| Общий альбом | `POST /projects/:id/fork`, `POST /projects/:id/sync` |
| Заказы | `POST /orders` (+ `Idempotency-Key`), `GET /orders`, `GET /orders/:id`, `POST /orders/:id/cancel`, `/favorite`, `/repeat` |
| Оператор | `GET /operator/orders`, `POST /operator/orders/:id/status`, `GET /operator/audit` |
| Поддержка | `POST /orders/:id/interactions`, `GET /interactions[/:id]`, `POST /interactions/:id/messages`, `/messages/photo`, `GET /interactions/:id/attachments/:messageId` |
| Приватность | `GET /me/data-summary`, `DELETE /me/data` |
| AI | `POST /ai/consent`, `DELETE /ai/consent`, `POST /ai/edit`, `/ai/adjust`, `/ai/chat`, `GET /ai/jobs/:id` |

### Ошибки

Единый формат, `message` по-русски:

```json
{
  "error": {
    "code": "PRICE_CHANGED",
    "message": "Цена изменилась, проверьте новую сумму и подтвердите заказ",
    "details": { "totalKop": 19370 }
  }
}
```

`details` необязательны. Основные коды: `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`, `BAD_REQUEST`, `CODE_INVALID`,
`CODE_EXPIRED`, `VARIANT_NOT_FOUND`, `VERSION_CONFLICT`, `PROJECT_ORDERED`, `LAYOUT_INVALID`, `LAYOUT_BLOCKED`,
`LAYOUT_PHOTO_MISSING`, `PAGES_OUT_OF_RANGE`, `PRICE_CHANGED`, `IDEMPOTENCY_KEY_REQUIRED`, `IDEMPOTENCY_KEY_REUSED`,
`POOL_NOT_ORDERABLE`, `ADDRESS_REQUIRED`, `PROMO_UNKNOWN`, `PROMO_EXPIRED`, `FILE_TOO_LARGE`, `FORMAT_UNSUPPORTED`,
`CANNOT_CANCEL`, `INVALID_TRANSITION`, `COMMENT_REQUIRED`, `INTERNAL`.

### Идемпотентность заказа

Клиент один раз придумывает UUID и передаёт его в заголовке `Idempotency-Key`.

- повтор с тем же телом возвращает существующий заказ и `200`;
- новое тело с тем же ключом даёт `422 IDEMPOTENCY_KEY_REUSED`;
- расхождение с `expectedTotalKop` даёт `409 PRICE_CHANGED` и новую сумму;
- пустые ячейки макета дают `422 LAYOUT_BLOCKED`, фото не из проекта — `422 LAYOUT_PHOTO_MISSING`.

### Статусы заказа

```
accepted → moderation → production → ready_for_delivery → completed
    └──────────┴──→ cancelled
```

Оператор двигает статус ровно на один шаг. Отменить можно только из `accepted` или `moderation`, оператор —
только с комментарием. `completed` и `cancelled` конечные. Клиент может отменить заказ сам только в `accepted`;
при отмене проект возвращается в черновик.

### Фото и макет

Только JPEG и PNG, проверка по сигнатуре файла, до 15 МБ. Размеры учитывают EXIF-ориентацию, время съёмки хранится
в UTC. Файлы лежат вне веб-корня и отдаются по ссылке `/files/<id>?exp&sig`, подписанной HMAC-SHA256; ссылка живёт
один час. Удаление проекта удаляет и файлы.

Макет (`Layout`) хранит страницы с ячейками, обложкой, темой, фоном, подписями и свободными слоями. Все новые поля
необязательны, старые макеты читаются как раньше. Сохранение идёт с проверкой версии (`VERSION_CONFLICT`).

## Цена, доставка и деньги

Все суммы — целые копейки, валюта BYN, форматирование `87,40 BYN`.

```
unitKop     = basePriceKop + max(0, pages − pagesMin) × pricePerPageKop
itemsKop    = unitKop × quantity
discountKop = round(itemsKop × discountPercent / 100)
totalKop    = itemsKop − discountKop + deliveryKop
```

Доставка бесплатна, если сумма после скидки не меньше `freeFromKop` выбранного способа.

| Способ | Цена | Бесплатно от | Срок | Оплата при получении |
|---|---|---|---|---|
| Курьер по Минску | 0 | — | 1–2 дня | да |
| EMS | 20,90 BYN | 300,00 BYN | 2–4 дня | нет |
| Белпочта (в том числе пункты выдачи) | 6,90 BYN | 150,00 BYN | 3–7 дней | нет |

Пример: «Classic 20×20», 30 страниц, 2 книги, промокод `HELLO10`, EMS. `8100 + 10 × 150 = 9600` за книгу,
`19200` за две, скидка `1920`, доставка `2090`, итог `19370` копеек — `193,70 BYN`.

## Тесты

```bash
npm test -w @picpac/shared          # цена, доставка, автомакет, качество, схемы, сборка альбомов
npm test -w api                     # модули API на реальной базе
npm test -w app                     # Jest: логика приложения (шаблоны, слои, макет, альбомы, синхронизация)
npx tsc --noEmit -p api             # типы API
(cd app && npx tsc --noEmit)        # типы приложения
```

**790 автотестов** — 384 в приложении, 303 в API и 103 в общей логике (`shared`).
Проверка типов прошла. В последнем полном прогоне один тест журнала оператора упал и прошёл при отдельном повторном запуске.
Тесты API требуют запущенного PostgreSQL и базу `picpac_test` (её создаёт `docker/initdb/01-test-db.sql`).
Перед запуском применяются миграции, а таблицы очищаются перед каждым тестом. Защита не даст очистить базу, имя
которой не оканчивается на `_test`. Фикстуры можно пересоздать: `npm run fixtures -w api`.

## Документация

| Документ | О чём |
|---|---|
| [Сценарий демо](docs/demo-script.md) | Основной путь и дополнительные сценарии на 5–7 минут |
| [Отчёт о тестировании](docs/test-report.md) | Покрытие сценариев и чем оно подтверждено |
| [Дизайн](docs/specs/2026-11-11-picpac-mobile-design.md) | Визуальная спецификация |
| [UX/UI](docs/specs/2026-11-11-picpac-mobile-ux.md) | Экраны и жесты |
| [Заметки по интеграции](docs/integration-notes.md) | Как переключить приложение на другой API |
| [`SECURITY.md`](SECURITY.md) | Политика безопасности |
| [`LICENSES.md`](LICENSES.md) | Лицензии зависимостей (`npm run licenses`) |
| [`AGENTS.md`](AGENTS.md) | Гайд для агентов по стеку и правилам репозитория |

## Ограничения

- **Оплата имитируется.** Платёжного шлюза и ЕРИП нет, заказ всегда тестовый, деньги не списываются.
- **Нет отправки SMS и писем.** Код входа пишется в лог; в демо — `000000`. До продакшена нужен лимит попыток и реальный канал.
- **Нет push-уведомлений.** Статус меняет оператор через API, клиент узнаёт его опросом.
- **Фото на локальном диске.** Для нескольких инстансов нужно объектное хранилище; в общем альбоме файлы копируются
  в каждую личную книгу.
- **Права в коде, без RLS.** Проверки покрыты тестами, но политики базы не дублируют их.
- **AI.** Без `AIAI_API_KEY` работают заглушки; с ключом — aiai.by. Конверт клиент↔API и шифрование файлов
  включаются ключами `PAYLOAD_ENCRYPTION_KEY` и `FILE_ENCRYPTION_KEY`.
- **Общий альбом:** книга собирается главами по дням съёмки (UTC); сам пул не заказывается — только личная книга участника.
