Центр управления предпочтениями уведомлений — веб-приложение для настройки каналов и типов уведомлений для пользователей с проверкой глобальных политик и тихих часов.
| Компонент | Технология |
|---|---|
| Фреймворк | Next.js 16.2.7 (App Router) |
| Язык | TypeScript 5 |
| UI | React 19.2.4, Tailwind CSS 4 |
| ORM | Sequelize 6.37.8 |
| База данных (dev/prod) | PostgreSQL 18 |
| База данных (test) | SQLite |
| Тестирование | Jest 30 + Testing Library (DOM, React) |
| Контейнеризация | Docker, Docker Compose |
.
├── __tests__/ # Тесты (зеркалируют структуру app/ и services/)
│ ├── app/ # Интеграционные тесты страниц и API
│ └── services/ # Модульные тесты сервисов
├── actions/ # Server Actions (обработчики форм и клиентских запросов)
├── app/ # Next.js App Router (страницы и API)
│ ├── api/ # API-маршруты
│ └── users/ # Страницы пользователей
├── config/ # Конфигурация Sequelize
├── data/ # Файлы БД (SQLite для тестов, volume для PostgreSQL)
├── helpers/ # Вспомогательные утилиты
├── migrations/ # Миграции Sequelize
├── models/ # Модели Sequelize
├── public/ # Статические файлы
├── seeders/ # Сиды Sequelize
├── services/ # Бизнес-логика приложения
├── Dockerfile # Production-сборка
├── docker-compose.yml # Оркестрация PostgreSQL + Next.js
├── jest.config.ts # Конфигурация Jest
└── jest.setup.ts # Очистка БД после каждого теста
Server Action, вызываемый из формы создания пользователя. Принимает FormData, преобразует в UserData, делегирует создание сервису services/createUser.ts, после чего редиректит на страницу созданного пользователя.
Server Action, вызываемый из клиентского компонента sourceTable.tsx при клике на чекбокс. Принимает userId, channelId, notificationTypeId, делегирует переключение источника сервису services/toggleSource.ts, возвращает { status: "ok" }.
Ссылки на список пользователей и создание пользователя.
Выводит всех пользователей с регионом и часовым поясом. Каждый элемент — ссылка на страницу пользователя.
Форма с полями: email, регион, начало/конец тихих часов. При отправке вызывает Server Action createNewUser, который:
- создаёт пользователя в выбранном регионе;
- создаёт source-записи по умолчанию (из
default_sources); - все операции — в одной транзакции.
Отображает email, регион, тихие часы и таблицу channel × notificationType с чекбоксами. Чекбокс через клиентский компонент SourceTable вызывает Server Action toggleUserSourceCheckbox, который включает/отключает источник уведомлений для данной пары.
Проверяет, разрешена ли отправка уведомления.
Тело запроса:
{
"userId": 1,
"channel": "email",
"notificationType": "marketing",
"region": "EU",
"datetime": "2026-06-08T14:00:00-04:00"
}Ответ:
{
"data": {
"decision": "allow" | "deny",
"reason": ""
},
"error": null
}Причины отказа: blocked_by_global_policy, blocked_by_quiet_hours, blocked_by_channel_and_notification_type.
Возвращает текущие предпочтения пользователя: дашборд источников и тихие часы.
Ответ:
{
"data": {
"dashboard": [
{ "channel": "email", "notificationType": "marketing", "active": true },
...
],
"quietHours": { "start": "22:00", "end": "08:00" }
},
"error": null
}Обновляет предпочтения пользователя.
Тело запроса (все поля опциональны):
{
"startQuietHours": "23:00",
"endQuietHours": "07:00",
"channel": "email",
"notificationType": "marketing",
"active": true
}Если передан active: true — активирует источник (создаёт запись в sources). Если active: false — деактивирует (удаляет). Тихие часы обновляются при наличии startQuietHours или endQuietHours.
| Поле | Тип | Описание |
|---|---|---|
| id | PK | |
| name | TEXT | Название (уникальное) |
| timezone | TEXT | IANA timezone (например Europe/London) |
Связи: Region → User (один ко многим)
| Поле | Тип | Описание |
|---|---|---|
| id | PK | |
| regionId | FK | Ссылка на Region |
| TEXT | Уникальный | |
| startQuietHours | TIME | Начало тихих часов |
| endQuietHours | TIME | Конец тихих часов |
Связи: User → Region (N:1), User → Source (1:N)
| Поле | Тип | Описание |
|---|---|---|
| id | PK | |
| name | TEXT | Уникальный (например sms, email) |
| Поле | Тип | Описание |
|---|---|---|
| id | PK | |
| name | TEXT | Уникальный (transactional, marketing) |
| Поле | Тип | Описание |
|---|---|---|
| id | PK | |
| userId | FK | Ссылка на User |
| channelId | FK | Ссылка на Channel |
| notificationTypeId | FK | Ссылка на NotificationType |
Уникальный индекс: (userId, channelId, notificationTypeId).
Определяет, через какой канал и какие уведомления получает конкретный пользователь.
| Поле | Тип | Описание |
|---|---|---|
| channelId | FK | Ссылка на Channel |
| notificationTypeId | FK | Ссылка на NotificationType |
Уникальный индекс: (channelId, notificationTypeId).
Определяет, какие источники автоматически создаются при регистрации нового пользователя.
| Поле | Тип | Описание |
|---|---|---|
| regionId | FK | Ссылка на Region |
| channelId | FK | Ссылка на Channel |
| notificationTypeId | FK | Ссылка на NotificationType |
Уникальный индекс: (regionId, channelId, notificationTypeId).
Запрещает определённые комбинации регион-канал-тип. Используется в AllowanceChecker.
| Поле | Тип | Описание |
|---|---|---|
| id | PK | |
| action | TEXT | Название операции |
| input | TEXT | Входные данные (JSON) |
| output | TEXT | Результат (JSON) |
Логирует все значимые действия: evaluate, createUser, activateSourceForUser, deactivateSourceForUser, updateUserQuietHours.
erDiagram
Region ||--o{ User : ""
User ||--o{ Source : ""
Source }o--|| Channel : ""
Source }o--|| NotificationType : ""
DefaultSource }o--|| Channel : ""
DefaultSource }o--|| NotificationType : ""
Policy }o--|| Region : ""
Policy }o--|| Channel : ""
Policy }o--|| NotificationType : ""
Класс, реализующий цепочку проверок для решения, можно ли отправить уведомление:
- Policy check — если есть политика, запрещающая комбинацию регион-канал-тип, вернуть
deny. - Quiet hours check — для
marketingуведомлений: если текущий час попадает в тихий час пользователя, вернутьdeny. - Source check — если у пользователя нет активного источника для канала+типа, вернуть
deny. - Если все проверки пройдены —
allow.
Каждый вызов check() логируется в Trace.
Создаёт пользователя в транзакции, затем создаёт source-записи для всех DefaultSource. Логирует шаги в Trace. Вызывается из actions/createNewUser.ts.
Переключает состояние источника: если запись есть — удаляет, если нет — создаёт. Вызывается из actions/toggleUserSourceCheckbox.ts.
Создаёт запись Source для пользователя (findOrCreate), логирует в Trace. Транзакционна.
Удаляет запись Source для пользователя, логирует в Trace. Транзакционна.
Обновляет startQuietHours и/или endQuietHours пользователя, логирует в Trace. Транзакционна.
Строит матрицу Channel × NotificationType для пользователя, помечая каждую пару флагом active. Не зависит от региона или политик — просто показывает, какие источники включены.
helpers/successResponse.ts— формируетNextResponseс{ data, error: null }и статусом 200.helpers/notFoundResponse.ts— формируетNextResponseс{ data: null, error }и статусом 404.helpers/buildSourceKey.ts— склеиваетchannelId:notificationTypeIdв строку для Set/Map.helpers/findHourInUserTimezone.ts— определяет час в часовом поясе пользователя по переданномуdatetime.
Тесты используют SQLite (файл data/test.sqlite). Перед запуском автоматически выполняются миграции.
npm run testКоманда эквивалентна:
NODE_ENV=test npx sequelize-cli db:migrate
npx jestПосле каждого теста база очищается (см. jest.setup.ts). Тесты используют @testing-library/react для рендера страниц и прямые вызовы API-обработчиков для интеграционных тестов.
docker compose up --buildDocker Compose поднимает:
- PostgreSQL 18 — база данных, том монтируется в
./data/postgresql; - Next.js — production-сборка, порт
3000на localhost.
При старте выполняются только: npm run start. Миграции и сиды запускаются вручную (см. ниже).
После запуска docker compose up --build откройте http://localhost:3000.
При первом запуске выполните миграции и сиды в соседнем терминале:
docker compose exec next npm run migrate
docker compose exec next npm run seed-
Главная — страница содержит две кнопки: «Список пользователей» и «Создать пользователя».
-
Создание пользователя — нажмите «Создать пользователя», заполните форму (email, регион, тихие часы) и отправьте. После создания произойдёт редирект на страницу пользователя.
-
Список пользователей — нажмите «Назад» (или перейдите на
/users/list). Убедитесь, что созданный пользователь отображается в таблице. -
Детальная страница пользователя — нажмите на пользователя в списке. Откроется матрица
Channel × NotificationTypeс чекбоксами, показывающая, какие источники уведомлений активны.
Получение предпочтений пользователя:
curl http://localhost:3000/api/users/1/preferencesОтвет:
{
"data": {
"dashboard": [
{ "channel": "email", "notificationType": "transactional", "active": true },
{ "channel": "email", "notificationType": "marketing", "active": true },
{ "channel": "push", "notificationType": "transactional", "active": true },
{ "channel": "sms", "notificationType": "marketing", "active": false },
...
],
"quietHours": { "start": "22:00", "end": "08:00" }
},
"error": null
}curl -X POST http://localhost:3000/api/users/1/preferences \
-H "Content-Type: application/json" \
-d '{"startQuietHours": "23:00", "endQuietHours": "07:00"}'curl -X POST http://localhost:3000/api/users/1/preferences \
-H "Content-Type: application/json" \
-d '{"channel": "sms", "notificationType": "marketing", "active": true}'curl -X POST http://localhost:3000/api/users/1/preferences \
-H "Content-Type: application/json" \
-d '{"channel": "sms", "notificationType": "marketing", "active": false}'Базовый запрос:
curl -X POST http://localhost:3000/api/evaluate \
-H "Content-Type: application/json" \
-d '{"userId": 1, "channel": "email", "notificationType": "marketing", "region": "EU", "datetime": "2026-06-08T14:00:00-04:00"}'Уведомление заблокировано глобальной политикой:
curl -X POST http://localhost:3000/api/evaluate \
-H "Content-Type: application/json" \
-d '{"userId": 1, "channel": "sms", "notificationType": "marketing", "region": "EU", "datetime": "2026-06-08T14:00:00-04:00"}'Ответ: blocked_by_global_policy (в EU запрещена комбинация sms+marketing).
Уведомление заблокировано тихими часами:
curl -X POST http://localhost:3000/api/evaluate \
-H "Content-Type: application/json" \
-d '{"userId": 1, "channel": "email", "notificationType": "marketing", "region": "EU", "datetime": "2026-06-08T23:30:00-04:00"}'Ответ: blocked_by_quiet_hours (если у пользователя тихие часы с 22:00 до 08:00).
Уведомление заблокировано отсутствием источника:
curl -X POST http://localhost:3000/api/evaluate \
-H "Content-Type: application/json" \
-d '{"userId": 1, "channel": "messenger", "notificationType": "marketing", "region": "EU", "datetime": "2026-06-08T14:00:00-04:00"}'Ответ: blocked_by_channel_and_notification_type (источник messenger+marketing не включён у пользователя).
Уведомление разрешено (все проверки пройдены):
curl -X POST http://localhost:3000/api/evaluate \
-H "Content-Type: application/json" \
-d '{"userId": 1, "channel": "email", "notificationType": "transactional", "region": "EU", "datetime": "2026-06-08T14:00:00-04:00"}'Ответ: allow.
| Сид | Данные |
|---|---|
regions |
EU (Europe/London), USA (America/New_York), Russia (Europe/Moscow) |
channels |
sms, email, messenger, push |
notification_types |
transactional, marketing |
default_sources |
email+transactional, email+marketing, push+transactional |
policies |
Запреты: EU/USA sms+marketing, Russia messenger+marketing, Russia messenger+transactional |
npm run migrate # Применить все миграции
npm run rollback # Откатить последнюю миграциюМиграции последовательно создают таблицы в порядке зависимостей: regions → users → channels → notification_types → sources → default_sources → policies → traces.