# Этап A — Product/UX specification

Дата: 2026-08-05  
Статус: `approved_by_owner — 2026-08-05`  
Режим: спецификация; без разрешения на production-реализацию

## 1. Краткая модель продукта

ThaiForTravel Referral Platform — не сокращатель ссылок и не платёжная партнёрская сеть. Это система управления реферальными агентами и доказуемой first-touch атрибуцией новых клиентов.

- Агент регистрируется через отдельного партнёрского Telegram-бота, проходит модерацию и после одобрения создаёт публикационный комплект «tracking-ссылка + QR».
- Ссылка и QR принадлежат одной кампании, но имеют разные стабильные `tracking_material` и независимо учитываются в аналитике.
- Публичный redirect фиксирует клик до перехода в Telegram, выдаёт новый одноразовый start token и перенаправляет клиента в `@ThaiForTravelBot`.
- Источник существующего клиента никогда не перезаписывается. Если он уже `Сарафан`, он таким и остаётся; для нового клиента агент назначается один раз, а поздние переходы не переписывают first touch.
- Владелец управляет заявками, агентами, материалами, CRM-доставкой и аудитом в отдельной admin-панели.
- Будущие заявки, оплаты, комиссии и выплаты присоединяются к неизменяемой атрибуции как последующие бизнес-события, а не меняют attribution core.

Главный критерий качества: агент без обучения создаёт рабочий комплект за несколько секунд, а владелец может восстановить и объяснить каждое решение об атрибуции.

## 2. Изученные источники и ограничения

Доступны и использованы:

