# Техническое задание на интеграцию интернет-магазина Принципала с Системой ПЛАТИМ

**Статус:** черновик Приложения №2 к договору возмездного оказания услуг (п. 2.1, 3.2, 17.4).  
**Аудитория:** разработчики сайта партнёра (Принципала). Документ передаётся партнёру после регистрации: подготовка ИМ и подключение к Системе без участия инженеров Агента.  
**Версия контракта API:** Partner API v0. Машиночитаемое приложение: [partner-api-v0.openapi.yaml](partner-api-v0.openapi.yaml). Человекочитаемый перечень ресурсов: [partner-api-v0.md](partner-api-v0.md).  
**Дата черновика:** 2026-08-14.

В тексте:

- **Должен / обязан / запрещено** — нормативные требования к интеграции.
- **Сейчас** — поведение, реализованное в Системе на staging.
- **Целевая модель (к реализации)** — обязательства, которые Агент обязан закрыть в продукте до боевого (production) партнёра; Принципал проектирует ИМ с их учётом.

Суммы во всех запросах и ответах API — **целые копейки** (`kop`). Денежные поля в рублях не принимаются.

---

## 1. Назначение и роли

| Роль | Кто | Что делает |
| --- | --- | --- |
| **Агент** | Оператор Системы ПЛАТИМ | Выдаёт доступ к API и виджету, ведёт реестр сертификатов, hold/списание/выпуск, взаиморасчёты |
| **Принципал** | Партнёр (владелец ИМ) | Обеспечивает интеграцию своего интернет-магазина с Системой **по настоящему ТЗ** (договор 3.2) |
| **Пользователь** | Покупатель | Получает и тратит сертификаты (СЭС / УЭС) на сайте Принципала |

**Обязанности ИМ Принципала:** идентификация посетителя/сделки, показ и применение сертификатов в checkout, передача статусов заказа (`paid` / `cancelled`), уведомление о возврате.

**Обязанности Системы:** сессия виджета, баланс, резерв (hold), списание, выпуск СЭС+УЭС, учёт взаиморасчётов.

### 1.1. Границы

| В scope | Вне scope настоящего ТЗ |
| --- | --- |
| Онлайн интернет-магазин (веб) | Офлайн QR / POS (договор 10.1) |
| JS-виджет + server-to-server Partner API | Нативный модуль конкретной CMS (Bitrix и др. — отдельные адаптеры) |
| Реферальный вход `pla_ref`, баланс, apply, paid/cancelled | Чат-бот, native-приложение, email-рассылка сертификатов |
| HTTPS, ключи партнёра, идемпотентность | Универсальная библиотека «всех CMS» |

Путь «только реф + webhook без UI сертификатов в корзине» **запрещён** (оферта 2.4; продукт: виджет в checkout обязателен).

### 1.2. Сертификаты

| Тип | Где тратится |
| --- | --- |
| **СЭС** (собственный) | только у партнёра-эмитента |
| **УЭС** (универсальный) | у любого партнёра сети |

Порядок списания при выборе обоих типов: сначала СЭС, остаток — УЭС. Списание FIFO по выпускам. Новые СЭС и УЭС выпускаются по **ставкам подписанного допсоглашения площадки** (% скидки и доли СЭС/УЭС), считаемой от **полной суммы заказа** (cash + погашенные сертификаты), даже если часть заказа оплачена сертификатами. Без подписанного ДС на origin заказ не закрывается (`no_addendum`).

---

## 2. Онбординг

Последовательность, которую Принципал обязан пройти до боевого трафика:

```mermaid
flowchart LR
  register[Регистрация партнёра]
  creds[Выдача credentials]
  origin[Whitelist site_origin]
  sandbox[Sandbox staging]
  accept[Чеклист приёмки]
  prod[Production]
  register --> creds --> origin --> sandbox --> accept --> prod
```

| Шаг | Сейчас | Целевая модель (к реализации) |
| --- | --- | --- |
| Регистрация | Учётная запись партнёра заводится Агентом (staging / сид) | Самостоятельная регистрация в ЛК партнёра |
| Ключ API | Строка `partner_api_key`, выдаётся Агентом (на демо: фиксированные ключи стенда) | Выпуск / просмотр / отзыв / ротация ключей **в ЛК партнёра**; отдельные ключи sandbox и production |
| Origin сайта | `site_origin` партнёра в реестре Системы; CORS только с этих origin | Тот же whitelist; партнёр указывает origin в ЛК; несколько origin через список |
| Sandbox | `https://app.demo.pla.team` | Отдельный sandbox-контур или тот же staging с sandbox-ключами |
| Production | `https://pla.team` | Отдельный origin, отдельные ключи, отдельная БД |

