# G7-A — Клиентская «Помощь» (техподдержка) — дизайн

**Дата:** 2026-06-19
**Тип:** новая фича (go-live находка G7, часть A)
**Источник решения:** `docs/superpowers/specs/2026-06-18-gfindings-session-handoff.md` (строка G7: «НЕ impersonation; вместо — почта техподдержки, клиент оставляет контакты, разбор вручную»).
**Декомпозиция:** G7 разделён на **G7-A** (клиентская «Помощь» — этот документ) и **G7-B** (достройка admin-impersonation — отдельная спека). Делаем обе подряд, G7-A первой.

---

## 1. Цель

Дать клиенту в портале простой канал связи с техподдержкой **вместо** входа-под-клиентом:
видимый email техподдержки + форма-заявка (клиент оставляет контакты и сообщение) + чат JivoSite.
Заявки уходят на почту техподдержки **и** сохраняются в БД; разбор — вручную.

**Решения владельца (зафиксированы):**
- Формат: форма «Оставить заявку» + видимый email **и** чат JivoSite.
- Заявки: письмо на почту техподдержки **+ журнал в БД**.
- Место: пункт **«Помощь»** в боковом меню (отдельный экран `/help`).
- JivoSite: **заготовка под конфиг** — виджет грузится по ID из env; пока ID не задан, чата нет (не блокирует).

---

## 2. Backend

### 2.1. Конфиг
`config/services.php` — два ключа:
- `support.email` = `env('SUPPORT_EMAIL', 'support@liderra.app')` — адрес назначения заявок и для отображения.
- `jivosite.widget_id` = `env('JIVO_WIDGET_ID')` (по умолчанию `null`).

`.env.example` — добавить `SUPPORT_EMAIL=support@liderra.app` и `JIVO_WIDGET_ID=` (пусто).

### 2.2. Таблица `support_requests` (RLS по tenant)
Колонки: `id BIGSERIAL PK`, `tenant_id BIGINT NOT NULL` (FK tenants, RLS), `user_id BIGINT NOT NULL` (FK users — кто отправил), `name VARCHAR(255) NOT NULL`, `contact VARCHAR(255) NOT NULL` (телефон или email, как ввёл клиент), `message TEXT NOT NULL`, `created_at TIMESTAMPTZ NOT NULL DEFAULT now()`.
- RLS-политика `tenant_isolation` (как у прочих tenant-таблиц).
- Индекс `idx_support_requests_tenant` на `(tenant_id, created_at DESC)`.
- GRANT SELECT, INSERT для `crm_app_user`, `crm_supplier_worker`.
- **Добавляется в `db/schema.sql` И в guard'нутую миграцию** (`CREATE TABLE IF NOT EXISTS`, `CREATE INDEX IF NOT EXISTS`, `DROP POLICY IF EXISTS` перед `CREATE POLICY`) — по урокам дрейфа схемы. + запись в `db/CHANGELOG_schema.md` + бамп версии schema.sql + header-метрики (таблицы +1, индексы +1, RLS +1). Прогон `migrate:fresh` (test) — зелёный, ZERO_DRIFT сохраняется.

### 2.3. Эндпоинт + модель + письмо
- Модель `App\Models\SupportRequest` (с явными `@property` — ide-helper:models может пропустить, как TenantRequisites; fillable; tenant-scoped).
- Роут `POST /api/support-requests` под `['auth:sanctum','tenant']`.
- Контроллер `Api\SupportRequestController@store`: валидация `name|required|string|max:255`, `contact|required|string|max:255`, `message|required|string|max:5000`. Внутри транзакции `SET LOCAL app.current_tenant_id` → `SupportRequest::create([...])` (основной канал — БД). Затем `Mail::queue(new SupportRequestMail(...))` на `config('services.support.email')` в try/catch + Log (без ПДн в логе — только id заявки) — сбой SMTP не валит запрос (паттерн G1 `sendCode`). Ответ `201 {ok:true}`.
- Мейлабл `App\Mail\SupportRequestMail` + шаблон `resources/views/emails/support_request.blade.php` (имя, контакт, сообщение, тенант, время).