- текущий репозиторий ThaiForTravel, включая клиентский Mini App, referral-прототип, Prisma-схему, тесты и визуальные артефакты;
- публичный сайт [thaifortravel.ru](https://thaifortravel.ru/);
- официальная документация [Telegram Mini Apps](https://core.telegram.org/bots/webapps), [Telegram deep links](https://core.telegram.org/bots/features#deep-linking) и [Telegram Login/OIDC](https://core.telegram.org/bots/telegram-login);
- [WCAG 2.2](https://www.w3.org/TR/WCAG22/), [PostgreSQL constraints](https://www.postgresql.org/docs/current/ddl-constraints.html), [AWS Transactional Outbox](https://docs.aws.amazon.com/prescriptive-guidance/latest/cloud-design-patterns/transactional-outbox.html).

Недоступны в этой среде и не интерпретировались:

- `/home/openclaw/.openclaw/workspace/obsidian/Projects/10 - Active/ThaiForTravel/thaifortravel.md`;
- `/home/openclaw/.openclaw/workspace/obsidian/Projects/10 - Active/ThaiForTravel/Business OS для ThaiForTravel - разбор видео 2026-06-18.md`;
- `/home/openclaw/.openclaw/workspace/tmp/tft-release-20260724r.uCkWGu/miniapp/`;
- `/home/openclaw/.openclaw/workspace/tmp/tft-release-20260724r.uCkWGu/plugin/tft-miniapp-api/README.md`;
- `/home/openclaw/projects/thaifortravel-line-webhook/`.

Их отсутствие помечено как риск неполного business/CRM context. Содержимое не выдумывалось.

## 3. Противоречия и основные риски

### 3.1. Продукт и доступ

1. Текущий прототип создаёт агента в статусе `ACTIVE` и после отправки контакта сразу открывает кабинет; обязательной анкеты и модерации нет.
2. Текущая модель знает только `ACTIVE/BLOCKED`, тогда как продукту нужны `applicant/pending/rejected/approved/suspended` и история повторных подач.
3. В текущем кабинете три раздела; обязательный четвёртый раздел «Профиль» отсутствует.
4. Referral-admin surfaces отсутствуют; существующая общая админка не должна автоматически становиться owner-панелью без отдельного RBAC и аудита.
5. Текущий партнёрский бот сам создаёт кампании, дублируя кабинет. Целевой V1 default: bot отвечает за onboarding/status/auth/notifications, а authoring живёт в одном месте — кабинете.

### 3.2. Атрибуция и tracking

1. Текущий публичный URL и Telegram `/start` используют один и тот же повторно применимый подписанный asset token. Мастер-модель требует стабильный material code снаружи и новый одноразовый click/start token на каждый переход.
2. При ошибке записи клика прототип продолжает redirect с пригодным для атрибуции token. Безопасный production default: при недоступной БД отправлять в обычный бот без атрибуции, чтобы не создавать атрибуцию без доказуемого клика.
3. `ClientAttribution.userId UNIQUE` уже защищает одну локальную атрибуцию на пользователя, но отсутствуют append-only `attribution_decision`, `rule_version`, исчерпывающий reason code и защита от изменения attribution-полей обычным `UPDATE`.
4. Существующий client определяется частично локальным Telegram-пользователем и CRM-поиском по телефону. Точный источник истины и порядок identity resolution ещё не утверждены.
5. **P0:** текущий flow делает CRM lookup/create до локальной транзакции attribution. Если CRM-контакт создан, а локальная запись падает, retry может увидеть этот же контакт как «существовавший ранее» и ошибочно отказать новому клиенту в attribution. Источник риска: `src/attribution-service.ts:111–141`. CRM side effect должен выполняться после durable decision/outbox либо иметь provenance/idempotency, позволяющие отличить контакт, созданный текущей попыткой.
6. Campaign/material имеют boolean `isActive`; этого недостаточно, чтобы различить архив агента и отключение владельцем с разными последствиями.

### 3.3. Аналитика

1. Текущий `visits` считает все human-or-unknown записи, а не уникальные переходы.
2. Текущий UI показывает запуски бота, клиентов и заявки как готовую воронку, хотя V1 должен показывать только честно определённые click-метрики.
3. Нет периодов день/неделя/месяц, произвольного диапазона, URL query state, времени обновления и стабильного trend chart.
4. Privacy key на основе IP + полного User-Agent полезен только как приближённая дедупликация; его нельзя называть уникальным человеком.

### 3.4. Безопасность и устойчивость

1. Mini App HMAC и `auth_date` уже проверяются на сервере, но replay policy не хранит использованный `query_id`/nonce.
2. Текущий bot-confirmed desktop login имеет короткий TTL и hash кода, но challenge не привязан к browser session, exchange не помечает challenge использованным, отсутствует отдельный CSRF state.
3. **P0:** customer webhook добавляет `update_id` в process-local Set до завершения бизнес-логики. Ошибка после отметки приводит к потере Telegram retry в этом процессе, а разные инстансы, наоборот, могут обработать update повторно. Нужен persistent inbox/event key, который фиксируется атомарно с результатом либо переводится в retriable failure.
4. **P0:** outbox claim допускает повторный claim уже `PROCESSING`-строки без ownership token/проверки lease; два worker могут одновременно вызвать CRM. Нужны atomic claim (`SKIP LOCKED`/compare-and-swap), lease owner, idempotent consumer и тест double-claim.
5. Нет referral-specific admin RBAC, append-only admin audit, fraud review, CRM delivery attempts/dead letter/reconciliation.
6. Нет зафиксированной политики retention для pseudonymous click data, журналов и PII.
7. Критический контур не покрыт тестами `finalizeReferralAttribution`, CRM-create→DB-failure, DB race двух первых starts, outbox double-claim и persistent webhook replay; оболочечные HMAC/HTTP-тесты этого не компенсируют.

### 3.5. Дизайн-процесс

1. В репозитории уже есть hi-fi, layoutboard и styleboard, хотя новый мастер-промт требует отдельного утверждения B1 и B2 до hi-fi.
2. Визуальные источники противоречат друг другу: клиентский Mini App использует teal + sun; referral `DESIGN.md` фиксирует cobalt + lime; live referral CSS использует deep marine + yellow.
3. Текущий кабинет выводит буквенный знак `T` вместо официального логотипа.
4. Social SVG и фотографии не имеют полного production-манифеста источника, лицензии, назначения, alt и focal point.

## 4. Безопасные допущения до решения владельца

| Тема | Принятый default | Почему безопасно |
|---|---|---|
| Существующий клиент | Точный Telegram ID → подтверждённый нормализованный телефон → сохранённый CRM ID. Любое однозначное совпадение означает existing; текущий CRM source не меняется; конфликт/недоступность CRM → `pending_verification` | Не присваивает агента при неопределённости и сохраняет `Сарафан`, если он уже был источником |
| Start token TTL | 24 часа | Достаточно для отложенного запуска Telegram; ограничивает replay window |
| Время | Хранение UTC, отчёты `Asia/Bangkok` | Соответствует операционному контексту Таиланда |
| Уникальный переход | Уникальная пара `material_id + rotating_visitor_key + local_date`; итог периода — сумма дневных uniques | Privacy-preserving и объяснимо; явно не равно уникальному человеку |
| Disabled owner link | Redirect в обычный клиентский бот без payload и без атрибуции; сохранить decision/audit reason | Не теряет клиента и не создаёт запрещённую атрибуцию |
| Ручная корректировка | Не включать в V1; позже — только отдельное superseding/reversal event с reason, actor и second approval | Не допускает тихого переписывания first touch |
| CRM | Текущий кандидат — OkoCRM, но IDs, webhook/API contract и authoritative fields считаются неутверждёнными | Следует фактическому коду без выдумывания production-контракта |
| Dark mode | Не является обязательным для V1; light-first, Telegram theme adapter сохраняется | Снижает объём до подтверждения реальной потребности |
| Комиссия/выплаты | Только extension points и future event types; без ledger/UI в V1 | Не перегружает запуск и не начисляет деньги по кликам |

## 5. Роли и permissions matrix

Легенда: `R` — чтение, `C` — создание, `U` — изменение, `A` — административное действие, `—` — нет доступа.

| Ресурс/действие | Applicant | Agent | Suspended agent | Moderator/Admin | Owner | Client |
|---|---:|---:|---:|---:|---:|---:|
| Собственная анкета | C/R/U до submit, U после reject | R | R | R | R | — |
| Подать/повторно подать | C | — | — | — | — | — |
| Решение по анкете | R только свой статус | R | R | A approve/reject с причиной | A | — |
| Свой профиль | R/U разрешённые поля | R/U | R, контакт поддержки | R | R/A | — |
| Создать кампанию + 2 материала | — | C | — | — | A только support flow | — |
| Свои кампании/аналитика | — | R/U archive | — | R в рамках scope | R/A | — |
| Чужие кампании | — | — | — | R по permission | R/A | — |
| Отключить material | — | — | — | A при permission + reason | A + reason | — |
| Suspend/restore агента | — | — | — | A при permission + reason | A + reason | — |
| Attribution decisions | — | Только агрегаты | — | R | R/A exception workflow | — |
| CRM retry/reconcile | — | — | — | A elevated | A elevated | — |
| Admin audit log | — | — | — | R в своём scope | R/export | — |
| Открыть public material | — | — | — | — | — | R |
| Быть атрибутированным | — | — | — | — | — | Только если новый и eligible |

Правила авторизации:

- every-resource authorization выполняется на сервере;
- agent-scoped запрос всегда добавляет `agent_id` из сессии, а не из доверенного client input;
- owner/admin работают на отдельном route namespace, с отдельными permission scopes и audit middleware;
- suspended agent не видит закрытую аналитику и не создаёт кампании, но получает понятную причину и канал связи;
- опасные admin actions требуют конкретной причины, impact confirmation и append-only audit event.

## 6. Информационная архитектура

```text
Партнёрский Telegram-бот
├── Начать регистрацию
├── Анкета
├── Статус заявки
├── Исправить и подать повторно
├── Открыть кабинет
├── Подтвердить desktop-вход
└── Уведомления

Agent Mini App / desktop web
├── Создать
│   ├── Тема кампании
│   ├── Площадка
│   └── Submit → Campaign detail: link + QR
├── Мои ссылки
│   ├── Поиск
│   ├── Фильтры platform/status
│   ├── Campaign detail
│   └── Архивирование
├── Статистика
│   ├── Summary
│   ├── Period/query state
│   ├── Trend chart + data table
│   └── Campaign breakdown: link / QR
└── Профиль
    ├── Редактируемые данные
    ├── Каналы/соцсети
    └── Поддержка

Owner/Admin — отдельное приложение
├── Заявки
├── Агенты
├── Кампании и материалы
├── Attribution decisions
├── Pending verification / fraud review
├── CRM sync / retries / reconciliation
└── Admin audit log

Public/bot boundary
├── GET /r/{materialCode}
├── Customer bot /start {opaqueToken}
└── Customer Mini App / CRM events
```

## 7. End-to-end user flows

### F1. Регистрация и модерация

1. Пользователь открывает партнёрского бота и видит назначение программы и privacy notice.
2. Бот получает подтверждённый Telegram identity и ведёт анкету: имя, дополнительный контакт, тип партнёра, добровольные каналы.
3. Submit с idempotency key создаёт immutable application revision и moderation event `submitted`.
4. Состояние становится `pending`; создание кампаний и аналитика закрыты.
5. Moderator открывает карточку, историю и принимает решение.
6. Approve активирует `agent`; reject требует причины и создаёт доступное заявителю объяснение.
7. После reject заявитель редактирует новую revision и resubmit; история не стирается.

### F2. Создание link + QR

1. Approved agent открывает «Создать».
2. Вводит тему, выбирает одну площадку.
3. Один submit с idempotency key создаёт campaign и ровно два материала: `link`, `qr`.
4. Double tap возвращает исходный результат, а не создаёт вторую кампанию.
5. После создания агент переходит на Campaign detail, где обе tracking-ссылки показаны по назначению: link для копирования, QR preview/download с отдельным material code.
6. Copy/download дают локальный feedback; recoverable error сохраняет форму.

### F3. Click → Telegram start → attribution

1. Клиент открывает стабильный `/r/{materialCode}`.
2. Система проверяет material, campaign и agent status.
3. В одной транзакции создаёт `referral_click`, случайный start token и hash token; raw token не сохраняется.
4. После commit отправляет `302/303` на `t.me/ThaiForTravelBot?start={opaqueToken}`.
5. Customer webhook проверяет persistent update id, token hash, TTL и one-time consume.
6. Identity resolution проверяет локальный registry/CRM.
7. Existing client → `not_assigned_existing_client`; текущий CRM source не меняется (`Сарафан` остаётся `Сарафан`).
8. New client → unique DB constraint создаёт одну immutable attribution.
9. CRM unavailable/ambiguous → `pending_verification`; клиентский сценарий продолжается без преждевременного назначения.
10. Decision и outbox event сохраняются в той же транзакции; CRM delivery выполняется асинхронно.

### F4. Повторные и конкурирующие переходы

1. Каждый новый клик получает отдельный token.
2. Replay одного token возвращает исходное решение/`already_consumed`, но не создаёт второе.
3. Два конкурентных первых старта могут дойти до INSERT, но unique `client_attribution.client_id` пропускает только один.
4. Проигравшая транзакция записывает `not_assigned_already_attributed` со ссылкой на winning attribution, не меняя её.

### F5. Архивирование и owner-disable

- Agent archive убирает кампанию из default active list; оба публичных материала продолжают работать.
- Owner disable применяется к конкретному material или кампании, требует reason и impact confirmation; новые клики не атрибутируются и уходят в обычный бот без payload.
- Restore — отдельное аудируемое действие; исторические click/decision не меняются.

### F6. Desktop login

Предпочтительно Telegram OIDC Authorization Code + PKCE, проверка `iss/aud/exp/nonce`, server-side code exchange и CSRF `state`.

Допустимый fallback — bot-confirmed challenge:

1. browser session создаёт challenge с random hash, TTL, CSRF state и binding hash;
2. агент подтверждает challenge в партнёрском боте;
3. browser единожды обменивает challenge на HttpOnly Secure SameSite session;
4. challenge атомарно помечается consumed; replay отклоняется;
5. session expiry показывает отдельное recoverable состояние.

### F7. CRM outage и reconciliation

1. Business state + outbox записываются одной транзакцией.
2. Worker доставляет idempotent event с exponential backoff + jitter.
3. После лимита попыток event попадает в dead-letter/reconciliation queue.
4. Admin видит статус, sanitized error, correlation ID и может retry/reconcile с permission + audit.
5. Redirect никогда не ждёт синхронный CRM.

## 8. State models

### Agent application

```mermaid
stateDiagram-v2
  [*] --> Draft
  Draft --> Submitting
  Submitting --> Pending: accepted
  Submitting --> Draft: recoverable_error
  Pending --> Approved: approve(reason)
  Pending --> Rejected: reject(reason)
  Rejected --> Draft: edit_new_revision
  Approved --> Suspended: suspend(reason)
  Suspended --> Approved: restore(reason)
```

### Campaign и material

```mermaid
stateDiagram-v2
  [*] --> Active
  Active --> Archived: agent_archive
  Archived --> Active: agent_restore_if_allowed
  Active --> DisabledByOwner: owner_disable(reason)
  Archived --> DisabledByOwner: owner_disable(reason)
  DisabledByOwner --> Active: owner_restore(reason)
```

`Archived` не влияет на redirect. `DisabledByOwner` запрещает новую атрибуцию.

### Attribution decision

```text
token_received
├── invalid/expired/replayed → not_assigned_*
├── ineligible/suspended/self-referral → not_assigned_* | flagged_for_review
└── identity_check
    ├── CRM unavailable/ambiguous → pending_verification
    ├── existing client → not_assigned_existing_client
    ├── already attributed → not_assigned_already_attributed
    └── new + eligible → assigned
```

Decision append-only. Повторная проверка создаёт новое decision event и не переписывает историю.

### Обязательные UI states

На каждом соответствующем экране должны быть предусмотрены: initial/loading (skeleton с задержкой показа), empty, inline validation, submitting, success, recoverable error + retry, offline, permission denied, session expired и fatal error с request/correlation ID.

Отдельные access states: application form, pending, rejected + reason + resubmit, approved, suspended + reason/contact, Telegram auth pending/expired/confirmed.

## 9. Сущности и инварианты

| Сущность | Назначение | Ключевые инварианты |
|---|---|---|
| `telegram_identity` | Внешняя Telegram identity | unique telegram user ID; username не key |
| `agent` | Текущий статус партнёра | переходы только через auditable state machine |
| `agent_application` | Revision анкеты | submit revision immutable |
| `moderation_event` | Решение по заявке | append-only; actor/reason/time |
| `campaign` | Тема + площадка + owner agent | принадлежит одному agent; не удаляется агентом |
| `tracking_material` | link или QR | unique random public code; unique campaign+type |
| `referral_click` | Факт redirect-request | создаётся до redirect; UTC; correlation ID |
| `start_token` | Одноразовая связь click→start | unique hash; TTL; consumed once; raw не хранится |
| `bot_start` | Идентифицированный запуск | persistent webhook event idempotency |
| `client` | Local CRM registry | authoritative external mappings versioned |
| `client_attribution` | Immutable first touch | unique client ID и referral click ID; attribution fields update forbidden |
| `attribution_decision` | Объяснение результата | append-only reason/rule version/evidence refs |
| `domain_event_outbox` | Надёжная доставка | business state + event в одной transaction |
| `crm_delivery_attempt` | Retry/reconcile trace | idempotency key, attempt, sanitized outcome |
| `admin_audit_event` | Действия администратора | append-only actor/reason/before-after refs |
| `daily_campaign_stats` | Optional read model | rebuildable; freshness timestamp |

Deletion policy: campaign/material, связанные с click/decision/attribution, не удаляются каскадно. PII может быть anonymized по retention policy без разрушения доказательной цепочки.

## 10. Словарь метрик V1

| Метрика | Определение | Не означает |
|---|---|---|
| `raw_click` | Каждый валидный запрос к активному tracking material, включая повторный; preview/bot помечается отдельно | Человека или клиента |
| `eligible_click` | `raw_click` без известных crawler/prefetch и технических ошибок | Гарантированно человека |
| `unique_click` | Distinct `material_id + rotating_visitor_key + Asia/Bangkok local_date`; period total — сумма дневных uniques | Точного уникального пользователя за весь период |
| `link_unique_click` | `unique_click` только material type `link` | QR scan |
| `qr_unique_click` | `unique_click` только material type `qr` | Физически уникальное устройство |
| `bot_start` | Валидный одноразовый start token сопоставлен Telegram identity | Новый клиент |
| `confirmed_new_client` | Identity resolution доказал отсутствие клиента в authoritative registry | Сам клик |
| `existing_client` | Однозначное совпадение найдено до назначения | Причину перепривязки |
| `attribution_assigned` | Создана immutable client attribution | Комиссию |
| `attribution_rejected` | Final `not_assigned_*` decision | Ошибку системы обязательно |
| `attribution_pending` | Требуется CRM/review verification | Назначенного агента |

V1 agent UI выводит `unique_click`, link/QR split, trend и campaign breakdown. `bot_start`, client и attribution outcomes доступны owner/admin для контроля качества, но не становятся финансовым обещанием агенту.

Даты: события хранятся UTC; группировка и подписи — `Asia/Bangkok`; UI показывает timezone и `last updated`.

## 11. Privacy-preserving deduplication

Рекомендованный V1 key:

```text
HMAC(rotation_secret,
     material_id | local_date | coarse_ip_prefix | normalized_ua_family)
```

- raw IP и полный User-Agent не попадают в analytics tables;
- IPv4 сокращается минимум до `/24`, IPv6 — до `/48` или грубее;
- `rotation_secret` выводится на ограниченный период и удаляется по retention policy;
- crawler/prefetch определяется отдельным signal и никогда не считается безошибочно;
- fingerprint не используется для client attribution; её подтверждает Telegram identity на `/start`.

Ограничения честно описываются в tooltip/data dictionary: NAT может объединять разных людей, смена сети/браузера может разделить одного, сумма daily unique не равна уникальным людям за месяц.

## 12. Экранный реестр

### Agent

- Onboarding: анкета, submitting, pending, rejected/resubmit, approved handoff, suspended.
- Auth: Mini App validation, desktop start, waiting, confirmed, expired, session expired.
- Создать: initial, inline validation, submitting/idempotent replay, redirect в created Campaign detail, recoverable/fatal error.
- Мои ссылки: list, search, platform/status filters, empty, loading, campaign detail, archive confirmation.
- Статистика: period controls, custom date, summary, stable chart, accessible tooltip/data table, campaign expand, zero/no-data, stale data.
- Профиль: editable identity/contact/channel data; approval status остаётся компактным badge, без отдельного системного блока для агента.

### Owner/Admin — физически отдельное приложение

- Admin auth/MFA/session expiry/access denied.
- Application queue + filters; application card + revision/moderation timeline; approve/reject reason dialog.
- Agent list/profile; suspend/restore; agent campaigns/materials.
- Material disable/restore impact confirmation.
- Attribution decisions detail: click→start→identity→decision→CRM trace.
- Pending verification/fraud queue.
- CRM sync, retries, dead letter, reconciliation.
- Append-only admin audit log.

## 13. Edge cases и ожидаемое поведение

| Ситуация | Поведение |
|---|---|
| Double submit campaign | Idempotency key возвращает первоначальные campaign/materials |
| Один idempotency key с другим body | `409 IDEMPOTENCY_KEY_REUSED` |
| Preview bot открывает ссылку | Click маркируется; primary analytics исключает; start token не назначает без Telegram start |
| DB недоступна на redirect | Redirect в обычный bot без referral payload; alert + correlation ID; нет attribution |
| CRM недоступна | `pending_verification`, retry outbox; redirect/customer journey продолжаются |
| CRM создала контакт, локальная transaction упала | Retry узнаёт correlation/provenance текущей попытки и не переклассифицирует нового клиента как pre-existing; событие уходит в reconciliation |
| CRM даёт несколько совпадений | Не назначать; manual verification queue |
| Existing client приходит по новому agent link | `not_assigned_existing_client`; текущий CRM source не меняется |
| Поздний start после TTL | `not_assigned_expired`; можно продолжить обычный bot journey |
| Token replay/webhook duplicate | Исходный decision или duplicate success; новых записей нет |
| Webhook падает после получения update | Durable inbox остаётся retriable; update не помечается окончательно обработанным раньше business commit |
| Два worker одновременно забирают outbox row | Только один владеет действующим lease; CRM consumer дополнительно дедуплицирует event key |
| Повторный exchange desktop challenge | Первый exchange атомарно consume-ит challenge; повтор получает typed expired/consumed error |
| Два concurrent first starts | Один wins unique constraint; второй `already_attributed` |
| Self-referral agent→same Telegram/phone | Reject или fraud review; причина видна admin |
| Agent archived campaign | Link/QR продолжают работать; UI hidden from active default |
| Owner disabled material | Обычный bot без attribution; audit reason |
| Suspended agent | Новые кампании и закрытая аналитика недоступны; owner policy определяет redirect существующих материалов |
| QR download failed | Inline action error + retry; campaign не дублируется |
| Clipboard denied | Выделяемое поле + ручное копирование; локальная ошибка рядом с действием |
| Offline / weak Telegram WebView | Сохранить form draft локально без PII; показать retry, не бесконечный spinner |
| Empty/zero statistics | Честный ноль, definition hint, CTA создать/распространить материал |
| Long Russian campaign name | Wrap/clamp без уменьшения шрифта; доступно полное имя |
| Admin destructive action replay | Persistent idempotency + одна audit запись |

## 14. Решения владельца перед продолжением

Предлагается утвердить defaults из раздела 4 или указать только изменения:

1. Existing client: Telegram ID → verified phone → CRM ID; любой однозначный match сохраняет текущий CRM source без изменения. Подтвердить, что baseline source для таких клиентов действительно `Сарафан`.
2. Start token TTL: 24 часа.
3. Analytics timezone: `Asia/Bangkok`; storage UTC.
4. Owner-disabled link: обычный бот без атрибуции.
5. Unique click: daily privacy key per material; period total — сумма дневных uniques.
6. Manual correction: вне V1; позже только audited superseding event.
7. Visual foundation: выбрать в Gate B1.
8. CRM: OkoCRM как текущий кандидат; подтвердить authoritative IDs/fields/API до C.
9. Suspended-agent materials: рекомендуемый default — redirect в обычный bot без attribution до restore.

## 15. Acceptance criteria этапа A

- Все роли, surfaces и обязательные состояния перечислены.
- Link и QR являются разными материалами одной кампании.
- Existing client никогда не получает перезапись source; `Сарафан` сохраняется, если уже был установлен.
- Archive и owner-disable имеют разные последствия.
- V1 analytics не выдаёт fingerprint за identity и не обещает комиссию.
- Attribution outcome всегда объясним reason code + rule version + audit trace.
- Недоступность CRM не ломает redirect и не создаёт безусловную attribution.
- Следующий дизайн-гейт не начинается до явного решения по B1.