**Переключение sandbox → production:** один и тот же код интеграции. После приёмки на sandbox Принципал меняет только конфигурацию: origin платформы / `data-app`, base URL API, go-origin, API-ключ. Sandbox-ключ **запрещено** оставлять в production-сборке. Для модуля Bitrix — поля в настройках модуля (`platform_origin`, `api_base`, `go_origin`, `api_key`).

**Эталон поведения витрины** (не копировать как боевой ИМ): демо-магазины на staging `https://north.demo.pla.team` / `https://south.demo.pla.team`.

После выдачи ключа Принципал обязан:

1. Сохранить ключ в секретах сервера ИМ (не в git, не в публичном JS).
2. Сообщить Агенту канонический origin витрины (`https://shop.example.ru`, без path).
3. Пройти чеклист §10 на sandbox.
4. Не использовать sandbox-ключ на production и наоборот (**целевая модель**; сейчас один контур — staging).

---

## 3. Модель безопасности

### 3.1. Сейчас (v0 / staging)

1. Вызовы Partner API — только **HTTPS**.
2. Аутентификация партнёра: заголовок  
   `Authorization: Bearer <partner_api_key>`.
3. Ключ идентифицирует Принципала; чужой ключ даёт `401`.
4. Идемпотентность: заголовок `Idempotency-Key` **или** поле тела `operationId` / стабильный `orderId`. Повтор с тем же ключом не должен создавать второе списание / второй выпуск.
5. CORS: браузерные запросы принимаются **только** с origin, зарегистрированного у партнёра (`site_origin`). Origin платформы всегда в allowlist.
6. Сессия покупателя в виджете: first-party cookie `plateam_vid` на origin платформы (скрытый iframe `/embed/bridge`). ИМ не должен подменять cookie платформы.

**Ограничение демо-виджета (не переносить в боевой ИМ):** `widget.js` на staging получает `partner.apiKey` в объекте сессии и вызывает hold/paid из браузера. Это удобство демо-витрин. **Для боевого подключения запрещено.** Ключ API обязан жить только на сервере ИМ.

### 3.2. Целевая модель (к реализации) — Принципал обязан заложить сразу

| Требование | Норма |
| --- | --- |
| Ключ API | Только server-side ИМ. Запрещено: HTML, `widget.js`, мобильное приложение, репозиторий, логи доступа |
| Публичный идентификатор | В разметке виджета — только **код партнёра** (`data-partner`), не ключ |
| Разделение контуров | Sandbox-ключ ≠ production-ключ |
| Ротация | Агент выдаёт новый ключ, старый действует ограниченное время, затем отзыв |
| Webhook-подпись | Если Система будет вызывать URL ИМ: заголовок `X-Plateam-Signature` = HMAC-SHA256 тела с **webhook signing secret**; ИМ обязан проверять подпись и timestamp |
| Secret вебхука | Хранится как ключ API (договор 13.1 — конфиденциально) |
| IP allowlist | Опционально: Агент ограничивает входящие API-вызовы по IP ИМ |
| Минимальные права | Ключ не даёт доступа к чужим партнёрам, админке, ЛК покупателя |
| Хранение | Секреты в vault / env сервера; ротация при утечке — немедленно уведомить `support@pla.team` |

ИМ **обязан** вызывать `POST /holds`, `POST /orders/paid`, `POST /orders/cancelled` (и целевые quote / returns) **со своего бэкенда**. Виджет показывает баланс и собирает выбор СЭС/УЭС; финальная операция — на сервере ИМ по `visitorId` / `userId` из сессии виджета.

### 3.3. Идентичность покупателя

| Слой | Смысл | Что передавать в API |
| --- | --- | --- |
| **Visitor** | Кошелёк в браузере без обязательного входа | `visitorId` из `PLATEAM.getSession()` |
| **User** | Залогиненный покупатель ПЛАТИМ | `userId`; visitor сливается с user **только после явного согласия** покупателя (не автоматически на каждый логин/session) |