### 2.4. SPA-shell
`resources/views/welcome.blade.php`:
- `<meta name="support-email" content="{{ config('services.support.email') }}">` (фронт читает для отображения).
- Условная вставка JivoSite-скрипта: `@if(config('services.jivosite.widget_id')) <script src="https://code.jivo.ru/widget/{{ config('services.jivosite.widget_id') }}" async></script> @endif`.
  - **Безопасность:** SRI (`integrity`) к JivoSite **неприменим** — это динамический widget-loader с меняющимся содержимым, `integrity` его сломает. Правильный контроль — **CSP-allowlist** домена `code.jivo.ru` (+ рантайм-домены JivoSite: `*.jivosite.com`, `*.jivo.ru`) в `script-src`/`connect-src`/`frame-src`. На проде CSP правится отдельно при включении виджета (когда владелец задаст `JIVO_WIDGET_ID`); пока id пуст — скрипта нет, CSP не трогаем. Зафиксировать как пункт деплоя.

---

## 3. Frontend

- **Роут** `/help` (name `help`, requiresAuth, layout app, devIndex следующий свободный) в `router/index.ts`.
- **Пункт меню «Помощь»** в боковом меню (`components/layout/AppSidebar.vue`) — в подходящей группе (напр. «Команда» рядом с «Настройки», или отдельной группой). Иконка Lucide (mdi/lucide help-circle).
- **`views/HelpView.vue`**:
  - Заголовок «Помощь».
  - Блок «Связаться с техподдержкой»: email из `<meta name="support-email">` (mailto-ссылка) + подсказка «или напишите в чат» (если JivoSite настроен — он плавающей кнопкой).
  - **Форма** «Оставить заявку»: поля Имя / Контакт (телефон или email) / Сообщение (textarea) + кнопка «Отправить». Имя/контакт по умолчанию префиллятся из профиля (`auth.user`) — клиенту удобнее.
  - Сабмит → `POST /api/support-requests`. Состояния: загрузка, успех (зелёный алерт «Заявка отправлена, ответим на ваш контакт») с очисткой формы, ошибка (422 — показать под полями; иное — общий алерт).
- **`api/support.ts`**: `submitSupportRequest({name, contact, message})`.
- Утилита чтения `<meta name="support-email">` (или прямо в HelpView).

---

## 4. Поток данных

Клиент → «Помощь» (`/help`) → видит email + форму + (если настроен) чат JivoSite → заполняет форму → `POST /api/support-requests` → запись в `support_requests` (БД, RLS) + письмо в очередь на `support.email` → 201 → клиенту «Заявка отправлена». Поддержка разбирает по почте/в БД вручную.

---

## 5. Обработка ошибок

- Валидация: фронт (обязательность) + бэк (422 с полями).
- Сбой почты (SMTP/Unisender) — **не теряем заявку**: она уже в БД; письмо в очереди с try/catch+Log, запрос не падает.
- JivoSite не настроен (`widget_id` пуст) → скрипт не вставляется, чата нет, экран «Помощь» полностью рабочий.
- RLS: явный `where(tenant_id)` поверх `SET LOCAL` (как в прочих контроллерах) — заявка привязана к тенанту отправителя.

---

## 6. Тестирование

- **Backend (Pest)**: `SupportRequestControllerTest` — (1) POST создаёт строку в `support_requests` с правильным tenant_id/user_id; (2) `Mail::assertQueued(SupportRequestMail)` на адрес из конфига; (3) валидация 422 при пустых полях; (4) RLS — запись видна только своему тенанту; (5) сбой почты не валит запрос (Mail fake throws → 201 + строка в БД). + `migrate:fresh` (test) зелёный, ZERO_DRIFT.
- **Frontend**: vitest сломан (G8) → `vue-tsc`(0) + `eslint`(0) + `build` + **живой Playwright**: открыть `/help`, увидеть email + форму, отправить заявку, увидеть успех; проверить появление строки в `support_requests` (tinker). Меню содержит «Помощь».
- **Schema**: `rls-reviewer`-проверка диффа (или ручная — GRANT/policy консистентны, 0 orphan).

**Критерий готовности:** Pest зелёный, type-check/eslint/build/stan чисты, migrate:fresh зелёный + ZERO_DRIFT, Playwright-сценарий проходит, заявка реально ложится в БД и письмо встаёт в очередь.

---

## 7. Вне объёма (G7-A)

- Достройка admin-impersonation — **G7-B** (отдельная спека, следующей).
- Клиентский список своих заявок (не нужен — разбор вручную).
- Рефактор хардкода `support@liderra.app` в страницах ошибок на `<meta>` — опционально, мелкий follow-up (не ломает G7-A).
- Реальный JivoSite widget_id — вставит владелец в env позже.
