# teeu — Статус и план продолжения (handoff)

Документ для продолжения разработки в новой сессии. Отражает состояние после **Этапа 14** —
**все 14 этапов ТЗ завершены**.

## Пост-релизные доработки (после запуска локально)

**Сессия 2026-07-10** (детали в топик-доках):
- **Масштабируемый маппинг категорий YML** — [yml-import.md](yml-import.md): глобальный словарь
  `category_aliases`, реестр/оверрайд `yml_category_maps`, админ-очередь Filament, self-service продавца,
  правило «нет категории → скип».
- **Настройки фида** — фикс-интервал 720/1440, выключение фида деактивирует товары, `source_url` +
  кнопка «Смотреть на сайте продавца».
- **Карточка продавца** — [seller.md](seller.md): контакты кнопками, раскрытие телефона по клику с
  аналитикой `seller_contact_clicks`, layout на всю ширину.
- **Мобильный UI** — [catalog.md](catalog.md): нижний бар + бургер-меню (`$store.catalog`), гео/поиск/
  «Сообщения» в шапке, `/catalog` плитками с иконками.
- Нюансы: Tailwind v4 сбрасывает `cursor` кнопок; новый arbitrary-класс ⇒ ребилд CSS
  ([deployment.md](deployment.md)); `teeu:icons` слушать на `window`; Filament v5 резолвит замыкания по
  имени; новую колонку — в `$fillable`.

- **Фикс интерактива шапки**: публичный и account `<body>` не имели `x-data` → Alpine v3 не привязывал
  `@click`/`x-show` к кнопкам (город/вход/регистрация «мертвы» без ошибок в консоли). Добавлен `x-data`
  на `<body>` в `layouts/app` и `layouts/account`. Нюанс на будущее: любой `@click="$store…"`/`x-show`
  работает только внутри области `x-data`.
- **Меню категорий (§19)**: кнопка «Категории» — мега-меню (`x-data="{open}"` + `x-show`), корневые
  категории с детьми из БД; полоса категорий — реальные корневые (`whereIsRoot()->active()`); данные
  берутся inline в `partials/header`.
- **Поиск с подсказками (§19)**: `SearchController@suggest` (JSON, от 3 символов, только `Product::public`
  + активные категории, throttle 120/мин) + `@index` (страница результатов `search/results`, paginate 24);
  Alpine `searchBox()` в `app.js` (debounce 250мс, дропдаун, Enter → `/search?q=`). Тесты `SearchTest` (7).
  NB: sqlite `LIKE` регистро-независим только для ASCII; кириллица case-insensitive в проде (MySQL
  utf8mb4_unicode_ci). **Всего 164 зелёных.**
- **PWA/dev**: service worker регистрируется только в `production`; в `local` — снимает регистрацию и
  чистит кэши (чтобы SW не отдавал стейл-сборку при разработке). Meta `app-env` в `partials/pwa-head`.
- **Локальный запуск**: Laragon-vhost `auto.teeu.test.conf` DocumentRoot исправлен на `…/teeu/public`
  (был корень проекта → листинг файлов). Динамический `/robots.txt` (статический удалён). Детали — deployment.md.

## Как работать