Канон: [../discovery/14-identity-lk-decisions.md](../discovery/14-identity-lk-decisions.md).

Запрещено выдумывать `userId` / `visitorId`. Идентификаторы только из сессии виджета или ответа API.

---

## 4. Архитектура встройки (путь B — MVP)

```mermaid
sequenceDiagram
  participant Browser
  participant Shop as PartnerShop
  participant Widget as WidgetJS
  participant API as PlateamAPI
  Browser->>Shop: открытие витрины или checkout
  Shop->>Widget: widget.js plus data-partner
  Widget->>API: session via embed bridge
  Widget-->>Browser: баланс СЭС/УЭС UI
  Browser->>Shop: выбор сертификатов и оплата
  Shop->>API: quote optional then hold
  Shop->>API: orders paid
  API-->>Shop: issue СЭС и УЭС
  opt отмена
    Shop->>API: orders cancelled
  end
  opt возврат
    Shop->>API: returns notify
  end
```

Два слоя, оба обязательны:

1. **Клиент:** виджет (показ, приветствие, плитки СЭС/УЭС).
2. **Сервер ИМ:** Partner API (hold → paid / cancelled → return).

---

## 5. Клиентская часть — виджет

### 5.1. Обязан ли ИМ использовать наш `widget.js`

**Для подключения по настоящему ТЗ без инженеров Агента — да: обязан подключить скрипт Системы** (`widget.js` с origin платформы). Свой «похожий» скрипт вместо него **запрещён** в режиме самообслуживания.

Причина: сессия покупателя, cookie `plateam_vid`, CORS/bridge, режимы `silent` / welcome, тексты окон и бренд «Активатор лояльности PLATEAM» живут в скрипте Агента. Дублировать это на стороне ИМ — отдельный проект и приёмка Агентом, не «просто свой JS».

| Что | Норма |
| --- | --- |
| Самостоятельное подключение (цель этого ТЗ) | Обязан вставить **наш** `widget.js` |
| Корзина / оплата / CMS | Пишет ИМ сам; виджет не заменяет checkout |
| Hold / paid / cancelled | Бэкенд ИМ по Partner API (§6–7) |
| Свой UI сертификатов вместо виджета | Только **по письменному согласованию с Агентом** (кастом крупного пилота). Тогда ИМ обязан воспроизвести §5.5–5.6 и канон окон один в один; сессию и cookie платформы всё равно нельзя подменить самописным third-party cookie. Ключ API во frontend по-прежнему запрещён |

Итого: функционал корзины и оплаты ИМ пишет сам; **узнавание покупателя и показ сертификатов на витрине — наш виджет**, если партнёр подключается сам.

### 5.2. Что делает `widget.js` (и чего не делает)

После вставки тега скрипт Системы:

1. Читает `data-partner` и `data-app`.
2. Вешает скрытый iframe `{data-app}/embed/bridge?partner=…` — first-party cookie `plateam_vid` на origin **платформы**.
3. По `postMessage` пингует bridge: отдаёт весь `location.search` страницы ИМ (и устаревшие `pla_ref` / `pla_wm`, если они ещё есть в URL).
4. Получает сессию: `visitorId`, опционально `userId`, баланс СЭС/УЭС, режим UI, параметры оффера (ставка скидки).
5. Рисует UI Системы: тишина / плашка / модалка приветствия (не на каждом хите).
6. Синхронизирует вкладки того же origin через `BroadcastChannel('plateam-widget')`; при фокусе соседней вкладки другого магазина сети — повторный ping.
7. Публикует `window.PLATEAM` для кода ИМ: сессия, подписка, прогноз выпуска сертификатов.

**Не делает** (это зона ИМ): каталог и корзину CMS, эквайринг, создание заказа в ИМ, вызов paid/cancelled с сервера, возвраты, вёрстку страницы магазина.

На staging демо-виджет умеет `hold` / `pay` / `cancel` из браузера с ключом в сессии — **только стенд**. Боевой ИМ эти методы для списания **не использует** (§3.1): идентификаторы берёт из `getSession()`, операции — с бэкенда.

### 5.3. Подключение

На всех страницах, где нужна идентификация сети и на **странице корзины / оформления**:

