- Q-02: цены mass в USD $6.99 и $69.90, пресеты пополнения в RUB и USD. - D-025: реферальный бонус — 30 дней к ключу обоим вместо денег; продления без оплаты (key-extensions); крайние случаи — Q-19. - Q-08, Q-09: help@veilix.app и help@veilixvpn.com, часы по Москве. - Q-16: ящики на почтовом хостинге (R-18), на этапе 2 — Chatwoot по IMAP. - R-01 сделано; R-17: путь HMR в Next 16.4 — /_next/hmr. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
26 KiB
03. Модель данных
1. Соглашения
- Все
id— UUID (idType: 'uuid'в адаптере Postgres). Номера для людей (заказы, ключи, платежи, реферальные коды) — случайные коды, не последовательные. - Сайт — тенант плагина
@payloadcms/plugin-multi-tenant. Поле тенанта называетсяsite. Коллекции с пометкой «сайт» содержат его автоматически; в админке у них есть переключатель сайта. - «(L)» — локализованное поле Payload: значение хранится на каждом языке, если перевода нет — показывается EN.
- Деньги — целое число в минимальных единицах (
amount: 249900 = 2 499,00 ₽) плюсcurrency— код ISO 4217 (RUB,USD). Код проверяется по справочнику «Валюты». - Даты —
timestamptz, в UTC. - Секреты в полях (конфиги WG, ссылки подписок, SMTP-пароль, id сохранённых карт) шифруются приложением (AES-256-GCM, ключ в
.env). - Валюта везде хранится кодом ISO 4217 — и в деньгах, и в настройках (валюты домена и платёжной системы, строки цен). Код проверяется по справочнику, в админке это выпадающий список из «Валют».
- Переводы полей Payload хранятся в таблицах
<коллекция>_l10n(настройка адаптераlocalesSuffix). Имя по умолчанию<коллекция>_localesсовпало бы с таблицей поляlocalesу сайтов и доменов.
2. Схема связей
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) |
| supportEmail | почта поддержки: help@veilix.app, help@veilixvpn.com |
|
| emailFrom | group: name, address | отправитель писем, напр. «Вейликс» no-reply@veilix.app |
| supportBot, mainBot | text | имена ботов (этап 3) |
| referralEnabled | checkbox | |
| referralBonusDays | number, по умолчанию 30 | бонус «месяц в подарок»: дни срока ключа обоим (D-025) |
| topupPresets | array: currency, amounts[] | суммы пополнения в кабинете (04-billing.md, раздел 4) |
| graceDays | number, по умолчанию 3 | льготный период |
| reminderDays | number, по умолчанию 3 | за сколько дней напоминать |
| renewalLeadHours | number, по умолчанию 24 | за сколько часов до окончания пробовать продлить |
| expiringBadgeDays | number, по умолчанию 7 | когда показывать «Истекает» |
| seo | group: titleTemplate (L), description (L), ogImage | значения по умолчанию |
На этапе 1a сделаны code (после создания не меняется), name, orderPrefix и locales. Остальные поля добавляют этапы, которые ими пользуются: SEO и контакты — 1b, отправитель писем — 1c, сроки и пресеты — 1d и 1f, рефералы — 1g, боты — 3.
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 | этап 1h |
| 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; на этапе 4 — stripe, yookassa, crypto_*, rucard_* |
| title | text (L) | что видит пользователь: «Банковская карта», «Карта МИР», «СБП», «Криптовалюта» |
| description | text (L) | подсказка: «USDT, BTC, ETH, курс фиксируется на 15 минут» |
| icon | upload | |
| currencies | multi-select валют | в каких валютах доступна |
| purposes | multi-select order | topup |
оплата заказа, пополнение |
| visibleToCustomers | checkbox | у manual всегда выключено (проверяется при сохранении): ей пользуются только администраторы |
| 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, необязательное | счётчик (решение о показе — этап 1b) |
| 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).
| Поле | Тип | Описание |
|---|---|---|
| email, null | уникален в пределах сайта, хранится в нижнем регистре | |
| emailVerifiedAt | date | |
| telegramId | text, null | уникален в пределах сайта (этап 3) |
| telegramUsername, telegramName | text | этап 3 |
| locale | select | язык писем и бота |
| currency | select | валюта аккаунта (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-адресу (dedicated) |
| payload | text, зашифрованное | содержимое конфига или URL |
| fileName | text | имя файла при скачивании |
| status | select active | revoked |
|
| source | select manual | auto |
|
| createdBy | relationship → admins |
QR-коды строятся на лету из payload, не хранятся.
5.3 Статусы ключа
| Статус | Значение |
|---|---|
provisioning |
оплачен, готовится. Срок ещё не идёт. На этапе 1 так создаются все ключи: подключения добавляет администратор |
active |
работает, срок идёт с activatedAt |
grace |
срок истёк, оплата не прошла, ключ ещё работает до graceUntil |
expired |
истёк, подключения отключаются |
cancelled |
отменён (возврат) |
suspended |
заблокирован администратором |
Переходы — в 04-billing.md.
5.4 key-extensions — Продления без оплаты (сайт)
Реферальный бонус и «Продлить вручную» (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 — возврат денег с баланса на карту (этап 4). Реферальный бонус — не деньги, а дни (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 — Страницы (сайт)
slug (уникальное в сайте: terms, privacy, refund, cookies), title (L), content (L, Lexical), seo (title (L), description (L)). Черновики и версии Payload включены.
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 — Заявки (сайт)
type (individual_terms | contact), name, email, telegram, whatsapp, topic, message, locale, pageUrl, customer, status (new | in_progress | done | spam), crmId и chatwootId (этап 2), 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.