Этап 0: документация и макеты

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
andreypr207andClaude Opus 5.5 committed 2026-10-21 03:30:38 +02:00
commit 3400f58a5a
75 files changed
+14283

No files matched your search

+77
View File
@@ -0,0 +1,77 @@
# 00. Карта документации
## 1. Документы
| Файл | О чём |
|---|---|
| [01-product.md](01-product.md) | Бренды, домены, продукты, тарифы, бизнес-правила |
| [02-architecture.md](02-architecture.md) | Стек, одно приложение на все домены, процессы, окружения, выкладка |
| [03-data-model.md](03-data-model.md) | Коллекции Payload, поля, связи, статусы |
| [04-billing.md](04-billing.md) | Счёт, заказы, платежи, продления, возвраты, ручные операции, рефералы |
| [05-auth.md](05-auth.md) | Вход по коду, сессии, капча, Telegram, объединение аккаунтов, админка |
| [06-i18n.md](06-i18n.md) | Языки, URL, определение языка, валютный переключатель, переводы |
| [07-frontend.md](07-frontend.md) | Макеты, маршруты, токены, темы, производительность |
| [08-integrations.md](08-integrations.md) | Почта, ALTCHA, MaxMind, Chatwoot, Twenty, Telegram, платёжные системы, Umami |
| [09-infrastructure.md](09-infrastructure.md) | Сервер, nginx, systemd, PostgreSQL, DNS, бэкапы, задачи для root |
| [10-security.md](10-security.md) | Безопасность и приватность |
| [decisions/](decisions/) | Принятые решения: по файлу на решение |
| [stages/](stages/) | Этапы: цель, границы, предусловия, шаги, критерии готовности |
Исходная постановка — [history/HUMAN_IDEA.md](history/HUMAN_IDEA.md) (копия файла пользователя). Это история, а не источник истины: всё актуальное — в этих документах.
## 2. Этапы
| Этап | Содержание | Статус |
|---|---|---|
| [0](stages/stage-0-docs.md) | Документация | готово |
| [1a](stages/stage-1a-foundation.md) | Фундамент: Next + Payload, сайты, домены, языки, валюты, темы | не начат |
| [1b](stages/stage-1b-marketing.md) | Маркетинговые страницы обоих брендов, контент, формы с ALTCHA, SEO, юр. тексты | не начат |
| [1c](stages/stage-1c-auth.md) | Вход по коду, почта, сессии | не начат |
| [1d](stages/stage-1d-billing.md) | Биллинг без эквайринга, оформление заказа, продления | не начат |
| [1e](stages/stage-1e-keys.md) | Ключи и подключения, ручная выдача | не начат |
| [1f](stages/stage-1f-cabinet.md) | Веб-кабинеты обоих брендов | не начат |
| [1g](stages/stage-1g-referral.md) | Реферальная программа | не начат |
| [1h](stages/stage-1h-launch.md) | Переводы, Umami и баннер cookies, PageSpeed, выкладка в prod | не начат |
| [2](stages/stage-2-support-crm.md) | Chatwoot и Twenty CRM | не начат |
| [3](stages/stage-3-telegram.md) | Telegram: боты, MiniApp, вход и привязка | не начат |
| [4](stages/stage-4-payments.md) | Платёжные системы: Stripe, RUB-процессинг, крипта | не начат |
| [5](stages/stage-5-keys-automation.md) | Автоматизация выдачи ключей | обсуждение при старте |
## 3. Как запускать этап в новой сессии
1. Открыть сессию Claude под пользователем vx_dev в `/home/vx_dev/veilix_dev`.
2. Написать: «Этап 1a. Прочитай CLAUDE.md и docs/stages/stage-1a-foundation.md, сверим план».
3. Claude сверяет план и открытые вопросы этапа, вы подтверждаете, дальше — работа.
4. В конце Claude обновляет статус этапа в этой таблице и документацию.
## 4. Открытые вопросы
### 4.1 Решённые
| № | Вопрос | Решение | Где отражено |
|---|---|---|---|
| Q-01 | Вторая валюта | USD | [D-010](decisions/D-010-currencies.md) |
| Q-03 | Валюта по умолчанию для нового посетителя | По языку: ru → RUB, остальные → USD; настраивается у домена | [06-i18n.md](06-i18n.md), раздел 6 |
| Q-04 | Валюта аккаунта | Задаётся при регистрации из выбранной; меняется, пока баланс нулевой и нет ключей; дальше — через поддержку | [04-billing.md](04-billing.md), раздел 2 |
| Q-05 | WG-конфиги mass на этапе 1 | Конфиги, добавленные администратором, по странам; другая страна — через поддержку; автоматически — этап 5 | [D-020](decisions/D-020-key-models.md) |
| Q-06 | Где Umami | На этом сервере, без Docker, под пользователем `umami`; панель — `umi.veilix.cc` | [D-018](decisions/D-018-analytics.md) |
| Q-07 | Как запускать сервисы | Каждый под своим пользователем ОС: vx_dev (dev), vx_prod (prod), umami; systemd user-юниты | [D-004](decisions/D-004-environments.md) |
| Q-15 | Защита prod от сессий Claude | Отдельно не нужна: сессии Claude работают под vx_dev без доступа к prod, а под vx_prod нет ни редактора, ни ИИ-агентов | [D-004](decisions/D-004-environments.md) |
### 4.2 Открытые
Предложение по умолчанию действует, пока не решено иначе.
| № | Вопрос | Предложение | Нужно к этапу |
|---|---|---|---|
| Q-02 | Цены mass в USD, суммы пополнения и реферального бонуса в USD для обоих брендов | — нужны цифры | 1b |
| Q-08 | Email поддержки брендов | `help@veilix.app` и `help@veilixvpn.com` (в макете были домены .cc) | 1b |
| Q-09 | Часовой пояс часов поддержки (10–20 в рабочие дни) | МСК | 1b |
| Q-10 | Имена Telegram-ботов: 2 основных, 2 поддержки, тестовые | — | 3 |
| Q-11 | Оплата в Telegram: Stars, внешняя страница или только с баланса | — | 3 |
| Q-12 | RUB-процессинг (KZ, TR) и криптопроцессинг: какие провайдеры | — | 4 |
| Q-13 | Юрлицо для юридических текстов | Пока заглушки | 1h |
| Q-14 | Куда складывать бэкапы prod-базы | — | 1h |
| Q-16 | Где принимать почту поддержки (`help@…`): свой почтовый хостинг или пересылка в Chatwoot | — | 1b |
| Q-17 | Нужны ли разные аккаунты одного провайдера для разных брендов | Пока нет. Если понадобятся — платёжной системе добавим поле «только для сайтов» | 4 |
| Q-18 | Кто запускает выкладку в prod | Пользователь одной командой; по желанию — sudo-правило, разрешающее vx_dev запускать только команду выкладки от имени vx_prod | 1h |
+155
View File
@@ -0,0 +1,155 @@
# 01. Продукт
## 1. Бренды
| | Вейликс | ВейликсВПН |
|---|---|---|
| Код сайта | `dedicated` | `mass` |
| Название | RU «Вейликс», EN «Veilix» | RU «ВейликсВПН», EN «VeilixVPN» |
| Цвет | фиолетовый | зелёный (бирюзовый) |
| Суть | личный VPN: у каждого ключа свой индивидуальный IP в выбранной стране | классический VPN: общий сервер, ключ даёт все страны тарифа |
| Протоколы | WireGuard / Amnezia WG и Xray (VLESS, Hysteria2, XHTTP) | Xray и WireGuard |
1. Это два разных продукта на одном бэкенде и одной базе. Сайт определяется по домену.
2. veilixvpn.com продаёт только mass. Перекрёстные ссылки между брендами допустимы: на mass есть блок «нужен свой IP — тарифы Вейликс».
3. Логотип — вариант «Слои» из макета (два перекрывающихся скруглённых квадрата). Подборщик вариантов знака из макета в продукт не переносим.
## 2. Домены
| Назначение | Вейликс prod | Вейликс dev | ВейликсВПН prod | ВейликсВПН dev |
|---|---|---|---|---|
| Сайт | veilix.app | dev.veilix.app | veilixvpn.com | dev.veilixvpn.com |
| Кабинет и оформление заказа | my.veilix.app | dev.my.veilix.app | my.veilixvpn.com | dev.my.veilixvpn.com |
| MiniApp | tg.veilix.app | dev.tg.veilix.app | tg.veilixvpn.com | dev.tg.veilixvpn.com |
1. Админка одна на оба бренда: `plc.veilix.cc` (prod) и `dev.plc.veilix.cc` (dev). Аналитика Umami — `umi.veilix.cc`.
2. Все домены указывают на 169.58.76.183 (этот сервер).
3. Домены хранятся в базе (коллекция «Домены»), у каждого свои языки и валюты. Новое зеркало добавляется записью в базе плюс блоком nginx и сертификатом.
## 3. Состав каждого бренда
1. Сайт: главная, цены, локации, поддержка, юридические страницы.
2. Веб-кабинет, в том числе оформление заказа (оформление живёт на my.*, а не на сайте).
3. MiniApp в Telegram — этап 3.
4. Два Telegram-бота: основной (команды, MiniApp, уведомления) и бот поддержки, подключённый к Chatwoot. Этапы 2–3.
## 4. Термины
1. **Сайт** — бренд (тенант). Почти все сущности привязаны к сайту.
2. **Домен** — хост, на котором открывается сайт, кабинет или MiniApp. У домена свои языки и валюты.
3. **Тариф** — то, что продаётся: цены в валютах, период, лимит устройств, страны.
4. **Страна** — общий справочник. **Локация** — страна на конкретном сайте: город, статус наличия.
5. **Ключ** — купленная единица доступа пользователя со сроком действия и автопродлением.
6. **Подключение** — конкретная конфигурация внутри ключа: WG или AWG-конфиг устройства, ссылка Happ, ссылка Xray.
7. **Заказ, Платёж, Движение по счёту** — см. [04-billing.md](04-billing.md).
8. **Валюта** и **Платёжная система** — справочники; платёжная система доступна в определённых валютах.
## 5. Dedicated (Вейликс)
1. Ключ — это индивидуальный IP в выбранной стране. Страна выбирается при покупке и не меняется никогда. Нужна другая страна — пользователь покупает новый ключ в другой локации и отключает автопродление старого. Отдельного функционала смены страны нет.
2. В каждый ключ входит выделенный RU-адрес: работает и как прямое соединение, и как мост в обе стороны.
3. До 5 устройств на ключ (поле тарифа), все с одним IP. WireGuard — по конфигу на устройство. Xray — ссылка для Happ (обычная или зашифрованная crypt-ссылка Happ, которая скрывает адрес подписки).
4. Тариф — ценовой уровень со своим набором стран:
| Тариф | Страны | Первый месяц | Далее в месяц | USD (из макета) |
|---|---|---|---|---|
| Базовый | 23 страны (см. раздел 7) | 4 900 ₽ | 2 499 ₽ | $49 / $24 |
| Редкие локации | CH, AE, SG, JP, KR | 6 900 ₽ | 3 499 ₽ | $69 / $34 |
| Дальние локации | AU, BR | 9 900 ₽ | 4 999 ₽ | $99 / $49 |
5. Первый месяц дороже, потому что включает подготовку сервера: выделение адреса, настройку протоколов и маршрутизации. Период — месяц.
6. Статусы локаций: «Серверы в наличии» и «Установим за 24 часа». Оплаченный период начинается с момента выдачи ключа. На этапе 1 ключи выдаёт администратор вручную, поэтому так работает для обоих статусов.
7. Каждый ключ продлевается независимо, в свою дату.
## 6. Mass (ВейликсВПН)
1. Ключ включает все страны тарифа. Страна при покупке не выбирается: список стран на сайте только показывает, что входит в ключ.
2. Happ — одна ссылка со всеми странами. WireGuard — пользователь может получить конфиги любой страны тарифа. На этапе 1 это конфиги, которые добавил администратор, по странам; нужна другая страна — через поддержку. Автоматически — на этапе 5.
3. Тарифы: «Месяц» 699 ₽ и «Год» 6 990 ₽ («582 ₽ в месяц, экономия 1 398 ₽»). Платы за подключение и доплат за страну нет: первый период стоит столько же, сколько продление.
4. До 5 устройств на ключ (поле тарифа).
5. Статусы локаций: «Включено» и «Скоро». Локации «Скоро» показываются, но не работают.
6. В кабинете — «Список ключей», как у dedicated; внутри ключа ссылка Xray и конфиги WG. Формулировки «текущая/запасная локация» из макета не используем.
7. Цены в USD для mass в макете нет — вопрос Q-02.
## 7. Локации из макета
Город, статус и ценовой уровень dedicated. На mass те же 30 стран: «готова» = «Включено», «24 часа» = «Скоро».
| Код | Страна | Город | Статус | Уровень |
|---|---|---|---|---|
| NL | Нидерланды | Амстердам | готова | базовый |
| DE | Германия | Франкфурт | готова | базовый |
| US | США | Нью-Йорк | готова | базовый |
| GB | Великобритания | Лондон | готова | базовый |
| FR | Франция | Париж | готова | базовый |
| FI | Финляндия | Хельсинки | готова | базовый |
| SE | Швеция | Стокгольм | готова | базовый |
| NO | Норвегия | Осло | 24 часа | базовый |
| DK | Дания | Копенгаген | 24 часа | базовый |
| CH | Швейцария | Цюрих | готова | редкие |
| AT | Австрия | Вена | готова | базовый |
| ES | Испания | Мадрид | готова | базовый |
| IT | Италия | Милан | готова | базовый |
| PL | Польша | Варшава | готова | базовый |
| CZ | Чехия | Прага | готова | базовый |
| RO | Румыния | Бухарест | 24 часа | базовый |
| LT | Литва | Вильнюс | готова | базовый |
| LV | Латвия | Рига | готова | базовый |
| EE | Эстония | Таллин | 24 часа | базовый |
| TR | Турция | Стамбул | готова | базовый |
| KZ | Казахстан | Алматы | 24 часа | базовый |
| AM | Армения | Ереван | 24 часа | базовый |
| GE | Грузия | Тбилиси | 24 часа | базовый |
| AE | ОАЭ | Дубай | готова | редкие |
| SG | Сингапур | Сингапур | готова | редкие |
| JP | Япония | Токио | готова | редкие |
| KR | Южная Корея | Сеул | 24 часа | редкие |
| CA | Канада | Торонто | готова | базовый |
| AU | Австралия | Сидней | 24 часа | дальние |
| BR | Бразилия | Сан-Паулу | 24 часа | дальние |
Координаты для карты — в массиве `L(...)` в `design/handoff/project/Price.dc.html`. Количество свободных серверов из MiniApp (NL 12, DE 10, …) — демо-данные. Показывать ли счётчик на сайте — решается в этапе 1b.
## 8. Пользователи
1. Аккаунты раздельные по сайтам: один email на veilix.app и на veilixvpn.com — два разных пользователя.
2. Регистрация и вход — одно поле email и одноразовый код. Отдельной регистрации нет.
3. Telegram (этап 3): аккаунт можно создать по Telegram без почты. Почта привязывается кодом, после этого работает вход на сайте. На сайте есть кнопка «Войти через Telegram». Правила объединения аккаунтов в пределах сайта — [05-auth.md](05-auth.md).
## 9. Деньги
1. У пользователя есть счёт (баланс) в валюте аккаунта.
2. Валюты — отдельный справочник: RUB и USD. Переключатель валюты не зависит от языка: из России часто заходят через VPN, поэтому страна по IP для выбора валюты не годится. По умолчанию валюта зависит от языка: ru → RUB, остальные → USD.
3. Платёжные системы привязаны только к валютам. Где система доступна, следует из цепочки: валюты → домены, на которых они включены → сайты:
a. RUB — процессинги, принимающие карты РФ (вероятно, из Казахстана и Турции), без автосписаний. ЮKassa — опционально, только для пополнений;
b. USD — Stripe (карты, сохранение карты, автосписания) и криптовалюта.
4. Карта привязывается к аккаунту, а не к ключу (Stripe). Отвязать карту можно в аккаунте.
5. Продление: у каждого ключа галочка автопродления и кнопка «Продлить сейчас». Сначала списывается баланс, нехватка — с карты.
6. Не хватило денег: льготный период 3 дня (ключ работает), затем ключ истекает. Напоминание за 3 дня до окончания.
7. Ручные операции в админке: платёж через платёжную систему manual (проводится как настоящий), корректировка баланса, возврат на баланс.
8. Возвраты (политика из макета): если сервер не заработал по нашей вине — полностью в течение первых 7 дней; если пользователь передумал — остаток неиспользованного периода.
9. Подробно — [04-billing.md](04-billing.md).
## 10. Реферальная программа
1. «Приведите друга — вы оба получите по месяцу в подарок».
2. «Месяц» начисляется деньгами на баланс обоим после первой оплаты заказа другом. Сумма задаётся в админке для каждого сайта и валюты; в макете — 2 499 ₽ (dedicated) и 699 ₽ (mass).
3. Бонус попадает на баланс и поэтому уходит в ближайшее списание.
4. Ссылки: `https://my.veilix.app/r/<код>` (для mass — `my.veilixvpn.com`) и `t.me/<основной бот>?start=<код>`.
5. Приглашения между брендами не засчитываются, самоприглашения отсекаются.
6. В кабинете показываем: приглашено, начислено, ожидает оплаты; список приглашённых с замаскированной почтой.
## 11. Поддержка и заявки
1. Страница поддержки: Telegram (бот поддержки бренда) и email, FAQ по разделам, форма связи.
2. Заявка «Нужны индивидуальные условия» (dedicated) и «Нужны особые условия» (mass): имя, email, Telegram, WhatsApp, коротко о задаче.
3. Заявки и обращения сохраняются в Payload и приходят на почту поддержки (этап 1), затем уходят в Twenty CRM и Chatwoot (этап 2).
4. Часы поддержки из макета: с 10 до 20 в рабочие дни (часовой пояс — Q-09).
## 12. Языки
en (по умолчанию, без префикса в URL), ru, de, fr, es. Позже — pt-br и ja. Подробно — [06-i18n.md](06-i18n.md).
## 13. Юридические страницы
Условия использования, политика конфиденциальности, политика возвратов, политика cookies. Черновики на RU и EN готовятся в этапе 1b, с заглушками вместо данных юрлица (Q-13). Это черновики, не юридическая консультация: перед запуском — показать юристу. Stripe при подключении проверяет эти страницы.
+116
View File
@@ -0,0 +1,116 @@
# 02. Архитектура
## 1. Стек
| Что | Версия на 20.10.2026 | Комментарий |
|---|---|---|
| Node.js | 24 LTS (на сервере 24.22) | |
| pnpm | 12.x | ставится root через `corepack enable pnpm`; версия фиксируется полем `packageManager` |
| Next.js | 16.4.x | App Router, React Server Components |
| React | 19.x | |
| Payload CMS | 3.91.x | Payload 4 в бете; переход после стабильного релиза ([D-002](decisions/D-002-payload-version.md)) |
| PostgreSQL | 18 (на хосте) | адаптер `@payloadcms/db-postgres`, `idType: 'uuid'` |
| Tailwind CSS | 4.x | токены из макета как CSS-переменные |
| next-intl | 4.x | переводы интерфейса, префиксы языков в URL |
| Nodemailer | через `@payloadcms/email-nodemailer` | транспорты SMTP, sendmail, «только журнал» |
| ALTCHA | 3.x | капча без третьих сторон |
| grammY | 1.x | Telegram-боты, этап 3 |
| Stripe SDK | актуальный | этап 4 |
Точные версии фиксируются в `package.json` на этапе 1a. Если к старту этапа 1a выйдет стабильный Payload 4, начинаем сразу на нём.
## 2. Одно приложение на все домены
1. Одно Next.js-приложение, Payload установлен внутрь как библиотека: админка, API и фронтенды — один процесс ([D-001](decisions/D-001-single-app.md)).
2. Proxy Next.js (`src/proxy.ts`, бывший middleware) по заголовку Host находит запись в коллекции «Домены» и узнаёт сайт (`dedicated` или `mass`) и тип фронтенда: сайт, кабинет или MiniApp. Список доменов кэшируется в памяти и сбрасывается при изменении записи.
3. Proxy переписывает запрос во внутренний маршрут вида `/{тип}/{сайт}/{язык}/…`, для маркетинговых страниц ещё и `/{валюта}/`. Снаружи эти пути недоступны: прямой запрос к ним даёт 404.
4. Хост админки (`plc.veilix.cc`, `dev.plc.veilix.cc`) задаётся в `.env`. Только на нём работают `/admin` и REST API Payload. На доменах брендов эти пути закрыты.
5. Сайтам, кабинетам и MiniApp хост админки не нужен:
a. данные Payload они читают в том же процессе через Local API — это вызов функции, а не HTTP-запрос;
b. запросы браузера (формы, вход, действия в кабинете) идут на тот домен, где открыта страница: server actions и эндпоинты `/svc/…`;
c. вебхуки платёжных систем и Telegram приходят на публичные домены, не на хост админки;
d. файлы медиатеки отдаются через домены сайтов.
6. Ядро общее (биллинг, ключи, вход, рефералы, почта), UI-кит общий, а страницы у каждого бренда свои: данные в дизайнах представлены по-разному.
## 3. Структура кода (план)
```
src/
proxy.ts — домен → сайт, тип фронтенда, язык, валюта
payload.config.ts
app/
(payload)/ — админка и REST API Payload (стандартные файлы)
(front)/m/[site]/[locale]/[currency]/… — маркетинговые сайты
(front)/c/[site]/[locale]/… — кабинеты и оформление заказа
(front)/t/[site]/… — MiniApp (этап 3)
svc/… — свои эндпоинты: ALTCHA, вебхуки, health
payload/
collections/ globals/ hooks/ access/ fields/
core/ — бизнес-логика без привязки к UI
billing/ keys/ auth/ referral/ email/ payments/ currency/ geo/
brands/
dedicated/ mass/ — страницы и компоненты каждого бренда
ui/ — общие компоненты: кнопки, поля, шиты, переключатели, флаги, карта
i18n/ — конфиг next-intl и messages/{en,ru,de,fr,es}.json
styles/ — Tailwind, токены брендов, темы
```
Бизнес-логика живёт в `core/` и вызывается одинаково из хуков Payload, server actions, вебхуков, фоновых задач и (на этапе 3) бота.
## 4. Окружения
| | dev | prod |
|---|---|---|
| Пользователь ОС | vx_dev (под ним работают редактор и сессии Claude) | vx_prod (без редактора и ИИ-агентов) |
| Папка | `/home/vx_dev/veilix_dev` — рабочая копия git | `/home/vx_prod/veilix` — `releases/`, симлинк `current`, `shared/` |
| Запуск | `pnpm dev` (горячая перезагрузка) | `pnpm start` из `current` после `pnpm build` |
| Порт | 3100 | 3000 |
| База | `veilix_dev` | `veilix_prod` |
| Схема базы | синхронизируется автоматически (push) | миграции Payload, применяются при выкладке |
| Домены | `dev.*`, `dev.plc.veilix.cc` | основные, `plc.veilix.cc` |
| Секреты | `/home/vx_dev/veilix_dev/.env` | `/home/vx_prod/veilix/shared/.env` |
| Файлы (media) | `/home/vx_dev/veilix_dev/media` | `/home/vx_prod/veilix/shared/media` |
1. Без Docker: оба окружения — обычные процессы Node, каждый под своим пользователем ОС, запускаются одинаково — systemd user-юнитами: `veilix-dev.service` у vx_dev, `veilix.service` у vx_prod. У vx_dev нет доступа к файлам, секретам, логам и базе prod. Подробно — [D-003](decisions/D-003-hosting.md), [D-004](decisions/D-004-environments.md).
2. Dev закрыт от посторонних в nginx и запрещён к индексации, см. [09-infrastructure.md](09-infrastructure.md).
3. Платёжные системы в dev — только в тестовом режиме.
## 5. Выкладка в prod
1. В dev: изменения схемы оформляются миграцией (`payload migrate:create`), коммит, push в Gitea, тег релиза.
2. Выкладка — команда `deploy <тег>` пользователя vx_prod, появится на этапе 1h. Запускает её пользователь; по желанию — sudo-правило, разрешающее vx_dev запускать только эту команду (Q-18). Шаги:
a. взять тег из Gitea (у vx_prod ключ только на чтение) в новую папку `releases/<дата-тег>`;
b. `pnpm install --frozen-lockfile` и `pnpm build`; ссылки на `shared/.env` и `shared/media`;
c. применить миграции к `veilix_prod`;
d. переключить симлинк `current` на новый релиз и перезапустить `veilix.service`;
e. проверить `/svc/health`; хранить 5 последних релизов.
3. Откат: переключить `current` на прошлый релиз и перезапустить. Миграции пишем так, чтобы прошлый релиз продолжал работать с новой схемой (сначала добавляем, удаляем позже).
4. Простой при выкладке — только время перезапуска процесса, несколько секунд.
## 6. Рендеринг и кэширование
1. Маркетинговые страницы собираются статически во всех вариантах «язык × валюта» и кэшируются. Proxy выбирает вариант по префиксу языка и cookie валюты.
2. Изменение в админке (цена, статус локации, FAQ) вызывает хук Payload, и Next.js пересобирает затронутые страницы за секунды (on-demand revalidation по тегам). Запасной вариант — плановое обновление раз в 5 минут.
3. При оформлении заказа наличие и цена проверяются заново на сервере. Устаревшая страница не может продать то, чего нет.
4. Кабинет рендерится на сервере (SSR) для каждого запроса, без кэша.
5. Маркетинговые домены не ставят cookies, кроме выбора языка, валюты и темы.
## 7. Фоновые задачи
Встроенная очередь Payload (Jobs Queue) с расписанием cron. Исполнитель работает внутри процесса приложения (`autoRun`), в dev его можно отключить переменной окружения.
| Задача | Расписание | Что делает |
|---|---|---|
| `renewals` | каждые 15 минут | продление ключей, у которых срок подходит (см. [04-billing.md](04-billing.md)) |
| `reminders` | каждый час | напоминания за 3 дня до окончания |
| `expiry` | каждые 15 минут | перевод в льготный период и в «истёк» |
| `send-email` | по событию | отправка письма с повторами при ошибке |
| `cleanup` | раз в сутки | старые коды входа, сессии, записи журнала писем |
| `reconcile` | раз в сутки | сверка кэша баланса с суммой движений по счёту |
## 8. Чего не используем
1. Docker — не нужен ни в dev, ни в prod ([D-003](decisions/D-003-hosting.md)).
2. n8n — боты пишем в коде на grammY ([D-015](decisions/D-015-telegram-bots.md)).
3. listmonk — шаблоны писем в Payload, отправка своим транспортом ([D-014](decisions/D-014-email.md)).
4. Redis — стоит на сервере, но пока не нужен: лимиты запросов и очередь задач работают на PostgreSQL.
+374
View File
@@ -0,0 +1,374 @@
# 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`).
## 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 : "пригласил"
CUSTOMERS ||--o{ CUSTOMER_SESSIONS : "сессии"
```
## 3. Настройки и справочники
### 3.1 `sites` — Сайты (тенанты)
| Поле | Тип | Описание |
|---|---|---|
| code | select `dedicated` \| `mass`, уникальное | бренд: по нему выбираются тема и страницы |
| name | text (L) | «Вейликс» / «Veilix» |
| orderPrefix | text | префикс номеров: `VX`, `VV` |
| supportEmail | email | почта поддержки (Q-08) |
| emailFrom | group: name, address | отправитель писем, напр. «Вейликс» <no-reply@veilix.app> |
| supportBot, mainBot | text | имена ботов (этап 3) |
| referralEnabled | checkbox | |
| referralBonus | array: currency, amount | бонус «месяц в подарок» в каждой валюте |
| topupPresets | array: currency, amounts[] | суммы пополнения в кабинете |
| graceDays | number, по умолчанию 3 | льготный период |
| reminderDays | number, по умолчанию 3 | за сколько дней напоминать |
| renewalLeadHours | number, по умолчанию 24 | за сколько часов до окончания пробовать продлить |
| expiringBadgeDays | number, по умолчанию 7 | когда показывать «Истекает» |
| seo | group: titleTemplate (L), description (L), ogImage | значения по умолчанию |
### 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` |
| 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 мин`.
| Поле | Тип | Описание |
|---|---|---|
| name | text | |
| roles | multi-select `owner` \| `admin` \| `support` \| `content` | см. раздел 8 |
| sites | доступ к тенантам | owner и admin видят все сайты |
### 4.2 `customers` — Пользователи (сайт)
Коллекция авторизации Payload со своей стратегией (вход по коду, [05-auth.md](05-auth.md)).
| Поле | Тип | Описание |
|---|---|---|
| email | email, null | уникален в пределах сайта, хранится в нижнем регистре |
| emailVerifiedAt | date | |
| telegramId | text, null | уникален в пределах сайта (этап 3) |
| telegramUsername, telegramName | text | этап 3 |
| 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-адресу (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](04-billing.md).
## 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` \| `referral_bonus` \| `adjustment` \| `payout` | `payout` — возврат денег с баланса на карту (этап 4) |
| payment, order, key, referral | relationship | источник записи |
| balanceAfter | number | баланс после записи |
| comment | text | обязателен для `adjustment` |
| createdBy | relationship → admins, null | null — система |
Уникальность: (`payment`, `topup`), (`order`, `order_charge`), (`referral`, `customer`, `referral_bonus`). Это защищает от двойного проведения.
### 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`, `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 | пользователь (через приложение) |
|---|---|---|---|---|
| Настройки и справочники | всё | чтение | чтение | — |
| pages, faq, email-templates, локализация | всё | чтение | всё | — |
| customers | всё | чтение, правка заметок, блокировка | — | только свой профиль |
| keys, connections | всё | всё | — | только свои, чтение |
| orders, payments | всё | чтение, ручной платёж | — | только свои, чтение |
| ledger-entries | создание корректировок, чтение | чтение | — | только свои, чтение |
| leads | всё | всё | — | — |
| email-log, audit-log | чтение | чтение без `debugCode` | — | — |
Пользователь никогда не ходит в REST Payload напрямую: кабинет читает данные на сервере через Local API с `overrideAccess: false` и `user`.
+188
View File
@@ -0,0 +1,188 @@
# 04. Биллинг
## 1. Принципы
1. Всё через счёт. Любой успешный платёж зачисляется на баланс (движение `topup`). Любой заказ оплачивается списанием с баланса (`order_charge`). Поэтому оплата картой, оплата с баланса, доплата разницы и пополнение заранее работают одним механизмом.
2. Баланс — это сумма движений по счёту. Поле `customers.balance` — кэш, он обновляется в той же транзакции базы, что и запись движения. Раз в сутки задача `reconcile` сверяет кэш с суммой.
3. Движения неизменяемы. Ошибка исправляется встречной записью.
4. Одна точка проведения платежа — `core/billing/processPayment(paymentId)`. Её вызывают вебхук провайдера, ручной платёж из админки и тесты. Повторный вызов ничего не делает.
5. Суммы — в минимальных единицах валюты аккаунта.
## 2. Валюта аккаунта
1. При регистрации валюта аккаунта = валюта, выбранная в переключателе (или валюта по умолчанию домена).
2. Сменить валюту в кабинете можно, пока баланс нулевой и нет ключей и неоплаченных заказов. Дальше — через поддержку: администратор пересчитывает вручную корректировками.
3. Цены в кабинете всегда в валюте аккаунта. Тариф без цены в этой валюте недоступен.
## 3. Покупка ключа (оформление заказа на my.*)
1. Ссылка «Купить» с сайта ведёт на `my.<домен>/{язык}/order?tariff=…&location=…&currency=…`.
2. Шаг «Почта»: email и код ([05-auth.md](05-auth.md)). Вошедший пользователь шаг пропускает.
3. Сервер проверяет: тариф активен, есть цена в валюте аккаунта, локация доступна (dedicated: `available` или `on_demand`).
4. Создаётся заказ `pending`: `new_key`, сумма — `firstPeriodAmount`, действует 24 часа. Если у пользователя уже есть такой же неоплаченный заказ, используется он.
5. Шаг «Оплата»: сводка заказа, согласие с условиями (обязательная галочка со ссылками на юридические страницы) и способ оплаты.
a. Баланса хватает — кнопка «Оплатить с баланса», заказ оплачивается сразу.
b. Баланса не хватает — список платёжных систем: активные, в валюте аккаунта, с назначением `order`, видимые пользователю. Создаётся платёж `pending` на сумму «стоимость минус баланс» (показываем: «С баланса спишется X, к оплате Y»), дальше — страница или виджет провайдера (этап 4).
6. На этапе 1 видимых платёжных систем нет. Заказ создаётся, пользователь видит «Заказ создан, ожидает оплаты». Оплату проводит администратор ручным платежом, либо пользователь оплачивает с баланса, который пополнил администратор. Так проверяется весь сценарий без эквайринга.
7. После оплаты: заказ `paid`, ключ создаётся в статусе `provisioning`, письма `order_paid` пользователю и `admin_provisioning_needed` на почту поддержки.
## 4. Пополнение баланса
1. Платёж с назначением `topup`. Суммы — пресеты сайта для валюты (`sites.topupPresets`) или своя сумма в пределах лимитов платёжной системы.
2. После зачисления — письмо `topup_succeeded`.
3. После любого пополнения система сама оплачивает с баланса неоплаченные заказы на продление этого пользователя, начиная со старого. Неоплаченные заказы на новые ключи сама не оплачивает.
4. Криптовалюта (этап 4): счёт в монете, курс фиксируется на 15 минут, зачисление в валюте аккаунта после подтверждения сети.
## 5. Проведение платежа: `processPayment`
Выполняется, когда платёж переходит в `succeeded`, в одной транзакции:
1. Заблокировать строку платежа. Если `processedAt` заполнен — выйти.
2. Заблокировать строку пользователя.
3. Движение `+amount` (`topup`).
4. Если назначение `order` и заказ `pending`: баланса хватает — движение `−order.amount` (`order_charge`), заказ `paid`. Не хватает — заказ остаётся `pending`, деньги остаются на балансе.
5. Заказ оплачен — исполнить: `new_key` создаёт ключ `provisioning` с копией цены продления, периода и лимита устройств; `renewal` продлевает ключ (раздел 6).
6. Проставить `processedAt`.
7. Проверить реферальное вознаграждение (раздел 11).
8. Поставить в очередь письма, записать журнал действий.
Если заказ к моменту оплаты отменён или истёк, деньги остаются на балансе, пользователь получает письмо о зачислении.
Оплата заказа с баланса без внешнего платежа — те же шаги 2, 4, 5, 7, 8.
## 6. Продление
### 6.1 Автоматическое (задача `renewals`, каждые 15 минут)
Берём ключи в статусе `active` или `grace` с `autoRenew = true`, у которых `expiresAt ≤ сейчас + renewalLeadHours` и нет оплаченного заказа на продление с `renewalFor = expiresAt`. Для каждого, с блокировкой ключа:
1. Найти или создать заказ `renewal`: `renewalFor = expiresAt`, сумма — `renewalAmount` ключа.
2. Баланса хватает — списание, заказ `paid`, `expiresAt = renewalFor + period`, статус `active`, письмо `renewal_succeeded`.
3. Не хватает, но есть сохранённая карта с автосписанием (Stripe, этап 4) — платёж на разницу без участия пользователя. Неудача — повтор: за 24 часа до окончания, в момент окончания, через 24 и 48 часов (внутри льготного периода). После первой неудачи — письмо `renewal_failed`.
4. Нет ни денег, ни карты — заказ ждёт оплаты. Его оплатит пополнение (раздел 4.3) или кнопка «Продлить сейчас».
### 6.2 «Продлить сейчас»
Доступна для ключей `active` и `grace`. Создаёт (или берёт существующий) заказ `renewal` на `renewalFor = expiresAt` и оплачивает его с баланса или через платёжную систему. Ключ продлевается на период от текущего `expiresAt`. Можно продлить заранее несколько раз подряд.
### 6.3 Автопродление и карта
1. Галочка автопродления — у каждого ключа.
2. «Отвязать карту» — в аккаунте, раздел «Счёт и платежи». После отвязки автопродление списывает только баланс.
## 7. Льготный период и окончание (задача `expiry`, каждые 15 минут)
1. `active`, срок истёк, продление не оплачено → `grace`, `graceUntil = expiresAt + graceDays`, письмо `grace_started`. Ключ работает.
2. Продление оплачено в льготный период → `active`, новый срок считается от старого `expiresAt`: бесплатных дней нет.
3. `grace`, `graceUntil` наступил → `expired`, неоплаченный заказ на продление → `expired`, письмо `key_expired`. На этапе 1 ключ попадает в список «К отключению» в админке; отключение автоматическое — этап 5.
4. Правило одинаковое для ключей с автопродлением и без. Без автопродления пользователь сам решает, продлевать ли в льготный период.
5. Истёкший ключ не продлевается: покупается новый. Администратор может вернуть ключ вручную.
## 8. Выдача ключа
1. Админка, ключ в статусе `provisioning`: администратор добавляет подключения (WG/AWG по устройствам, ссылку Happ, RU-адрес), IP-адреса и нажимает «Выдать ключ».
2. Нужно хотя бы одно активное подключение. Затем: `active`, `activatedAt = сейчас`, `expiresAt = сейчас + период`, письмо `key_ready`.
3. Список «Очередь выдачи» в админке: ключи `provisioning` по времени создания; локации `on_demand` помечены сроком «24 часа».
## 9. Ручные операции в админке
| Операция | Что происходит |
|---|---|
| Ручной платёж | Платёж через платёжную систему `manual`: пользователь, назначение, заказ (необязательно), сумма в валюте аккаунта, статус, комментарий. Когда статус становится `succeeded`, срабатывает `processPayment` — как у настоящего платежа. Можно завести `pending` (ждём перевод) и позже перевести в `succeeded`. Галочки «провести как реальную» нет: проводится всегда |
| Корректировка баланса | Движение `adjustment` ±сумма, комментарий обязателен. Цепочка действий не запускается |
| Возврат на баланс | На заказе: сумма (не больше оплаченной минус возвращённое), галочка «Отменить ключ». Движение `refund`, заказ `refunded` или `partially_refunded`, ключ `cancelled`, если отмечено. Письмо `refund_issued` |
| Выдать ключ | `provisioning` → `active` (раздел 8) |
| Продлить вручную | Продлить ключ на N дней без оплаты (компенсация) |
| Заблокировать | Пользователь `blocked`: войти нельзя. Ключ `suspended` |
Все операции пишутся в журнал действий.
## 10. Возвраты
1. Политика: если сервер не заработал по нашей вине — полностью в течение первых 7 дней; если пользователь передумал — остаток неиспользованного периода. Сумму считает администратор, система подсказывает остаток пропорционально дням.
2. Этап 1 — только возврат на баланс.
3. Этап 4 — возврат на исходный способ оплаты, если провайдер умеет. Запись `refund` (+) на баланс по заказу, затем `payout` (−) — деньги уходят на карту. Итог: баланс не меняется, деньги у пользователя на карте.
## 11. Реферальное вознаграждение
1. Привязка: при регистрации, если есть cookie `vx_ref` (ставит `my.<домен>/r/<код>`, живёт 60 дней) или параметр `/start <код>` в боте. Код того же сайта, не свой. Создаётся приглашение `pending`.
2. Условие: оплачен первый заказ `new_key` приглашённого. Пополнение баланса условием не считается.
3. Проверка: совпадает хэш IP регистрации приглашённого и пригласившего → `review`, решает администратор. Иначе — начисление.
4. Начисление: движение `referral_bonus` обоим, в валюте каждого, сумма из `sites.referralBonus`. Нет суммы для валюты — бонус не начисляется, в журнале предупреждение. Приглашение `rewarded`, письма `referral_reward`.
## 12. Уведомления
| Когда | Шаблон | Условие |
|---|---|---|
| Заказ оплачен | `order_paid` | всегда |
| Ключ ждёт подготовки (24 часа) | `key_provisioning` | локация `on_demand` |
| Ключ выдан | `key_ready` | |
| За `reminderDays` до окончания | `renewal_reminder` | автопродление включено, но баланса и карты не хватит |
| За `reminderDays` до окончания | `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` | на почту поддержки сайта |
С этапа 3 те же уведомления дублируются в основного бота, если Telegram привязан.
## 13. Статусы
### 13.1 Ключ
```mermaid
stateDiagram-v2
[*] --> provisioning: заказ оплачен
provisioning --> active: выдан
active --> active: продление оплачено
active --> grace: срок истёк, оплаты нет
grace --> active: продление оплачено
grace --> expired: льготный период закончился
provisioning --> cancelled: возврат
active --> cancelled: возврат с отменой
active --> suspended: блокировка
suspended --> active: разблокировка
```
### 13.2 Заказ
```mermaid
stateDiagram-v2
[*] --> pending
pending --> paid
pending --> cancelled: отменён
pending --> expired: истёк срок заказа или ключ истёк
paid --> partially_refunded
paid --> refunded
partially_refunded --> refunded
```
### 13.3 Платёж
```mermaid
stateDiagram-v2
[*] --> pending
pending --> succeeded
pending --> failed
pending --> cancelled
succeeded --> partially_refunded
succeeded --> refunded
```
## 14. Конкурентность и идемпотентность
1. Все денежные операции — в транзакции базы. Баланс меняется только под блокировкой строки пользователя (`SELECT … FOR UPDATE`).
2. Уникальные ограничения защищают от двойного проведения: движения (см. [03-data-model.md](03-data-model.md), 6.3), платежи (`adapter` + `providerPaymentId`), заказы (`key` + `renewalFor`).
3. Вебхуки: проверка подписи, сохранение id события, идемпотентная обработка, быстрый ответ 200.
4. Задачи берут ключи с `FOR UPDATE SKIP LOCKED`, чтобы параллельные запуски не продлили ключ дважды.
## 15. Как показываем в кабинете
1. История платежей — одна строка на событие для человека. Платёж картой за заказ (зачисление и списание) показывается одной строкой: «Первый месяц · Нидерланды · Амстердам · Карта ···· 4242 · −4 900 ₽». Списание с баланса — «С баланса». Пополнение, реферальный бонус, возврат, корректировка — отдельными строками.
2. «Следующее списание» — ближайшее продление среди ключей с автопродлением: сумма, дата, ключ; ниже — следующие.
3. Бейдж «Истекает» — до окончания не больше `expiringBadgeDays` и (автопродление выключено или не хватит ни баланса, ни карты).
4. На странице ключа: «Продлится автоматически через N дней», если денег или карты хватает, иначе «Осталось N дней» (предупреждающий цвет).
+103
View File
@@ -0,0 +1,103 @@
# 05. Вход, сессии, аккаунты
## 1. Аккаунты
1. Аккаунты раздельные по сайтам ([D-011](decisions/D-011-accounts.md)). Уникальность: (`site`, `email`) и (`site`, `telegramId`).
2. Один человек на двух сайтах — два разных пользователя с разными балансами и ключами.
3. Администраторы — отдельная коллекция `admins` с паролем, к пользователям отношения не имеют.
## 2. Вход по коду из письма
Одно поле email: если аккаунта нет — он создаётся, если есть — выполняется вход. Ответ всегда одинаковый, по нему нельзя узнать, есть ли аккаунт.
### 2.1 Запрос кода
1. Пользователь вводит email и проходит ALTCHA.
2. Сервер нормализует email (обрезает пробелы, переводит в нижний регистр) и проверяет лимиты:
a. по email — не чаще раза в 60 секунд и не больше 5 кодов в час;
b. по IP — не больше 20 запросов в час.
3. Прошлые неиспользованные коды для этого email и сайта аннулируются.
4. Создаётся код: 6 цифр, действует 10 минут. В базе хранится только HMAC-SHA256 (секрет из `.env`, сайт, email, код).
5. Письмо `login_code` на языке текущей страницы. Если в настройках почты включено «Записывать коды входа в журнал», код пишется в журнал писем (раздел 4).
6. Ответ: «Код отправлен на {email}. Он действует 10 минут». Кнопка «Отправить ещё раз» — через 60 секунд, «Другая почта» — назад.
### 2.2 Проверка кода
1. Берётся последний действующий код для email и сайта. Счётчик попыток +1; после 5 неудачных код аннулируется.
2. Сравнение — за постоянное время.
3. Код верный:
a. найти пользователя (сайт, email) или создать: `registeredVia = email`, язык — текущий, валюта — выбранная (Q-04), реферальная привязка из cookie `vx_ref`, хэш IP регистрации;
b. пользователь `blocked` — отказ: «Аккаунт заблокирован, напишите в поддержку»;
c. проставить `emailVerifiedAt` и `lastLoginAt`, создать сессию, вернуть на страницу, с которой пришёл (кабинет или оформление заказа).
## 3. Сессии
1. Cookie `vx_session`: случайные 32 байта, только для хоста кабинета (`my.veilix.app` и т.д., без атрибута Domain), `HttpOnly`, `Secure`, `SameSite=Lax`.
2. В базе (`customer-sessions`) — только SHA-256 токена. Срок — 30 дней, продлевается при активности (не чаще раза в сутки).
3. «Выйти» отзывает текущую сессию, «Выйти на всех устройствах» — все.
4. Payload: коллекция `customers` — коллекция авторизации с `disableLocalStrategy: true` и своей стратегией. Стратегия читает cookie, находит сессию и возвращает пользователя. Поэтому правила доступа Payload работают и для пользователей.
5. Защита от CSRF: `SameSite=Lax`; server actions Next.js сверяют Origin и Host; свои POST-эндпоинты проверяют Origin.
6. Cookie хостовые, поэтому dev и prod не пересекаются: `dev.my.veilix.app` и `my.veilix.app` не видят cookie друг друга.
## 4. Коды в журнале для отладки
1. Глобальная настройка «Почта» → «Записывать коды входа в журнал».
2. Включено: в записи журнала писем видно поле `debugCode` с кодом открытым текстом. В dev-логе процесса код тоже печатается.
3. Видят только роли owner и admin. Пока настройка включена, в админке висит предупреждение. Записи с кодом удаляются через сутки.
4. Вместе с транспортом «только журнал» (письма не отправляются, только пишутся в журнал) это позволяет тестировать вход без настроенной почты.
5. В prod по умолчанию выключено: с кодами из журнала можно войти в любой аккаунт.
## 5. Оформление заказа
Шаг «Почта» на странице заказа — тот же вход по коду. После подтверждения пользователь остаётся на шаге «Оплата».
## 6. Telegram (этап 3)
### 6.1 Вход из MiniApp
1. MiniApp передаёт серверу `initData` от Telegram.
2. Сервер проверяет подпись: HMAC-SHA256 с ключом `HMAC_SHA256("WebAppData", токен основного бота)`, `auth_date` не старше суток.
3. Ищем пользователя (сайт, telegramId). Нет — создаём: `registeredVia = telegram`, язык — из `language_code` Telegram (если он среди включённых), валюта — по умолчанию домена.
4. Сессия `kind = miniapp`. Как её хранить (cookie внутри WebView Telegram или токен в памяти) — решаем на этапе 3.
### 6.2 Привязка почты из Telegram
1. Пользователь вводит email в боте или MiniApp и получает код с назначением `link_email`.
2. Код верный — три случая:
a. на этом сайте нет пользователя с такой почтой — почта привязывается, теперь работает вход на сайте по коду;
b. есть пользователь с этой почтой без Telegram — аккаунты объединяются (раздел 6.4);
c. у пользователя с этой почтой привязан другой Telegram — автоматически не объединяем: «Эта почта привязана к другому Telegram-аккаунту, напишите в поддержку».
### 6.3 Привязка Telegram с сайта и вход через Telegram
1. «Подключить Telegram» в кабинете создаёт одноразовый токен (10 минут) и открывает `t.me/<основной бот>?start=link_<токен>`. Бот проверяет токен и привязывает Telegram к аккаунту. Если этот Telegram уже у другого пользователя сайта — правила раздела 6.4.
2. «Войти через Telegram» на сайте — официальный виджет Telegram Login. Для основного бота в BotFather указывается домен кабинета. Нет пользователя с этим Telegram — создаётся новый.
### 6.4 Объединение аккаунтов (в пределах сайта)
1. Основным остаётся аккаунт с почтой, к нему переходит Telegram второго.
2. Ключи, заказы, платежи, сохранённые карты, заявки переназначаются на основной аккаунт.
3. Баланс переносится парой движений `adjustment` с комментарием «Объединение аккаунтов»: минус на втором, плюс на основном. Сами движения не меняются.
4. Реферальная привязка: если у обоих есть пригласивший — остаётся более ранний. Реферальный код — основного аккаунта.
5. Второй аккаунт блокируется со ссылкой «объединён с …». Всё пишется в журнал действий.
6. Конфликты переводят объединение в ручной режим, его делает администратор кнопкой «Объединить аккаунты»: разные валюты при ненулевых балансах, у обоих привязан Telegram.
## 7. Администраторы
1. Стандартная авторизация Payload: email и пароль, cookie только на хосте админки.
2. Админка открывается только на `plc.veilix.cc` и `dev.plc.veilix.cc`; nginx пускает туда только с разрешённых IP.
3. 5 неудачных попыток — блокировка на 15 минут.
4. Первый администратор каждого окружения создаётся на экране первого входа Payload.
## 8. Cookies
| Cookie | Где | Срок | Зачем |
|---|---|---|---|
| `vx_session` | хост кабинета | 30 дней | сессия пользователя |
| `vx_ref` | хост кабинета | 60 дней | реферальный код |
| `NEXT_LOCALE` | каждый хост | 1 год | выбранный язык |
| `vx_currency` | хост сайта и кабинета | 1 год | выбранная валюта (у вошедшего — валюта аккаунта) |
| `vx_consent` | хост сайта и кабинета | 1 год | согласие на cookies (этап 1h) |
| `payload-token` | хост админки | сессия админки | вход администратора |
Тема (светлая, тёмная, как в системе) хранится в localStorage, как в макете, и применяется скриптом до отрисовки, без мигания.
+79
View File
@@ -0,0 +1,79 @@
# 06. Языки и валюты на сайте
## 1. Языки
1. Сейчас: `en` (по умолчанию), `ru`, `de`, `fr`, `es`.
2. Позже: `pt-br` (бразильский португальский) и `ja`. Для японского понадобится запасной шрифт: Commissioner и Unbounded не содержат японских знаков.
3. Список языков задаётся в конфиге Payload и next-intl (код). У каждого домена в базе — включённые языки и язык по умолчанию.
4. Интерфейс админки — `ru` и `en`.
## 2. Два слоя переводов
| Что | Где | Кто правит |
|---|---|---|
| Интерфейс: кнопки, подписи, ошибки, сообщения форм | `src/i18n/messages/{en,ru,de,fr,es}.json`, next-intl | разработка, через git |
| Контент: тарифы, локации, FAQ, страницы, шаблоны писем, тексты бота | локализованные поля Payload | админка, без деплоя |
1. В коде компонентов нет строк на конкретном языке, только ключи переводов.
2. Нет перевода на языке — показывается английский. Отдельная проверка выводит список непереведённых ключей.
3. Множественные числа — ICU в next-intl: «12 серверов», «3 сервера», «1 сервер».
## 3. URL
1. Английский без префикса, остальные — с префиксом: `veilix.app/pricing`, `veilix.app/ru/pricing`, `veilix.app/de/pricing`.
2. Так же в кабинете: `my.veilix.app/keys`, `my.veilix.app/ru/keys`.
3. MiniApp: адрес в BotFather один, поэтому язык берётся из профиля пользователя или из Telegram; префикс в URL необязателен.
4. Пути одинаковые для всех языков (английские): `/pricing`, `/locations`, `/support`, `/legal/terms`, `/keys`, `/billing`, `/referral`, `/order`, `/login`.
5. Язык, не включённый на домене, — 404.
## 4. Определение языка
1. Порядок: язык из URL → выбранный ранее (cookie `NEXT_LOCALE`, у вошедшего — язык профиля) → язык браузера (`Accept-Language`) среди включённых → страна из заголовка nginx `X-Geo-Country` (MaxMind) → `en`.
2. Перенаправление на версию с префиксом — только при первом заходе на адрес без префикса, без cookie, и только если выбран не английский. Поисковых ботов не перенаправляем никогда: так рекомендует Google.
3. Переключатель языка ставит cookie `NEXT_LOCALE`. У вошедшего пользователя язык сохраняется ещё и в профиле: на нём уходят письма и сообщения бота.
4. Соответствие стран языкам по умолчанию: RU, BY, KZ, KG → ru; DE, AT → de; FR → fr; ES и страны Латинской Америки → es; остальные → en.
## 5. SEO
1. `hreflang` на все включённые языки и `x-default` на английскую версию.
2. `canonical` на основной домен.
3. `sitemap.xml` для каждого маркетингового домена с вариантами всех языков.
4. Кабинет и MiniApp не индексируются (`noindex`).
5. Атрибут `lang` у `<html>` соответствует языку страницы.
## 6. Валюта
1. Переключатель валюты отдельный от языка, стоит рядом с переключателем языка в шапке и подвале. В макете его нет — добавляется в стиле переключателя языка.
2. Валюта не определяется по стране: из России часто заходят через VPN.
3. По умолчанию — валюта домена для языка (`domains.localeCurrency`: ru → RUB), иначе `domains.defaultCurrency` (USD).
4. Выбор хранится в cookie `vx_currency`. Валюта не входит в URL: маркетинговые страницы заранее собраны во всех валютах, proxy выбирает вариант по cookie. Поисковые боты видят валюту по умолчанию.
5. Ссылка «Купить» передаёт валюту в оформление заказа (`?currency=`). У вошедшего пользователя в кабинете всегда валюта аккаунта ([04-billing.md](04-billing.md), раздел 2).
6. Форматирование — `Intl.NumberFormat` с узким символом валюты: «4 900 ₽» в русском, «₽4,900» в английском, «$49». Неразрывные пробелы ставит Intl. Даты — `Intl.DateTimeFormat`: «14 октября 2026».
## 7. Как переводим
1. Источник: русские тексты макетов и английские из словаря макета `design/handoff/project/i18n.js` (около 515 строк). Они ложатся в `ru.json` и `en.json` и в контент Payload.
2. Немецкий, французский, испанский — машинный перевод LLM с глоссарием на этапе 1h. Вычитки носителями нет, поэтому глоссарий и контекст обязательны.
3. В юридических текстах пункт: «При расхождениях между версиями действует английская».
4. Новая строка интерфейса: разработчик добавляет ключ в `en.json` и `ru.json`; de, fr, es переводятся пакетно перед выкладкой.
## 8. Глоссарий
| RU | EN | Примечание |
|---|---|---|
| Вейликс / ВейликсВПН | Veilix / VeilixVPN | не переводится |
| ключ | key | купленная единица доступа |
| локация | location | |
| тариф | plan | |
| подключение | connection | |
| устройство | device | |
| индивидуальный IP | dedicated IP | |
| RU-адрес | RU address | |
| баланс, счёт | balance | |
| пополнить | top up | |
| продление, автопродление | renewal, auto-renewal | |
| льготный период | grace period | |
| реферальная программа | referral program | |
| установим за 24 часа | ready within 24 hours | статус локации dedicated |
Колонки для de, fr, es добавляются на этапе 1h.
+104
View File
@@ -0,0 +1,104 @@
# 07. Фронтенд
## 1. Макеты
1. Источник — [design/handoff/project/](../design/handoff/project/), экспорт Claude Design. Прототипы на HTML: разметка внутри `<x-dc>`, логика в `<script data-dc-script>`, стили — inline плюс CSS-переменные в `<style>`. `support.js` — среда выполнения прототипа, в продукт не переносится.
2. Задача — воспроизвести внешний вид точно, читая код макетов: отступы, размеры, цвета, состояния. Внутреннюю структуру прототипа не копируем.
3. Тексты макетов — на русском, английские версии — в `i18n.js` (словарь «русская строка → английская»).
4. Бриф дизайна — `design/handoff/project/uploads/` (файл «Дизайн VEILIX.md»). Где бриф расходится с прототипами или с [01-product.md](01-product.md), верны прототипы и 01-product.
## 2. Какой макет куда
| Макет | Вейликс | ВейликсВПН |
|---|---|---|
| `Main` / `MassMain` | veilix.app `/` | veilixvpn.com `/` |
| `Price` / `MassPrice` | `/pricing` | `/pricing` |
| `Locations` / `MassLocations` | `/locations` | `/locations` |
| `Support` / `MassSupport` | `/support` | `/support` |
| `Order` / `MassOrder` | my.veilix.app `/order` | my.veilixvpn.com `/order` |
| `App` / `MassApp` | my.veilix.app: `/login`, `/`, `/keys`, `/keys/[id]`, `/buy`, `/billing`, `/referral` | то же на my.veilixvpn.com |
| `AppTG` / `MassAppTG` | tg.veilix.app (этап 3) | tg.veilixvpn.com (этап 3) |
Плюс страницы без макета: `/legal/terms`, `/legal/privacy`, `/legal/refund`, `/legal/cookies`, 404 и 500 — в стиле сайта.
## 3. Маршруты кабинета
| Путь | Что |
|---|---|
| `/login` | вход по коду |
| `/` | главная: ключи, «Купить ключ», FAQ |
| `/keys`, `/keys/[id]` | список ключей, страница ключа: срок, автопродление, «Продлить сейчас», WG-конфиги (скачать, QR), ссылка Happ/Xray (копировать, QR) |
| `/buy` | покупка внутри кабинета: dedicated — сетка локаций с ценами, mass — «Месяц» и «Год» |
| `/order` | оформление заказа: шаги «Почта» и «Оплата», сводка |
| `/billing` | баланс, пополнение, карта, ближайшие списания, история |
| `/referral` | реферальная программа |
| `/r/[код]` | реферальная ссылка: ставит cookie и ведёт на сайт бренда |
Навигация по макету: на широком экране — боковая панель, на узком — плавающая панель вкладок «Главная, Ключи, Купить, Ещё».
## 4. Темы и токены
1. Tailwind v4: токены макета — CSS-переменные, подключённые в `@theme`. Бренд задаёт атрибут `data-site="dedicated|mass"` на `<html>`, тему — `data-theme="light|dark"`. Режим «Как в системе» следует `prefers-color-scheme`.
2. Переключатель темы — 3 режима, как в макете. Выбор хранится в localStorage, тема применяется скриптом в `<head>` до отрисовки.
3. Токены в OKLCH, как в макете. Ниже — для ориентира; точные значения — в `<style>` прототипов, при расхождении верен прототип.
| Токен | Вейликс светлая | Вейликс тёмная | ВейликсВПН светлая | ВейликсВПН тёмная |
|---|---|---|---|---|
| `--bg` | 99% .005 296 (#fcfbff) | 17% .022 296 (#100d18) | 99.6% .0015 215 | 23% .026 210 |
| `--surface` | 100% 0 0 | 21% .026 296 | 100% 0 0 | 26.5% .028 210 |
| `--surface-2` | 97.5% .01 296 | 24% .03 296 | 98.4% .003 215 | 30% .03 210 |
| `--ink` | 24% .04 296 (#211b30) | 96% .01 296 | 24% .04 204 | 96% .012 196 |
| `--ink-2` | 45% .03 296 | 79% .02 296 | 45% .03 204 | 80% .02 196 |
| `--ink-3` | 60% .025 296 | 63% .025 296 | 60% .025 204 | 65% .025 196 |
| `--border` | 92% .014 296 | 31% .032 296 | 92% .014 200 | 36% .04 204 |
| `--accent` | 45% .17 296 (#6137a6) | 74% .15 296 (#b495fe) | 48% .105 178 (#00705e) | 76% .105 190 (#52c6c0) |
| `--accent-hover` | 38% .17 296 | 80% .13 296 | 41% .1 178 | 83% .09 190 |
| `--accent-soft` | 95.5% .028 296 | 27% .05 296 | 95.5% .032 180 | 31% .05 190 |
| `--on-accent` | 99% .01 296 | 17% .03 296 | 99% .01 180 | 20% .04 204 |
| `--glow-1/2/3` | 86% .1 296 / 89% .09 266 / 91% .08 332 | 45% / 40% / 42% | 86% .085 190 / 88% .075 205 / 89% .07 172 | 52% .09 200 / 47% .085 215 / 50% .08 180 |
| `--sel` | — | — | 96.5% .016 190 | 30% .03 195 |
Общие для брендов: `--warn` (62% .14 62 / 78% .13 62), `--tg` (62% .15 250 / 70% .13 250), `--veil` (белый 55% / 7%), `--shadow`, цвета карты `--land`, `--land-line`, `--ocean`.
## 5. Шрифты, иконки, логотип
1. Commissioner 300–600 — весь текст; Unbounded 600 — только надпись логотипа. Подключение через `next/font`: файлы скачиваются при сборке и отдаются с нашего домена, без запросов к Google Fonts. Наборы символов: latin, latin-ext, cyrillic.
2. Иконки, флаги и логотип в макете нарисованы CSS-фигурами. Переносим их React-компонентами, сохраняя вид.
3. Логотип — вариант «Слои». Фавиконы — `favicon.svg` и `favicon-mass.svg` из макета.
## 6. Карта мира
1. В макете карта строится в браузере через d3 и topojson с загрузкой данных с CDN. В продукте SVG-карта рендерится на сервере при сборке: d3-geo, проекция Natural Earth, данные `world-atlas` (countries-110m) из npm.
2. В браузере — только интерактив: подсветка, всплывающая карточка, синхронизация с сеткой локаций.
3. На узком экране карта прокручивается горизонтально, как в макете.
## 7. Решения по расхождениям в макетах
1. Устройств — 5, это поле тарифа. Цена продления dedicated — 2 499 ₽, цены задаются тарифами (3 уровня).
2. Оформление заказа получает тариф, локацию и валюту из ссылки. В макете там всегда Нидерланды и 4 900 ₽.
3. Поля номера карты из макета `Order` не делаем: карта вводится в виджете провайдера (этап 4). На этапе 1 платёжных систем для пользователя нет ([04-billing.md](04-billing.md), раздел 3.6).
4. Mass: в кабинете «Список ключей», без «текущей/запасной локации» и без экрана смены страны. Внутри ключа — ссылка Xray и WG-конфиги по странам.
5. Статус локации показываем везде одинаково: в списках, карточках и на карте.
6. Кнопка «Получить VPN» на главной ведёт к блоку цен на этой же странице, на остальных страницах — на `/pricing`.
7. FAQ — одна коллекция с местами показа: главная, цены, поддержка (3 раздела), кабинет, MiniApp.
8. Контакты поддержки берутся из настроек сайта, Telegram — ссылка на бота поддержки.
9. Плашки способов оплаты в подвале — по активным платёжным системам сайта.
10. Добавляем то, чего нет в макете: переключатель валюты, `<title>` и мета-описания, H1 на каждой странице, поддержку `prefers-reduced-motion` для анимаций.
11. Показывать ли на сайте количество свободных серверов (в MiniApp макет его показывает) — решаем на этапе 1b. По умолчанию — только статус.
## 8. Состояния, которых нет в макетах
Рисуем в стиле макета на соответствующих этапах:
1. Вход: ошибка кода, истёкший код, таймер повторной отправки, загрузка.
2. Ключ: готовится (до 24 часов), льготный период, истёк, заблокирован; пустой список ключей; ключ без подключений.
3. Заказ: «ожидает оплаты» (этап 1), не хватает баланса, оплата не прошла; 3-D Secure и счёт в криптовалюте — этап 4.
4. Счёт: своя сумма пополнения, подтверждение отвязки карты, результат пополнения.
5. Общие: уведомления (тосты), скелетоны загрузки, страницы 404 и 500, ошибки валидации форм.
## 9. Производительность и доступность
1. Маркетинговые страницы: Lighthouse mobile ≥ 90 (цель 95+), LCP ≤ 2 с, CLS ≤ 0.05, INP ≤ 200 мс.
2. JavaScript в браузере — только интерактивные островки: переключатели языка, валюты и темы, FAQ, выбор локации, формы, карта.
3. Сторонние скрипты при первой загрузке — только Umami (асинхронно). ALTCHA подгружается только на формах.
4. Доступность — WCAG 2.1 AA: контраст, видимый фокус, работа с клавиатуры для выпадающих списков, аккордеонов и шитов, области нажатия ≥ 44 px.
+82
View File
@@ -0,0 +1,82 @@
# 08. Интеграции
## 1. Почта (этап 1c)
1. Шаблоны — в Payload (`email-templates`): тема и текст на каждом языке, для каждого сайта свои. Шапка (логотип) и подвал (контакты, юридические ссылки) — в коде, у каждого бренда свои.
2. Отправка — через email-адаптер Payload на Nodemailer. Транспорт переключается в админке (глобал «Почта»):
a. `smtp` — внешний SMTP-провайдер (данные в настройках, пароль зашифрован);
b. `sendmail` — локальный Postfix (`/usr/sbin/sendmail`);
c. `log_only` — не отправлять, только записать в журнал (для разработки).
3. Письма уходят через фоновую задачу `send-email` с повторами при ошибке. Каждое письмо пишется в журнал писем; коды входа — по флагу ([05-auth.md](05-auth.md), раздел 4).
4. Кнопка «Отправить тестовое письмо» проверяет текущий транспорт.
5. Чтобы письма через Postfix не попадали в спам (root, [09-infrastructure.md](09-infrastructure.md), раздел 6):
a. Postfix отправляет с IP 169.58.76.183 под именем `fr-ub2gs3.veilix.cc` — у этого IP прямая и обратная DNS-записи совпадают;
b. для veilix.app и veilixvpn.com нужны SPF, DKIM (OpenDKIM уже установлен) и DMARC;
c. проверить, что исходящий порт 25 открыт и IP нет в чёрных списках.
6. Новые IP почтовые сервисы поначалу принимают настороженно, поэтому SMTP-провайдер держим запасным вариантом.
## 2. ALTCHA (этап 1c)
1. Капча на proof-of-work без сторонних сервисов: браузер решает небольшую вычислительную задачу, сервер проверяет подпись.
2. Эндпоинт выдачи задачи — `/svc/altcha/challenge`, ключ подписи — в `.env`.
3. Где: запрос кода входа, формы заявок и обратной связи.
4. Вместе с лимитами по IP и email ([05-auth.md](05-auth.md)). Провайдер капчи изолирован в `core/`, его можно заменить.
## 3. MaxMind (этап 1a)
1. Модуль nginx `geoip2` (уже установлен) определяет страну по базе GeoLite2-Country и передаёт её приложению заголовком `X-Geo-Country`. Клиентский заголовок с таким именем nginx перезаписывает.
2. База обновляется `geoipupdate` по таймеру (root, нужен бесплатный аккаунт MaxMind). Модуль nginx перечитывает базу сам (`auto_reload`).
3. Страна используется только для выбора языка по умолчанию, не для валюты.
4. Если понадобится страна произвольного IP (антифрод), приложение читает тот же `.mmdb` библиотекой `maxmind`.
## 4. Chatwoot (этап 2)
1. Один экземпляр на отдельном VPS обслуживает оба бренда. У каждого бренда свои входящие каналы:
a. виджет на сайте и в кабинете;
b. бот поддержки в Telegram, подключённый к Chatwoot напрямую как Telegram-канал.
2. Для вошедших пользователей виджет получает идентификацию с HMAC-подписью: оператор видит, кто пишет, а подделать её нельзя. Передаём атрибуты: сайт, id пользователя, email, число ключей.
3. Почему бот поддержки отдельный: Telegram отдаёт обновления одного бота только одному получателю. Основной бот принадлежит нашему приложению, бот поддержки — Chatwoot.
## 5. Twenty CRM (этап 2)
1. Заявки из `leads` уходят в Twenty через API: человек и сделка с полями заявки, брендом и страницей. Отправка — фоновой задачей с повторами; id в Twenty сохраняется в `leads.crmId`.
2. До этапа 2 заявки видны в админке Payload и приходят письмом `admin_new_lead` на почту поддержки.
## 6. Telegram (этап 3)
1. Боты для каждого бренда:
a. основной: команды, кнопка MiniApp, уведомления. Код — grammY в нашем приложении, вебхук `/svc/telegram/<сайт>/webhook` с секретным токеном;
b. поддержки: подключён к Chatwoot.
Для dev — отдельные тестовые боты.
2. MiniApp — на tg.*, вход по `initData` ([05-auth.md](05-auth.md), раздел 6). Dev-MiniApp доступна только Telegram-аккаунтам из списка разрешённых.
3. Черновик команд основного бота: `/start` (в том числе с реферальным кодом или токеном привязки), `/keys`, `/buy`, `/balance`, `/topup`, `/referral`, `/language`, `/support` (ссылка на бота поддержки), `/help`. Тексты и кнопки — в Payload, локализованные.
4. Уведомления из [04-billing.md](04-billing.md) (раздел 12) дублируются в бот, если Telegram привязан.
5. Оплата в Telegram (Q-11): по правилам Telegram цифровые товары в ботах и MiniApp оплачиваются Telegram Stars, за нарушения ботов блокируют. Варианты: Stars, внешняя страница оплаты, покупки в Telegram только с баланса. Решаем на старте этапа.
## 7. Платёжные системы
1. Каждый провайдер — адаптер в `core/payments/` с единым интерфейсом:
a. `createPayment` — создать платёж и вернуть адрес страницы оплаты или данные виджета;
b. `handleWebhook` — проверить подпись и вернуть новый статус платежа;
c. по возможностям: `chargeSavedMethod` (автосписание), `refund`, `detachMethod`.
2. Запись в коллекции `payment-systems` включает адаптер в валютах и для назначений ([03-data-model.md](03-data-model.md), 3.4). На каком сайте система доступна, следует из валют его доменов. Ключи доступа — в `.env`.
3. Этап 1: только `manual` — для администраторов.
4. Этап 4:
a. Stripe — USD: карты, сохранение карты, автосписания без участия пользователя на переменную сумму («цена минус баланс»), без Stripe Subscriptions. Сначала тестовый режим;
b. RUB — процессинги, принимающие карты РФ (вероятно, KZ и TR), только пополнения. Провайдеров выбираем на этапе (Q-12);
c. криптовалюта — пополнения в RUB и USD, провайдер выбирается на этапе;
d. ЮKassa — опционально, только пополнения, если примет категорию.
5. Вебхуки — `/svc/payments/<адаптер>/webhook`; обработка идемпотентная ([04-billing.md](04-billing.md), раздел 14).
## 8. Umami и баннер cookies (этап 1h)
1. Umami — своя аналитика без cookies, данные не уходят третьим лицам ([D-018](decisions/D-018-analytics.md)).
a. Ставится на этот сервер из исходников, без Docker: Node.js и PostgreSQL здесь уже есть. Umami 3.x поддерживает только PostgreSQL.
b. Отдельный пользователь ОС `umami`, своя база `umami`, systemd user-юнит, порт 3300.
c. Панель — `umi.veilix.cc`, доступ только с разрешённых IP.
2. Скрипт и отправка событий проксируются через домены брендов под нейтральными путями (переменные Umami `TRACKER_SCRIPT_NAME` и `COLLECT_API_ENDPOINT`), чтобы их не резали блокировщики рекламы. Реальный IP посетителя Umami берёт из заголовка nginx (`CLIENT_IP_HEADER`). Телеметрия Umami выключена (`DISABLE_TELEMETRY=1`).
3. Баннер cookies: обязательные (сессия, язык, валюта) и функциональные (чат Chatwoot). Решение пользователя хранится в cookie `vx_consent`, необязательные скрипты загружаются только после согласия. Библиотека — vanilla-cookieconsent или своя лёгкая реализация.
## 9. Автоматизация ключей (этап 5)
Подбор свободного сервера по тарифу (страны, теги) и автоматическая выдача подключений. Обсуждаем при старте этапа.
+108
View File
@@ -0,0 +1,108 @@
# 09. Инфраструктура
## 1. Сервер (проверено 18–20.10.2026)
| Что | Значение |
|---|---|
| ОС | Ubuntu 26.04.1 LTS |
| Ресурсы | 4 vCPU, 7.7 ГБ RAM, 8 ГБ swap, 145 ГБ диска (занято 24 ГБ) |
| IP | 169.58.76.183 — сайты (PTR `fr-ub2gs3.veilix.cc`, прямая запись совпадает); 173.212.216.183 — основной, через него по умолчанию идёт исходящий трафик (PTR `vmi3593751.contaboserver.net`) |
| Установлено | Node 24.22, PostgreSQL 18.6, nginx 1.28 (модули geoip2 и brotli подключены), Redis 8, Postfix 3.10, OpenDKIM, certbot 4.0, Docker 29 (не используем), git |
| Нет | pnpm, база GeoLite2 (`.mmdb`), сертификаты TLS, geoipupdate |
| DNS | все 15 хостов проекта (вместе с `umi.veilix.cc`) указывают на 169.58.76.183; DNS-хостинг — Gcore |
## 2. Папки
```
/home/vx_dev/veilix_dev/ — dev (пользователь vx_dev): рабочая копия git, .env, media/
/home/vx_prod/veilix/ — prod (пользователь vx_prod), закрыто для остальных
releases/<дата-тег>/ — собранные релизы (храним 5 последних)
current -> releases/… — симлинк на работающий релиз
shared/.env — секреты prod
shared/media/ — загруженные файлы prod
/home/umami/umami/ — Umami (пользователь umami)
```
vx — личный пользователь администратора, к проекту отношения не имеет. Под vx_prod нет редактора и ИИ-агентов, VSCode Remote под vx_prod не подключаем ([D-004](decisions/D-004-environments.md)). Домашние папки всех пользователей — `750`, владелец — сам пользователь. Пустые папки `/home/vx/veilix.app`, `/home/vx/my.veilix.app`, `/home/vx/tg.veilix.app` проекту не нужны.
## 3. Процессы
1. Каждый сервис — под своим пользователем ОС, как systemd user-юнит. Root один раз включает `loginctl enable-linger` для vx_dev, vx_prod и umami, чтобы юниты работали без входа в систему.
2. `veilix-dev.service` (vx_dev): `pnpm dev` на порту 3100 из `/home/vx_dev/veilix_dev`, перезапуск при сбое.
3. `veilix.service` (vx_prod): `pnpm start` на порту 3000 из `/home/vx_prod/veilix/current`, `NODE_ENV=production`, перезапуск всегда.
4. `umami.service` (umami): Umami на порту 3300.
5. Фоновые задачи Payload выполняются внутри процессов приложения ([02-architecture.md](02-architecture.md), раздел 7).
6. Логи — journald, у каждого пользователя свои (`journalctl --user -u veilix` под vx_prod). Сессии Claude логи prod не видят: при разборе проблем prod их присылает пользователь.
7. Проверка живости — `/svc/health` (с проверкой базы).
## 4. nginx
| Хосты | Куда | Доступ |
|---|---|---|
| veilix.app, veilixvpn.com, my.veilix.app, my.veilixvpn.com, tg.veilix.app, tg.veilixvpn.com | 127.0.0.1:3000 | открыт |
| plc.veilix.cc | 127.0.0.1:3000 | только разрешённые IP |
| dev.veilix.app, dev.my.veilix.app, dev.veilixvpn.com, dev.my.veilixvpn.com | 127.0.0.1:3100 | только разрешённые IP или пароль; `X-Robots-Tag: noindex` |
| dev.tg.veilix.app, dev.tg.veilixvpn.com | 127.0.0.1:3100 | открыт для Telegram; приложение пускает только разрешённые Telegram-аккаунты |
| dev.plc.veilix.cc | 127.0.0.1:3100 | только разрешённые IP |
| umi.veilix.cc | 127.0.0.1:3300 | только разрешённые IP |
1. Слушает 169.58.76.183, порты 80 (редирект на HTTPS и проверка certbot) и 443. TLS 1.2 и 1.3, HTTP/2, HSTS.
2. Сертификаты — certbot, проверка через HTTP.
3. Сжатие — brotli и gzip.
4. GeoIP: блок `geoip2` в `/etc/nginx/conf/ip.conf`, база `/var/lib/GeoIP/GeoLite2-Country.mmdb` с `auto_reload`. Переменная `$geoip_country_code` уходит приложению заголовком `X-Geo-Country`; такой же заголовок от клиента nginx затирает. Старый модуль GeoIP v1 (`modules-enabled/50-mod-http-geoip.conf`) отключён: он занимает те же имена переменных, и `nginx -t` падает с ошибкой «the duplicate "geoip_country_code" variable».
5. Заголовки к приложению — общий файл `/etc/nginx/snippets/veilix_proxy.conf` (плюс `maps/upgrade.conf` для WebSocket), его подключает каждый `location` с `proxy_pass`: `Host`, `X-Real-IP` и `X-Forwarded-For` (оба — `$remote_addr`: nginx стоит первым, цепочку от клиента не принимаем), `X-Forwarded-Proto`, `X-Geo-Country`, заголовки WebSocket (нужны горячей перезагрузке dev). `proxy_set_header` не наследуется в `location` со своими `proxy_set_header`, поэтому файл подключается в каждый такой `location`.
6. В `nginx.conf` подключаются только `sites-enabled/*.conf`: файлы сайтов должны заканчиваться на `.conf`. Сайты: `sites-available/02_vx_veilix-prod.conf`, `02_vx_veilix-dev.conf`, `02_vx_plc-prod.conf`, `02_vx_plc-dev.conf`. Сертификаты Let's Encrypt (certbot, проверка через nginx): `veilix.app` (6 prod-хостов брендов), `dev.veilix.app` (6 dev-хостов), `plc.veilix.cc`, `dev.plc.veilix.cc`.
a. Доступ по IP — файл `conf/ip_list_admin.conf` со строками `allow …;`, в `location` после него `deny all;`. Подключается на хостах админки и dev, кроме `dev.tg.*`.
b. `dev.tg.*` — отдельный `server` без ограничения по IP, с `X-Robots-Tag: noindex`.
c. На порту 80 — `location / { return 301 https://$host$request_uri; }`. Именно внутри `location`, а не на уровне `server`: так certbot при продлении может вставить свой `location` для проверки домена.
7. Лимиты запросов (`limit_req`) на эндпоинты входа и формы.
8. Для tg.* (этап 3) разрешить показ во фрейме Telegram Web (`frame-ancestors` для доменов telegram.org).
9. Пути трекера Umami на доменах брендов проксируются на 127.0.0.1:3300 ([08-integrations.md](08-integrations.md), раздел 8).
10. Конфигурацию готовит Claude текстом на согласование, применяет root.
## 5. PostgreSQL
1. Базы `veilix_dev`, `veilix_prod` и `umami`. Владелец каждой — роль с именем пользователя ОС: `vx_dev`, `vx_prod`, `umami`.
2. Вход по Unix-сокету с проверкой пользователя ОС: стандартное правило Ubuntu `local all all peer` в `pg_hba.conf` пускает пользователя ОС только под ролью с тем же именем. Паролей нет, `pg_hba.conf` и `pg_ident.conf` править не нужно.
3. У каждой базы снято право подключения для всех (`REVOKE CONNECT … FROM PUBLIC`): войти может только владелец. vx_dev в `veilix_prod` не попадёт.
4. Роль владеет своей базой: Payload нужны права на изменение схемы (push в dev, миграции в prod).
5. Строка подключения: `postgresql://vx_dev@/veilix_dev?host=/var/run/postgresql` (для prod — `vx_prod@/veilix_prod`).
## 6. Почта (Postfix)
1. `smtp_bind_address = 169.58.76.183`, `myhostname = fr-ub2gs3.veilix.cc`: письма уходят с IP, у которого обратная запись совпадает с прямой.
2. OpenDKIM: ключи для veilix.app и veilixvpn.com (селектор, напр. `mail`).
3. DNS для каждого из двух доменов:
a. SPF: `v=spf1 ip4:169.58.76.183 -all` (плюс SMTP-провайдер, если используем);
b. DKIM: TXT `mail._domainkey.<домен>` с открытым ключом;
c. DMARC: `_dmarc.<домен>` — сначала `v=DMARC1; p=none; rua=mailto:…`, после проверки — `p=quarantine`.
4. Проверить, что хостер не закрывает исходящий порт 25 и что 169.58.76.183 нет в чёрных списках.
5. Приём почты поддержки (`help@…`) — где ящик, вопрос Q-16.
## 7. Бэкапы
1. Prod-база: каждую ночь `pg_dump -Fc veilix_prod`, хранить 14 ежедневных и 8 еженедельных, копия на другой сервер (куда — Q-14). Раз в месяц — проверочное восстановление в отдельную базу.
2. `shared/media` — синхронизация на тот же внешний сервер.
3. Dev не бэкапим: код — в git, данные dev — тестовые.
## 8. Задачи для root по этапам
| № | Этап | Задача | Статус на 21.10.2026 |
|---|---|---|---|
| R-00 | 0 | Переезд разработки: перенести `/home/vx/veilix_dev` в `/home/vx_dev/veilix_dev` (владелец vx_dev); SSH-ключ vx_dev для VSCode; Claude Code под vx_dev | не сделано |
| R-01 | 0 | Репозиторий в Gitea; SSH-ключ vx_dev с правом push; для vx_prod — ключ только на чтение (deploy key) | не сделано |
| R-02 | 1a | PostgreSQL: роли `vx_dev`, `vx_prod`, `umami` и их базы (раздел 5) | сделано и проверено: vx_dev входит в `veilix_dev`, в `veilix_prod` — отказ |
| R-03 | 1a | pnpm: `corepack enable pnpm` | сделано |
| R-04 | 1a | Пользователи vx_dev, vx_prod, umami; `loginctl enable-linger` для них | сделано |
| R-05 | 1a | nginx и сертификаты для всех хостов (раздел 4) | работает; осталось исправить опечатку в `conf/ip_list_admin.conf` и добавить `X-Robots-Tag` для `dev.tg.*` |
| R-06 | 1a | MaxMind: `geoipupdate` (ключ в `/etc/GeoIP.conf`, база в `/var/lib/GeoIP`, таймер `geoipupdate.timer`); отключить старый модуль GeoIP v1; блок `geoip2` (раздел 4, пункт 4) | сделано |
| R-07 | 1c | Postfix, OpenDKIM, DNS-записи SPF, DKIM, DMARC — или данные SMTP-провайдера | |
| R-08 | 1h | PostgreSQL для prod — если не сделано вместе с R-02 | сделано в R-02 |
| R-09 | 1h | nginx и сертификаты для prod-хостов | сделано вместе с R-05 |
| R-10 | 1h | Umami: nginx и сертификат для `umi.veilix.cc` (DNS уже есть; база — в R-02) | |
| R-11 | 1h | Бэкапы prod-базы и media на внешний сервер (Q-14) | |
| R-12 | 2 | VPS с Chatwoot и Twenty, ключи API; 2 бота поддержки в BotFather | |
| R-13 | 3 | Боты в BotFather: 2 основных и тестовые; домен для Telegram Login; адреса MiniApp | |
| R-14 | 4 | Аккаунты платёжных систем (сначала тестовые), адреса вебхуков | |
| R-15 | 1h | По желанию (Q-18): sudo-правило, разрешающее vx_dev запускать от имени vx_prod только команду выкладки | |
| R-16 | 1h | По желанию: правило nftables — к 127.0.0.1:3000 подключаются только `www-data` (nginx) и vx_prod ([10-security.md](10-security.md), раздел 7) | |
+56
View File
@@ -0,0 +1,56 @@
# 10. Безопасность и приватность
## 1. Идентификаторы
1. Все `id` — UUID. В URL, API и консоли браузера нет последовательных номеров.
2. Номера заказов, ключей, платежей и реферальные коды — случайные строки (алфавит Crockford base32, без похожих символов).
## 2. Секреты
1. Секреты — только в `.env` своего окружения, права 600, вне git: строка подключения к базе, секрет сессий и кодов, ключ шифрования полей, ключ ALTCHA, ключи платёжных систем, токены ботов.
2. Чувствительные поля в базе шифруются приложением (AES-256-GCM): содержимое подключений (конфиги WG, ссылки подписок), SMTP-пароль, id сохранённых карт у провайдера.
3. Для prod ключи свои, не такие, как в dev.
## 3. Админка
1. Только на `plc.veilix.cc` и `dev.plc.veilix.cc`, вход только с разрешённых IP (nginx).
2. Роли owner, admin, support, content ([03-data-model.md](03-data-model.md), раздел 10). Поле `debugCode` журнала писем видят только owner и admin.
3. Блокировка после 5 неудачных попыток входа.
4. Денежные операции, выдача и блокировка ключей, объединение аккаунтов пишутся в журнал действий. Журнал нельзя изменить.
## 4. Пользователи
1. Коды входа хранятся только как HMAC, проверяются за постоянное время, 5 попыток, 10 минут жизни.
2. ALTCHA и лимиты по IP и email на запрос кода.
3. Ответ на запрос кода не выдаёт, есть ли аккаунт.
4. Токены сессий в базе — только SHA-256. Cookie — `HttpOnly`, `Secure`, `SameSite=Lax`, привязаны к хосту.
5. CSRF: `SameSite=Lax`, проверка Origin в server actions и своих POST-эндпоинтах.
6. Доступ к данным — правила Payload: пользователь видит только свои ключи, заказы, платежи и движения. Кабинет ходит в Local API с `overrideAccess: false`.
7. Коды входа в журнале ([05-auth.md](05-auth.md), раздел 4) — только для тестирования. В prod выключено, включение видно в админке.
## 5. Платежи
1. Номер карты никогда не попадает на наши серверы: только виджеты и страницы провайдеров.
2. Вебхуки проверяются по подписи провайдера, повторная доставка не проводит платёж дважды.
3. Деньги — целые числа, операции в транзакциях с блокировками ([04-billing.md](04-billing.md), раздел 14).
## 6. Приватность
1. Храним минимум: email, Telegram, язык, валюту, история заказов и платежей.
2. IP пользователей в базе не храним в открытом виде — только HMAC для сравнения (самоприглашения, лимиты). Логи nginx — ротация 14 дней.
3. В списке приглашённых почта замаскирована: `m···@gmail.com`.
4. Аналитика — Umami без cookies, данные не уходят третьим лицам.
5. Что и зачем храним — в политике конфиденциальности (этап 1b).
## 7. Окружения
1. Dev и prod — разные пользователи ОС (vx_dev и vx_prod), базы, роли и секреты. Dev закрыт от посторонних и от индексации.
2. Сессии Claude и редактор работают под vx_dev и не имеют доступа к файлам, секретам, логам и базе prod. Под vx_prod нет ни редактора, ни ИИ-агентов, VSCode Remote под vx_prod не подключаем. Своего SSH-входа у vx_prod нет: под него заходят из root (`sudo -iu vx_prod`). Код попадает в prod только через тег в Gitea и команду выкладки ([02-architecture.md](02-architecture.md), раздел 5).
3. Umami работает под своим пользователем: уязвимость в нём не открывает доступ к секретам Veilix.
4. В базу каждый пользователь ОС входит только своей ролью (`peer` через Unix-сокет), паролей нет ([09-infrastructure.md](09-infrastructure.md), раздел 5).
5. Порты приложений 3000 и 3100 слушают 127.0.0.1, к ним может подключиться любой пользователь сервера в обход ограничения по IP в nginx. Секретов это не открывает: админку защищает пароль с блокировкой после 5 попыток. Строже — правило nftables, которое пускает к порту prod только nginx (`www-data`) и vx_prod (R-16).
## 8. Обновления
1. Payload и Next.js выпускают исправления безопасности часто: проверять при каждом этапе, обновлять патч-версии.
2. `pnpm audit` перед выкладкой.
+24
View File
@@ -0,0 +1,24 @@
# D-001. Одно Next.js-приложение на все домены
Статус: принято, 20.10.2026.
## Контекст
Два бренда, у каждого сайт, кабинет и MiniApp — 6 доменов плюс админка. Бизнес-логика общая, дизайны разные.
## Решение
1. Одно Next.js-приложение, Payload установлен в него как библиотека.
2. Proxy Next.js определяет по Host сайт и тип фронтенда и переписывает запрос во внутренний маршрут.
3. Ядро (биллинг, ключи, вход, рефералы, почта) и UI-кит общие, страницы у каждого бренда свои.
## Рассмотренные варианты
1. Отдельные приложения на каждый фронтенд и отдельный Payload как API — больше деплоев, логика дублируется или ходит по HTTP.
2. Один набор страниц с переключением цветов — не подходит: данные в дизайнах представлены по-разному.
## Последствия
1. Один процесс на окружение, один деплой.
2. Кабинет и сайт читают данные через Local API Payload, без HTTP.
3. Нужна дисциплина: бренд-зависимый код — только в `src/brands/`.
+21
View File
@@ -0,0 +1,21 @@
# D-002. Payload 3, переход на 4 после стабильного релиза
Статус: принято, 20.10.2026.
## Контекст
На 20.10.2026 стабильная версия — Payload 3.91.0. Payload 4 в бете (4.0.0-beta.2 от 16.10.2026, требует Node ≥ 24.15 и Next.js ≥ 16.4). У Payload есть официальный codemod для перехода с 3 на 4.
## Решение
1. Стартуем на последней 3.x и Next.js 16.4.x.
2. Код пишем совместимо с 4: в Local API всегда явно `overrideAccess`, rich text — только Lexical, без устаревших API.
3. Переходим на 4 после стабильного релиза. Если он выйдет до старта этапа 1a — начинаем сразу на 4.
## Рассмотренные варианты
Начать на бете 4 — нет переезда потом, но риск ошибок и меняющихся API в бете, которой неделя.
## Последствия
Один плановый переезд, пока данных в prod мало или нет.
+22
View File
@@ -0,0 +1,22 @@
# D-003. Без Docker: всё на хосте, процессы под systemd
Статус: принято, 20.10.2026.
## Контекст
На сервере уже работают PostgreSQL 18, nginx и Redis. Пользователь хочет, чтобы dev и prod запускались одинаково и без Docker.
## Решение
1. PostgreSQL и nginx — на хосте.
2. Приложение — обычный процесс Node: dev — `pnpm dev`, prod — `pnpm build` и `pnpm start`. Оба — systemd user-юниты: dev у пользователя vx_dev, prod у vx_prod ([D-004](D-004-environments.md)).
3. Prod выкладывается в отдельную папку релиза и переключается симлинком `current`. Откат — переключить симлинк обратно.
## Рассмотренные варианты
Docker-образ для prod — воспроизводимая сборка и изоляция, но лишний слой. Тот же откат даёт схема с папками релизов.
## Последствия
1. Сборка prod идёт на этом же сервере (нагрузка 2–4 ГБ памяти на время сборки).
2. Node и pnpm общие для dev и prod: обновляются вместе.
+41
View File
@@ -0,0 +1,41 @@
# D-004. Dev и prod на одном сервере под разными пользователями
Статус: принято, 21.10.2026.
## Контекст
Dev и prod живут на одном сервере без Docker. Редактор и сессии Claude работают в dev и не должны иметь доступа к prod.
## Решение
| | dev | prod |
|---|---|---|
| Пользователь ОС | vx_dev | vx_prod |
| Папка | `/home/vx_dev/veilix_dev` | `/home/vx_prod/veilix` |
| База и роль | `veilix_dev`, роль `vx_dev` | `veilix_prod`, роль `vx_prod` |
| Порт | 3100 | 3000 |
| Домены | `dev.veilix.app`, `dev.my.veilix.app`, `dev.tg.veilix.app`, так же для veilixvpn.com; `dev.plc.veilix.cc` | основные; `plc.veilix.cc` |
1. vx — личный пользователь администратора, к проекту отношения не имеет.
2. Вся разработка — под vx_dev: VSCode (Remote-SSH), Claude Code и другие ИИ-агенты запускаются только под этим пользователем.
3. Под vx_prod — только prod:
a. редактора и ИИ-агентов нет, VSCode Remote под vx_prod не подключаем: при подключении VSCode ставит на сервер свою серверную часть и может подтянуть туда расширения, в том числе ИИ-агентов;
b. своего SSH-входа нет, под vx_prod заходят из root (`sudo -iu vx_prod`).
4. Оба окружения запускаются одинаково — systemd user-юнитами, каждый у своего пользователя.
5. Домашние папки — `750`, владелец — сам пользователь: секреты, файлы и логи prod сессиям Claude недоступны.
6. В базу каждый пользователь ОС входит только своей ролью (`peer` через Unix-сокет), паролей нет; чужие базы закрыты `REVOKE CONNECT … FROM PUBLIC`.
7. Код попадает в prod только через Gitea: из dev публикуется тег, vx_prod забирает его ключом только на чтение и собирает релиз.
8. Выкладку запускает пользователь под vx_prod. По желанию — sudo-правило, разрешающее vx_dev запускать от имени vx_prod только команду выкладки (Q-18).
9. Домены хранятся в базе, поэтому у dev просто свои записи в своей базе.
## Рассмотренные варианты
1. Один пользователь для обоих окружений — проще, но сессии Claude и любой процесс dev технически достают до секретов и базы prod.
2. Prod под личным пользователем vx — под ним работают редактор и ИИ-агенты, защита держалась бы только на дисциплине.
3. Docker-контейнеры для изоляции — пользователь не хочет Docker.
## Последствия
1. Ошибки prod сессии Claude не видят напрямую: логи присылает пользователь.
2. Отдельные deny-правила Claude Code для prod не нужны.
3. Переезд разработки на vx_dev — задача R-00 ([09-infrastructure.md](../09-infrastructure.md), раздел 8).
+18
View File
@@ -0,0 +1,18 @@
# D-005. Сайты — тенанты, домены в базе, маршрутизация по Host
Статус: принято, 20.10.2026.
## Решение
1. Официальный плагин `@payloadcms/plugin-multi-tenant`: тенант — Сайт, поле `site` на коллекциях, привязанных к сайту, переключатель сайта в админке.
2. Коллекция «Домены»: хост, сайт, тип фронтенда, языки, валюты.
3. Proxy Next.js находит домен по Host (с кэшем в памяти) и переписывает запрос во внутренний маршрут.
4. Общие справочники без привязки к сайту: страны, теги, валюты, платёжные системы. Платёжная система привязана к валютам, а сайт выводится через валюты его доменов ([D-010](D-010-currencies.md)).
## Рассмотренные варианты
Простое поле «сайт» без плагина — придётся самим писать фильтры и доступ в админке.
## Последствия
Определение сайта по домену плагин не делает — это наш код в proxy.
+20
View File
@@ -0,0 +1,20 @@
# D-006. Переводы: поля Payload и next-intl; 5 языков
Статус: принято, 20.10.2026.
## Решение
1. Языки: `en` (по умолчанию, без префикса в URL), `ru`, `de`, `fr`, `es`. Позже — `pt-br` и `ja`.
2. Контент — локализованные поля Payload (`localized: true`), откат на английский.
3. Интерфейс — next-intl, файлы `messages/*.json` в репозитории.
4. Язык всегда в URL (кроме английского). Список языков — в конфиге, у домена — включённые языки. Отдельной сущности «Языки» нет.
5. В админке Payload `filterAvailableLocales` скрывает языки, выключенные для сайта.
## Рассмотренные варианты
1. Языки как записи в базе — Payload задаёт локали в конфиге, сущность дублировала бы конфиг.
2. Интерфейсные строки тоже в Payload — нет версионирования и типизации, лишние запросы.
## Последствия
Новый язык — правка конфига, переводы и деплой.
@@ -0,0 +1,17 @@
# D-007. Определение языка; MaxMind через nginx
Статус: принято, 20.10.2026.
## Решение
1. Порядок: язык в URL → сохранённый выбор (cookie, профиль) → `Accept-Language` → страна по MaxMind → `en`.
2. Перенаправление — только при первом заходе без префикса и только людей, не поисковых ботов. Плюс `hreflang` и `x-default`.
3. Страну определяет nginx (модуль geoip2, база GeoLite2-Country) и передаёт заголовком `X-Geo-Country`. Приложение само читает `.mmdb` только там, где нужна страна произвольного IP.
## Рассмотренные варианты
Язык только по стране — страна не равна языку, а Google не рекомендует перенаправлять по IP.
## Последствия
База MaxMind обновляется `geoipupdate` (root).
+16
View File
@@ -0,0 +1,16 @@
# D-008. UUID и случайные публичные коды
Статус: принято, 20.10.2026.
## Решение
1. `idType: 'uuid'` в адаптере Postgres Payload: все id в базе, REST и URL админки — UUID. Автоинкремента нет.
2. Номера для людей — короткие случайные коды: заказы `VX-7K3M9Q`, ключи `K-…`, платежи, реферальные коды.
## Рассмотренные варианты
Автоинкремент плюс отдельное поле UUID для внешнего мира — нужно следить, чтобы внутренний id нигде не утёк. Штатная настройка Payload решает задачу без кастома.
## Последствия
Уникальность случайных кодов обеспечивается уникальным индексом и повтором генерации при коллизии.
+24
View File
@@ -0,0 +1,24 @@
# D-009. Заказы, платежи, движения по счёту; всё через баланс
Статус: принято, 20.10.2026.
## Контекст
У пользователя есть счёт. Заказ можно оплатить картой, с баланса или частично. Нужна ручная оплата из админки, которая запускает те же действия, что и настоящая.
## Решение
1. Три сущности: Заказ (что покупают), Платёж (деньги извне), Движение по счёту (неизменяемые записи «+» и «−»).
2. Любой успешный платёж зачисляется на баланс, любой заказ списывается с баланса.
3. Одна точка проведения `processPayment`, идемпотентная.
4. Ручная оплата — платёжная система `manual`. Платёж проводится, когда получает статус `succeeded`. Галочки «провести как реальную» нет.
5. Корректировка баланса — отдельная операция: только движение, без цепочки действий.
## Рассмотренные варианты
1. Одна сущность «транзакция» — смешивает коммерческий документ, деньги провайдера и баланс; сложно с частичной оплатой и повторами вебхуков.
2. Галочка «провести как реальную оплату» — не нужна: проведение запускает смена статуса.
## Последствия
Правило «списывается только разница с балансом» получается само. Подробно — [04-billing.md](../04-billing.md).
+22
View File
@@ -0,0 +1,22 @@
# D-010. Валюты и платёжные системы — сущности; платёжные системы привязаны к валютам
Статус: принято, 20.10.2026.
## Контекст
Из России часто заходят через VPN, поэтому страна по IP не говорит, какая у пользователя валюта и какие карты. Карты РФ принимают одни платёжные системы, зарубежные — другие.
## Решение
1. Сущность «Валюты»: RUB и USD.
2. Сущность «Платёжные системы»: адаптер, валюты, назначения (оплата заказа, пополнение). RUB — процессинги для карт РФ (вероятно, KZ и TR), USD — Stripe и криптовалюта.
3. Платёжная система привязана только к валютам. Где она доступна, следует из цепочки: валюты → домены, на которых они включены → сайты. В оформлении заказа показываются системы в валюте аккаунта.
4. У домена — включённые валюты, валюта по умолчанию и валюта для языка: ru → RUB, остальные → USD.
5. Переключатель валюты отдельный от языка: любая валюта с любым языком.
6. Цены тарифов — строкой на каждую валюту.
7. Валюта аккаунта задаётся при регистрации из выбранной и меняется, пока баланс нулевой и нет ключей. Дальше — через поддержку.
## Последствия
1. Маркетинговые страницы собираются в вариантах «язык × валюта» ([D-017](D-017-static-pages.md)).
2. Платёжная система в RUB доступна сразу на обоих брендах. Если понадобятся разные аккаунты одного провайдера для разных брендов, платёжной системе добавим необязательное поле «только для сайтов» (Q-17).
+17
View File
@@ -0,0 +1,17 @@
# D-011. Раздельные аккаунты по сайтам
Статус: принято, 20.10.2026.
## Решение
1. Пользователь привязан к сайту. Один email или Telegram на двух сайтах — два разных пользователя.
2. Внутри сайта: аккаунт можно создать по Telegram, почта привязывается кодом; на сайте есть «Войти через Telegram»; аккаунты объединяются по правилам [05-auth.md](../05-auth.md), раздел 6.4.
## Рассмотренные варианты
1. Общий аккаунт на оба бренда — бренды смешиваются в кабинете и в балансе.
2. Общий вход и отдельный профиль в каждом бренде — без дублей, но сложнее модель. Пользователь выбрал простоту.
## Последствия
Переход клиента с mass на dedicated — новая регистрация на veilix.app.
+18
View File
@@ -0,0 +1,18 @@
# D-012. Вход по коду через свою стратегию Payload; оформление заказа на my.*
Статус: принято, 20.10.2026.
## Решение
1. Входа по коду в Payload из коробки нет. Используем штатный механизм своих стратегий: коллекция `customers` с `disableLocalStrategy` и стратегией, которая читает нашу cookie сессии.
2. Коды и сессии — свои коллекции; коды и токены хранятся только как хэши.
3. Оформление заказа живёт на домене кабинета (my.*), а не на маркетинговом. Cookie сессии — только для хоста кабинета.
## Рассмотренные варианты
1. Better Auth или Auth.js поверх Payload — ещё одна система со своими таблицами и миграциями.
2. Cookie на весь домен бренда (`.veilix.app`) — тогда маркетинговые страницы перестают быть полностью кэшируемыми, а cookie видят dev-поддомены.
## Последствия
Маркетинговые сайты остаются без пользовательских cookies и полностью кэшируются.
+16
View File
@@ -0,0 +1,16 @@
# D-013. Капча ALTCHA
Статус: принято, 20.10.2026.
## Решение
ALTCHA — капча на proof-of-work, работает на нашем сервере, без третьих сторон. Вместе с лимитами по IP и email.
## Рассмотренные варианты
1. Google reCAPTCHA — передаёт данные пользователей в Google и строже проверяет пользователей VPN, то есть нашу аудиторию.
2. Cloudflare Turnstile — с июня 2025 российские провайдеры замедляют Cloudflare, пользователи из РФ могут не пройти проверку.
## Последствия
Провайдер капчи изолирован в `core/` и заменяется без переделки форм.
+18
View File
@@ -0,0 +1,18 @@
# D-014. Почта: шаблоны в Payload, переключаемый транспорт, без listmonk
Статус: принято, 20.10.2026.
## Решение
1. Шаблоны писем — в Payload, переведены, для каждого сайта свои.
2. Транспорт переключается в админке: SMTP, локальный sendmail (Postfix) или «только журнал».
3. Все письма пишутся в журнал. Флаг «Записывать коды входа в журнал» — для тестирования.
4. listmonk не используем.
## Рассмотренные варианты
listmonk — прежде всего сервис рассылок; переводов шаблонов в нём нет: на каждое письмо понадобилось бы 10 шаблонов (2 бренда × 5 языков).
## Последствия
Для отправки через Postfix нужны SPF, DKIM, DMARC и отправка с IP с правильной обратной записью ([09-infrastructure.md](../09-infrastructure.md), раздел 6).
+19
View File
@@ -0,0 +1,19 @@
# D-015. Боты на grammY без n8n; два бота на бренд
Статус: принято, 20.10.2026.
## Решение
1. Основной бот каждого бренда (команды, кнопка MiniApp, уведомления) написан в коде на grammY и работает внутри нашего приложения через вебхук. Тексты и кнопки — в Payload, переведены, правятся в админке.
2. Бот поддержки каждого бренда подключён к Chatwoot напрямую.
3. MiniApp — фронтенд в том же приложении, без n8n.
4. n8n не используем.
## Рассмотренные варианты
1. n8n как движок бота — визуальные схемы для покупок, ключей и баланса на 5 языках × 2 бота трудно поддерживать и тестировать, логика денег всё равно живёт в бэкенде.
2. Один бот на всё с пересылкой сообщений поддержки в Chatwoot через API — свой код, который нужно поддерживать.
## Последствия
Telegram отдаёт обновления бота только одному получателю, поэтому основной бот и бот поддержки — разные боты.
+15
View File
@@ -0,0 +1,15 @@
# D-016. Платёжные системы по валютам; автосписания — Stripe
Статус: принято, 20.10.2026. Провайдеры для RUB и криптовалюты выбираются на этапе 4 (Q-12).
## Решение
1. Каждый провайдер — адаптер с единым интерфейсом и флагами возможностей: автосписание, возврат, сохранение карты.
2. USD — Stripe (карты, сохранение карты, автосписания на переменную сумму, без Stripe Subscriptions) и криптовалюта.
3. RUB — процессинги, принимающие карты РФ (вероятно, KZ и TR), только пополнения, без автосписаний. ЮKassa — опционально, только пополнения.
4. Пользователи с картами РФ продлевают ключи с баланса; напоминание — за 3 дня.
5. Оплата внутри Telegram (Stars) — решаем на этапе 3.
## Последствия
На этапе 1 работает только `manual`. Модель данных готова к любому набору провайдеров.
+14
View File
@@ -0,0 +1,14 @@
# D-017. Статические маркетинговые страницы с пересборкой по событию
Статус: принято, 20.10.2026.
## Решение
1. Маркетинговые страницы собираются статически в вариантах «язык × валюта» и кэшируются.
2. Изменение в админке (цены, статус локации, FAQ, страницы) пересобирает затронутые страницы за секунды через пересборку по тегам. Запасной вариант — плановое обновление раз в 5 минут.
3. Наличие и цена перепроверяются на сервере при оформлении заказа.
4. Кабинет рендерится на сервере для каждого запроса.
## Последствия
Высокий балл PageSpeed, а устаревшая страница не может продать недоступную локацию.
+20
View File
@@ -0,0 +1,20 @@
# D-018. Umami на этом сервере и баннер cookies
Статус: принято, 20.10.2026.
## Решение
1. Аналитика — своя Umami без cookies, на этом сервере: Node.js и PostgreSQL здесь уже есть, а трекер проксирует этот же nginx.
2. Ставится из исходников, без Docker (официальный способ наряду с Docker-образом): `pnpm install`, `pnpm build`, `pnpm start`. Отдельный пользователь ОС `umami`, своя база `umami`, systemd user-юнит, порт 3300.
3. Панель — `umi.veilix.cc`, доступ только с разрешённых IP.
4. Скрипт и отправка событий проксируются через домены брендов под нейтральными путями (`TRACKER_SCRIPT_NAME`, `COLLECT_API_ENDPOINT`), чтобы их не резали блокировщики рекламы.
5. Баннер cookies: обязательные (сессия, язык, валюта) и функциональные (чат). Необязательные скрипты — только после согласия.
## Рассмотренные варианты
1. Umami на VPS с Chatwoot и Twenty — трекеру пришлось бы ходить на другой сервер, где Docker и тяжёлые сервисы.
2. Яндекс Метрика и GA4 — передают данные третьим лицам и требуют согласия; нужны, только если будет реклама.
## Последствия
У Umami свой пользователь ОС, поэтому уязвимость в ней не открывает доступ к секретам Veilix.
+13
View File
@@ -0,0 +1,13 @@
# D-019. Chatwoot и Twenty CRM на отдельном VPS
Статус: принято, 20.10.2026.
## Решение
1. Chatwoot — один экземпляр на оба бренда: виджеты сайтов и боты поддержки как отдельные входящие каналы.
2. Twenty CRM — заявки из Payload уходят в Twenty через API.
3. Оба сервиса на отдельном VPS, ставит и обслуживает пользователь. Наше приложение работает с ними только по API.
## Последствия
До этапа 2 заявки видны в админке Payload и приходят письмом на почту поддержки.
+16
View File
@@ -0,0 +1,16 @@
# D-020. Модели ключей dedicated и mass
Статус: принято, 20.10.2026.
## Решение
1. Dedicated: ключ — индивидуальный IP в стране, выбранной при покупке. Страна не меняется никогда: нужна другая — покупается новый ключ. В ключ входит RU-адрес. Подключения: WG/AWG-конфиг на каждое устройство (до лимита тарифа), ссылка Happ или Xray, подключения к RU-адресу.
2. Mass: ключ включает все страны тарифа, страна не выбирается. Happ — одна ссылка со всеми странами. WG — конфиги любой страны тарифа: у подключения есть поле страны.
3. Тип ключа определяется полем `type` тарифа.
4. Подключения гибкие: у ключа любой набор подключений разных видов, поэтому ручная выдача на этапе 1 покрывает любой вариант.
5. Оплаченный период начинается с момента выдачи ключа.
6. На этапе 1 WG-конфиги mass — те, что добавил администратор, по странам. Нужна другая страна — через поддержку. Автоматически — на этапе 5.
## Последствия
Автоматическая выдача (этап 5) создаёт те же подключения, что сейчас добавляет администратор.
+15
View File
@@ -0,0 +1,15 @@
# D-021. Админка на plc.veilix.cc с доступом по IP
Статус: принято, 20.10.2026.
## Решение
1. Админка Payload и её REST API работают только на `plc.veilix.cc` (prod) и `dev.plc.veilix.cc` (dev). На доменах брендов эти пути закрыты.
2. nginx пускает на хосты админки только разрешённые IP.
3. Свои эндпоинты приложения (ALTCHA, вебхуки, health) — под префиксом `/svc/`.
## Последствия
1. Адрес админки не связан с доменами брендов и закрыт от посторонних на уровне nginx.
2. Сайты, кабинеты и MiniApp к хосту админки не обращаются, поэтому ограничение по IP касается только администраторов ([02-architecture.md](../02-architecture.md), раздел 2).
3. Чтобы заходить в админку откуда угодно, в список разрешённых IP можно внести адреса своих VPN-серверов.
+27
View File
@@ -0,0 +1,27 @@
# Решения
По файлу на решение. Формат: контекст, решение, рассмотренные варианты, последствия. Новое решение — новый файл со следующим номером. Пересмотр — новый файл со ссылкой на старый, у старого статус «заменено».
| № | Решение | Статус |
|---|---|---|
| [D-001](D-001-single-app.md) | Одно Next.js-приложение на все домены | принято |
| [D-002](D-002-payload-version.md) | Payload 3, переход на 4 после стабильного релиза | принято |
| [D-003](D-003-hosting.md) | Без Docker: всё на хосте, процессы под systemd | принято |
| [D-004](D-004-environments.md) | Dev и prod на одном сервере под разными пользователями | принято |
| [D-005](D-005-multitenancy.md) | Сайты — тенанты, домены в базе, маршрутизация по Host | принято |
| [D-006](D-006-localization.md) | Переводы: поля Payload и next-intl; 5 языков | принято |
| [D-007](D-007-language-detection.md) | Определение языка; MaxMind через nginx | принято |
| [D-008](D-008-uuid.md) | UUID и случайные публичные коды | принято |
| [D-009](D-009-billing-model.md) | Заказы, платежи, движения по счёту; всё через баланс | принято |
| [D-010](D-010-currencies.md) | Валюты и платёжные системы — сущности; платёжные системы привязаны к валютам | принято |
| [D-011](D-011-accounts.md) | Раздельные аккаунты по сайтам | принято |
| [D-012](D-012-auth.md) | Вход по коду через свою стратегию Payload; оформление заказа на my.* | принято |
| [D-013](D-013-captcha.md) | Капча ALTCHA | принято |
| [D-014](D-014-email.md) | Почта: шаблоны в Payload, переключаемый транспорт, без listmonk | принято |
| [D-015](D-015-telegram-bots.md) | Боты на grammY без n8n; два бота на бренд | принято |
| [D-016](D-016-payments.md) | Платёжные системы по валютам; автосписания — Stripe | принято |
| [D-017](D-017-static-pages.md) | Статические маркетинговые страницы с пересборкой по событию | принято |
| [D-018](D-018-analytics.md) | Umami на этом сервере и баннер cookies | принято |
| [D-019](D-019-external-services.md) | Chatwoot и Twenty CRM на отдельном VPS | принято |
| [D-020](D-020-key-models.md) | Модели ключей dedicated и mass | принято |
| [D-021](D-021-admin-host.md) | Админка на plc.veilix.cc с доступом по IP | принято |
+226
View File
@@ -0,0 +1,226 @@
# Общее
Сервис продажи VPN. Ключи для приложений Happ (vless, histeria) и Wiregueard/Amnezia WG.
# Своими словами
Есть 2 сайта.
Один - продаёт Dedicated VPN, где каждому пользователю даётся свой IP.
Второй - более классический вариант, где на 1 IP множество пользователей.
Фундаментально они работают одинаково, это привязанные к пользователю ключи. Ключи уже открывают доступ либо к dedicated либо mass.
Скрипты - которыми обрабатывается оба сайта - общие.
Поэтому это одна база, один бэк, но разные домены. Отличаться они будут по доменам.
Дизайн отличается по цветам, но в общей концепции, но дизайны всё таки разные, тк по разному представлены данные и делать именно общий смысла нет.
- Фиолетовый - это dedicated.
- Зелёный - mass.
- Дизайн сделал в Claude Design лежит в файле VEILIX-handoff.zip.
Сайт x2
Личный кабинет x2
Личный кабинет в Telegram (MiniApp) x2
Бот в TG x2
veilix.app - dedicated
veilixvpn.com - mass
my.veilix.app - личный кабинет dedicated
my.veilixvpn.com - личный кабинет mass
tg.veilix.app - tg miniapp dedicated
tg.veilixvpn.com - tg miniapp mass
Сайты многоязычные.
Боты чтобы и миниапп были или в самом tg можно было командами всё делать.
# Стэк
- CMS Payload
- Next
- React
- SSR
- Postgres
Вопрос. Payload через докер ставится? Нужно чтобы он с базой и nginx которые на хосте работал, чтобы не ставить базу в докер. Это нормально или лучше чтобы база в докере тоже была?
# Сущности
- Сайты
- Домены
- Языки
- Страны
- Пользователи
- Тарифы
- Ключи
- Подключения
- Заказы
- Транзации счёта пользователя
# Сайты и Домены
Каждая из сущностей привязывается к Сайту.
Языки привязываются к Домену, Домены привязываются к сайтам.
Вопрос.
В Payload связка сайты/языки может какой то нативный мехнизм есть?
И в целом где-то переводы всего в базе тоже нужно иметь на разных языках или это есть тоже нативное?
# Языки
По умолчанию язык EN без постфикса в URL, остальные языки с postfix /de /es /jp /ru
Английский, немецкий, французский, испанский, португальский, японский и дальше RU.
Язык именно через URL везде проводить, те запомнить выбор можно в настройках пользователя или куки или localstorage, но в URL показывать именно с постфиксом.
Определять по MAXMIND LITE в Nginx.
# Telegram боты
Команды в ботах через n8n, чтобы можно было править логику ответов.
Вопрос.
n8n для этого подходит или есть другие варианты?
Miniapp я думаю имеет смысл напрямую делать без n8n?
# Авторизация и регистрация
По email по коду без паролей.
Одно поле email, проверять есть или нет. Либо регистрировать либо авторизовывать.
Защить Google Captcha.
Отправка будет через локально установленный listmonk.
# Оплата, заказы, транзации
У пользователя есть счёт.
Его можно пополнить криптой и картой, может ещё как то в итоге.
Привязка карты к счёту и рекурентная оплата в зависимости от суммы всех ключей.
Если положить на счёт деньги заранее, то ежемесячно списывается только сумма за вычетом того что есть на счёте.
Понять как лучше сделать в плане связки Заказы и Транзакции.
Оплачивает клиент Заказ, к заказу справочно привязывается Тариф и Ключ и Пользователь.
Юкасса Страйп (демо режимы пока)
Вопрос.
Заказы и Транзакции - не понятно, может одной сущности достаточно.
Нужна возможность делать оплату вручную, те то что генерируется при оплате с карточки или при рекурентном списании, чтобы любые из этих действий я мог добавить в админке payload и иницировались остальные действия, пользователь получил ключ или начисления на счёт и тп. Может по какой то галочке - "Провести как реальную оплату", тогда это будет не просто запись в коллекции, а произойдёт весь набор действий, который при реальной оплате.
# Ключи VPN
Страны привязыны к Тарифам.
Тэги привязаны к Тарифам.
Страны, чтобы на сайте отображать и возможно для получения с бэка в дальнейшем ключей.
Тэги, нужно будут для получения с бэка ключей для фильтраций каких-то произвольных.
Пользователь выбирает Тариф, на основе Тарифа ему подбирается свободный сервер и с бэка прилетают Подключения для ключа.
Подключения могут быть вручную добавлены, ссылка на подписку для happ, и ключ для wg.
Срок истечения у Ключа. Те следующий заказ продлевает этот же ключ.
К Ключу тоже привязывается Тариф, чтобы при достижении срока оплата списалась.
# ID и UUID
Не должно быть явных ID нигде, которые идут подряд, чтобы не было понятно, сколько и чего есть в базе.
Нужно использовать UUID, в тч при работе с API, чтобы и в консоли браузера нельзя было увидеть реальные ID.
Если payload предполагает автоикремент ID обычный, возможно не стоит городить кастом, и пусть так будет. Но нужно тогда всё равно всё обыграть все обращения по UUID.
Вопрос. Обсудить.
# Дополнительный софт
На этом же vps:
- N8N - для TG бота
- Listmonk - отправка транзакционных писем
На отдельном vps:
- Chatwoot - чат на сайте
- 12CRM - лиды собирать
# Этапы
Составить всю .md документацию и план. Далее, чтобы я мог запускать по этапам в отдельных сессиях.
В первую очередь собрать полноценный сайт с дизайном, всем функционалом (регистрация, авторизация, реферальная программа). Без эквайринга и ключей по внешним api.
При этом чтобы вручную я мог и добавить ключи и оплаты.
Далее интеграция с chatwoot, 12crm.
Далее listmonk.
Далее TG боты.
Далее эквайринг и ключи. Но ключи автоматизация пока с вопросами, это нужно оставить обсуждение к моменту запуска этапа.
Эквайринг тестовый режим страйпа и юкассы, я зарегистриуюсь, если они дают песочницу, то сначала так.
# Общее
- Отраслевые стандарты. Стараться придерживаться отраслевых стандартов и использовать максимально современные решения. Если я предлагаю решения, которые плохо связываются друг с другом или кастом, который может решается иными методами, то обсудить со мной. Я могу не знать, но с другой стороны может быть что нужно и делать кастом, в любом случае это повод для обсуждения.
- Нумерация пунктов в документации и чатаха с тобой - цифрами и латинскими буквами, чтобы не было путаницы, где А русская, где анлийская, гда B - толи БЭ латинская, толи ВЭ русская.
- Не писать никаких скриптов пока не будет всё утверждено, сначала обсуждаем.
- Если есть противоречия на любом этапе, сначала обсуждаем, потом делаем.
- Софт, который не достаёт и требуется установки от root - говоришь, я сделаю. Не нужно пытаться какие то обходные пути делать внутри пользователя, чтобы меня не беспокоить. Что нужно и правильно установить глобально на сервере - я установлю.
- Настройки на сервере - также как и софт, что правильнее сделать глобально, делаю глобально я.
- Доступ к root у меня есть, я в этом разбираюсь, нет никаких проблем и софт и настройки сделать.
+19
View File
@@ -0,0 +1,19 @@
# Этап 0. Документация
Статус: готово 20.10.2026.
## 1. Цель
Зафиксировать продукт, архитектуру, модель данных и план этапов так, чтобы каждый этап можно было запускать в отдельной сессии.
## 2. Сделано
1. Обсуждение исходной задачи ([history/HUMAN_IDEA.md](../history/HUMAN_IDEA.md)) и макетов, решения [D-001…D-021](../decisions/README.md).
2. Документы `docs/01…10`, файлы этапов, [CLAUDE.md](../../CLAUDE.md).
3. Макеты скопированы в `design/handoff/`.
4. Репозиторий инициализирован локально, без коммитов.
## 3. Осталось
1. Переезд разработки под пользователя vx_dev (R-00), репозиторий в Gitea и SSH-доступ для vx_dev (R-01), затем первый коммит и push по просьбе пользователя.
2. Вопросы к следующим этапам — в [00-index.md](../00-index.md), раздел 4.2. Этап 1a они не блокируют.
+39
View File
@@ -0,0 +1,39 @@
# Этап 1a. Фундамент
Статус: не начат.
## 1. Цель
Рабочий каркас: одно приложение отвечает на все dev-домены, определяет сайт, язык и валюту, показывает оболочку обоих брендов в своих цветах; админка с базовыми справочниками.
## 2. Входит
1. Проект: Next.js 16.4, Payload 3.x (или 4, если вышел стабильный), TypeScript strict, pnpm, ESLint, Prettier. Структура папок — [02-architecture.md](../02-architecture.md), раздел 3.
2. PostgreSQL: адаптер с `idType: 'uuid'`, push в dev, настроенные миграции для prod.
3. Payload: админка только на хосте админки; `admins` с ролями; плагин multi-tenant (поле `site`); локализация `en, ru, de, fr, es` с откатом на `en`; интерфейс админки `ru` и `en`; `filterAvailableLocales`.
4. Коллекции: `sites`, `domains`, `currencies`, `countries`, `locations`, `tags`, `tariffs`, `payment-systems` (пока только адаптер `manual`), `media` — по [03-data-model.md](../03-data-model.md).
5. Proxy: Host → домен → сайт и тип фронтенда; префиксы языков (next-intl, английский без префикса); определение языка по [06-i18n.md](../06-i18n.md), раздел 4; cookie валюты; внутренние маршруты; закрыть `/admin` и REST Payload на доменах брендов.
6. Темы: токены брендов ([07-frontend.md](../07-frontend.md), раздел 4), Tailwind v4, `data-site`, `data-theme`, три режима темы без мигания; шрифты через `next/font`.
7. Оболочки: шапка и подвал маркетинговых сайтов обоих брендов с переключателями языка, валюты и темы; оболочка кабинета (боковая панель и мобильные вкладки) с заглушками страниц.
8. Начальные данные (seed): 2 сайта, dev-домены, валюты, 30 стран и RU, 30 локаций на каждом сайте, тарифы dedicated (3 уровня) и mass (месяц, год) с ценами в RUB и USD, платёжная система `manual`.
9. `/svc/health`.
10. systemd user-юнит `veilix-dev.service`; текст конфигурации nginx для dev-хостов на согласование.
11. Раздел «Команды» в [CLAUDE.md](../../CLAUDE.md).
## 3. Не входит
Страницы по макетам (1b), вход (1c), биллинг (1d).
## 4. Перед стартом
Root: R-00…R-06 ([09-infrastructure.md](../09-infrastructure.md), раздел 8). Открытых решений к этапу нет.
## 5. Готово, когда
1. `dev.veilix.app` и `dev.veilixvpn.com` открываются по HTTPS: шапка и подвал в цветах бренда, три режима темы.
2. Работают `/ru`, `/de`, `/fr`, `/es`; первый заход без префикса перенаправляет по правилам определения языка; поисковые боты не перенаправляются.
3. Переключатель валюты запоминает выбор.
4. `dev.plc.veilix.cc/admin`: переключение сайтов, справочники редактируются, локализованные поля переключаются по языкам, у сайта видны только его языки.
5. В URL админки — UUID.
6. `dev.my.*` показывает оболочку кабинета; `/admin` на доменах брендов — 404.
7. `pnpm build`, линтер и проверка типов проходят.
+34
View File
@@ -0,0 +1,34 @@
# Этап 1b. Маркетинговые страницы
Статус: не начат.
## 1. Цель
Сайты обоих брендов по макетам, с контентом из Payload, формами и SEO.
## 2. Входит
1. Страницы по макетам для обоих брендов: главная, цены, локации (карта, отрисованная на сервере), поддержка ([07-frontend.md](../07-frontend.md), раздел 2). Юридические страницы `/legal/*`, 404 и 500.
2. Коллекции `pages`, `faq` (с местами показа), `leads`; поля контактов в `sites`.
3. Контент из макетов на RU и EN: тексты, FAQ, локации, тарифы.
4. Черновики юридических текстов на RU и EN с заглушками вместо данных юрлица: условия, конфиденциальность, возвраты, cookies.
5. ALTCHA ([08-integrations.md](../08-integrations.md), раздел 2). Формы «Индивидуальные условия» и обратной связи с ALTCHA пишут в `leads`. Письмо администратору появится в 1c.
6. SEO: `<title>` и описания на каждом языке, `hreflang`, `canonical`, `sitemap.xml` и `robots.txt` на каждый домен, картинки для соцсетей.
7. Статическая сборка «язык × валюта» и пересборка по событию из Payload ([D-017](../decisions/D-017-static-pages.md)).
8. Решить, показывать ли количество свободных серверов.
## 3. Не входит
Оформление заказа (1d), кабинет (1f), переводы на de, fr, es (1h; до этого — английский запасной).
## 4. Перед стартом
Q-02 (цены mass в USD), Q-08 (email поддержки), Q-09 (часовой пояс), Q-16 (ящик поддержки).
## 5. Готово, когда
1. Страницы обоих брендов совпадают с макетами: светлая и тёмная тема, десктоп и мобильный, RU и EN.
2. Правка цены, статуса локации или FAQ в админке видна на сайте в течение нескольких секунд.
3. Lighthouse mobile ≥ 90 на главной и странице цен обоих брендов.
4. Формы создают заявки, ALTCHA работает.
5. `sitemap.xml` и `hreflang` проходят проверку.
+32
View File
@@ -0,0 +1,32 @@
# Этап 1c. Вход по коду и почта
Статус: не начат.
## 1. Цель
Регистрация и вход по коду из письма в кабинетах обоих брендов; почтовая подсистема с журналом.
## 2. Входит
1. Почта ([08-integrations.md](../08-integrations.md), раздел 1): глобал «Почта» (транспорты `smtp`, `sendmail`, `log_only`), email-адаптер, `email-templates` с шапкой и подвалом бренда, `email-log`, задача `send-email`, тестовое письмо, флаг «Записывать коды входа в журнал».
2. Шаблоны на RU и EN: `login_code`, `admin_new_lead` (и подключить его к формам из 1b).
3. `customers` со своей стратегией, `customer-sessions`, `auth-codes`; запрос и проверка кода, лимиты, ALTCHA ([05-auth.md](../05-auth.md), разделы 2–4).
4. Страница `/login` по макету с состояниями: код отправлен, ошибка, истёк, повторная отправка через 60 секунд.
5. Выход, выход на всех устройствах.
6. Язык и валюта аккаунта при регистрации ([04-billing.md](../04-billing.md), раздел 2).
7. Задача `cleanup` для кодов, сессий и журнала.
## 3. Не входит
Telegram (этап 3), реферальная привязка при регистрации (1g).
## 4. Перед стартом
Root: R-07 (Postfix, OpenDKIM, DNS) или данные SMTP-провайдера. Без них этап делается на транспорте `log_only`.
## 5. Готово, когда
1. Регистрация и вход работают на `dev.my.veilix.app` и `dev.my.veilixvpn.com`; аккаунты сайтов независимы.
2. С флагом коды видны в журнале; на `log_only` письма не уходят.
3. Через `sendmail` или `smtp` письмо доходит до Gmail не в спам; оценка mail-tester ≥ 9/10.
4. Лимиты и счётчик попыток работают; сессии отзываются.
+36
View File
@@ -0,0 +1,36 @@
# Этап 1d. Биллинг без эквайринга
Статус: не начат.
## 1. Цель
Деньги и заказы работают целиком: оформление заказа, оплата с баланса, ручные платежи, продления, льготный период — без внешних платёжных систем.
## 2. Входит
1. Коллекции `orders`, `payments`, `ledger-entries`, `payment-methods` (структура), `keys` (поля и статусы), `audit-log`.
2. `core/billing`: `processPayment`, оплата с баланса, задачи `renewals`, `expiry`, `reminders`, `reconcile`; блокировки и уникальные ограничения ([04-billing.md](../04-billing.md)).
3. `/order` по макетам обоих брендов: шаг «Почта» из 1c, шаг «Оплата» (с баланса; «Заказ создан, ожидает оплаты», когда платёжных систем нет), согласие с условиями, сводка из тарифа, локации и валюты.
4. `/buy` в кабинете: dedicated — сетка локаций с ценами, mass — «Месяц» и «Год».
5. Админка: ручной платёж, корректировка баланса, возврат на баланс, продление вручную, блокировка, «Очередь выдачи»; всё — в журнал действий.
6. Шаблоны писем на RU и EN: `order_paid`, `key_provisioning`, `renewal_reminder`, `expiry_reminder`, `renewal_succeeded`, `renewal_failed`, `grace_started`, `key_expired`, `topup_succeeded`, `refund_issued`, `admin_new_order`, `admin_provisioning_needed`.
## 3. Не входит
Подключения и выдача ключа (1e), реальные платёжные системы (этап 4). Для проверок ключ переводится в `active` вручную в админке.
## 4. Перед стартом
Этап 1c готов.
## 5. Готово, когда
Автотесты `core/billing` проходят сценарии:
1. Ручное пополнение → оплата заказа с баланса → заказ оплачен → ключ `provisioning`.
2. Частичный баланс + ручной платёж на разницу → заказ оплачен.
3. Продление с баланса за 24 часа; денег нет → заказ ждёт → пополнение само оплачивает продление.
4. Переходы в льготный период и «истёк» с письмами.
5. Частичный и полный возврат на баланс, с отменой ключа и без.
6. Повторный `processPayment` и параллельные запуски задач не списывают дважды.
7. `reconcile` не находит расхождений.
+26
View File
@@ -0,0 +1,26 @@
# Этап 1e. Ключи и подключения
Статус: не начат.
## 1. Цель
Администратор выдаёт ключи вручную; пользователь получает конфиги и ссылки на странице ключа.
## 2. Входит
1. Коллекция `connections` с шифрованием содержимого ([03-data-model.md](../03-data-model.md), 5.2).
2. Админка: на странице ключа — добавление подключений (WG/AWG — файлом или текстом, ссылка Happ, ссылка Xray, RU-адрес, номер устройства, страна), действие «Выдать ключ», список «К отключению».
3. Страница ключа в кабинете: срок, галочка автопродления, «Продлить сейчас», WG-конфиги (скачать, QR), Happ/Xray (копировать, QR); для mass — WG по странам: конфиги, которые добавил администратор; нужна другая страна — ссылка на поддержку.
4. Состояния: готовится, льготный период, истёк, нет подключений.
5. Содержимое конфигов отдаётся отдельным запросом только владельцу, в HTML страницы не попадает. QR строится в браузере из полученного содержимого.
6. Шаблон `key_ready` на RU и EN.
## 3. Перед стартом
Этап 1d готов.
## 4. Готово, когда
1. Администратор выдал ключ — пользователь видит его активным, срок пошёл с момента выдачи.
2. Скачанный `.conf` импортируется в WireGuard; QR сканируется в WireGuard и Happ.
3. Чужой пользователь получает 404 на конфиг (тест).
+24
View File
@@ -0,0 +1,24 @@
# Этап 1f. Веб-кабинеты
Статус: не начат.
## 1. Цель
Кабинеты обоих брендов по макетам `App` и `MassApp`.
## 2. Входит
1. Главная: ключи, «Купить ключ», FAQ.
2. Список ключей; бейдж «Истекает» по правилу [04-billing.md](../04-billing.md), раздел 15.
3. «Счёт и платежи»: баланс, пополнение (пресеты и своя сумма; пока платёжных систем нет — понятное сообщение), сохранённая карта и «Отвязать карту» (видно с этапа 4), ближайшие списания, история.
4. Настройки: язык, валюта (правило — [04-billing.md](../04-billing.md), раздел 2), тема; выход.
5. Мобильная навигация: вкладки «Главная, Ключи, Купить, Ещё».
6. Различия брендов: у mass «Список ключей» без «текущей/запасной локации».
## 3. Не входит
Реферальная страница (1g), MiniApp (этап 3).
## 4. Готово, когда
Кабинеты обоих брендов совпадают с макетами: светлая и тёмная тема, десктоп и мобильный, RU и EN. Все данные — реальные, из базы.
+23
View File
@@ -0,0 +1,23 @@
# Этап 1g. Реферальная программа
Статус: не начат.
## 1. Цель
«Приведите друга — вы оба получите по месяцу в подарок» по правилам [04-billing.md](../04-billing.md), раздел 11.
## 2. Входит
1. Реферальные коды пользователей.
2. Маршрут `my.<домен>/r/<код>`: cookie `vx_ref` на 60 дней и переход на сайт бренда.
3. Привязка при регистрации; коллекция `referrals`.
4. Бонус обоим после первой оплаты заказа `new_key` приглашённого; статус `review` при совпадении хэша IP.
5. Суммы бонуса по валютам в `sites`.
6. Страница `/referral` по макету: приглашено, начислено, ожидает; ссылка с копированием; приглашённые с замаскированной почтой.
7. Шаблон `referral_reward` на RU и EN; список приглашений в админке.
## 3. Готово, когда
1. A делится ссылкой → B регистрируется → B оплачивает первый заказ → обоим начислен бонус.
2. Самоприглашение с того же IP попадает в `review`.
3. Ссылка другого бренда не засчитывается.
+26
View File
@@ -0,0 +1,26 @@
# Этап 1h. Переводы, аналитика, запуск prod
Статус: не начат.
## 1. Цель
Сайты и кабинеты на 5 языках работают на основных доменах.
## 2. Входит
1. Переводы de, fr, es: интерфейс и контент Payload, машинный перевод LLM с глоссарием ([06-i18n.md](../06-i18n.md), раздел 7); проверка непереведённых ключей.
2. Umami на этом сервере ([D-018](../decisions/D-018-analytics.md)) с проксированием через домены брендов; баннер cookies ([08-integrations.md](../08-integrations.md), раздел 8).
3. Проверка производительности (Lighthouse mobile ≥ 90) и доступности.
4. Prod под пользователем vx_prod: `.env`, команда выкладки с папками релизов и откатом ([02-architecture.md](../02-architecture.md), раздел 5), первая выкладка, начальные данные prod, первый администратор, smoke-тесты.
5. Бэкапы базы и media с проверкой восстановления.
6. Юридические черновики на сайтах (Q-13 — пока с заглушками).
## 3. Перед стартом
Root: R-08…R-11, по желанию R-15. Решения: Q-14 (куда бэкапы), Q-18 (кто запускает выкладку).
## 4. Готово, когда
1. Основные домены работают по HTTPS, dev и prod разделены.
2. Выкладка и откат отрепетированы.
3. Восстановление из бэкапа проверено.
+19
View File
@@ -0,0 +1,19 @@
# Этап 2. Chatwoot и Twenty CRM
Статус: не начат.
## 1. Входит
1. Chatwoot ([08-integrations.md](../08-integrations.md), раздел 4): виджет на сайтах и в кабинетах, отдельные входящие каналы для каждого бренда; идентификация вошедших пользователей с HMAC; категория в баннере cookies.
2. Боты поддержки обоих брендов подключены к Chatwoot как Telegram-каналы.
3. Почта поддержки (Q-16) — по решению, например email-канал Chatwoot.
4. Twenty ([08-integrations.md](../08-integrations.md), раздел 5): заявки уходят через API фоновой задачей с повторами, `leads.crmId`.
## 2. Перед стартом
Root: R-12 — VPS, Chatwoot, Twenty, ключи API, боты поддержки в BotFather.
## 3. Готово, когда
1. Сообщения из виджетов и ботов поддержки приходят в Chatwoot с указанием бренда; вошедший пользователь опознан.
2. Заявки появляются в Twenty.
+23
View File
@@ -0,0 +1,23 @@
# Этап 3. Telegram
Статус: не начат.
## 1. Входит
1. Основные боты обоих брендов на grammY: вебхук, команды, меню, тексты в Payload на всех языках ([08-integrations.md](../08-integrations.md), раздел 6).
2. MiniApp по макетам `AppTG` и `MassAppTG` с Telegram SDK: BackButton, MainButton, тема Telegram, отклик вибрацией; вход по `initData`.
3. Привязка почты из Telegram, «Подключить Telegram» в кабинете, «Войти через Telegram» на сайте, объединение аккаунтов ([05-auth.md](../05-auth.md), раздел 6).
4. Уведомления в бот; реферальный код через `/start`.
5. Тестовые боты для dev с доступом по списку Telegram-аккаунтов.
6. Оплата в Telegram — по решению Q-11.
## 2. Перед стартом
1. Root: R-13 — основные и тестовые боты, домен для Telegram Login, адреса MiniApp.
2. Решения: Q-10 (имена ботов), Q-11 (оплата в Telegram).
## 3. Готово, когда
1. Из Telegram можно зарегистрироваться, купить ключ (по Q-11), получить конфиги, пополнить баланс, пригласить друга.
2. Уведомления приходят в бот.
3. Сценарии привязки и объединения аккаунтов проходят.
+26
View File
@@ -0,0 +1,26 @@
# Этап 4. Платёжные системы
Статус: не начат.
## 1. Входит
1. Адаптеры ([08-integrations.md](../08-integrations.md), раздел 7):
a. Stripe: Payment Element, сохранение карты, автосписания на переменную сумму, возвраты, отвязка карты, вебхуки;
b. RUB: процессинги для карт РФ — пополнения (провайдеров выбираем на старте, Q-12);
c. криптовалюта — пополнения, экран счёта с адресом, суммой и таймером курса;
d. ЮKassa — опционально, пополнения.
2. Состояния оформления и пополнения: 3-D Secure, отказ, ожидание подтверждения сети.
3. Повторы автосписаний ([04-billing.md](../04-billing.md), 6.1); возврат на карту (`refund` + `payout`).
4. Тексты согласия на автосписания в оформлении заказа и в юридических документах.
5. Настройка платёжных систем в админке: валюты, назначения, лимиты, тестовый или боевой режим. Если бренды получат разные аккаунты одного провайдера — Q-17.
## 2. Перед стартом
1. Root: R-14 — аккаунты провайдеров (сначала тестовые).
2. Решения: Q-12; для боевого Stripe нужны данные юрлица (Q-13).
## 3. Готово, когда
1. Каждый адаптер в тестовом режиме проходит путь от оплаты до зачисления, включая повторную доставку вебхуков.
2. Автопродление через Stripe работает на тестовых картах. Если карта требует 3-D Secure, пользователь получает письмо оплатить вручную.
3. Возвраты на карту работают.
+13
View File
@@ -0,0 +1,13 @@
# Этап 5. Автоматизация выдачи ключей
Статус: обсуждение при старте этапа.
## 1. Что известно
1. По тарифу (страны, теги) подбирается свободный сервер, бэкенд возвращает подключения для ключа.
2. Автоматика создаёт те же подключения, что на этапе 1 добавляет администратор ([D-020](../decisions/D-020-key-models.md)), и отключает их, когда ключ истекает.
3. Наличие в локациях обновляется автоматически, сайты пересобираются по событию.
## 2. Обсудить при старте
Источник серверов и подключений, способ интеграции, как получать WG-конфиги mass по странам, обработка ошибок выдачи.