```html
<script
  src="https://app.demo.pla.team/widget.js"
  data-partner="КОД_ПАРТНЁРА"
  data-app="https://app.demo.pla.team"
  async
></script>
```

| Атрибут | Обязанность |
| --- | --- |
| `src` | URL `widget.js` с origin **платформы**, не с origin ИМ |
| `data-partner` | Публичный код партнёра (латиница, как выдан Агентом). **Не** API-ключ |
| `data-app` | Origin платформы текущего контура (staging / production) |

Скрипт создаёт скрытый iframe `{data-app}/embed/bridge?partner=…` (cookie `plateam_vid`) и корень UI `#plateam-widget-root`.

Production-URL скрипта: `https://pla.team/widget.js` с `data-app="https://pla.team"`. До приёмки использовать только staging (пример выше).

### 5.4. Реферальный вход

ИМ **должен** сохранять реферальный query при первом заходе и не сбрасывать его до завершения сессии виджета. Партнёр может задать для origin **свою пару** имя=значение (ЛК → Компания) — тогда она пробрасывается на витрину и считается реферальным входом. Если пара не задана — канон `pla_ref`. Редиректор сети (`go…/r/TOKEN`) **не ставит** `pla_wm` в URL ИМ (атрибуция вебмастера — cookie на origin платформы). Виджет читает весь `location.search` и передаёт его в `POST /api/v0/widget/session`.

Типичный вход: `https://shop.example.ru/?data=FIFA` (пара origin) или `https://shop.example.ru/?pla_ref=TOKEN`, либо переход `go…/r/TOKEN` → 302 на витрину с этой парой / `pla_ref`, без `pla_wm`.

### 5.5. UI-режимы (обязан воспроизвести смысл)

| UI | Когда | Поведение ИМ |
| --- | --- | --- |
| `silent` | чужой посетитель: сеть неизвестна и нет spendable | **zero UI** сертификатов; не показывать пустые «у вас 0 ₽» |
| `welcome_referral` | посетитель известен сети, сертификатов нет | плашка / приветствие: после оплаты будут сертификаты |
| `welcome_certs` | есть СЭС у этого партнёра и/или УЭС | приветствие «используйте сертификаты» |
| блок в корзине | known или certs | основной путь apply |

Модалка приветствия — при первом переходе в `welcome_*`, не на каждом хите. Копирайт окон — канон владельца (бренд в UI: «Активатор лояльности PLATEAM»).

В корзине при наличии сертификатов:

- две плитки: «Собственный» (СЭС) / «Универсальный» (УЭС);
- сумма вручную **не** вводится: только вкл/выкл типа;
- к оплате картой = `max(0, orderKop − автосписание)`;
- полное покрытие → оформление без карты (`cashKop = 0`).

**Скидка в сумме заказа (обязательно для боевого ИМ):** выбор сертификатов в UI **не достаточен** — магазин обязан уменьшить сумму к оплате эквайрингом (или итог заказа в CMS) на величину автосписания. Иначе покупатель заплатит полную цену, а в `orders/paid` уйдёт заниженный `cashKop`. Правило: `orderTotalKop = cashKop + sesKop + uesKop` (копейки, целые). Эталон UI — демо-витрины (`shop.js`); референс Bitrix — компонент `plateam:checkout` + `OrderDiscount` в модуле.

JS API виджета (staging): `window.PLATEAM.getSession()`, `onSession`, `refresh`, `projectIssue(orderKop)`, `hold`, `pay`, `cancel`. Боевой ИМ **должен** использовать сессию для UI и идентификаторов; **не должен** полагаться на `pay()` из браузера с ключом в сессии — см. §3.1.

### 5.6. Чеклист CORS / cookie

- Витрина открывается по **тому же origin**, что зарегистрирован в Системе (схема + host, без лишнего path).
- `https` на staging/prod; смешанный контент запрещён.
- iframe bridge не блокируется CSP: разрешить `frame-src` origin платформы; платформа должна разрешать embed с origin ИМ.
- Сторонние cookie: first-party cookie живёт на платформе внутри iframe; ИМ не требует third-party cookie на своём домене.

---

## 6. Серверная часть — Partner API v0

**Base URL staging:** `https://app.demo.pla.team/api/v0`  
**Base URL production:** `https://pla.team/api/v0`

Заголовки каждого запроса:

```
Authorization: Bearer <partner_api_key>
Content-Type: application/json
Idempotency-Key: <уникальный ключ операции>   # рекомендуется
```

Коды: `401` ключ; `404` сущность; `409` конфликт идемпотентности; `422` бизнес-правило (недостаточно баланса и т.п.).

### 6.1. Проверка ключа — сейчас

`GET /partners/me`

Ответ: `{ id, code, name, siteOrigin }`.

### 6.2. Баланс — сейчас

`GET /users/{userId}/balance`

Для visitor-only сценария боевой ИМ передаёт идентификатор из сессии (user или visitor — как в контракте реализации). Ответ (копейки):

```json
{ "ses": [{ "id", "issuerPartnerId", "remainingKop" }], "ues": [{ "id", "remainingKop" }] }
```

Виджет агрегирует `sesForThisPartnerKop` / `uesKop` в объекте сессии — этого достаточно для UI корзины.

### 6.3. Предпросмотр списания — целевая модель (к реализации)

`POST /quote`

Контракт зафиксирован в [partner-api-v0.md](partner-api-v0.md). **Маршрута в текущем runtime нет.** До появления endpoint ИМ считает payable на клиенте по правилам §5.5 и всё равно обязан создать hold перед оплатой. После появления quote — ИМ **должен** вызывать quote до hold.

```json
{ "userId", "partnerId", "orderId", "cartTotalKop", "wantSesKop", "wantUesKop" }
```

### 6.4. Резерв (hold) — сейчас

`POST /holds`

```json
{
  "userId": "… или null",
  "visitorId": "… или null",
  "orderId": "стабильный id заказа ИМ",
  "sesKop": 0,
  "uesKop": 0,
  "operationId": "hold-<orderId>"
}
```

Ответ: `{ holdId, status: "held", … }`. Нужен хотя бы один из `userId` / `visitorId`.

Подтверждение / отзыв:

- `POST /holds/{holdId}/confirm` — после успешной оплаты (если не используется путь через `orders/paid` с `holdId`);
- `POST /holds/{holdId}/release` — оплата не прошла.

`POST /orders/paid` с `holdId` подтверждает hold в том же шаге (демо-поток).

### 6.5. Оплата — сейчас

`POST /orders/paid`

Вызывать **только после** факта оплаты в ИМ (или при `cashKop = 0` и полном покрытии сертификатами). Issue сертификатов привязан к **paid**, не к «создан заказ».

```json
{
  "orderId": "id заказа ИМ",
  "userId": null,
  "visitorId": "из сессии",
  "cashKop": 0,
  "holdId": "из POST /holds или null",
  "operationId": "paid-<orderId>"
}
```

Эффект: confirm hold (если был); выпуск СЭС+УЭС по ставкам допсоглашения площадки с **полной** суммы заказа (cash + списанные сертификаты). Без ДС — `no_addendum`.

ИМ обязан передать тот же `orderId`, что использовался в hold.

### 6.6. Отмена — сейчас

`POST /orders/cancelled`

```json
{ "orderId": "id заказа ИМ", "operationId": "cancel-<orderId>" }
```

Эффект: release активного hold по заказу; сторно взаиморасчётов / начислений вебмастера, если paid уже был.

### 6.7. Возврат — `returns/notify` (договор 12.2)

`POST /returns/notify`

```json
{ "partnerId": "…", "orderId": "…", "operationId": "return-<orderId>", "amountKop": 150000, "reason": "…", "lines": [] }
```

**Runtime есть** (`/api/v0/returns/notify`). Принципал обязан уведомить ≤1 р.д. после возврата (ЛК или API, договор 12.2.1). До confirm Агента деньги покупателю / annul / restore сертификатов не проводятся (12.2.3). Статус в Системе: `pending` (в UI — «ожидает подтверждения»). Confirm агента — следующий срез.

ИМ передаёт: `orderId`, `operationId`, опционально сумму возврата и UID сертификатов.

---

## 7. Обязанности магазина (CMS / бэкенд)

1. **Момент вызова API**
   - hold — когда покупатель переходит к оплате с выбранными сертификатами;
   - `orders/paid` — только после успешного списания денег эквайрингом **или** при нулевой доплате картой;
   - `orders/cancelled` / `holds/release` — если оплата сорвалась или заказ отменён до выдачи;
   - `returns/notify` — при возврате товара/оплаты в ИМ.
