# G1/SP3b — форма реквизитов (фронт) + UX гейта первого проекта — дизайн

**Дата:** 19.06.2026
**Статус:** дизайн утверждён владельцем (брейншторм)
**Спринт:** G1/SP3b (фронт реквизитов). Предшественники: SP1 (самозапись), SP2 (бэкенд реквизитов+гейт), SP3a (фронт входа).
**Граница:** только фронт. Бэкенд SP2 готов и не меняется.

---

## 1. Цель

Дать клиенту экран для ввода реквизитов компании и понятно провести его через
гейт первого проекта (бэкенд не даёт создать первый проект без минимальных
реквизитов — отвечает `422 {error:'requisites_required'}`).

«Лёгкие» реквизиты (минимум, который снимает гейт) — это:

- тип лица (`individual` / `sole_proprietor` / `legal_entity`),
- контактное имя,
- контактный телефон,
- ИНН (обязателен только для ИП и юрлица).

Полные поля (КПП / ОГРН / юр.адрес / р-с) — **вне SP3b**, это SP3c (дополнит ту же
вкладку позже).

---

## 2. Готовый бэкенд (контракт, не меняем)

| Метод | Путь | Назначение |
|---|---|---|
| GET | `/api/tenant/requisites` | вернуть `{data: RequisitesResource | null}` |
| PUT | `/api/tenant/requisites` | upsert; вернуть `{data: RequisitesResource}` |
| POST | `/api/tenant/requisites/lookup-inn` | мягкая подтяжка по DaData; ничего не сохраняет |

Все три — под `auth:sanctum` + `tenant` (см. `app/routes/web.php:75`).

**RequisitesResource** (поля ответа): `subject_type`, `contact_name`,
`contact_phone`, `inn`, `legal_name`, `kpp`, `ogrn`, `legal_address`,
`bank_name`, `bank_bik`, `bank_account`, `corr_account`,
`requisites_completed_at`.

**lookup-inn** — ответ:
- не найдено: `{found: false}`
- найдено: `{found: true, legal_name, kpp, ogrn, legal_address, subject_type_hint}`
  где `subject_type_hint` ∈ `sole_proprietor | legal_entity`.

На dev/тестах DaData выключена (NullPartyLookup) → lookup всегда `{found:false}`.
На проде включается `DADATA_PARTY_ENABLED=true` + ключ.

**Валидация PUT** (`UpdateRequisitesRequest`) — зеркалим на клиенте:
- `subject_type` — required, ∈ `individual|sole_proprietor|legal_entity`;
- `contact_name` — required, ≤255;
- `contact_phone` — required, проходит `PhoneNormalizer::normalize`;
- `inn` — required для ИП/юрлица; для них проверяется контрольная цифра
  (`InnValidator`); для физлица — необязателен;
- `kpp` — `^\d{9}$` (nullable), `ogrn` — `^\d{13}|\d{15}$`, `bank_bik` —
  `^\d{9}$`, `bank_account`/`corr_account` — `^\d{20}$` (всё nullable, в SP3b не
  показываем).

**Гейт первого проекта** (`ProjectController::store`): если у тенанта 0 проектов
и реквизиты не «лёгко-полные» → `422 {error:'requisites_required'}`.
`isLightComplete`: заполнены `subject_type`+`contact_name`+`contact_phone`, и
для ИП/юрлица заполнен `inn`.

---

## 3. Что добавляем на фронте

### 3.1. `resources/js/api/requisites.ts` (новый)

Тонкая обёртка над `apiClient` (паттерн как у других `api/*.ts`):

```ts
export interface Requisites {
  subject_type: 'individual' | 'sole_proprietor' | 'legal_entity' | null;
  contact_name: string | null;
  contact_phone: string | null;
  inn: string | null;
  legal_name: string | null;
  kpp: string | null;
  ogrn: string | null;
  legal_address: string | null;
  bank_name: string | null;
  bank_bik: string | null;
  bank_account: string | null;
  corr_account: string | null;
  requisites_completed_at: string | null;
}

export interface InnLookupResult {
  found: boolean;
  legal_name?: string;
  kpp?: string;
  ogrn?: string;
  legal_address?: string;
  subject_type_hint?: 'sole_proprietor' | 'legal_entity';
}

getRequisites(): Promise<Requisites | null>   // GET, разворачивает {data}
updateRequisites(payload): Promise<Requisites> // PUT, разворачивает {data}
lookupInn(inn: string): Promise<InnLookupResult> // POST
```

`ensureCsrfCookie()` перед мутирующими вызовами (как в NewProjectDialog).

### 3.2. `resources/js/views/settings/RequisitesTab.vue` (новый)

«Лёгкая» форма. Поля:

- **Тип лица** — `v-btn-toggle` (или `v-radio-group`): Физлицо / ИП / Юрлицо.
- **Контактное имя** — `v-text-field`, required.
- **Контактный телефон** — `v-text-field`, required.
- **ИНН** — `v-text-field`, виден и обязателен только при ИП/Юрлице.
  Рядом кнопка **«Найти по ИНН»** → `lookupInn`:
  - `found:true` → подставить `legal_name` (показать как подтверждение), сохранить
    `kpp/ogrn/legal_address` в состоянии формы (пригодятся SP3c), при желании
    подставить тип лица из `subject_type_hint`;
  - `found:false` → подсказка «Организация не найдена по ИНН — проверьте номер или
    введите название вручную»; форму это не блокирует.
