# Страница конвертера (`/converter`)

Покупка/конвертация валюты. Маршрут `ROUTES.CONVERTER` (`/converter`), **защищён**
`ProtectedRoute` (только для авторизованных) и обёрнут в `WalletLayout footer center`
(см. `app/providers/RouterProvider.tsx`).

## Главная идея

`ConverterPage` — **тонкий роутер по типу аккаунта**. Сам по себе он почти ничего не
рендерит: показывает заглушку / спиннер и выбирает один из двух виджетов-конвертеров.
Вся логика и презентация живут в виджетах и нижних слоях (`features/*`, `shared`).
Это результат рефакторинга под FSD (`app → pages → widgets → features → entities →
shared`; нижние слои не импортируют верхние).

```
ConverterPage
├─ PURCHASE_ENABLED === false → заглушка «Покупка временно недоступна»
├─ useMe() ещё грузится       → <Spinner/>
└─ по account_type:
   ├─ legal (юрлицо)      → <LegalConverterSection/>  (@widgets/legal-converter)
   └─ individual (физлицо)→ <ConverterSection/>       (@widgets/converter-page)
```

> ⚠️ Сейчас `PURCHASE_ENABLED = false` в `ConverterPage.tsx` — оба конвертера
> скрыты заглушкой. Поставьте `true`, чтобы вернуть рабочий контент.

Развилка по аккаунту: `isLegal = data.account_type !== 'individual'`. Тот же критерий,
что `isLegalEntity()` в `features/auth/lib/profile.ts` (профиль). Здесь оставлен инлайн,
т.к. `ConverterPage` не импортирует профильные хелперы; при желании можно свести к
одному источнику.

## Карта файлов

```
pages/converter/
  ui/ConverterPage.tsx          ← роутер по account_type (заглушка/спиннер/виджет)
  ui/ConverterPage.module.css   ← стили только заглушки
  index.ts                      ← export { ConverterPage }
  README.md                     ← этот файл

widgets/legal-converter/        ← ВЕТКА ЮРЛИЦ: форма B2B-заявки
  model/useLegalConverterForm.ts← вся логика формы (стейт, расчёт, submit)
  ui/LegalConverterForm.tsx     ← разметка (LegalConverterSection), без логики
  ui/LegalConverterForm.module.css
  index.ts                      ← export { LegalConverterSection }

widgets/converter-page/         ← ВЕТКА ФИЗЛИЦ: обычный конвертер (не трогался)
  model/useConverterSection.ts
  ui/ConverterSection.tsx
  ...

features/b2b/                    ← домен B2B-заявок
  lib/purchaseRequest.ts        ← чистая доменная логика (без React) — см. ниже
  hooks/useCreatePurchaseRequest.ts ← мутация POST /purchase-requests
  hooks/useMyPurchaseRequests.ts    ← список заявок аккаунта (queryKey b2b/purchase-requests)
  api/b2bApi.ts                 ← fetch + типы B2bPurchaseRequest и т.д.
```

`LegalConverterForm.tsx` экспортирует компонент `LegalConverterSection` (имя
по аналогии с `ConverterSection` физлиц). Файл назван `*Form`, компонент — `*Section`.

## Ветка юрлиц: где что лежит

Раньше всё (стейт формы + доменная математика + разметка) жило одним файлом
`pages/converter/ui/LegalConverterPage.tsx`. После рефакторинга:

| Слой | Файл | Ответственность |
|---|---|---|
| `features/b2b/lib` | `purchaseRequest.ts` | **чистая** логика без React: расчёт комиссии, упаковка/парсинг comment, склонения, форматирование ₽ |
| `widgets/legal-converter/model` | `useLegalConverterForm.ts` | стейт полей формы, автозаполнение, submit-мутация, нотификация |
| `widgets/legal-converter/ui` | `LegalConverterForm.tsx` | только разметка, дёргает хук |

### `features/b2b/lib/purchaseRequest.ts`

| Экспорт | Что делает |
|---|---|
| `MIN_ORDER` | минимальный объём заявки (500 000 ₽) |
| `TERM_OPTIONS` | таблица «срок ожидания → ставка комиссии». Чем дольше ждать, тем ниже ставка (3 дня → 5 %, 14 дней → 1 %) |
| `termRate(days)` | ставка (доля) для срока; фоллбэк — первая строка таблицы |
| `calcQuote(amount, days)` | `{ rate, commission, total }` — **расчёт комиссии и итога** |
| `buildPurchaseComment(parts)` | **упаковка** имени/контакта/срока/сумм в строку `comment` |
| `parseContactFromComment(comment)` | **парсинг** имени и контакта обратно из `comment` |
| `latestPurchaseRequest(items)` | последняя по `created_at` заявка аккаунта (или `null`) |
| `dayLabel(days)` | русское склонение: `1 день` / `2 дня` / `5 дней` |
| `formatRub(n)` | целое с разделителями групп: `500000 → "500 000"` |