2. **Стабильный `orderId`** — уникален в рамках партнёра; не менять между hold и paid.
3. **`cashKop`** — сумма, реально списанная картой/счётом; не включает погашенные сертификаты.
4. **Таймаут hold** — если покупатель бросил оплату, вызвать release; не оставлять вечный резерв.
5. **Идемпотентность** — ретраи шлюза не должны выпускать сертификаты дважды.
6. **Частичное покрытие** — hold на выбранные типы + оплата остатка картой + один `orders/paid`.
7. **Полное покрытие** — `cashKop = 0`, paid всё равно обязателен (иначе сертификаты не выпускаются).
8. **Скидка в корзине / checkout** — сумма к оплате картой и сумма платежа в CMS **должны** уменьшаться на `sesKop + uesKop`; свойства заказа / checkout stash хранят выбор до `hold`. Не полагаться только на подпись «к оплате картой» в JS без изменения заказа.
9. **Возвраты** — не возвращать cash и не «откатывать» сертификаты локально до ответа Агента.
10. **Ключ** — только сервер; виджет не содержит секретов.
11. **ПДн** — не передавать в API ФИО, телефон, email, состав корзины, пока контракт этого не требует (§8).

---

## 8. Состав данных

Сейчас Система **не пушит** события на URL ИМ. Обмен — запрос ИМ / виджета → ответ Системы. Push-webhook — целевая модель (§3.2).

ИМ **не обязан** (и до отдельного поручения **не должен**) присылать ФИО, телефон, email, адрес, состав корзины, способ оплаты. Идентификация покупателя — `visitorId` / `userId` Системы, не кабинет ИМ.

### 8.1. Что ИМ / виджет передаёт в Систему

| Когда | Канал | Поля | Зачем |
| --- | --- | --- | --- |
| Загрузка страницы с виджетом | bridge → `POST /api/v0/widget/session` | `partnerCode`; `search` витрины; cookie платформы (`plateam_vid`, при клике сети — `plateam_wm`) | Сессия, атрибуция, баланс, режим UI |
| Покупатель применяет сертификаты | `POST /holds` с сервера ИМ | `orderId` ИМ; `visitorId` и/или `userId` из сессии; `sesKop`; `uesKop`; `operationId` | Резерв номинала |
| Оплата прошла (или полное покрытие) | `POST /orders/paid` | те же id; `cashKop` (копейки, только карта/счёт); `holdId` если был hold; `operationId` | Списание + выпуск СЭС/УЭС |
| Оплата не прошла / заказ отменён | `POST /orders/cancelled` или `POST /holds/{id}/release` | `orderId` / `holdId`; `operationId` | Снять резерв; сторно, если paid уже был |
| Возврат в ИМ | `POST /returns/notify` (**runtime**) | `orderId`; сумма; UID сертификатов; `operationId` | Договор 12.2; status `pending`; confirm агента — позже |

`orderId` — идентификатор заказа **в ИМ**, не внутренний id Системы. Состав товарных позиций Система не хранит.

### 8.2. Что Система отдаёт ИМ (ответы)

| Когда | Что в ответе / сессии | Не входит |
| --- | --- | --- |
| Сессия виджета (`getSession`) | `visitorId`; `userId` если залогинен на платформе; `ui`; `networkKnown`; `balance.sesForThisPartnerKop`, `uesKop`, `sesByPartner[]`; `discountPct`; тексты окон; код/имя партнёра | ФИО, email, телефон покупателя; история чужих заказов |
| `POST /holds` | `holdId`; `status: held`; `expiresAt` | Список товаров ИМ |
| `POST /orders/paid` | `ok`; `issued.sesKop` / `uesKop` и id сертификатов; `certUsedKop`; `cashKop`; `holdId` | Платёжные реквизиты карты |
| `GET /users/{id}/balance` | списки СЭС/УЭС: id, остаток, эмитент СЭС | ПДн владельца кошелька |

**Staging / демо:** в объекте `partner` сессии сейчас есть `apiKey`. Для боевого ИМ это поле **игнорировать**; в целевой модели его в сессии не будет (§3.1).

### 8.3. Чего Система не отправляет (сейчас)