- **Название организации** (`legal_name`) — read-only поле-подтверждение, видно
  для ИП/юрлица (заполняется после lookup; можно оставить пустым).
- **Кнопка «Сохранить»** → `updateRequisites(form)` → зелёный снэк «Реквизиты
  сохранены».

Поведение:
- `onMounted → getRequisites()` → предзаполнить форму (или пусто).
- Клиентская валидация зеркалит бэкенд (см. §2). Серверные `422` ошибки
  раскладываются по полям (как в NewProjectDialog).
- При типе «Физлицо» поле ИНН и название скрыты, ИНН не требуется.

PUT отправляет весь payload формы; «полные» поля (банк и т.п.) в SP3b не
заполняются → уходят null/неизменными.

### 3.3. Вкладка «Реквизиты» в `SettingsView.vue`

- Добавить в массив `tabs` запись `{ id: 'requisites', label: 'Реквизиты',
  icon: 'mdi-file-document-outline' }` (после «Профиль»).
- Отрисовать `<RequisitesTab v-else-if="activeTab === 'requisites'" />`.
- **Deep-link:** при монтировании читать `route.query.tab`; если совпадает с id
  вкладки — установить `activeTab`. (Нужно для перехода из гейта.)

### 3.4. UX гейта в `NewProjectDialog.vue`

В `persist()` (catch) добавить ветку **до** общего `generalError`:

```ts
if (err.response?.status === 422 && err.response.data?.error === 'requisites_required') {
  requisitesRequired.value = true;
} else if (...balance_insufficient...) { ... }
else if (...422 && data.errors...) { ... }   // полевые ошибки — как сейчас
else { generalError = ... }
```

В шаблоне — `v-alert` (type=warning), показывается при `requisitesRequired`:

> «Сначала заполните реквизиты компании — без них нельзя создать первый проект.»
> Кнопка **«Заполнить реквизиты»** → `close()` + `router.push({ path: '/settings',
> query: { tab: 'requisites' } })`.

`requisitesRequired` сбрасывается при открытии/закрытии окна (рядом с
`generalError`).

---

## 4. Поток клиента (happy path)

1. Клиент регистрируется (SP1) → подтверждает почту → дашборд. У него 0 проектов
   и 0 реквизитов.
2. Проекты → «Создать проект» → бэкенд `422 requisites_required` → в окне видно
   предупреждение гейта + кнопка.
3. Жмёт «Заполнить реквизиты» → попадает на `/settings?tab=requisites`.
4. Заполняет лёгкую форму, сохраняет.
5. Возвращается в Проекты → «Создать проект» → проект создаётся.

---

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

- `422` с полевыми `errors` (на PUT реквизитов) → разложить по полям формы.
- `422 requisites_required` (на создании проекта) → ветка гейта (§3.4).
- `lookup-inn found:false` → мягкая подсказка, не блок.
- Сетевые/прочие → общий `v-alert`/`generalError` через `extractErrorMessage`.

---

## 6. Верификация

vitest сломан (G8) — фронт-юнитов нет. Проверяем:

- `npm --prefix app run type-check` (`vue-tsc`) = exit 0.
- eslint по новым/изменённым файлам = exit 0.
- **Живой Playwright (обязательно):**
  1. Регистрация свежего клиента (SP1) → подтверждение dev-кодом → дашборд.
  2. Проекты → «Создать проект» → **видно окно гейта** → «Заполнить реквизиты» →
     вкладка реквизитов открыта.
  3. Заполнить (Физлицо — кратчайший валидный путь) → Сохранить → снэк.
  4. Назад в Проекты → «Создать проект» → **проект создаётся** (201).
  5. Доп.: ИНН для ИП с неверной контрольной цифрой → видимая ошибка поля.

Перед фронт-проверкой — пересобрать ассеты: `npm --prefix app run build`
(vite-dev не запущен, backend отдаёт собранные ассеты).

---

## 7. YAGNI / границы

- НЕ делаем полные поля (банк/КПП/ОГРН/адрес) — это SP3c.
- НЕ делаем авто-повтор создания проекта после сохранения реквизитов (владелец
  выбрал «понятное окно + кнопка», не «авто-повтор»).
- НЕ трогаем бэкенд SP2.
- НЕ чиним сторонние находки (register-500, стейл-тест, NBSP-lint) — вне SP3b.

---

## 8. Затрагиваемые файлы

| Файл | Действие |
|---|---|
| `app/resources/js/api/requisites.ts` | новый |
| `app/resources/js/views/settings/RequisitesTab.vue` | новый |
| `app/resources/js/views/SettingsView.vue` | +вкладка, +deep-link |
| `app/resources/js/views/projects/NewProjectDialog.vue` | +ветка гейта, +alert, +кнопка |