## Ключевой нюанс: имя/контакт упакованы в `comment`

У API B2B-заявки (`B2bPurchaseRequest`, `api/b2bApi.ts`) **нет отдельных полей** под
имя, контакт, срок ожидания и суммы в ₽. Есть только `usdt_amount` и свободный
`comment`. Поэтому всё, что нужно менеджеру, кодируется в человекочитаемый `comment`:

```
Имя: Иван; Контакт: +7 900 000-00-00; Срок ожидания: 3 дня; Сумма: 500 000 ₽; Комиссия: ≈25 000 ₽; Итого: ≈525 000 ₽
```

- **Запись:** `buildPurchaseComment()` собирает строку при отправке.
- **Чтение:** `parseContactFromComment()` достаёт `Имя:`/`Контакт:` регулярками — это
  обратная операция к записи. Используется для **автозаполнения** формы из последней
  заявки аккаунта (один раз, по флагу `prefilled` в хуке).

> Связка хрупкая по контракту: меняете формат строки в `buildPurchaseComment` —
> синхронно правьте регулярки в `parseContactFromComment`, иначе автозаполнение
> молча перестанет находить поля.

## Поток данных (ветка юрлиц)

| Источник | Откуда | Что даёт |
|---|---|---|
| `useMe()` | `@features/auth` | `account_type` для выбора ветки (в `ConverterPage`) |
| `useMyPurchaseRequests()` | `@features/b2b` | список прошлых заявок → автозаполнение |
| `useCreatePurchaseRequest()` | `@features/b2b` | мутация создания заявки; на успех инвалидирует `['b2b','purchase-requests']` |

Числовая сумма берётся из строки ввода: `Number(amount.replace(/\D/g, ''))`. Поле
форматируется на лету (`onAmountChange` → `formatRub`), поэтому в стейте лежит
строка с пробелами, а в API уходит число `numAmount`.

## Правила при доработке

1. **Страница остаётся роутером.** Новый вид конвертера = новый виджет +
   ветка в `ConverterPage`. Не тащите стейт/хуки данных в `ConverterPage.tsx`.
2. **Чистая доменная логика** (расчёты, упаковка/парсинг comment, форматирование) →
   `features/b2b/lib/purchaseRequest.ts`. Без React, чтобы переиспользовать и тестировать.
3. **Стейт формы и мутации** → `widgets/legal-converter/model`. UI-компонент только рендерит.
4. **`comment` пишется и читается одной парой функций** (`build*`/`parse*`) — не
   разносите формат по разным местам.
5. Соблюдайте порядок слоёв: виджет импортирует из `features`/`shared`, не наоборот.

## Известные точки расширения / TODO

- `PURCHASE_ENABLED = false` — глобальный выключатель в `ConverterPage.tsx`. Пока `false`,
  оба конвертера не показываются.
- `formatRub` живёт в `features/b2b/lib` (локально для домена). Если понадобится
  форматирование ₽ в других местах — кандидат на переезд в `shared/lib/utils`
  (рядом с `formatUsd`).
- Хрупкость `comment`-контракта (см. выше). При появлении в API нормальных полей под
  имя/контакт/срок — `build/parseComment` можно выкинуть.

## Проверка

```bash
npm run build   # tsc -b + vite build (тестов в проекте нет)
npm run lint
npm run dev     # ручная проверка /converter (под legal-аккаунтом, PURCHASE_ENABLED=true)
```

Ручной чек-лист `/converter` (legal-аккаунт, `PURCHASE_ENABLED = true`):
- ввод объёма форматируется (`500000 → 500 000`); подсказка при объёме < 500 000 ₽;
- выбор срока меняет ставку комиссии и пересчитывает «Сумму комиссии» / «Итого»;
- кнопка неактивна при пустой/слишком малой сумме и во время отправки;
- успешная отправка → тост «Заявка отправлена», форма очищается;
- при повторном заходе имя/контакт подставляются из последней заявки;
- individual-аккаунт по тому же адресу видит обычный `ConverterSection`.
```