- Стек/окружение — [architecture.md](architecture.md). Пути (Laragon/Windows):
  - PHP: `d:\laragon\bin\php\php-8.3.30-Win32-vs16-x64\php.exe`
  - Composer: `php d:\laragon\bin\composer\composer.phar`
  - Node/npm: `d:\laragon\bin\nodejs\node-v22\` — сборку/`npm` запускать через **PowerShell**
    (`$env:Path = "d:\laragon\bin\nodejs\node-v22;" + $env:Path; npm run build`).
  - MySQL: `root` без пароля, БД `teeu`.
- Тесты: `php artisan test` (sqlite in-memory). Сейчас **157 зелёных**.
- Dev-логин: админ `admin@teeu.test / password` (Filament `/developer`); demo-продавец `seller@teeu.test`.

## Что готово (этапы 0–7)

| Этап | Итог |
|---|---|
| 0 Аудит | `docs/architecture.md`, `design-inventory.md`, `placeo-reference-audit.md` |
| 1 Foundation | users/user_phones/auth_identities, роли spatie, app_settings+`SettingsService` (шифрование), audit_logs+`AuditLogger`, `QueueName`, базовый layout+токены |
| 2 Auth | дозвон DB-challenge + драйверы Manual/VoicePassword, Yandex/VK/Sber адаптеры, `AccountService` merge, единая форма — [authentication.md](authentication.md) |
| 3 Geo/Categories | cities+`CityContext`+селектор, `GeoProvider`→Nominatim, категории nestedset+path+`CategoryService`(move/301/анти-цикл), публичный /catalog, Filament CategoryResource — [catalog.md](catalog.md) |
| 4 Seller | анкета+email-верификация, статусы, sellers/memberships, /merchant тёмный layout, /seller/{slug}, **AI-слой** `AiProvider`→`DeepSeekAiProvider`+ai_requests, модерация описания fail-closed, Telegram — [seller.md](seller.md), [ai-moderation.md](ai-moderation.md) |
| 5 Products | products/images/characteristics/price_history, image pipeline (intervention v4), модерация product_text, `ProductService`, `Product::scopePublic` §60, CRUD /merchant, /product/{slug}-{id}+301, листинг — [catalog.md](catalog.md) |
| 6 YML | `SsrfGuard`, потоковый `YmlParser` (XXE-safe), `YmlImportService` (staging/lock/идемпотентность/override/disappeared/**failed-не-деактивирует**/partial), `CategoryClassifier`→DeepSeek, jobs+`teeu:yml:sync-due`+scheduler, /merchant/feeds — [yml-import.md](yml-import.md) |
| 7 Buyer commerce | favorites, корзина в БД, `ProductPurchaseEligibilityService` §61, `CheckoutService` split+snapshots, `OrderTransitionService`, /account + /merchant/orders (IDOR), price_drop_subscriptions, события OrderCreated/OrderStatusChanged/ProductPriceDecreased — [cabinet.md](cabinet.md) |
| 8 Чаты | `conversations` (пара buyer+seller, unique) + `messages` (sender_kind/client_uuid/moderation), `SenderKind`/`ConversationStatus`, `ChatService` (transport-agnostic, старт/send/portal/visibility), `ChatMessageModerationService` (`chat_v1`, не банит контакты §13.3) + `ModerateChatMessageJob` fail-closed, `AbstractChatController` + Account/Seller cabinets, polling (`/chat/{c}/poll?after`) + `chat-unread` badge, `chat-send` rate-limit, «Написать продавцу» (`<x-chat-start>`) на карточках товара/продавца, изоляция участников/seller (IDOR), portal («Команда teeu») |
| 9 Отзывы/рейтинг | `reviews` (order_id **unique** §40.1) + `Review` (scopeApproved), `ReviewService` (canReview eligibility §40.1 / create → pending / **approve** → published+recalc+event / recalcFor), `ReviewModerationService` (`review_v1`, блокирует контакты/рекламу §13.4) + `ModerateReviewJob` fail-closed (rating-only auto-approve), событие `ReviewApproved` (доставка Этап 10), `Account\ReviewController` POST /account/orders/{order}/review, форма-звёзды на order show (только свой completed, 1/заказ), список approved-отзывов на /seller/{slug}, команда `teeu:products:rebuild-rating` |

## Конвенции проекта (соблюдать)

- **Enums** для всех статусов (`app/Enums`), никаких магических строк.
- **Сервисы** в `app/Services/<Domain>`; контроллеры тонкие; Form Requests для валидации; **IDOR** —
  проверка владения в контроллере (`abort_unless(...->seller_id === ...)`).
- **AI-модерация fail-closed** (§12.5): при недоступности AI контент остаётся pending; никогда не
  «угадывать» allow/block. Всё через `AiProvider` + `AbstractModerationService`. Политики versioned
  (`ai_requests.prompt_version`): есть `seller_description_v1`, `product_text_v1`, `category_v1`, `chat_v1`, `review_v1`.
- **Публичная видимость** — только через `Product::scopePublic` / `isPublic()` (§60). Не размазывать.
- **Очереди** — `App\Enums\QueueName` (`ai`/`images`/`yml-*`/`notifications`/`default`); джобы задают
  `onQueue`; per-job retry/backoff.
- **События** уже эмитятся (SellerActivated, OrderCreated, OrderStatusChanged, ProductPriceDecreased,
  AiProviderUnavailable через alert). **Слушатели-доставка — Этап 10.** Регистрация listener'ов —
  `AppServiceProvider::boot` (`Event::listen`).
- **Деньги** — DECIMAL/касты `decimal:2`, не float.
- **Тесты**: `Tests\Doubles\FakeAiProvider` (модерация/классификация), `FakeCategoryClassifier`.
  Биндинг в контейнере: `$this->app->instance(AiProvider::class, new FakeAiProvider(...))`.
- **JSON на web-маршрутах**: `bootstrap/app.php` уже рендерит JSON при `expectsJson()`.
- **intervention/image v4**: `Image::decode()`/`decodePath()` (нет `read()`), `->encode(new WebpEncoder(q))`.
- **libxml** (PHP 8.3): `libxml_set_external_entity_loader()` возвращает bool — восстанавливать через `(null)`.

## Осталось (этапы 8–14)

### Этап 8 — Чаты ✅ ГОТОВО
Один диалог на **пару (buyer, seller)**, не на товар (отличие от Placeo). Сделано:
- Миграция `2026_07_08_190000`: `conversations` (buyer_user_id, seller_id, status, last_message_at;
  unique buyer+seller) + `messages` (sender_kind [buyer|seller|portal], sender_user_id/portal_admin_id
  nullable, body, moderation_status, `client_uuid`, read_at, approved_at; unique (conversation_id,client_uuid)).
- Enums `SenderKind`, `ConversationStatus`; переиспользован `ModerationStatus`. Модели `Conversation`,
  `Message` (`scopeVisibleTo` — своё в любом статусе + любое approved).
- `App\Services\Chat\ChatService` — **transport-agnostic** (§30.6): startForBuyer / startForSeller (общий
  firstOrCreate — диалог на пару один, с какой стороны его ни открыли; запрет писать самому себе),
  send (dedupe по client_uuid, pending + dispatch job), sendPortal (approved
  сразу), visibleMessages/messagesAfter (polling), markRead, unreadCount, kindFor/canAccess.
- `ChatMessageModerationService` (`chat_v1`) + `ModerateChatMessageJob` (fail-closed: unavailable→pending;
  allow→approved+approved_at; block→rejected; review→needs_review). **НЕ** банит телефон/email/контакты
  (§13.3) — политика обмена контактами оставлена на будущее.
- `AbstractChatController` (шаблонные методы) → `Account\ChatController` (buyer, /account/chat + start) и
  `Seller\ChatController` (seller, /merchant/chat). Транспорт — polling `GET .../poll?after=id` (~4с,
  Alpine `chatRoom`), badge `chat-unread` (Alpine `chatUnread`, ~8с), rate-limit `chat-send` (20/мин/user),
  max 4000. Изоляция: не-участник и чужой seller → 403 (kindFor null).
- Portal (§30.4): `sendPortal` пишет как «Команда teeu» (portal_admin_id, sender_user_id null). UI админа —
  Этап 11.
- UI: `<x-chat-start :seller>` («Написать продавцу») на product/seller card (гость → auth-modal),
  «Написать покупателю» на странице заказа продавца (`POST merchant.chat.start`, собеседник берётся из
  заказа — по идентификатору из запроса продавец открыл бы диалог с кем угодно), окна
  чата (bubbles, escape через `x-text`), nav «Сообщения» в обоих кабинетах.
- Тесты `tests/Feature/ChatTest.php` (11, §56.12): guest, non-participant 403, seller isolation, свой
  магазин, pending скрыт/approved виден/blocked скрыт, duplicate uuid, portal, send-endpoint, start-reuse.

### Этап 9 — Отзывы и рейтинг ✅ ГОТОВО
- Миграция `2026_07_08_200000`: `reviews` (order_id **unique** §40.1, seller_id, user_id nullable, rating
  1–5, text, moderation_status, published_at). Модель `Review` (`scopeApproved`), связи `Seller::reviews`,
  `Order::review`/`isCompleted`.
- `App\Services\Review\ReviewService`: `canReview` (свой order + status Completed + нет отзыва §40.1),
  `create` (strip tags, pending + dispatch job; **rating-only → auto-approve**), `approve` (published_at +
  recalcFor + ReviewApproved), `recalcFor` (AVG/COUNT по approved, ТЗ §40.4 — cache на seller, source of
  truth = reviews).
- `ReviewModerationService` (`review_v1`, в отличие от чата **блокирует** явные контакты/рекламу §13.4) +
  `ModerateReviewJob` (fail-closed: unavailable→pending; allow→approve; block→rejected; review→needs_review).
- Событие `ReviewApproved` (доставка уведомлений — Этап 10, слушателя пока нет).
- `Account\ReviewController@store` → POST `/account/orders/{order}/review`; форма-звёзды (Alpine inline) на
  order show только при `canReview`, иначе показывает существующий отзыв + статус. Approved-отзывы на
  /seller/{slug} (`SellerCardController` грузит `reviews()->approved()`).
- Команда `teeu:products:rebuild-rating` (`RebuildSellerRatings`, chunkById, идемпотентна §59).
- Тесты `tests/Feature/ReviewTest.php` (11, §56.11): no-completed/cancelled/чужой order → forbidden,
  completed → allowed, duplicate → forbidden, pending/blocked не в рейтинге, approved меняет рейтинг,
  rating-only auto-approve, ReviewApproved dispatched, rebuild-rating command.

### Этап 10 — Уведомления ✅ ГОТОВО
- Миграции `2026_07_08_2101/2102/2103`: `notifications` (Laravel), `push_subscriptions`, `stale_reminder_at` в orders.
- Notification-классы: `NewOrderNotification` (database staff + mail seller, `via` по AnonymousNotifiable),
  `OrderStatusChangedNotification` (database+WebPush), `PriceDropNotification` (database+WebPush),
  `ReviewApprovedNotification` (database), `StaleOrderReminderNotification`. Все `ShouldQueue`+`Queueable`,
  очередь `notifications`, **afterCommit через `$this->afterCommit()` в конструкторе** (НЕ типизированное
  свойство `public bool $afterCommit` — конфликтует с трейтом `Queueable` → fatal «same property … incompatible»).
- События+слушатели (регистрация в `AppServiceProvider::boot`): `OrderCreated`→`SendNewOrderNotification`,
  `OrderStatusChanged`→`SendOrderStatusNotification`, `ProductPriceDecreased`→`SendPriceDropNotifications`
  (bounded по `last_notified_price` §23.2), `ReviewApproved`→`SendReviewApprovedNotification`, новый
  `UserRegistered`→`SendNewUserAlert`, `SellerActivated`→`SendNewSellerAlert` (Telegram, был).
- WebPush: контракт `Notifications\Contracts\WebPushSender` + `WebPush\NullWebPushSender` (feature-flag,
  без боевого push) + `WebPush\WebPushChannel`; биндинг в контейнере. `push_subscriptions` готова.
- Notification center `/account/notifications` (`Account\NotificationController`: index/unread-count/
  read-all/{id}/read) + вьюха `account/notifications.blade.php`.
- Команды `teeu:orders:send-stale-reminders` (>48ч, `stale_reminder_at`) и `teeu:ai:retry-failed`;
  scheduler в `routes/console.php` (stale hourly, retry-failed каждые 30 мин).
- Тесты `tests/Feature/NotificationTest.php` (10). **Всего 120 зелёных.**

### Этап 11 — Admin (Filament /developer) ✅ ГОТОВО
Все ресурсы в Filament 5 (структура `Resources/<X>/{Resource,Tables,Schemas,Pages,RelationManagers,Actions}`),
навигация группами «Маркетплейс»/«Модерация»/«Система». Read-only где уместно (никаких CRUD-форм на
пользователях/заказах/товарах — только view + действия).
- **SellerResource** (§32): список (счётчики products/orders, фильтры status/verified), view-infolist
  (анкета/контакты/адреса/membership/аудит + RelationManagers products/orders/feeds/reviews), действия
  suspend/block/unblock (`Actions\SellerStatusActions`) с подтверждением+причиной. Логика — `SellerStatusService`
  (audit `seller.suspend|block|unblock`); блок скрывает товары через `scopePublic` (seller не Active).
- **UserResource** (§33): список (телефон, OAuth-бейджи, продавец да/нет, заказы, активность), view
  (профиль/активность/продавец), RelationManagers identities (**только provider/provider_user_id, без
  metadata/токенов**) и orders.
- **OrderResource** (§34): список+фильтр статуса, view-infolist (снимки позиций, связь на товар/продавца,
  доставленные `DatabaseNotification` по `data->order_id`), RelationManager истории статусов, header-action
  **admin override** (`ViewOrder`): `OrderTransitionService` actor=Admin (reason обязателен) + audit
  `order.admin_override`.
- **ProductResource** (§58 operational, read-only): список с фильтрами (moderation/publication/availability,
  `needs_category_review`, `no_images`), view (изображения/характеристики/статусы/isPublic).
- **YmlFeedResource** (§35/§35.1): список (URL безопасно через `YmlFeedsTable::safeUrl` — host+path без query),
  view (оперпоказатели: pending moderation/needs_category/no_images + адреса + stats JSON), RelationManager
  runs с модалкой item-errors. Действия (`Actions\FeedActions`): run/pause/activate/reprocessAI/retryImages/
  block/unblock → `FeedAdminService` (audit `feed.block|unblock`; retryImages берёт picture-URL из последнего
  `YmlImportOffer.data['pictures']`; reprocessAI ре-диспатчит `ModerateProductTextJob` для pending/needs_review).
- **ConversationResource** (§37, Диалоги): список+поиск/фильтр, view показывает **все** сообщения (включая
  rejected/pending) с moderation-бейджами, header-action «Ответить как Команда teeu» → `ChatService::sendPortal`
  + audit `chat.portal_intervene`.
- **ManageSettings** (custom Page, §38): DeepSeek+Telegram, секреты encrypted/masked (пустое поле НЕ стирает —
  `SettingsService`), toggle-события Telegram (реестр `config/teeu.php` + `telegram.event.*`), header-actions
  Test DeepSeek/Test Telegram (rate-limit 3/мин + audit `settings.test_*`). `SendTelegramAlert` теперь принимает
  `?eventKey` и `TelegramService::eventEnabled()` гейтит по toggle (new_user/new_seller/new_feed/ai_*).
- **Dashboard** (§31): виджеты `MarketplaceStatsWidget` (users/sellers/orders/products KPI),
  `OperationalAlertsWidget` (§58: AI pending/failed, YML сбои, needs_category, no_images, pending reviews — с
  ссылками на отфильтрованные списки), графики `RegistrationsChartWidget`/`OrdersChartWidget` (7/30 дней,
  DB-портируемый `App\Support\ChartSeries`). Зарегистрированы в `AdminPanelProvider`.
- Тесты `tests/Feature/AdminTest.php` (12): seller suspend/block/unblock+audit+скрытие товаров, order override+
  история+требование reason, feed block/unblock+visibility, reprocessAI/retryImages, секрет encrypt/mask/keep,
  toggle гейтит Telegram, non-admin→403, рендер всех index + dashboard + settings + всех view-страниц.
  **Всего 132 зелёных.**

Нюанс Filament 5: параметры замыканий `formatStateUsing`/`color` резолвятся **по имени** — enum-типизированный
параметр обязан называться `$state` (иначе Filament пытается инстанцировать enum из контейнера → 500). Search-
query замыкания — `fn (Builder $query, string $search)`. `Fieldset`/`Section` — из `Filament\Schemas\Components`
(не `Forms\Components`). Кастомная Page с формой: публичный метод `form(Schema $schema): Schema` + `$this->form`
работает через discovery; вью `{{ $this->form }}` в `wire:submit`.

### Этап 12 — SEO ✅ ГОТОВО
- **`App\Support\Seo`**: `productJsonLd()` (schema.org Product — name/description/image/sku(vendor_code)/
  brand(vendor)/offers price+currency+availability+seller; **aggregateRating только если у продавца
  reviews_count>0** — рейтинг seller-уровня, никакого fake), `breadcrumbJsonLd()` (BreadcrumbList),
  `availabilitySchema()` (Availability→schema.org URL).
- **SEO через `@section('seo')`/`@yield('seo')`** (в layout head), партиалы `partials/seo/meta.blade.php`
  (canonical+OG+twitter) и `partials/seo/json-ld.blade.php` — **echo напрямую, БЕЗ `@push` и БЕЗ анонимных
  компонентов** (см. нюанс ниже). Подключены в product/seller/category/catalog: canonical + OG везде,
  Product+Breadcrumb JSON-LD на товаре, Breadcrumb на категории. Canonical категории = чистый URL пути
  (без sort/page/tracking, §41.7). Stale slug товара → 301 (уже был в `ProductController`).
- **robots noindex**: `<meta robots noindex,nofollow>` в layouts account/merchant + Filament panel
  (`renderHook(HEAD_END)`); `/robots.txt` (Disallow /account,/merchant,/developer,/auth/,/cart + Sitemap:).
- **Sitemap** (`App\Services\Seo\SitemapService` — только публичное: `Product::scopePublic`, active
  категории/продавцы): маршруты `/sitemap.xml` (index), `/sitemap-categories.xml`, `-products.xml`,
  `-sellers.xml` (`SitemapController`, XML). Команда `teeu:sitemap:generate` пишет статику в `public/` +
  scheduler dailyAt 04:00.
- Тесты `tests/Feature/SeoTest.php` (11, §56.13): canonical/metadata/OG, Product+Breadcrumb JSON-LD,
  no-fake-aggregateRating (без отзывов нет ключа / с отзывами есть), sitemap исключает blocked seller и
  непубличные товары, stale slug 301, robots noindex в /account. **Всего 143 зелёных.**

ИЗВЕСТНЫЙ risky (для Этапа 14 Hardening): рендер полной карточки продавца в HTTP-тесте даёт PHPUnit-warning
«did not close its own output buffers» — доэтапный кумулятивный дисбаланс ob в разметке карточки (каждый блок
по отдельности чист, страница отдаёт корректный 200; на прод не влияет). Не связан с SEO.

Нюанс Blade (важно): анонимный компонент ИЛИ `@include`, чьё тело **только `@push`** (без прямого вывода),
оставляет незакрытый output buffer → PHPUnit «risky», а следующий такой же рендер его случайно закрывает
(поэтому страница с 2+ push-подшаблонами «чистая», а с одним — нет). Поэтому SEO сделан через `@section('seo')`
+ echo-партиалы, без `@push`/`@stack`.

### Этап 13 — PWA ✅ ГОТОВО
- **Иконки** сгенерированы из `teeu-logo.png` (intervention v4: `createImage`→`fill`→`decode`+`scaleDown`→
  `insert($logo,0,0,'center')`→`encode(new PngEncoder)`) в `public/images/icons/`: icon-192, icon-512,
  icon-maskable-512 (safe-zone padding), apple-touch-icon (180). Лого на белом фоне.
- **`PwaController`** + маршруты: `/manifest.webmanifest` (JSON, `application/manifest+json`; name/short_name
  teeu/display standalone/theme #1877FF/bg #fff/start_url,scope `/`/icons 192+512+maskable), `/sw.js`
  (отдаёт `resources/pwa/sw.js`, `Content-Type application/javascript` + `Service-Worker-Allowed: /`),
  `/offline` (вьюха `pwa/offline`).
- **Service worker** `resources/pwa/sw.js` (§43.1): install precache offline+лого+icon → skipWaiting;
  activate чистит старые кэши (не с текущим VERSION) → clients.claim; fetch: не-GET и cross-origin мимо;
  **приватные `/account,/merchant,/developer,/auth/` → network-only** (navigate→offline при сбое);
  статика (destination style/script/font/image) → cacheFirst; навигации/документы → networkFirst с offline-
  fallback. Версионированные кэши `teeu-v1-static`/`-pages`.
- **Head-теги** — partial `partials/pwa-head.blade.php` (manifest link + theme-color + apple-touch-icon +
  mobile-web-app-capable) включён в layouts app/account/merchant. SW регистрируется в `resources/js/app.js`
  (`navigator.serviceWorker.register('/sw.js')` on load, best-effort).
- Статический `public/robots.txt` удалён → динамический маршрут `/robots.txt` (Этап 12) теперь авторитетный.
- Тесты `tests/Feature/PwaTest.php` (7): manifest валиден/installable (192+512+maskable), иконки на диске,
  sw.js root-scope, sw содержит приватные префиксы + offline + cacheFirst/networkFirst, offline рендерится,
  layout линкует manifest+theme-color, robots динамический. **Всего 150 зелёных.** `npm run build` выполнен.
- Push-фундамент (`push_subscriptions`+VAPID+`WebPushChannel`+`NullWebPushSender`, feature-flag PUSH_ENABLED)
  готов с Этапа 10 — боевой провайдер не подключён (по ТЗ §44: без fake push).

### Этап 14 — Hardening ✅ ГОТОВО (все 14 этапов завершены)
- **Индексы (§46)**: аудит показал полное покрытие (products seller_id+publication_status/moderation/
  availability/published_at/yml_feed_id; orders seller_id+status/buyer; messages conv+id; reviews
  seller+moderation + order unique; favorites unique; user phone unique; auth provider+provider_user_id
  unique; products feed+external unique). Добавлен единственный недостающий — **`orders.created_at`**
  (миграция `2026_07_08_220000`, для сортировки админ/кабинет-списков).
- **Безопасность (§47)** — `tests/Feature/SecurityTest.php` (7): guest→redirect с /account,/merchant;
  non-admin→403 /developer; IDOR (чужой заказ покупателя 403 / свой 200; чужой товар продавца edit 403;
  чужой заказ продавца 403); non-participant→403 в чате; mass-assignment (клиент не может протащить
  `publication_status=active` мимо AI-модерации при block). CSRF/XSS(e()/plain-text §47.3)/Form Requests/
  rate-limit/SSRF/XXE/secret-encryption/safe-redirect уже покрыты предыдущими этапами.
- **Performance (§57)**: spot-аудит листингов — eager loading везде (каталог `with(images,seller)`+paginate(24);
  account/seller orders `with(...)`+paginate(20); favorites `with(product.seller,product.images)`; seller
  products `with(images)`+paginate; seller card reviews `with(user)`). Явных N+1 нет.
- **Docs**: создан `deployment.md` (PHP-расширения, установка, config/route/view/event/filament cache,
  supervisor `queue:work`, cron `schedule:run`, nginx-нюансы для sw.js/manifest/robots/sitemap, чек после
  деплоя); `queues.md` актуализирован (реальные scheduler-задачи).
- **DoD §74**: пройдено по фичам — authorization (backend, не UI), server-validation (Form Requests),
  fail-closed модерация, деньги DECIMAL, секреты encrypted, no-fake (rating/push), robots noindex приватного.
- **Тестов 157 зелёных** (SeoTest 11 + PwaTest 7 + SecurityTest 7 сверх 132).

**Известный benign `risky`** (2 в suite): HTTP-рендер полной карточки продавца даёт PHPUnit-warning
«did not close output buffers». Исчерпывающе разобран: **не** SEO, **не** chat-start, **не** reviews-блок,
**не** `@section('seo')` — каждый блок разметки по отдельности чист, полный набор даёт кумулятивный ob-
дисбаланс. Страница отдаёт корректный 200 (проверено), на прод не влияет. Не удалось локализовать до
конкретного элемента дешёвыми средствами; оставлено как принятый тест-hygiene-варнинг.

## Быстрый чек перед стартом сессии

1. `php artisan test` → 157 зелёных (+2 risky benign — карточка продавца, ob-артефакт; см. Этап 14).
2. `php artisan migrate:fresh --seed` при необходимости чистой БД.
3. `npm run build` (через PowerShell) если менялся фронтенд.
4. **Все 14 этапов ТЗ завершены.** Дальнейшее — сопровождение/доп-фичи: боевой Web Push provider
   (VAPID-архитектура готова, включить `PUSH_ENABLED`), mobile-аудит вживую на 320–1440 (нет автотеста —
   визуальная проверка), локализовать benign risky карточки продавца при желании. Деплой — `deployment.md`.

---

## Пост-релиз, сессия 2026-07-23…28 (сопровождение)

Все изменения на проде teeu.ru, тесты зелёные (единственный стабильно красный — известный
`teeu-seller-phone-jsonld-leak`: телефон продавца в Store JSON-LD; не трогалось).

- **Справочный центр /help**: 25 статей наполнены текстом (`HelpContentSeeder`, idempotent, места под
  скриншоты — врезки `<blockquote>`). Фикс редактирования статьи в панели: в Filament v5 у RichEditor
  НЕТ `fileAttachmentProvider()` — провайдер вложений (WebP) задаётся на модели через `HasRichContent`
  + `InteractsWithRichContent::setUpRichContent()`; `content` остаётся HTML (`json()` не включать).
  Убрано упоминание роли «менеджер» (в v1 не реализована, `MembershipRole::Manager` reserved).
- **Диалог поддержки «Команда teeu»**: у каждого юзера в «Сообщениях» (conversation `type=support`,
  `seller_id=null`), сообщение сразу approved + Telegram-алерт админу (кто/текст/ссылка на диалог в
  /developer). Служебные заметки `SenderKind::System` («Чат открыт из товара/заказа …») — видны только
  продавцу. Ссылки на страницу продавца из заказа и из шапки чата. См. `teeu-support-chat` (память).
- **Гоча ПОЧИНЕНА — двойные уведомления/алерты**: Laravel 11 авто-обнаруживает слушатели в
  `app/Listeners`; ручные `Event::listen` в `AppServiceProvider::boot()` дублировали их (×2 на событие).
  Убрал ручную регистрацию — оставил авто-обнаружение.
- **Корзина**: степпер количества −/+ (PATCH fetch, `update` отдаёт JSON на AJAX).
- **Email-баннер для писем партнёров**: настройка `email.banner.korzilla` (правится в Настройки→Email-
  баннер), публичный эндпоинт `GET /embed/email-banner` (Cache-Control, ETag/304, noindex). Сайты тянут
  сниппетом `docs/email-banner-embed.php` (PHP 5.6–7.4, локальный кэш + таймаут 1–2с + фолбэк, опции
  TLS `TEEU_BANNER_CAINFO`/`TEEU_BANNER_INSECURE_TLS`).
- **Товары/фиды**: «Актуально на {yml_feeds.last_success_at}» на карточке (для присутствующих в фиде;
  свой `synced_at` пробовали и удалили). Продавцу запрещено править title/price/availability у фид-
  товаров. **Адрес склада фида редактируется** на странице фида (`POST merchant.feeds.address`), срок
  доставки per-feed пересчитывается автоматически (`DeliveryEstimator` читает `feed->address`; в профиле
  магазина адреса НЕТ). Вершины полигона зоны доставки — перетаскиваемые.
- **UI**: логотип продавца `object-contain` (не обрезается). Переиспользуемая модалка `<x-confirm-form>`
  (x-teleport, вместо native `confirm()`) — см. `teeu-confirm-modal` (память).

Тестов ~400 зелёных. Новые тест-файлы: `HelpArticleResourceTest`, `EmailBannerTest`, `FeedAddressTest`,
`SellerFeedIndexTest` (+ расширены `ChatTest`, `CommerceTest`, `ProductTest`).