- Нет исходящего webhook на сервер ИМ («заказ оплачен», «сертификаты выданы»).
- Нет выгрузки клиентов ИМ и нет обратной записи в CRM партнёра.
- Нет передачи платёжных данных эквайринга (их ИМ обрабатывает сам).

**Целевая модель:** при появлении webhook — тело с `orderId`, статусом, суммами сертификатов; подпись HMAC. Состав ПДн в webhook не расширять без поручения и Политики ПДн.

---

## 9. Тестовый контур и production

| Параметр | Sandbox | Production |
| --- | --- | --- |
| Платформа / API / widget | `https://app.demo.pla.team` | `https://pla.team` |
| go | `https://go.demo.pla.team` | `https://go.pla.team` |
| Эталон витрин | `https://north.demo.pla.team`, `https://south.demo.pla.team` | — |
| Баннер | «тестовая среда — не боевые деньги» | боевой UI |
| Покупатель демо | `demo@pla.team` / `demo1234` (стенд Агента) | реальные пользователи |
| Support | `support@pla.team` | `support@pla.team` |

Ключ sandbox Принципал получает у Агента (целевая модель — в ЛК). Демо-ключи стенда (`demo-north-key` и аналоги) **не** использовать как ключи боевого партнёра.

Критерий «готово к пилоту»: все пункты §10 в статусе pass на sandbox; origin и ключ боевого контура выданы отдельно. Публичная выжимка для разработчиков: `https://pla.team/docs/integration`.

---

## 10. Чеклист приёмки

Разработчик Принципала отмечает pass/fail. Агент может повторить smoke на staging.

| ID | Проверка | Pass |
| --- | --- | --- |
| W1 | `widget.js` с `data-partner` (код, не ключ) и `data-app` контура | |
| W2 | Инкогнито без рефа и без сертификатов — UI `silent` | |
| W3 | Заход с нужной парой origin (или `?pla_ref=…`, если пара не задана) — welcome сети | |
| W4 | Корзина/checkout: плитки СЭС/УЭС, пересчёт «к оплате картой», **скидка отражена в сумме заказа** (не только подпись в UI) | |
| W5 | CSP/CORS: виджет и bridge работают, cookie сессии на платформе | |
| A1 | `GET /partners/me` с Bearer ключом с **сервера** → 200 | |
| A2 | Неверный ключ → 401 | |
| A3 | Hold + paid: сертификаты появились в сессии/балансе | |
| A4 | Повтор paid с тем же `operationId` / `orderId` не дублирует выпуск | |
| A5 | Отмена до оплаты: hold снят, сертификаты не списаны | |
| A6 | Полное покрытие: `cashKop = 0`, заказ paid, выпуск есть | |
| A7 | Частичное покрытие: карта + сертификаты, `orderTotal = cash + cert` в заказе и в API | |
| A8 | Ключ отсутствует в HTML, JS бандле, репозитории | |
| R1 | Заложен вызов `returns/notify` (или ЛК) при возврате | |
| S1 | Sandbox-ключ не зашит в production-сборку | |

Smoke Агента: сверка ledger (выпуск СЭС/УЭС, комиссия) по тестовому `orderId`.

---

## 11. Вне scope / roadmap

| Тема | Статус |
| --- | --- |
| Нативный модуль Bitrix | Репозиторий: [code-philosophy/plateam-bitrix](https://github.com/code-philosophy/plateam-bitrix) (`plateam:checkout`, `OrderDiscount`); страница: [/docs/integration](https://pla.team/docs/integration) |
| `POST /quote` | Целевая модель, контракт в partner-api-v0 |
| `POST /returns/notify` | Runtime есть; confirm Агента — следующий срез; обязанность 12.2 уже на Принципале |
| OAuth / соцвход покупателя на витрине | Вне v0; вход телефон+SMS — см. [../discovery/14-identity-lk-decisions.md](../discovery/14-identity-lk-decisions.md) |
| Письмо на почту о сертификатах | Канон владельца, не v0 |
| Push-webhook Системы на URL ИМ | Целевая модель + HMAC §3.2 |
| Ротация ключей и sandbox/prod keys в ЛК | Целевая модель §2–3 |
| Офлайн QR | Не входит в это ТЗ |
| Боевой эквайринг PSP | Зона ИМ; Система принимает факт paid |

Вопросы по ТЗ: `support@pla.team`.
