Files
pchelpmaster/docs/data-model.md
T

149 lines
11 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.
# Модель данных (PocketBase)
Схема создаётся JS-миграциями в `pocketbase/pb_migrations/` (одна миграция на изменение, пишется руками, не через админку). Кастомные маршруты и хуки — в `pocketbase/pb_hooks/`.
У всех коллекций, кроме `users`, есть поле `owner` — ссылка на владельца. Это основа изоляции: любые правила доступа начинаются с `owner = @request.auth.id`.
Поля `id`, `created`, `updated` (autodate) есть у всех коллекций и ниже не перечисляются.
## users (auth-коллекция, уже существует в PocketBase)
| Поле | Тип | Ограничения |
| --- | --- | --- |
| `email` | email | обязательно, уникально |
| `password` | password | минимум 8 символов |
| `name` | text | необязательно, до 100 символов |
| `verified` | bool | системное; гость не может выставить `true` |
Способ входа — только email + пароль (OAuth2, OTP, MFA выключены).
| Правило | Значение | Смысл |
| --- | --- | --- |
| list / view | `id = @request.auth.id` | пользователь видит только себя |
| create | `""` (всем) | открытая регистрация |
| update | `id = @request.auth.id` | меняет только себя |
| delete | `null` (только суперпользователь) | удаление аккаунта в MVP не делаем |
Поле `avatar`, созданное PocketBase по умолчанию, можно оставить, в MVP оно не используется.
## tags — метки
Метки заменяют и категории, и источники заявок: «Установка Windows», «YouDo», «Профи.ру», «Сарафан», «Гарантия» — это всё метки. Набор у каждого пользователя свой.
| Поле | Тип | Ограничения |
| --- | --- | --- |
| `owner` | relation → users | обязательно, одна, `cascadeDelete: true` |
| `name` | text | обязательно, 1–50 символов |
| `color` | select | одно из: `gray`, `red`, `orange`, `yellow`, `green`, `teal`, `blue`, `purple`, `pink`; обязательно |
Индекс: уникальный `(owner, name COLLATE NOCASE)` — у одного пользователя нет двух меток с одинаковым названием без учёта регистра; у разных пользователей названия могут совпадать.
Цвет — ключ из фиксированной палитры, а не произвольный HEX: так UI гарантирует контраст AA в светлой и тёмной теме.
## clients — клиенты
| Поле | Тип | Ограничения |
| --- | --- | --- |
| `owner` | relation → users | обязательно, `cascadeDelete: true` |
| `name` | text | обязательно, 1–200 символов (как клиента зовут или как он подписан на площадке) |
| `phones` | json | массив нормализованных номеров (`["79991234567"]`), до 5 штук, можно пустой |
| `email` | email | необязательно |
| `contacts` | text | свободный текст до 2000 символов: мессенджеры и прочие контакты |
| `external_urls` | json | необязательно; массив ссылок на профили клиента на любых площадках (YouDo, Профи.ру, Avito и др.), до 5 штук, каждая — валидный `http(s)` URL |
| `address` | text | необязательно, до 500 символов |
| `note` | text | внутренняя заметка, до 5000 символов |
Нормализация телефона (делает фронтенд, функция в `clients/domain`): оставить только цифры; `8XXXXXXXXXX` → `7XXXXXXXXXX`; 10 цифр, начинающиеся на `9`, → добавить `7`. Остальные номера хранятся как есть (только цифры).
Удалить клиента, у которого есть заявки, нельзя (связь `orders.client` обязательная и без каскада): сначала удаляются или переносятся заявки.
## orders — заявки
| Поле | Тип | Ограничения |
| --- | --- | --- |
| `owner` | relation → users | обязательно, `cascadeDelete: true` |
| `client` | relation → clients | обязательно, одна, без каскада |
| `title` | text | обязательно, 1–200 символов |
| `description` | text | описание проблемы со слов клиента, до 10000 |
| `status` | select | обязательно: `new`, `scheduled`, `in_progress`, `done`, `failed` |
| `scheduled_at` | date | дата и время визита, необязательно |
| `address` | text | адрес выезда, до 500 (по умолчанию в форме подставляется адрес клиента) |
| `price` | number | целые рубли, ≥ 0, необязательно |
| `result` | text | что сделано, до 10000; **виден по публичной ссылке** |
| `note` | text | примечания мастера, до 10000; **видны по публичной ссылке** |
| `review` | text | отзыв клиента (например, с площадки), до 5000; **виден по публичной ссылке** |
| `external_url` | url | необязательно; ссылка на страницу заказа на любой площадке (YouDo, Профи.ру, Avito и др.) |
| `tags` | relation → tags | несколько, до 20, необязательно |
| `attachments` | file | несколько, до 20 файлов, до 50 МБ каждый, **protected**; изображения (`image/jpeg`, `image/png`, `image/webp`, `image/heic`) и аудио (`audio/mpeg`, `audio/mp4`, `audio/x-m4a`, `audio/aac`, `audio/ogg`, `audio/wav`, `audio/webm`, `audio/amr`); миниатюры `200x200` |
| `share_token` | text | необязательно; если задан — 43 символа `[A-Za-z0-9_-]`; уникален среди непустых |
Статусы и подписи в UI:
| Значение | Подпись |
| --- | --- |
| `new` | Новая |
| `scheduled` | Запланирована |
| `in_progress` | В работе |
| `done` | Выполнена |
| `failed` | Не выполнена |
Переходы между статусами не ограничены: мастер может выставить любой.
Индексы: `owner`, `(owner, status)`, `(owner, created)`, уникальный частичный `share_token WHERE share_token != ''`.
### Соответствие полям YouDo (для справки)
| YouDo | Pchelpcrm |
| --- | --- |
| title, description | `title`, `description` |
| price | `price` |
| address, privateInfo | `address` |
| customerName | `clients.name` |
| customerUrl | `clients.external_urls` |
| taskUrl | `external_url` |
| category | метка |
| review | `review` |
| taskDate | `created` / `scheduled_at` |
## Правила доступа для tags, clients, orders
Общий шаблон (для `orders` добавляются проверки связей):
| Правило | Значение |
| --- | --- |
| list / view | `owner = @request.auth.id` |
| create | `@request.auth.id != "" && owner = @request.auth.id` |
| update | `owner = @request.auth.id && (@request.body.owner:isset = false \|\| @request.body.owner = @request.auth.id)` |
| delete | `owner = @request.auth.id` |
Дополнительно для `orders` (create и update):
- клиент принадлежит тому же пользователю: `client.owner = @request.auth.id`;
- все метки принадлежат тому же пользователю (для пустого списка условие выполняется).
Если какое-то условие невозможно выразить правилом, его реализуют хуком в `pocketbase/pb_hooks/`. Решение принимается при реализации, поведение фиксируют тесты.
### Как PocketBase отвечает на нарушения правил
API-тесты проверяют именно это поведение. Если фактический код в PocketBase 0.40 окажется другим, тесты фиксируют фактический, а главное требование неизменно: **чужие данные никогда не попадают в ответ и не меняются**.
| Операция | Правило не выполнено | Правило `null` (не суперпользователь) |
| --- | --- | --- |
| list | 200, чужие записи отфильтрованы | 403 |
| view | 404 | 403 |
| create | 400 | 403 |
| update | 404 | 403 |
| delete | 404 | 403 |
## Публичный маршрут
`GET /api/public/orders/{token}` (хук в `pocketbase/pb_hooks/`), без авторизации.
- Ищет заявку по непустому `share_token`, совпадающему точно.
- Ответ 200 содержит **только**: `title`, `description`, `result`, `note`, `review`, `status`, `scheduled_at`, `price`, `created`, `tags: [{ name, color }]`.
- Никогда не содержит: `id` заявки, `owner`, данные клиента (имя, телефоны, контакты, адрес, ссылки на профили), `address` заявки, `external_url`, `attachments`, `share_token`, email мастера.
- Нет заявки или токен пустой/неверного формата — 404 с одинаковым телом.
- Заголовки: `Cache-Control: no-store`, `X-Robots-Tag: noindex`.
Ссылку создаёт владелец, записывая в `share_token` случайную строку (32 случайных байта в base64url, сгенерированные в браузере через `crypto.getRandomValues`). Отключить ссылку — записать пустую строку; перевыпустить — записать новую.