# Wishpad: продуктовая и техническая спецификация
## 1. Обзор
Wishpad — это веб-приложение для ведения личных списков подарков. Зарегистрированный пользователь создаёт один или несколько списков и делится каждым списком по специальной публичной ссылке.
Основной production-домен проекта: `wishpad.me`.
Гости, получившие ссылку, могут открыть список и забронировать подарок. Владелец списка не должен видеть, какие подарки были забронированы: ни после входа в аккаунт, ни при открытии собственной публичной ссылки.
## 2. Цели
- Дать пользователю возможность зарегистрироваться, войти и вести несколько списков подарков.
- Дать владельцу списка возможность поделиться списком через публичную ссылку.
- Дать гостям возможность бронировать свободные подарки, указав имя.
- Запретить нескольким гостям бронировать один и тот же подарок.
- Скрыть от владельца списка статусы бронирования.
- Дать администратору возможность видеть все списки и управлять пользователями.
- Сделать пользовательскую часть кроссплатформенной: web и mobile app.
- Оставить админку web-only рабочим интерфейсом для tablet/desktop.
- Сделать проект удобным для развёртывания на VPS через Docker и NPM-based frontend tooling.
## 3. Состав MVP
В MVP входит:
- единая auth-страница для регистрации и входа;
- выход из аккаунта не имеет отдельной страницы: это действие в интерфейсе, например кнопка "Выйти", которая вызывает backend logout endpoint;
- роль администратора;
- responsive web-интерфейс для desktop и mobile browsers;
- mobile app через Capacitor для Android/iOS на базе того же Vue frontend;
- mobile app включает только пользовательскую часть: public guest UI и кабинет владельца;
- dashboard владельца со списком wishlists;
- создание, редактирование и удаление wishlist;
- создание, редактирование и удаление подарков;
- публичная ссылка на wishlist;
- публичный guest view;
- бронирование подарка по имени гостя;
- guest cookie для определения собственных бронирований;
- отмена собственной брони через confirmation modal;
- email-уведомления зарегистрированным гостям при новой или отменённой брони в wishlist, где у них уже есть бронь;
- email-уведомления зарегистрированным гостям при изменении или удалении подарка или wishlist, на который они подписаны через бронь;
- SMTP-отправка писем через database queue и Laravel Scheduler;
- файловые audit/application logs с просмотром в админке;
- best-effort импорт метаданных подарка по ссылке;
- представление владельца без статусов бронирования;
- admin dashboard;
- управление пользователями в admin UI;
- просмотр и управление всеми wishlists в admin UI;
- просмотр бронирований и имён гостей в admin UI;
- PostgreSQL migrations;
- Docker-based local environment;
- NPM scripts для разработки и сборки frontend.
## 4. Не входит в MVP
- Роль организатора или совладельца списка.
- Аккаунты гостей.
- Абсолютная защита от намеренного обхода spoiler protection владельцем списка через чистый браузер.
- Redis, отдельный кеш или поисковая индексация.
- Оплаты, трекинг покупки, доставка и расширенные уведомления.
- Сложная ролевая модель с granular permissions.
- Отдельная нативная mobile-разработка на Swift/Kotlin.
Монетизация, future-модель `free`/`plus` и модель "Поддержать проект" описаны отдельно: `MONETIZATION_SPEC.md`.
## 5. Роли
### Владелец списка
Владелец списка — это авторизованный пользователь, который создал wishlist.
Владелец может:
- создавать, редактировать и удалять свои списки;
- добавлять, редактировать, удалять и сортировать подарки;
- открыть публичную ссылку и убедиться, что список существует;
- видеть список и подарки без статусов бронирования.
Владелец не может:
- видеть, забронирован ли конкретный подарок;
- видеть, кто забронировал подарок;
- получить данные бронирования для своего списка через owner API.
### Гость
Гость — это любой человек, у которого есть публичная ссылка на список.
Гость может быть:
- анонимным пользователем без аккаунта;
- зарегистрированным пользователем, который авторизован в приложении.
Гость может:
- открыть публичную страницу списка;
- видеть все подарки;
- видеть, свободен подарок или уже забронирован;
- видеть имя человека, который забронировал подарок;
- забронировать свободный подарок, указав имя;
- забронировать несколько подарков;
- отменить собственную бронь с того же браузера, где она была создана;
- получать email-уведомления о новых и отменённых бронях в wishlist, где у него уже есть бронь, если гость является зарегистрированным пользователем;
- получать email-уведомления об изменении или удалении подарка или wishlist, если гость является зарегистрированным пользователем и подписан на список через бронь.
Гость не может:
- редактировать данные списка;
- редактировать подарки;
- забронировать уже занятый подарок.
### Администратор
Администратор — это авторизованный пользователь с повышенными правами.
Администратор может:
- видеть список всех пользователей;
- создавать, блокировать, разблокировать и удалять пользователей;
- менять базовые данные пользователя;
- видеть все wishlists в системе;
- открывать административную карточку любого wishlist;
- редактировать или удалять wishlist при необходимости модерации;
- видеть список подарков внутри любого wishlist;
- видеть бронирования и имена гостей;
- видеть техническую информацию: даты создания, обновления, активность, количество подарков и бронирований.
Администратор не должен использовать публичную гостевую ссылку как основной способ управления чужими списками. Для этого должна быть отдельная admin-поверхность.
Администратор видит полные данные списков, включая бронирования. Ограничение на просмотр бронирований относится к владельцу списка, чтобы не испортить сюрприз.
## 6. Авторизация
Пользователям нужна полноценная авторизация, потому что один пользователь может вести несколько списков.
MVP-авторизация:
- email;
- password;
- Laravel Sanctum.
Auth implementation:
- web frontend и Laravel API используют Sanctum в first-party SPA режиме;
- mobile app через Capacitor использует Sanctum API tokens / bearer token flow;
- JWT не используется в MVP.
Возможные будущие варианты:
- вход по magic link;
- одноразовый код на почту;
- social login.
Первый администратор:
- первый admin user создаётся отдельной Artisan command;
- email и временный пароль первого администратора берутся из `.env`;
- после первого входа администратор должен сменить пароль;
- интерфейс должен настойчиво требовать смены временного пароля до продолжения работы.
## 7. Приватность и защита от спойлеров
Главное правило приватности:
> Владелец списка не должен получать статусы бронирования для собственного wishlist.
Это правило должно выполняться на backend. Frontend не должен получать данные бронирования и просто скрывать их визуально.
Для owner view API возвращает:
- данные списка;
- данные подарков;
- без `reserved`;
- без `reserved_by`;
- без `reserved_at`;
- без идентификаторов бронирований.
В интерфейсе владельца можно показать нейтральное сообщение:
> Статусы бронирования скрыты, чтобы не испортить сюрприз.
### Владелец открывает публичную ссылку
Если владелец открывает собственную публичную ссылку в авторизованном состоянии, backend определяет владельца по auth session и возвращает версию списка без статусов бронирования.
Если владелец ранее управлял списком в этом же браузере, приложение может сохранить cookie-маркер, который связывает браузер с конкретным публичным токеном wishlist. Это защищает от случайного спойлера, когда владелец уже не авторизован, но открывает собственную публичную ссылку в том же браузере.
Точная cookie-модель описана в разделе `Cookie и идентификация браузера`.
Эта cookie не является полноценной границей безопасности. Владелец может намеренно открыть публичную ссылку в другом браузере, приватном окне или после очистки cookie. Для MVP это принимаемое ограничение.
Специальный режим "preview as guest" для владельца не нужен. Владелец не должен намеренно открывать собственный список как гость через интерфейс приложения.
## 8. Cookie и идентификация браузера
### Cookie владельца
- имя: `owner_wishlist_tokens`;
- значение: массив public tokens списков, связанных с владельцем в этом браузере;
- срок жизни: 1 год;
- не удаляется при logout;
- используется только для anti-spoiler защиты владельца.
### Cookie гостя
- имя: `guest_token`;
- значение: случайный токен гостевого браузера;
- срок жизни: 1 год;
- не удаляется при logout;
- в БД хранится только `guest_token_hash`, не plaintext token;
- используется для определения собственных бронирований и права отмены.
### Поведение cookie гостя
- при первом бронировании backend создаёт гостевой токен, если его ещё нет;
- гостевой токен сохраняется в cookie браузера;
- в `reservations` сохраняется не сам токен, а `guest_token_hash`;
- гость видит отметку "забронировано вами" для броней, созданных из этого же браузера;
- гость может отменить только свою бронь, если cookie соответствует `guest_token_hash`;
- если гость потерял cookie, сменил браузер или устройство, он не сможет самостоятельно отменить бронь через guest UI;
- администратор может отменить любую бронь через admin UI.
Имя гостя:
- frontend может локально запоминать последнее введённое имя гостя в текущем браузере или mobile app storage;
- сохранённое имя используется только для предзаполнения формы следующего бронирования;
- сохранённое имя не даёт права отмены брони и не заменяет `guest_token`.
### Флаги cookie
- production cookies должны быть `Secure`;
- cookies должны быть `HttpOnly`, если frontend не обязан читать их напрямую;
- `guest_token` и `owner_wishlist_tokens` должны быть backend-managed и `HttpOnly`;
- при one-domain схеме предпочтительный `SameSite`: `Lax`;
- frontend узнаёт состояние через API-поля, а не через чтение cookie.
## 9. Правила бронирования
- Один подарок может иметь только одну активную бронь.
- Один гость может забронировать несколько подарков.
- Попытка забронировать уже занятый подарок возвращает конфликт.
- При создании брони пишется audit log событие `reservation.created`.
- При создании брони зарегистрированным гостям, у которых уже есть брони в этом wishlist, отправляется email-уведомление о новой брони другого гостя.
- Владелец wishlist не получает уведомления о новых бронях в своём списке.
- Отмена брони выполняется после подтверждения в интерфейсе.
- Отмена брони гостем разрешена только при совпадении guest cookie с `guest_token_hash`.
- При отмене брони пишется audit log событие `reservation.cancelled`.
- При отмене брони зарегистрированным гостям, у которых есть другие брони в этом wishlist, отправляется email-уведомление об отмене брони.
- Владелец wishlist не получает уведомления об отмене броней в своём списке.
- Имя гостя нужно обрезать по краям и валидировать.
- Пустое имя запрещено.
- Если guest cookie потерян, самостоятельная отмена гостем невозможна. В таком случае бронь может отменить администратор.
Рекомендуемый ответ при конфликте:
- HTTP `409 Conflict`
Рекомендуемый ответ при ошибке валидации:
- HTTP `422 Unprocessable Entity`
## 10. Правила списков и подарков
- Публичная ссылка постоянная для одного wishlist и не регенерируется в MVP.
- Публичная ссылка не отзывается отдельно от деактивации самого wishlist.
- Владелец может деактивировать wishlist, если хочет временно скрыть список и вернуться к нему позже.
- Деактивированный wishlist можно восстановить.
- Владелец может удалить wishlist навсегда отдельным hard delete действием.
- При hard delete wishlist удаляются связанные wishlist_items и reservations.
- Если владелец изменил wishlist, зарегистрированные гости, подписанные на этот wishlist через бронь, получают email-уведомление.
- Если владелец изменил подарок, зарегистрированные гости, подписанные на этот wishlist через бронь, получают email-уведомление.
- Подарок можно удалить, даже если он уже забронирован.
- Связанная бронь удаляется вместе с подарком.
- Если у подарка была бронь, перед удалением пишется audit log событие `reservation.deleted_with_item`.
- Если подарок удалён, зарегистрированные гости, подписанные на этот wishlist через бронь, получают email-уведомление.
- Если бронь принадлежит анонимному гостю, уведомление не отправляется.
- Если wishlist деактивирован или удалён, зарегистрированные гости, подписанные на этот wishlist через бронь, получают email-уведомление.
- Анонимные гости не получают уведомления об удалении wishlist.
- Для MVP это принимаемое ограничение.
## 11. Валидация и лимиты
### Лимиты сущностей
- Один пользователь может иметь не более 256 wishlists.
- Один wishlist может иметь не более 100 подарков.
### users
- `email`: обязателен, `varchar(255)`, уникальный, должен проходить стандартную email-валидацию Laravel.
- `password`: обязателен, минимальная длина определяется auth implementation.
### wishlists
- `title`: обязателен, максимум 128 символов.
- `description`: необязателен, максимум 256 символов.
- `public_token`: генерируется backend, не принимается от frontend.
### wishlist_items
- `title`: обязателен, максимум 128 символов.
- `description`: необязателен, максимум 256 символов.
- `url`: необязателен, хранится как текст без строгой URL-валидации.
- `image_url`: необязателен, хранится как текст без строгой URL-валидации.
- `price`: обязателен, `numeric(12, 2)`, по умолчанию `0`, не может быть отрицательным, допускает дробные значения до 2 знаков после запятой.
- `currency`: обязателен, `varchar(8)`, по умолчанию берётся из глобальных настроек языка пользователя, может быть изменён владельцем или введён как custom currency code.
- `sort_order`: обязателен, integer, по умолчанию `0`.
Custom currency code нужно обрезать по краям, приводить к uppercase и ограничивать буквами/цифрами длиной до 8 символов.
### reservations
- `guest_name`: обязателен, максимум 64 символа.
64 символа достаточно для обычного имени, фамилии, никнейма или подписи гостя. Если нужно написать длинный комментарий, это уже отдельное поле, не имя.
## 12. Уведомления и почта
- Письма отправляются через SMTP.
- SMTP provider: SMTP.bz.
- Шаблоны писем должны быть в визуальном стиле публичной части приложения.
- Пользователь может отключить email-уведомления в настройках аккаунта.
- Master-настройка хранится в `users.email_notifications_enabled`.
- Детальные настройки уведомлений хранятся в `notify_reservations_enabled`, `notify_wishlist_changes_enabled`, `notify_gift_changes_enabled`.
- Email-уведомления отправляются только зарегистрированным пользователям, у которых `email_notifications_enabled = true` и включён соответствующий дочерний тип уведомлений.
- Если пользователь выключил все дочерние типы уведомлений, `email_notifications_enabled` должен автоматически стать `false`.
- Если пользователь включает master-настройку, frontend позволяет включать дочерние типы уведомлений отдельно.
- Владелец wishlist не получает уведомления о бронированиях и отменах в своём списке.
Подписка гостя на wishlist:
- отдельной сущности подписки в MVP нет;
- зарегистрированный гость считается подписанным на wishlist, если у него есть или была бронь в этом wishlist;
- анонимный гость не получает email-уведомления, даже если у него есть бронь;
- подписка используется для уведомлений о новых/отменённых бронях, изменениях и удалениях подарков, изменениях, деактивации и удалении wishlist.
Очередь:
- Для отправки писем используется Laravel database queue.
- Redis для очередей в MVP не используется.
- Laravel Scheduler запускает периодические задачи отправки/повтора писем.
- На VPS scheduler вызывается через cron, обычно каждую минуту командой `php artisan schedule:run`.
- Queue worker обрабатывает задачи отправки писем из database queue.
Ошибки SMTP:
- Если SMTP временно недоступен, задача отправки письма остаётся в очереди и повторяется.
- Количество retry attempts и backoff настраиваются в Laravel job.
- После исчерпания попыток задача попадает в `failed_jobs`.
- Админка должна позволять видеть failed mail jobs или как минимум видеть ошибку в логах.
- Для MVP допустимо не блокировать пользовательское действие из-за ошибки отправки письма.
Local development:
- для локальной разработки используется Mailpit или `log` mail driver;
- production SMTP.bz credentials не должны использоваться в обычной локальной разработке;
- Mailpit можно подключить как optional Docker service для проверки писем в браузере.
## 13. Подарки, ссылки и изображения
### Импорт метаданных по ссылке
Подарок можно создавать вручную или на основе ссылки.
Если пользователь указал ссылку на подарок, backend выполняет best-effort импорт метаданных:
- название;
- описание;
- изображение;
- цена;
- дополнительные данные, если они доступны и полезны.
Источники метаданных:
- Schema.org JSON-LD;
- microdata;
- Open Graph meta tags;
- Twitter Card meta tags;
- обычные HTML meta tags как fallback.
Правила:
- импорт метаданных не должен быть обязательным для создания подарка;
- импорт разрешён только для `https://` URL;
- если сайт не отдаёт микроразметку или блокирует запрос, пользователь вводит данные вручную;
- frontend запускает импорт автоматически после вставки ссылки на товар с debounce;
- при изменении ссылки на другую frontend запускает импорт заново;
- import endpoint возвращает найденные данные для preview и не сохраняет подарок;
- применение найденных данных выполняется в форме на frontend после подтверждения владельца;
- применение preview может один раз перезаписать текущие поля формы для этой ссылки;
- пользователь может отредактировать любые импортированные данные перед сохранением;
- если валюта пришла из метаданных, она подставляется в форму;
- если валюта не пришла из метаданных, используется валюта по умолчанию из глобальных настроек языка пользователя;
- если валюта пришла из метаданных, но её нет в системном списке валют, frontend может подставить её как custom currency code без добавления в глобальный справочник;
- если цена не пришла из метаданных, используется `0`;
- marketplace-specific scraping в MVP не делаем;
- для популярных маркетплейсов вроде Ozon, Wildberries, Яндекс Маркет и Lamoda можно ожидать наличие части метаданных, но приложение не должно зависеть от этого;
- ошибки импорта не должны блокировать создание подарка.
Отображение ссылки на подарок:
- backend или frontend может вычислять домен ссылки и человекочитаемый marketplace label;
- marketplace label нужен только для UI-кнопки перехода, а не для marketplace-specific scraping;
- MVP allowlist: Ozon, Wildberries, Яндекс Маркет, Lamoda;
- если домен не распознан, UI показывает универсальное действие `Открыть ссылку`.
SSRF/security правила для импорта:
- запрещены `localhost`, loopback, private IP ranges, link-local IP ranges и internal hostnames;
- backend должен проверять итоговый IP после DNS resolution;
- redirects разрешены только с лимитом;
- каждый redirect тоже должен проходить SSRF-проверку;
- обязательный timeout запроса;
- максимальный размер читаемого HTML-ответа: 2 MB;
- backend должен отправлять понятный `User-Agent`, например `WishlistBot/1.0 (+app-url)`;
- изображения не скачиваются backend в MVP, сохраняется только найденный `image_url`.
### Изображения
В MVP изображение подарка хранится как ссылка:
- `wishlist_items.image_url`;
- пользователь может вставить ссылку на изображение вручную;
- импорт метаданных может заполнить `image_url`, если изображение найдено.
Загрузка файлов в MVP не входит.
Будущее расширение:
- загрузка изображений пользователем;
- хранение изображений в S3-compatible storage;
- анализ стоимости S3/storage до включения в scope;
- fallback на внешнюю ссылку остаётся доступным даже после добавления upload.
## 14. Безопасность public token и rate limiting
Public token:
- `public_token` генерируется только backend.
- Токен должен создаваться через cryptographically secure random generator.
- Токен не должен быть UUID wishlist.
- Минимальная энтропия: 128 bits.
- Рекомендуемый формат: URL-safe random string длиной 32+ символа.
- `public_token` хранится в `wishlists.public_token` и уникален.
Rate limiting для MVP:
- Auth endpoints: 5 попыток входа в минуту по IP и email.
- Public wishlist endpoint: 120 запросов в минуту по IP.
- Reservation create/delete endpoints: 10 действий в минуту по IP, `public_token` и guest cookie.
- Metadata import endpoint: 10 запросов в час на пользователя и 30 запросов в час по IP.
- Admin endpoints: 120 запросов в минуту по auth user и IP.
Значения лимитов должны быть вынесены в конфиг и могут быть скорректированы после первых реальных данных. Rate limiting должен быть включён с первого MVP.
## 15. Модель удаления аккаунта
В проекте различаются два вида удаления.
### Деактивация из интерфейса
Деактивация происходит, когда пользователь сам удаляет аккаунт или администратор удаляет аккаунт через admin UI.
Правила:
- пользователь физически остаётся в базе;
- у пользователя заполняется `deleted_at`;
- у пользователя заполняется `restore_until`, обычно `deleted_at + 6 months`;
- пользователь не может войти в аккаунт;
- wishlists пользователя становятся неактивными;
- публичные ссылки на wishlists пользователя перестают открывать списки для гостей;
- данные можно восстановить до даты `restore_until`.
### Физическое удаление системой
Физическое удаление выполняет система для старых деактивированных аккаунтов, у которых истёк срок восстановления.
Правила:
- пользователь удаляется из базы;
- wishlists пользователя удаляются каскадно;
- wishlist_items удаляются каскадно;
- reservations удаляются каскадно;
- восстановление после физического удаления невозможно.
Срок восстановления:
- 6 месяцев после деактивации аккаунта.
## 16. Основные сценарии
### Владелец создаёт wishlist
1. Пользователь регистрируется или входит.
2. Пользователь создаёт wishlist.
3. Система генерирует уникальный `public_token`.
4. Пользователь добавляет подарки.
5. Пользователь копирует публичную ссылку и отправляет её гостям.
### Гость бронирует подарок
1. Гость открывает `/wishlist/:publicToken`.
2. Система загружает публичный wishlist.
3. Гость видит свободные и забронированные подарки.
4. Гость выбирает свободный подарок.
5. Гость вводит имя.
6. Если гость авторизован, система связывает бронь с его `user_uuid`.
7. Система создаёт бронь, если подарок всё ещё свободен.
8. Подарок становится забронированным для всех гостей.
### Гость отменяет бронь
1. Гость открывает публичный wishlist.
2. Гость выбирает забронированный подарок.
3. Система показывает confirmation modal: "Вы уверены, что хотите отменить бронь?"
4. Гость подтверждает действие.
5. Backend проверяет guest cookie.
6. Система удаляет бронь, если cookie соответствует `guest_token_hash` брони.
7. Подарок снова становится свободным.
### Владелец открывает собственную публичную ссылку
1. Владелец открывает `/wishlist/:publicToken`.
2. Backend определяет владельца через auth session или `owner_wishlist_tokens`.
3. Система возвращает публичное представление владельца без статусов бронирования.
4. Владелец видит список и подарки, но не видит статусы бронирования.
## 17. Основные сущности
Техническое описание БД, полей, constraints, индексов, связей и миграционного порядка вынесено в отдельный документ: `DATABASE_SPEC.md`.
Общее правило:
- все первичные ключи сущностей используют UUID;
- все внешние ключи на сущности тоже используют UUID;
- даты хранятся как `timestamptz`;
- `created_at` и `updated_at` обязательны для основных сущностей.
### users
Зарегистрированные владельцы списков.
| Поле | Тип PostgreSQL | NULL | Описание |
| --- | --- | --- | --- |
| `uuid` | `uuid` | нет | Первичный ключ пользователя. |
| `email` | `varchar(255)` | нет | Email для входа. Должен быть уникальным. |
| `password` | `varchar(255)` | нет | Хеш пароля. |
| `role` | `varchar(32)` | нет | Роль пользователя: `user` или `admin`. |
| `is_blocked` | `boolean` | нет | Заблокирован ли пользователь. По умолчанию `false`. |
| `must_change_password` | `boolean` | нет | Нужно ли попросить пользователя сменить пароль после входа. По умолчанию `false`. Для первого администратора `true`. |
| `email_notifications_enabled` | `boolean` | нет | Разрешены ли email-уведомления. По умолчанию `true`. |
| `notify_reservations_enabled` | `boolean` | нет | Уведомления о новых и отменённых бронях. По умолчанию `true`. |
| `notify_wishlist_changes_enabled` | `boolean` | нет | Уведомления об изменении, деактивации и удалении wishlist. По умолчанию `true`. |
| `notify_gift_changes_enabled` | `boolean` | нет | Уведомления об изменении и удалении подарков. По умолчанию `true`. |
| `locale` | `varchar(16)` | нет | Язык интерфейса пользователя. По умолчанию `ru`. |
| `timezone` | `varchar(64)` | нет | Timezone пользователя, определяется автоматически. |
| `date_format` | `varchar(32)` | да | Пользовательский формат даты. `NULL` означает формат языка по умолчанию. |
| `time_format` | `varchar(32)` | да | Пользовательский формат времени. `NULL` означает формат языка по умолчанию. |
| `number_format` | `varchar(32)` | да | Пользовательский формат чисел. `NULL` означает формат языка по умолчанию. |
| `deleted_at` | `timestamptz` | да | Дата деактивации аккаунта. `NULL` означает активный аккаунт. |
| `restore_until` | `timestamptz` | да | Дата, до которой аккаунт можно восстановить после деактивации. |
| `created_at` | `timestamptz` | нет | Дата создания. |
| `updated_at` | `timestamptz` | нет | Дата обновления. |
Минимальные роли:
- `user`
- `admin`
Для MVP достаточно поля `role`. Более сложную permission-модель можно добавить позже.
Ограничения:
- primary key: `uuid`;
- unique: `email`;
- check или enum-level validation: `role` только `user` или `admin`.
### wishlists
Список подарков, принадлежащий одному пользователю.
| Поле | Тип PostgreSQL | NULL | Описание |
| --- | --- | --- | --- |
| `uuid` | `uuid` | нет | Первичный ключ wishlist. |
| `user_uuid` | `uuid` | нет | Владелец списка. FK на `users.uuid`. |
| `title` | `varchar(128)` | нет | Название списка. |
| `description` | `varchar(256)` | да | Описание списка. |
| `public_token` | `varchar(128)` | нет | Секретный публичный токен для доступа по ссылке. |
| `is_active` | `boolean` | нет | Активен ли список. По умолчанию `true`. |
| `deleted_at` | `timestamptz` | да | Дата деактивации списка. `NULL` означает активный список. |
| `created_at` | `timestamptz` | нет | Дата создания. |
| `updated_at` | `timestamptz` | нет | Дата обновления. |
Правила:
- `public_token` должен быть уникальным;
- `public_token` должен быть сложно угадываемым;
- публичный доступ работает через `public_token`, а не через `uuid`.
Ограничения:
- primary key: `uuid`;
- foreign key: `user_uuid` -> `users.uuid`;
- unique: `public_token`.
### wishlist_items
Подарок внутри списка.
| Поле | Тип PostgreSQL | NULL | Описание |
| --- | --- | --- | --- |
| `uuid` | `uuid` | нет | Первичный ключ подарка. |
| `wishlist_uuid` | `uuid` | нет | Wishlist, которому принадлежит подарок. FK на `wishlists.uuid`. |
| `title` | `varchar(128)` | нет | Название подарка. |
| `description` | `varchar(256)` | да | Описание подарка. |
| `url` | `text` | да | Ссылка на подарок. |
| `image_url` | `text` | да | Ссылка на изображение подарка. |
| `price` | `numeric(12, 2)` | нет | Ориентировочная цена. По умолчанию `0`. Не может быть отрицательной. |
| `currency` | `varchar(8)` | нет | Валюта цены подарка. По умолчанию берётся из языка пользователя. Может быть custom currency code. |
| `sort_order` | `integer` | нет | Порядок отображения. По умолчанию `0`. |
| `created_at` | `timestamptz` | нет | Дата создания. |
| `updated_at` | `timestamptz` | нет | Дата обновления. |
Ограничения:
- primary key: `uuid`;
- foreign key: `wishlist_uuid` -> `wishlists.uuid`;
- index: `wishlist_uuid`;
- index: `wishlist_uuid, sort_order`.
### reservations
Бронь подарка гостем.
| Поле | Тип PostgreSQL | NULL | Описание |
| --- | --- | --- | --- |
| `uuid` | `uuid` | нет | Первичный ключ бронирования. |
| `wishlist_item_uuid` | `uuid` | нет | Забронированный подарок. FK на `wishlist_items.uuid`. |
| `guest_user_uuid` | `uuid` | да | Зарегистрированный пользователь-гость. FK на `users.uuid`. `NULL` для анонимного гостя. |
| `guest_name` | `varchar(64)` | нет | Имя гостя, который забронировал подарок. |
| `guest_token_hash` | `varchar(255)` | нет | Хеш гостевого cookie-токена, который даёт право отменить бронь. |
| `created_at` | `timestamptz` | нет | Дата создания брони. |
| `updated_at` | `timestamptz` | нет | Дата обновления брони. |
Ограничения:
- primary key: `uuid`;
- foreign key: `wishlist_item_uuid` -> `wishlist_items.uuid`;
- nullable foreign key: `guest_user_uuid` -> `users.uuid`;
- unique: `wishlist_item_uuid`.
Это гарантирует на уровне базы: один подарок может быть забронирован только одним человеком.
### app_currencies
Глобальный список валют, доступных в формах подарков.
| Поле | Тип PostgreSQL | NULL | Описание |
| --- | --- | --- | --- |
| `uuid` | `uuid` | нет | Первичный ключ валюты. |
| `code` | `varchar(8)` | нет | Код валюты, например `RUB`, `USD`, `EUR`. |
| `symbol` | `varchar(8)` | да | Символ валюты, например `₽`, `$`, `€`. |
| `title` | `varchar(64)` | нет | Название валюты для интерфейса. |
| `is_enabled` | `boolean` | нет | Доступна ли валюта пользователям. По умолчанию `true`. |
| `created_at` | `timestamptz` | нет | Дата создания. |
| `updated_at` | `timestamptz` | нет | Дата обновления. |
Ограничения:
- primary key: `uuid`;
- unique: `code`.
Правила:
- `GET /api/app/currencies` возвращает только валюты с `is_enabled = true`;
- отключение валюты запрещает выбирать её для новых подарков;
- существующие подарки с отключённой валютой остаются без изменений;
- custom currency code в `wishlist_items.currency` не добавляет валюту в `app_currencies`.
### app_locale_settings
Глобальные языковые настройки приложения.
| Поле | Тип PostgreSQL | NULL | Описание |
| --- | --- | --- | --- |
| `uuid` | `uuid` | нет | Первичный ключ настройки языка. |
| `locale` | `varchar(16)` | нет | Код языка, например `ru` или `en`. |
| `is_enabled` | `boolean` | нет | Доступен ли язык пользователям. По умолчанию `true`. |
| `date_format` | `varchar(32)` | нет | Глобальный формат даты для языка. |
| `time_format` | `varchar(32)` | нет | Глобальный формат времени для языка. |
| `number_format` | `varchar(32)` | нет | Глобальный формат чисел для языка. |
| `currency` | `varchar(8)` | нет | Валюта по умолчанию для языка, например `RUB`. FK на `app_currencies.code`. |
| `created_at` | `timestamptz` | нет | Дата создания. |
| `updated_at` | `timestamptz` | нет | Дата обновления. |
Ограничения:
- primary key: `uuid`;
- unique: `locale`;
- foreign key: `currency` -> `app_currencies.code`.
## 18. Логи и аудит
Для MVP аудит ведётся в файлах, а не в отдельной таблице событий.
Инструменты:
- Laravel Logging на базе Monolog для записи логов;
- отдельный audit log channel для доменных событий;
- structured JSON lines как формат audit log;
- `opcodesio/log-viewer` как готовое Laravel-решение для просмотра файловых логов в админке.
Почему файлы:
- проще стартовать без отдельной audit-схемы;
- логи хорошо подходят для технической диагностики;
- их можно ротировать, архивировать и удалять;
- админка может показывать связанные логи из файлов через log viewer и фильтры.
MVP logs:
- бронирование подарка: `reservation.created`;
- отмена брони: `reservation.cancelled`;
- удаление подарка с бронью: `reservation.deleted_with_item`;
- вход пользователя: `auth.login`;
- создание wishlist: `wishlist.created`;
- редактирование wishlist: `wishlist.updated`;
- деактивация wishlist: `wishlist.deactivated`;
- восстановление wishlist: `wishlist.restored`;
- удаление wishlist навсегда: `wishlist.deleted`;
- создание подарка: `wishlist_item.created`;
- редактирование подарка: `wishlist_item.updated`;
- удаление подарка: `wishlist_item.deleted`;
- регистрация пользователя: `user.registered`;
- создание пользователя администратором: `user.created`;
- редактирование пользователя: `user.updated`;
- деактивация пользователя: `user.deactivated`;
- удаление пользователя навсегда: `user.deleted`.
Future logs:
- неуспешные попытки входа и подозрительная активность;
- просмотр публичного wishlist;
- rate limit events;
- отправка и ошибки email-уведомлений как отдельный расширенный журнал;
- действия с настройками уведомлений;
- импорт данных или массовые операции.
Требования к логам:
- хранить structured JSON lines;
- включать `event`, `actor_type`, `actor_uuid`, `entity_type`, `entity_uuid`, `ip`, `user_agent`, `created_at`;
- не писать в лог plaintext guest token;
- не писать в лог password, password hash, SMTP credentials и другие секреты;
- админка должна уметь открывать связанные логи из карточек пользователей, wishlists, подарков и бронирований;
- должна быть ручная очистка логов из админки;
- должна быть архивация старых логов.
Retention:
- активные логи хранятся 90 дней;
- после 90 дней логи архивируются автоматически;
- архивы логов хранятся 12 месяцев;
- после 12 месяцев архивы могут удаляться автоматически;
- администратор может вручную удалить выбранные логи или архивы через админку.
`spatie/laravel-activitylog` не выбираем для MVP, потому что он хранит аудит в таблице `activity_log`, а выбранное направление — файловые логи.
## 19. Маршруты
OpenAPI-контракт MVP вынесен в отдельный документ: `OPENAPI.yaml`.
Правила ведения документации вынесены в отдельный документ: `DOCUMENTATION_WORKFLOW.md`.
Решение по OpenAPI-документации:
- source of truth: `docs/OPENAPI.yaml`;
- viewer: Scalar;
- рекомендуемый backend route: `/api/docs`;
- в production доступ к `/api/docs` должен быть закрыт admin-авторизацией или отключён конфигурацией.
### Frontend routes
Маршруты владельца:
- `/auth`
- `/dashboard`
- `/wishlists/:wishlistUuid/edit`
- `/settings`
Публичный маршрут для гостей:
- `/wishlist/:publicToken`
Admin routes:
- `/admin`
- `/admin/users`
- `/admin/users/:userUuid`
- `/admin/wishlists`
- `/admin/wishlists/:wishlistUuid`
- `/admin/logs`
- `/admin/failed-jobs`
- `/admin/settings`
### Auth API
Auth endpoints:
| Метод | Endpoint | Что делает |
| --- | --- | --- |
| `POST` | `/api/auth` | Единая точка входа/регистрации. Режим определяется телом запроса: login или register. |
| `POST` | `/api/auth/logout` | Завершает auth session/token, но не удаляет `owner_wishlist_tokens` и `guest_token`. |
| `GET` | `/api/auth/me` | Возвращает текущего авторизованного пользователя и его настройки. |
| `PATCH` | `/api/auth/password` | Меняет пароль текущего пользователя, включая обязательную смену временного пароля первого администратора. |
### Owner API
Account endpoints:
| Метод | Endpoint | Что делает |
| --- | --- | --- |
| `PATCH` | `/api/me/settings` | Обновляет настройки текущего пользователя: email-уведомления, язык, timezone и форматы. |
| `PATCH` | `/api/me/deactivate` | Деактивирует аккаунт текущего пользователя и связанные wishlists. |
App settings endpoints:
| Метод | Endpoint | Что делает |
| --- | --- | --- |
| `GET` | `/api/app/currencies` | Возвращает активные валюты для пользовательских форм, включая gift form. |
Wishlist endpoints:
| Метод | Endpoint | Что делает |
| --- | --- | --- |
| `GET` | `/api/wishlists` | Возвращает списки текущего владельца без статусов бронирования. |
| `POST` | `/api/wishlists` | Создаёт новый wishlist для текущего владельца и генерирует `public_token`. |
| `GET` | `/api/wishlists/:wishlistUuid` | Возвращает один wishlist текущего владельца без статусов бронирования. |
| `PATCH` | `/api/wishlists/:wishlistUuid` | Обновляет данные wishlist текущего владельца. |
| `PATCH` | `/api/wishlists/:wishlistUuid/deactivate` | Деактивирует wishlist текущего владельца и делает публичную ссылку недоступной для гостей. |
| `PATCH` | `/api/wishlists/:wishlistUuid/restore` | Восстанавливает деактивированный wishlist текущего владельца. |
| `DELETE` | `/api/wishlists/:wishlistUuid` | Удаляет wishlist текущего владельца навсегда. |
Wishlist item endpoints:
| Метод | Endpoint | Что делает |
| --- | --- | --- |
| `POST` | `/api/wishlists/:wishlistUuid/items` | Добавляет подарок в wishlist текущего владельца. |
| `PATCH` | `/api/wishlists/:wishlistUuid/items/:itemUuid` | Обновляет подарок в wishlist текущего владельца. |
| `DELETE` | `/api/wishlists/:wishlistUuid/items/:itemUuid` | Удаляет подарок из wishlist текущего владельца. |
| `POST` | `/api/wishlists/:wishlistUuid/items/import-metadata` | Выполняет best-effort импорт метаданных подарка по ссылке без сохранения подарка. |
Owner API не должен возвращать статусы бронирования для списков владельца.
Owner wishlist list rules:
- `GET /api/wishlists` должен поддерживать фильтр по состоянию списка: active/deactivated;
- списки сортируются по `created_at desc`, последние созданные сверху;
- ответ может включать агрегаты для owner dashboard: всего списков, активных списков, деактивированных списков и общее количество подарков;
- ответ не должен включать количество бронирований, статусы бронирования или имена гостей.
### Admin API
Admin list endpoints:
Все табличные списки в админке должны поддерживать фильтрацию, сортировку и пагинацию на уровне API.
Это относится минимум к:
- `/api/admin/users`;
- `/api/admin/wishlists`;
- `/api/admin/logs`;
- `/api/admin/queue/failed-jobs`;
- `/api/admin/settings/locales`;
- `/api/admin/settings/currencies`.
Базовые query-параметры:
- `page` - номер страницы;
- `perPage` - количество записей на странице;
- `sort` - поле сортировки;
- `direction` - направление сортировки: `asc` или `desc`;
- `filters[...]` - набор полей фильтрации для конкретной таблицы.
Поиск в admin tables не входит в MVP. Параметр `search` можно добавить позже как future extension.
Ответ табличного endpoint должен возвращать данные и мета-информацию пагинации:
```json
{
"data": [],
"meta": {
"page": 1,
"perPage": 25,
"total": 100
}
}
```
Admin dashboard endpoints:
| Метод | Endpoint | Что делает |
| --- | --- | --- |
| `GET` | `/api/admin/dashboard` | Возвращает обзорную статистику для админского dashboard: пользователи, wishlists, подарки, брони и failed jobs. |
Admin user endpoints:
| Метод | Endpoint | Что делает |
| --- | --- | --- |
| `GET` | `/api/admin/users` | Возвращает список пользователей для админки с фильтрацией, сортировкой и пагинацией. |
| `POST` | `/api/admin/users` | Создаёт пользователя через админку. |
| `GET` | `/api/admin/users/:userUuid` | Возвращает административную карточку пользователя. |
| `PATCH` | `/api/admin/users/:userUuid` | Обновляет данные пользователя через админку. |
| `DELETE` | `/api/admin/users/:userUuid` | Деактивирует аккаунт пользователя и связанные wishlists. |
| `POST` | `/api/admin/users/:userUuid/block` | Блокирует пользователя без удаления аккаунта. |
| `POST` | `/api/admin/users/:userUuid/unblock` | Снимает блокировку пользователя. |
Admin wishlist endpoints:
| Метод | Endpoint | Что делает |
| --- | --- | --- |
| `GET` | `/api/admin/wishlists` | Возвращает все wishlists в системе с фильтрацией, сортировкой и пагинацией. |
| `GET` | `/api/admin/wishlists/:wishlistUuid` | Возвращает административную карточку wishlist, включая подарки, бронирования и имена гостей. |
| `PATCH` | `/api/admin/wishlists/:wishlistUuid` | Обновляет wishlist через админку. |
| `DELETE` | `/api/admin/wishlists/:wishlistUuid` | Деактивирует или удаляет wishlist через админку. |
Admin log endpoints:
| Метод | Endpoint | Что делает |
| --- | --- | --- |
| `GET` | `/api/admin/logs` | Возвращает файловые логи с фильтрацией, сортировкой и пагинацией. |
| `POST` | `/api/admin/logs/archive` | Архивирует старые логи. |
| `DELETE` | `/api/admin/logs` | Удаляет выбранные или старые логи вручную через админку. |
Admin queue endpoints:
| Метод | Endpoint | Что делает |
| --- | --- | --- |
| `GET` | `/api/admin/queue/failed-jobs` | Возвращает failed jobs, включая ошибки отправки email, с фильтрацией, сортировкой и пагинацией. |
| `POST` | `/api/admin/queue/failed-jobs/:jobUuid/retry` | Повторяет failed job, если это поддерживается реализацией. |
Admin settings endpoints:
| Метод | Endpoint | Что делает |
| --- | --- | --- |
| `GET` | `/api/admin/settings/locales` | Возвращает глобальные языковые настройки приложения. |
| `PATCH` | `/api/admin/settings/locales/:localeUuid` | Обновляет глобальные форматы и доступность языка. |
| `GET` | `/api/admin/settings/currencies` | Возвращает глобальный список валют. |
| `POST` | `/api/admin/settings/currencies` | Добавляет валюту в глобальный список. |
| `PATCH` | `/api/admin/settings/currencies/:currencyUuid` | Обновляет валюту или её доступность. |
Admin API должен быть защищён отдельной проверкой роли `admin`.
### Public API
Публичный wishlist endpoint:
| Метод | Endpoint | Что делает |
| --- | --- | --- |
| `GET` | `/api/public/wishlists/:publicToken` | Возвращает публичный wishlist по токену. Для гостей включает статусы бронирования, для владельца возвращает представление без статусов бронирования. |
Reservation endpoints:
| Метод | Endpoint | Что делает |
| --- | --- | --- |
| `POST` | `/api/public/wishlists/:publicToken/items/:itemUuid/reservation` | Бронирует свободный подарок на имя гостя и создаёт guest cookie, если её ещё нет. |
| `DELETE` | `/api/public/wishlists/:publicToken/items/:itemUuid/reservation` | Отменяет бронь подарка, если guest cookie соответствует `guest_token_hash` брони. |
Тело запроса для бронирования:
```json
{
"name": "Alex"
}
```
Отмена бронирования выполняется без тела запроса. Frontend должен предварительно показать confirmation modal.
Backend разрешает отмену только если guest cookie соответствует `guest_token_hash` брони.
## 20. Правила API-ответов
### Public wishlist response для гостя
Гость получает статусы бронирования.
Public response содержит блок доступа:
```json
{
"access": {
"viewer": "guest",
"reservationsVisible": true
}
}
```
Пример подарка:
```json
{
"uuid": "2e1a879c-81a2-4ad7-b6e0-cbbf21d01f75",
"title": "Mechanical keyboard",
"description": "Any compact model is fine",
"url": "https://example.com/keyboard",
"urlHost": "example.com",
"marketplaceLabel": null,
"imageUrl": "https://example.com/image.jpg",
"price": "120.00",
"currency": "RUB",
"reservation": {
"status": "reserved",
"guestName": "Alex",
"reservedByCurrentGuest": false
}
}
```
Для свободного подарка:
```json
{
"reservation": {
"status": "available",
"guestName": null
}
}
```
### Public wishlist response для владельца
Владелец не получает статусы бронирования.
Public response содержит блок доступа:
```json
{
"access": {
"viewer": "owner",
"reservationsVisible": false
}
}
```
Пример подарка:
```json
{
"uuid": "2e1a879c-81a2-4ad7-b6e0-cbbf21d01f75",
"title": "Mechanical keyboard",
"description": "Any compact model is fine",
"url": "https://example.com/keyboard",
"urlHost": "example.com",
"marketplaceLabel": null,
"imageUrl": "https://example.com/image.jpg",
"price": "120.00",
"currency": "RUB"
}
```
Для владельца поле `reservation` в подарках отсутствует полностью.
Frontend показывает сообщение о скрытых статусах бронирования, когда получает `access.viewer = "owner"` и `access.reservationsVisible = false`.
## 21. Стандарт API-ошибок
API использует общепринятый JSON-формат ошибок Laravel.
Общий формат:
```json
{
"message": "Validation failed",
"errors": {
"title": ["The title field is required."]
}
}
```
Основные HTTP statuses:
- `400 Bad Request`: некорректный запрос общего характера.
- `401 Unauthorized`: пользователь не авторизован.
- `403 Forbidden`: пользователь авторизован, но не имеет прав.
- `404 Not Found`: сущность или публичный токен не найдены.
- `409 Conflict`: подарок уже забронирован или действие конфликтует с текущим состоянием.
- `422 Unprocessable Entity`: ошибка валидации.
- `429 Too Many Requests`: сработал rate limit.
- `500 Internal Server Error`: непредвиденная ошибка backend.
Frontend должен показывать пользователю дружелюбный текст ошибки, но для отладки сохранять технический `message` в логах клиента, если это будет реализовано.
## 22. Локализация, форматы и timezone
MVP language:
- основной язык интерфейса: русский.
Frontend:
- с первого MVP подключается i18n-подход, чтобы не зашивать строки напрямую в компоненты;
- тексты интерфейса должны храниться в translation files;
- даже если в MVP доступен только русский язык, архитектура должна позволять добавить другие языки.
Пользовательские настройки:
- пользователь может выбрать язык интерфейса;
- timezone пользователя определяется автоматически и не редактируется вручную в owner settings MVP;
- пользователь может выбрать формат даты/времени и чисел, если это поддерживается выбранной локалью.
Глобальные настройки администратора:
- администратор может включать поддерживаемые языки приложения;
- для каждого языка администратор может задать глобальные форматы дат, времени, чисел и валюты;
- пользовательские настройки имеют приоритет над глобальными настройками языка.
## 23. Тестовая стратегия
Backend tests:
- feature tests для auth;
- feature tests для owner/public/admin API;
- privacy tests: владелец не получает `reservation`;
- reservation race condition test: два гостя одновременно бронируют один подарок, один получает успех, второй `409 Conflict`;
- tests для guest cookie и отмены собственной брони;
- tests для email notification jobs;
- tests для metadata import SSRF-защиты;
- tests для soft/hard delete.
Frontend tests:
- component tests для ключевых форм и состояний;
- tests для frontend validation schemas на VeeValidate + Zod;
- e2e smoke tests для owner flow, guest flow и admin flow;
- проверка mobile viewport для public/owner UI.
Mobile tests:
- Capacitor smoke test для запуска приложения;
- проверка auth;
- проверка открытия public wishlist;
- проверка бронирования и отмены.
## 24. Технологический стек
Архитектура приложений:
- Backend и frontend — два отдельных приложения.
- Backend application: Laravel API.
- Frontend application: Vue 3 + Vite.
- Frontend общается с backend только через API.
- Laravel не рендерит Vue-страницы и не является Laravel-first frontend host.
- Web frontend и API доступны на одном домене.
- API живёт под префиксом `/api` и следует RESTful conventions.
- В production frontend собирается в static assets и отдаётся отдельно через Nginx или другой web server.
- Mobile app через Capacitor использует тот же frontend application и тот же Laravel API.
Frontend:
- Vue ecosystem.
- NPM scripts.
- Vite как build tool для отдельного Vue-приложения.
- PrimeVue используется как единая UI-библиотека для публичной части, кабинета владельца и админки.
- Lucide используется как единый icon set для публичной части, кабинета владельца и админки.
- PrimeIcons не используется как самостоятельный icon set, даже если отдельные PrimeVue примеры его подключают.
- VeeValidate + Zod используются для frontend validation.
- Frontend validation нужна для UX и не заменяет backend validation.
Backend validation:
- Laravel Form Request validation используется как backend source of truth;
- все критичные правила валидации должны проверяться на backend независимо от frontend;
- ограничения из раздела `Валидация и лимиты` должны быть отражены во frontend schemas там, где это улучшает UX.
Кроссплатформенность:
- Основная пользовательская часть должна быть web-first и responsive.
- Web-версия должна корректно работать на desktop и mobile browsers.
- Mobile app должен собираться из той же Vue codebase через Capacitor.
- Android и iOS приложения используют backend API Laravel.
- Mobile app не должен требовать отдельной бизнес-логики, отличной от web-версии.
- Платформенные отличия должны быть изолированы в небольшом слое adapters/services.
- Mobile app включает только user-facing часть: public guest UI и owner UI.
- Admin UI не входит в mobile app.
- Admin UI не обязан поддерживать телефонный viewport; минимальная целевая ширина — tablet viewport.
Выбранное направление для mobile app:
- Vue 3 + Vite.
- Capacitor как native runtime для Android/iOS.
- Общие UI-компоненты и бизнес-логика для web и mobile.
- Responsive mobile-first public/owner UI.
- PrimeVue используется и для публичной/user-facing части, и для admin UI.
Почему Capacitor:
- хорошо ложится на web-first Vue-приложение;
- позволяет добавить Android/iOS без переписывания продукта на Swift/Kotlin;
- даёт доступ к native APIs через plugins, если позже понадобятся push notifications, share sheet, camera или deep links;
- проще для MVP, чем отдельная нативная разработка;
- позволяет включить mobile app в MVP без отдельной нативной разработки.
Подход для public/owner UI:
- Vue 3.
- Vite.
- PrimeVue как основной UI component library.
- базовый PrimeVue preset: `Aura`;
- на старте MVP используется PrimeVue `Aura` с default tokens;
- отдельный Wishpad custom preset через `definePreset(Aura, ...)` создаётся позже, после появления первых UI-экранов;
- Lucide как основной набор иконок.
- Собственный визуальный стиль поверх PrimeVue.
- Mobile-first responsive layout.
- Gift cards, wishlist pages, forms, dialogs, toast-уведомления и loading states строятся на PrimeVue компонентах.
- Публичная часть не должна выглядеть как админка: PrimeVue используется как техническая база компонентов, а не как готовый визуальный стиль.
Подход для admin UI:
- Vue 3.
- Vite.
- PrimeVue как основной UI component library.
- Lucide как основной набор иконок.
- Собственный спокойный admin layout.
- Таблицы с сортировкой и фильтрами.
- Формы редактирования пользователей и списков.
- Layout с sidebar navigation.
Почему PrimeVue:
- не тянет за собой тяжёлую чужую архитектуру;
- даёт готовые компоненты для таблиц, форм, диалогов, меню, кнопок, toast-уведомлений и пагинации;
- хорошо подходит для CRUD-админки;
- позволяет держать публичную часть, кабинет владельца и админку в одном Vue ecosystem без покупки коммерческого шаблона;
- имеет MIT-лицензию, поэтому подходит для свободного использования в проекте.
В MVP PrimeVue используется:
- в public wishlist pages;
- в owner dashboard;
- в admin dashboard;
- для списков пользователей, списков wishlists, просмотра бронирований, фильтрации, пагинации и форм редактирования.
Backend:
- Laravel.
- PostgreSQL.
- Laravel migrations для управления схемой.
Infrastructure:
- Docker.
- Docker Compose для локальной разработки.
- Nginx.
- PHP-FPM.
- PostgreSQL container.
- Node container для frontend development/build или локальный Node для разработки.
- Laravel database queue.
- Laravel Scheduler через cron.
Docker services:
- `backend`: Laravel API на PHP-FPM.
- `frontend`: dev/build service для Vue 3 + Vite. В production он не обязан быть постоянно запущенным, потому что frontend собирается в static assets.
- `postgres`: PostgreSQL database.
- `nginx`: отдаёт frontend static assets и проксирует API-запросы в backend.
- `queue`: Laravel queue worker для database queue.
- `scheduler`: cron/scheduler service для `php artisan schedule:run`.
- `mailpit`: optional service для локальной проверки писем, если не используется `log` mail driver.
Redis для MVP не нужен.
Redis обычно используют для:
- кеширования данных;
- очередей;
- pub/sub;
- rate limiting storage;
- сессий;
- быстрых временных ключей.
Redis не кеширует код и не нужен для индексации контента в нашем MVP. Для очередей используем database queue, для хранения данных PostgreSQL, для поиска/индексации отдельного решения пока не требуется.
## 25. Домены и environment
Production routing:
- основной production-домен: `wishpad.me`;
- web frontend и API доступны на одном домене `wishpad.me`;
- frontend отдаётся с корня домена;
- API доступен под `/api`;
- публичные ссылки имеют вид `https://wishpad.me/wishlist/:publicToken`.
Ключевые env-переменные:
- `APP_ENV`
- `APP_KEY`
- `APP_URL`
- `APP_DEBUG`
- `DB_HOST`
- `DB_DATABASE`
- `DB_USERNAME`
- `DB_PASSWORD`
- `SANCTUM_STATEFUL_DOMAINS`
- `SESSION_DOMAIN`
- `QUEUE_CONNECTION=database`
- `MAIL_MAILER=smtp`
- `MAIL_HOST`
- `MAIL_PORT`
- `MAIL_USERNAME`
- `MAIL_PASSWORD`
- `MAIL_FROM_ADDRESS`
- `MAIL_FROM_NAME`
- `ADMIN_EMAIL`
- `ADMIN_PASSWORD`
- `LOG_CHANNEL`
- `AUDIT_LOG_CHANNEL`
Правила:
- production `.env` не хранится в git;
- `.env.example` должен содержать все ключи без секретных значений;
- секреты SMTP.bz, БД и `APP_KEY` не пишутся в логи;
- для local development используется отдельный `.env`.
## 26. Backup и restore
Backup нужен для PostgreSQL и конфигурации deploy.
MVP policy:
- PostgreSQL backup выполняется ежедневно;
- backup хранится вне основного контейнера БД;
- срок хранения ежедневных backup: 14 дней;
- еженедельный backup хранится 2 месяца;
- backup должен включать все таблицы приложения, включая users, wishlists, wishlist_items, reservations, jobs и failed_jobs;
- `.env` не включается в обычный database backup, но production secrets должны быть сохранены владельцем проекта отдельно и безопасно;
- логи живут по log retention policy из раздела `Логи и аудит`.
Restore:
- должен быть описан ручной restore flow для VPS;
- после restore нужно выполнить проверку входа, открытия dashboard, публичной ссылки и бронирования;
- restore-check нужно выполнять хотя бы вручную после настройки backup pipeline.
## 27. Mobile delivery для MVP
Mobile app входит в MVP как Capacitor-приложение на базе того же Vue frontend.
Mobile app включает только пользовательскую часть:
- public guest UI;
- owner UI.
Admin UI в mobile app не включается.
Для MVP достаточно:
- Android debug/internal build;
- iOS local/TestFlight-ready build, если есть доступ к Apple Developer tooling;
- smoke test запуска приложения;
- проверка auth;
- проверка public wishlist;
- проверка бронирования и отмены.
Публикация в App Store и Google Play не является обязательной для первого MVP release и остаётся future scope.
## 28. Юридические и пользовательские документы
Минимально нужны страницы:
- политика конфиденциальности;
- пользовательское соглашение или условия использования;
- информация об использовании cookies.
Причины:
- приложение хранит email;
- приложение использует cookies;
- приложение отправляет email-уведомления;
- приложение хранит пользовательский контент: списки, подарки, ссылки и имена гостей.
Для MVP допустимы простые статические страницы, доступные из footer/menu.
## 29. Направление deploy
Проект должен быть удобно разворачивать на VPS.
Детальное решение по закрытому репозиторию, Gitea, CI/CD и deploy pipeline описано в `docs/REPOSITORY_DEPLOYMENT_SPEC.md`.
Выбранное направление для MVP:
- исходный код закрытый, proprietary / all rights reserved;
- исходный код размещается в self-hosted Gitea;
- репозитории приватные;
- CI/CD выполняется через Gitea Actions и Gitea Runner;
- deploy на VPS выполняется через Docker Compose;
- отдельная deploy-платформа вроде Coolify или Dokploy не входит в обязательный MVP и остаётся future option.
Целевая структура исходников разделена на три основных репозитория:
```text
wishpad-backend/
app/
database/
routes/
tests/
wishpad-frontend/
src/
public/
capacitor/
wishpad-docs/
docs/
brand/
README.md
```
Назначение репозиториев:
- `wishpad-backend` - Laravel API, migrations, queue jobs, mail templates, admin/backend integration и backend tests;
- `wishpad-frontend` - Vue 3 + Vite user-facing frontend, admin frontend, Capacitor mobile app layer, frontend tests и NPM tooling;
- `wishpad-docs` - продуктовая, UI, DB, OpenAPI, monetization, repository/deployment-документация, documentation workflow, changelog и brand assets.
На VPS backend и frontend собираются в окружения через отдельные checkouts:
```text
/var/www/wishpad-dev/backend
/var/www/wishpad-dev/frontend
/var/www/wishpad-production/backend
/var/www/wishpad-production/frontend
```
Документация разворачивается отдельно из `wishpad-docs`:
```text
/var/www/wishpad-docs
/var/www/wishpad-docs-site
```
Публичный documentation engine использует Scalar и отдаёт `docs/OPENAPI.yaml` на `docs.wishpad.me`.
Ожидаемый flow:
1. Склонировать нужный репозиторий: `wishpad-backend`, `wishpad-frontend` или `wishpad-docs`.
2. Вести разработку backend/frontend в ветке `dev`.
3. Проверять интеграцию на `dev.wishpad.me`.
4. Мержить готовые изменения в `main`.
5. Deploy production выполняется из ветки `main` на `wishpad.me`.
6. Документация публикуется из `wishpad-docs:main` на `docs.wishpad.me`.
## 30. Будущие точки расширения
- Монетизация `free`/`plus` через admin-настройку.
- Модель "Поддержать проект".
- Роль организатора или shared administration.
- Guest-specific cancellation tokens.
- Расширенные email notifications.
- Срок действия wishlist.
- Несколько изображений для подарка.
- Загрузка изображений в S3-compatible storage.
- Приоритеты.
- Категории.
- Диапазоны цен.
- Marketplace-specific metadata adapters для Ozon, Wildberries, Яндекс Маркет, Lamoda и других сайтов.
- Public/private toggle для wishlist.
- Архивация старых списков.
- Granular admin permissions.
- Push notifications.
- Deep links для открытия публичного wishlist сразу в mobile app.
- Share sheet integration для быстрого шаринга wishlist.
- Публикация mobile app в App Store и Google Play.
- Encrypted remote backups.
- Автоматическая restore-проверка backup.
- Backup uploaded images после появления S3/upload.