# 03. Модель данных ## 1. Соглашения 1. Все `id` — UUID (`idType: 'uuid'` в адаптере Postgres). Номера для людей (заказы, ключи, платежи, реферальные коды) — случайные коды, не последовательные. 2. Сайт — тенант плагина `@payloadcms/plugin-multi-tenant`. Поле тенанта называется `site`. Коллекции с пометкой «сайт» содержат его автоматически; в админке у них есть переключатель сайта. 3. «(L)» — локализованное поле Payload: значение хранится на каждом языке, если перевода нет — показывается EN. 4. Деньги — целое число в минимальных единицах (`amount`: 249900 = 2 499,00 ₽) плюс `currency` — код ISO 4217 (`RUB`, `USD`). Код проверяется по справочнику «Валюты». 5. Даты — `timestamptz`, в UTC. 6. Секреты в полях (конфиги WG, ссылки подписок, SMTP-пароль, id сохранённых карт) шифруются приложением (AES-256-GCM, ключ в `.env`). 7. Валюта везде хранится кодом ISO 4217 — и в деньгах, и в настройках (валюты домена и платёжной системы, строки цен). Код проверяется по справочнику, в админке это выпадающий список из «Валют». 8. Переводы полей Payload хранятся в таблицах `<коллекция>_l10n` (настройка адаптера `localesSuffix`). Имя по умолчанию `<коллекция>_locales` совпало бы с таблицей поля `locales` у сайтов и доменов. ## 2. Схема связей ```mermaid erDiagram SITES ||--o{ DOMAINS : "домены" SITES ||--o{ LOCATIONS : "локации" SITES ||--o{ TARIFFS : "тарифы" SITES ||--o{ CUSTOMERS : "пользователи" COUNTRIES ||--o{ LOCATIONS : "страна" TARIFFS }o--o{ COUNTRIES : "страны тарифа" TARIFFS }o--o{ TAGS : "теги" DOMAINS }o--o{ CURRENCIES : "валюты домена" PAYMENT_SYSTEMS }o--o{ CURRENCIES : "валюты системы" CUSTOMERS ||--o{ KEYS : "ключи" TARIFFS ||--o{ KEYS : "тариф" LOCATIONS ||--o{ KEYS : "локация (dedicated)" KEYS ||--o{ CONNECTIONS : "подключения" CUSTOMERS ||--o{ ORDERS : "заказы" KEYS ||--o{ ORDERS : "продления" ORDERS ||--o{ PAYMENTS : "оплаты" CUSTOMERS ||--o{ PAYMENTS : "платежи" PAYMENT_SYSTEMS ||--o{ PAYMENTS : "через" CUSTOMERS ||--o{ LEDGER_ENTRIES : "движения" PAYMENTS ||--o{ LEDGER_ENTRIES : "зачисление" ORDERS ||--o{ LEDGER_ENTRIES : "списание, возврат" CUSTOMERS ||--o{ PAYMENT_METHODS : "карты" CUSTOMERS ||--o{ REFERRALS : "пригласил" KEYS ||--o{ KEY_EXTENSIONS : "продления без оплаты" REFERRALS ||--o{ KEY_EXTENSIONS : "бонус" CUSTOMERS ||--o{ CUSTOMER_SESSIONS : "сессии" ``` ## 3. Настройки и справочники ### 3.1 `sites` — Сайты (тенанты) | Поле | Тип | Описание | |---|---|---| | code | select `dedicated` \| `mass`, уникальное | бренд: по нему выбираются тема и страницы | | name | text (L) | «Вейликс» / «Veilix» | | orderPrefix | text | префикс номеров: `VX`, `VV` | | locales | multi-select из языков конфига | языки контента сайта; en обязателен; домены показывают часть этих языков ([D-023](decisions/D-023-site-locales.md)) | | supportEmail | email | почта поддержки: `help@veilix.app`, `help@veilixvpn.com` | | emailFrom | group: name, address | отправитель писем, напр. «Вейликс» | | supportBot | text | Telegram поддержки — имя для ссылки `t.me/…`: пока `veilix_support` у обоих брендов, с этапа 3e — бот поддержки бренда | | supportHours | text (L) | часы поддержки: «с 10 до 20 по московскому времени в рабочие дни» | | mainBot | text | имя основного бота (этап 2a) | | referralEnabled | checkbox | | | referralBonusDays | number, по умолчанию 30 | бонус «месяц в подарок»: дни срока ключа обоим ([D-025](decisions/D-025-referral-days.md)) | | topupPresets | array: currency, amounts[] | суммы пополнения в кабинете ([04-billing.md](04-billing.md), раздел 4) | | graceDays | number, по умолчанию 3 | льготный период | | reminderDays | number, по умолчанию 3 | за сколько дней напоминать | | renewalLeadHours | number, по умолчанию 24 | за сколько часов до окончания пробовать продлить | | expiringBadgeDays | number, по умолчанию 7 | когда показывать «Истекает» | | provisioningMode | select `manual` \| `demo` \| `api` | режим выдачи ключей ([D-028](decisions/D-028-provisioning-adapter.md)): `demo` — с этапа 1f, `api` — с 3g | | seo | group: titleTemplate (L), description (L), ogImage | значения по умолчанию | На этапе 1a сделаны `code` (после создания не меняется), `name`, `orderPrefix` и `locales`, на этапе 1b — `supportEmail`, `supportBot`, `supportHours` и `seo`. Остальные поля добавляют этапы, которые ими пользуются: SEO, контакты и часы поддержки — 1b, отправитель писем — 1c, порог «Истекает» — 1d, пресеты пополнения — 1e, режим выдачи — 1f, сроки продлений и напоминаний — 1g, рефералы — 1h, основной бот — 2a. ### 3.2 `domains` — Домены (сайт) | Поле | Тип | Описание | |---|---|---| | hostname | text, уникальное | `veilix.app`, `my.veilix.app`, … | | app | select `marketing` \| `cabinet` \| `miniapp` | тип фронтенда | | locales | multi-select из языков конфига | включённые языки — часть языков сайта | | defaultLocale | select | обычно `en`; открывается без префикса в адресе | | currencies | multi-select валют | включённые валюты | | defaultCurrency | select | валюта по умолчанию | | localeCurrency | array: locale, currency | валюта по умолчанию для языка: ru → RUB | | isPrimary | checkbox | основной домен этого типа (у сайта один на каждый тип); используется в ссылках и sitemap | | indexable | checkbox | разрешена ли индексация (для кабинета и MiniApp — нет) | | umamiWebsiteId | text | этап 3f | | active | checkbox | | Язык и валюта по умолчанию должны входить в списки домена, язык в правилах «язык → валюта» не повторяется — это проверяется при сохранении. ### 3.3 `currencies` — Валюты | Поле | Тип | Описание | |---|---|---| | code | text, уникальное | ISO 4217: `RUB`, `USD` | | symbol | text | `₽`, `$` | | name | text (L) | | | minorUnits | number | 2 | | active, sort | | | ### 3.4 `payment-systems` — Платёжные системы Платёжная система привязана только к валютам. Где она доступна, следует из цепочки: валюты → домены, на которых они включены → сайты. В оформлении заказа и пополнении показываются системы в валюте аккаунта. | Поле | Тип | Описание | |---|---|---| | name | text | название в админке | | adapter | select | реализация в коде: `manual`, `demo` (1e, [D-027](decisions/D-027-demo-payments.md)); `yookassa` (3b); `stripe`, `crypto_*` (4b) | | title | text (L) | что видит пользователь: «Банковская карта», «Карта МИР», «СБП», «Криптовалюта» | | description | text (L) | подсказка: «USDT, BTC, ETH, курс фиксируется на 15 минут» | | icon | upload | | | currencies | multi-select валют | в каких валютах доступна | | purposes | multi-select `order` \| `topup` | оплата заказа, пополнение | | visibleToCustomers | checkbox | у `manual` всегда выключено (проверяется при сохранении): ей пользуются только администраторы | | allowedIps | text | для `demo`: IP и подсети, с которых система видна; пусто — не видна никому | | mode | select `test` \| `live` | | | limits | array: currency, min, max | | | capabilities | только чтение, из адаптера | `recurring`, `refunds`, `saveMethod`; объявлены в `src/core/payments/adapters.ts`, в базе не хранятся | | active, sort | | | Ключи доступа к провайдерам — только в `.env` (`PAYMENT_<КОД>_…`), не в базе. ### 3.5 `countries` — Страны (общие) | Поле | Тип | Описание | |---|---|---| | iso2 | text, уникальное | `NL`, `DE`, … | | name | text (L) | | | lat, lng | number | точка на карте по умолчанию | | sort | number | | ### 3.6 `locations` — Локации (сайт) | Поле | Тип | Описание | |---|---|---| | country | relationship → countries | уникальна в пределах сайта | | city | text (L) | | | lat, lng | number | точка города на карте | | status | select `available` \| `on_demand` \| `soon` | dedicated: «Серверы в наличии» / «Установим за 24 часа»; mass: «Включено» / «Скоро». Чужой для бренда статус не сохранится | | freeServers | number, необязательное | счётчик; на сайте показывается, если заполнен ([D-032](decisions/D-032-free-servers.md)) | | visible, sort | | | ### 3.7 `tags` — Теги (общие) `name`, `code` (уникальное). Для фильтрации тарифов и будущей автоматизации выдачи ключей. ### 3.8 `tariffs` — Тарифы (сайт) | Поле | Тип | Описание | |---|---|---| | name | text (L) | | | code | text, уникальное в сайте | | | type | select `dedicated` \| `mass` | поведение ключа: dedicated — страна выбирается при покупке, mass — все страны тарифа | | description | text (L) | | | features | array of text (L) | пункты «что входит» | | period | group: unit `month` \| `year`, count | | | devicesLimit | number, по умолчанию 5 | | | prices | array: currency, firstPeriodAmount, renewalAmount | строка на валюту; нет строки — тариф в этой валюте не продаётся | | countries | relationship → countries, many | страны тарифа | | tags | relationship → tags, many | | | badge | text (L) | «выгоднее» | | isDefault | checkbox | выбран по умолчанию на странице цен | | active, sort | | | ## 4. Люди ### 4.1 `admins` — Администраторы Стандартная авторизация Payload (email и пароль), `maxLoginAttempts: 5`, `lockTime: 15 мин`. Первый администратор окружения (экран первого входа) становится owner. Последнего owner нельзя лишить роли и удалить. Роли и сайты меняет только owner, остальные правят только своё имя и пароль. | Поле | Тип | Описание | |---|---|---| | name | text | | | roles | multi-select `owner` \| `admin` \| `support` \| `content` | см. раздел 8 | | sites | доступ к тенантам | owner и admin видят все сайты, поэтому поле показывается только ролям support и content; на экране первого входа скрыто | ### 4.2 `customers` — Пользователи (сайт) Коллекция авторизации Payload со своей стратегией (вход по коду, [05-auth.md](05-auth.md)). | Поле | Тип | Описание | |---|---|---| | email | email, null | уникален в пределах сайта, хранится в нижнем регистре | | emailVerifiedAt | date | | | telegramId | text, null | уникален в пределах сайта (этап 2a) | | telegramUsername, telegramName | text | этап 2a | | locale | select | язык писем и бота | | currency | select | валюта аккаунта ([04-billing.md](04-billing.md), раздел 2) | | balance | number, только чтение | кэш суммы движений по счёту | | referralCode | text, уникальное в сайте | случайный | | referredBy | relationship → customers | | | status | select `active` \| `blocked` | | | registeredVia | select `email` \| `telegram` \| `admin` | | | registrationIpHash | text | HMAC от IP: только для сравнения при проверке самоприглашений | | marketingConsent | checkbox | | | lastLoginAt | date | | | defaultPaymentMethod | relationship → payment-methods | | | adminNotes | textarea | видят только администраторы | ### 4.3 `customer-sessions` — Сессии (служебная) `customer`, `kind` (`web` \| `miniapp`), `tokenHash` (SHA-256, уникальное), `createdAt`, `expiresAt`, `lastSeenAt`, `ipHash`, `userAgent`, `revokedAt`. ### 4.4 `auth-codes` — Коды входа (служебная) `site`, `email`, `purpose` (`login` \| `link_email`), `customer` (для привязки почты), `codeHash`, `expiresAt`, `attempts`, `consumedAt`, `requestIpHash`. ## 5. Ключи ### 5.1 `keys` — Ключи (сайт) | Поле | Тип | Описание | |---|---|---| | code | text, уникальное | публичный код, напр. `K-7F2C9Q` | | customer | relationship | | | tariff | relationship | | | type | select `dedicated` \| `mass` | копия из тарифа | | location | relationship → locations | только dedicated | | status | select | см. 5.3 | | autoRenew | checkbox, по умолчанию да | | | activatedAt, expiresAt, graceUntil, cancelledAt | date | | | renewalAmount, currency | | цена продления, зафиксированная при покупке; администратор может изменить | | period, devicesLimit | | копии из тарифа на момент покупки | | ipAddress, ruIpAddress | text | для показа (dedicated) | | adminNotes | textarea | | ### 5.2 `connections` — Подключения | Поле | Тип | Описание | |---|---|---| | key | relationship | | | kind | select `wireguard` \| `amneziawg` \| `happ` \| `xray` \| `other` | `happ` — ссылка подписки или crypt-ссылка Happ, `xray` — ссылка `vless://` и подобные | | title | text | «Устройство 1», «Ноутбук» | | deviceSlot | number | номер устройства для WG/AWG | | country | relationship → countries | для WG у mass — страна конфига; для RU-адреса — RU | | isRu | checkbox | подключение к RU-адресу; у обоих брендов ([D-031](decisions/D-031-mass-ru-address.md)) | | payload | text, зашифрованное | содержимое конфига или URL | | fileName | text | имя файла при скачивании | | status | select `active` \| `revoked` | | | source | select `manual` \| `auto` | | | createdBy | relationship → admins | | QR-коды строятся на лету из `payload`, не хранятся. ### 5.3 Статусы ключа | Статус | Значение | |---|---| | `provisioning` | оплачен, готовится. Срок ещё не идёт. Подключения создаёт адаптер выдачи ([D-028](decisions/D-028-provisioning-adapter.md)) или администратор | | `active` | работает, срок идёт с `activatedAt` | | `grace` | срок истёк, оплата не прошла, ключ ещё работает до `graceUntil` | | `expired` | истёк, подключения отключаются | | `cancelled` | отменён (возврат) | | `suspended` | заблокирован администратором | Переходы — в [04-billing.md](04-billing.md). ### 5.4 `key-extensions` — Продления без оплаты (сайт) Реферальный бонус и «Продлить вручную» ([04-billing.md](04-billing.md), раздел 6.4). Только создание, как у движений по счёту. | Поле | Тип | Описание | |---|---|---| | key | relationship | | | days | number | сколько дней добавлено | | reason | select `referral` \| `compensation` | реферальный бонус или компенсация из админки | | referral | relationship → referrals | для `referral` | | appliedAt | date, null | когда сдвинут срок; пусто — ключ готовится, дни добавятся при выдаче | | expiresAtBefore, expiresAtAfter | date | срок до и после | | comment | text | обязателен для `compensation` | | createdBy | relationship → admins, null | null — система | Уникальность: (`referral`, `key`) — бонус не начисляется дважды. ## 6. Деньги ### 6.1 `orders` — Заказы (сайт) | Поле | Тип | Описание | |---|---|---| | number | text, уникальное | `VX-7K3M9Q` | | customer | relationship | | | type | select `new_key` \| `renewal` | | | tariff | relationship | | | location | relationship | для `new_key` у dedicated | | key | relationship | для `renewal`; для `new_key` заполняется после оплаты | | renewalFor | date | `expiresAt` ключа, который продлеваем; уникально вместе с `key` | | currency, amount | | полная стоимость | | status | select `pending` \| `paid` \| `cancelled` \| `expired` \| `refunded` \| `partially_refunded` | | | source | select `web` \| `miniapp` \| `bot` \| `admin` \| `auto_renewal` | | | paidAt, cancelledAt, validUntil | date | `validUntil` — неоплаченный заказ истекает через 24 часа | | refundedAmount | number | | | adminNotes | textarea | | ### 6.2 `payments` — Платежи (сайт) | Поле | Тип | Описание | |---|---|---| | number | text, уникальное | | | customer | relationship | | | paymentSystem | relationship | | | adapter | text | копия из платёжной системы | | purpose | select `order` \| `topup` | | | order | relationship | для `order` | | currency, amount | | | | status | select `pending` \| `succeeded` \| `failed` \| `cancelled` \| `refunded` \| `partially_refunded` | | | providerPaymentId | text | уникально вместе с `adapter` | | providerStatus, providerData | text, json | только для администраторов | | methodSummary | group: type, brand, last4 | «Карта ···· 4242», «USDT (TRC-20)» | | savedPaymentMethod | relationship | если карта сохранена | | idempotencyKey | text, уникальное | | | succeededAt, processedAt, failedAt | date | `processedAt` — цепочка действий выполнена | | failureReason | text | | | createdBy | relationship → admins | для ручных | | comment | textarea | | ### 6.3 `ledger-entries` — Движения по счёту (сайт) Только создание. Изменять и удалять нельзя никому, ошибка исправляется встречной записью. | Поле | Тип | Описание | |---|---|---| | customer | relationship | | | currency | | валюта аккаунта | | amount | number со знаком | `+` зачисление, `−` списание | | type | select `topup` \| `order_charge` \| `refund` \| `adjustment` \| `payout` | `payout` — возврат денег с баланса на карту (этап 4b). Реферальный бонус — не деньги, а дни (5.4) | | payment, order, key | relationship | источник записи | | balanceAfter | number | баланс после записи | | comment | text | обязателен для `adjustment` | | createdBy | relationship → admins, null | null — система | Уникальность: (`payment`, `topup`), (`order`, `order_charge`). Это защищает от двойного проведения. ### 6.4 `payment-methods` — Сохранённые способы оплаты (сайт) `customer`, `paymentSystem`, `providerMethodId` (зашифрованное), `type`, `brand`, `last4`, `expMonth`, `expYear`, `isDefault`, `status` (`active` \| `detached`), `detachedAt`. ### 6.5 `referrals` — Приглашения (сайт) `referrer`, `referee` (уникальное), `status` (`pending` \| `review` \| `rewarded` \| `rejected`), `qualifyingOrder`, `days` (копия `sites.referralBonusDays` при начислении), `refereeKey`, `referrerKey` (пусто, пока у пригласившего нет подходящего ключа: дни ждут его следующего ключа), `rewardedAt`, `rejectReason`. ## 7. Контент и коммуникации ### 7.1 `pages` — Страницы (сайт) Страницы сайта из блоков ([D-029](decisions/D-029-pages-blocks.md)): `slug` (уникальное в сайте: `home`, `pricing`, `locations`, `support`, `terms`, `privacy`, `refund`, `cookies`), `title` (L), `layout` (блоки, поля блоков локализованы), `seo` (title (L), description (L), image). Черновики и версии Payload включены. Блоки (`src/payload/blocks/`), поля блоков локализованы, в текстах — подстановки `{priceFirst}`, `{priceRenewal}`, `{priceMonth}`, `{priceYear}`, `{pricePerMonth}`, `{savings}`, `{countries}`, `{devices}`: | Блок | Секция макета | |---|---| | `hero` | первый экран главной: плашки, H1, две кнопки, плашка под глобусом | | `advantages` | «Что это даёт на практике»: большая карточка, малые карточки, карточка маршрутов | | `pricing` | цены на главной: у dedicated — выбор локации и заявка, у mass — планы «Месяц» и «Год» | | `crossLink` | ссылка на сайт другого бренда | | `faq` | вопросы из мест показа: одно место — список, несколько — вкладки | | `pricingHero` | верх страницы цен: крупная цена, «Что входит» | | `locationGrid` | сетка локаций: у dedicated с ценами и «Купить», у mass — со статусами | | `cards` | карточки внизу страниц: текст, ссылка на другой бренд, заявка | | `mapHero` | верх страницы локаций: H1, карта, легенда | | `contacts` | верх поддержки: H1, Telegram и почта из настроек сайта, часы | | `contactForm` | «Написать нам» с темами обращения | | `richText` | текст: юридические страницы | ### 7.2 `faq` — Вопросы и ответы (сайт) `question` (L), `answer` (L, rich text), `placements` (multi: `home`, `pricing`, `support_connection`, `support_billing`, `support_locations`, `cabinet`, `miniapp`), `sort`, `active`. Один вопрос можно показать в нескольких местах. ### 7.3 `email-templates` — Шаблоны писем (сайт) `code` (уникальное в сайте), `subject` (L), `body` (L, rich text с переменными `{{code}}`, `{{keyTitle}}`, `{{expiresAt}}`, `{{amount}}` …), `active`. Шапка и подвал письма — в коде, для каждого бренда свои. Коды шаблонов: `login_code`, `link_email_code`, `order_paid`, `key_provisioning`, `key_ready`, `renewal_reminder`, `expiry_reminder`, `renewal_succeeded`, `renewal_failed`, `grace_started`, `key_expired`, `topup_succeeded`, `referral_reward`, `refund_issued`, `admin_new_order`, `admin_provisioning_needed`, `admin_new_lead`. ### 7.4 `leads` — Заявки (сайт) Этап 3d. До него формы — только интерфейс ([D-026](decisions/D-026-stage-order.md)); нужна ли коллекция как буфер перед Twenty — решаем на этапе. Поля: `type` (`individual_terms` \| `contact`), `name`, `email`, `telegram`, `whatsapp`, `topic`, `message`, `locale`, `pageUrl`, `customer`, `status` (`new` \| `in_progress` \| `done` \| `spam`), `crmId` (3d) и `chatwootId` (3e), `ipHash`. ### 7.5 `media` — Файлы Загрузки Payload: изображения для контента, иконки платёжных систем, картинки для соцсетей. ## 8. Журналы ### 8.1 `email-log` — Журнал писем `site`, `to`, `templateCode`, `subject`, `transport` (`smtp` \| `sendmail` \| `log_only`), `status` (`queued` \| `sent` \| `failed`), `error`, `messageId`, `sentAt`, `customer`, `debugCode`. `debugCode` — код входа открытым текстом. Заполняется, только если в настройках почты включено «Записывать коды входа в журнал». Видят роли owner и admin. Записи с кодом удаляются через сутки, остальные — через `logRetentionDays`. ### 8.2 `audit-log` — Журнал действий `actor` (администратор или система), `action` (`payment.manual`, `ledger.adjustment`, `key.activate`, `order.refund`, `customer.merge`, …), `entityType`, `entityId`, `before`, `after`, `ipHash`, `createdAt`. Только создание. ## 9. Глобальные настройки ### 9.1 `email-settings` — Почта | Поле | Описание | |---|---| | transport | `smtp` \| `sendmail` \| `log_only` (только журнал, без отправки) | | smtp | host, port, secure, user, password (зашифрованный) | | sendmailPath | по умолчанию `/usr/sbin/sendmail` | | logLoginCodes | «Записывать коды входа в журнал». В prod по умолчанию выключено; когда включено, в админке висит предупреждение | | logRetentionDays | по умолчанию 30 | | Кнопка | «Отправить тестовое письмо» на указанный адрес | ## 10. Доступ | Коллекция | owner, admin | support | content | пользователь (через приложение) | |---|---|---|---|---| | Настройки и справочники | всё (удаление сайта — только owner) | чтение | чтение | — | | admins | owner — всё, admin — только себя | только себя | только себя | — | | media | всё | чтение | всё | файлы открыты всем: их показывают сайты | | pages, faq, email-templates, локализация | всё | чтение | всё | — | | customers | всё | чтение, правка заметок, блокировка | — | только свой профиль | | keys, connections | всё | всё | — | только свои, чтение | | orders, payments | всё | чтение, ручной платёж | — | только свои, чтение | | ledger-entries, key-extensions | создание корректировок и компенсаций, чтение | чтение | — | только свои, чтение | | leads | всё | всё | — | — | | email-log, audit-log | чтение | чтение без `debugCode` | — | — | Пользователь никогда не ходит в REST Payload напрямую: кабинет читает данные на сервере через Local API с `overrideAccess: false` и `user`.