Диспуты и Апелляции
Разрешение спорных ситуаций по платежам: создание апелляций, чат с арбитром, полный контракт Appeal Service в Syncra V2
Диспуты и Апелляции (Appeals)
В процессе P2P-эквайринга могут возникать спорные ситуации: например, когда клиент утверждает, что совершил перевод, но P2P-трейдер заявляет об отсутствии поступления на свою карту.
Для разрешения таких конфликтов в Syncra V2 предусмотрен сервис апелляций (Appeal Service): мерчант открывает апелляцию по сделке, прикладывает файлы-доказательства и ведёт переписку в чате спора с арбитром платформы.
Все эндпоинты раздела — под HMAC-парой мерчанта; создание апелляции и чат доступны и из кабинета (cabinet-JWT сессия, тот же контракт — инициатор резолвится из аутентификации).
Процедура рассмотрения спора (Workflow)
- Инициирование спора. Мерчант создаёт апелляцию, указывая причину и прикладывая файлы-доказательства (UUID предварительно загруженных файлов).
- Арбитраж. Арбитры Syncra проверяют подлинность доказательств, сверяют время и реквизиты перевода, после чего выносят решение.
Статусы апелляций
Канонический жизненный цикл — domain.AppealStatus (5 значений). Полная
таблица с V1-маппингом — в
Справочнике статусов.
| Статус V2 | Финальный? | Описание |
|---|---|---|
OPEN | Нет | Апелляция создана, ожидает назначения арбитра. |
IN_PROGRESS | Нет | Арбитр рассматривает спор, ведётся чат. |
REOPENED | Нет | Апелляция повторно открыта для доп. расследования. |
SATISFIED | Да | Спор разрешён в пользу инициатора (мерчанта). |
REJECTED | Да | Спор отклонён — доказательства недостаточны. |
Исход резолва для сделки (2026-09-08): SATISFIED завершает живую
сделку в COMPLETED; REJECTED переводит сделку через возврат в
CANCELLED (терминально) — эскроу-резервы возвращаются полностью
(order-level и все per-match холды). Финальный статус сделки приходит
отдельным вебхуком (deal.completed / deal.cancelled).
V2 использует только эти 5 canonical значений. Legacy-имена IN_REVIEW,
RESOLVED_MERCHANT, RESOLVED_TRADER, CLOSED, REFUNDED в V2
не существуют — они эмулируются только на стороне V1 compat-слоя для
мигрирующих мерчантов.
Зачёт просроченного платежа по резолву (Settle-as-Received)
Типичный кейс спора: игрок перевёл меньше заявленного (partial) или
перевёл с опозданием — сделка успела истечь в EXPIRED, при этом
received_amount > 0. Деньги у трейдера, сделка мертва. Resurrect-режим
запрещён: EXPIRED терминален и не меняется никогда. Вместо этого оператор
закрывает спор созданием сеттлмента — новой сделки, зачитывающей
фактически полученную сумму:
POST /api/v1/p2p/appeals/{appeal_id}/settlementЭто операторское действие кабинета (арбитражная панель). Тело:
received_amount_override (необязательно, при ручном уточнении суммы —
тогда reason обязательна), reason, idempotency_key (UUID). Полное
описание семантики — PayIn → Зачёт просроченного
платежа.
Свойства резолва-через-сеттлмент:
- апелляция резолвится
SATISFIED, вresolution_note— ссылка на сделку-сеттлмент (parent_deal_id→ истёкший оригинал,origin_appeal_id→ апелляция); - одна апелляция → максимум один сеттлмент (UNIQUE-констрейнт на уровне БД): повторный вызов возвращает уже созданную запись (replay), а не ошибку;
- сеттлмент проходит полный стандартный путь (эскроу → verify → complete), курс — новый, на момент резолва;
- по сеттлменту приходят обычные вебхуки
deal.created…deal.completed(V1 и V2); оригинал навсегда остаётсяEXPIREDсо своимreceived_amount.
Отказы: родитель не EXPIRED или апелляция уже закрыта → 409;
received_amount = 0 без override → 400; override без reason → 400.
Создание апелляции
POST /api/v1/p2p/merchant/appealsПараметры запроса (JSON Body)
| Поле | Тип | Обяз. | Описание |
|---|---|---|---|
deal_id | string(UUID) | Да | UUID сделки, по которой открывается спор. |
reason | string | Да | Причина открытия спора — минимум 10 символов (после обрезки пробелов; короче → 400 reason is required (at least 10 characters)). Текст видит арбитр — пишите содержательно. |
file_ids | string[] | Нет | Массив UUID предварительно загруженных файлов-доказательств. |
Инициатор резолвится из аутентификации (HMAC claims или кабинетный
JWT) — поля merchant_id/tenant_id в теле не нужны и advisory. Сделка
должна принадлежать вызывающему мерчанту: чужая → 403, несуществующая →
404. По сделке в статусе, допускающем спор (после эскроу-лока), FSM
переводит сделку в APPEALED и шлёт вебхук deal.appealed.
Пример запроса
{
"deal_id": "7ac148fe-19a3-45bb-b992-019fac55b721",
"reason": "Клиент перевёл 2500 рублей, но платёж не зачислен в течение 30 минут",
"file_ids": ["f3a1b2c3-4d5e-6f7a-8b9c-0d1e2f3a4b5c"]
}Пример ответа (200 OK)
{
"appeal": {
"appeal_id": "a82d77a0-0d3a-4422-b91c-7cb052a21def",
"deal_id": "7ac148fe-19a3-45bb-b992-019fac55b721",
"status": "OPEN",
"reason": "Клиент перевёл 2500 рублей, но платёж не зачислен в течение 30 минут",
"resolution_note": "",
"created_at": "2026-07-12T17:25:00Z",
"resolved_at": "",
"updated_at": "2026-07-12T17:25:00Z"
}
}Ошибки создания (живые ответы стейджа)
400 INVALID_ARGUMENT — reason короче 10 символов:
{
"type": "about:blank",
"title": "Bad Request",
"status": 400,
"detail": "{\"code\":3,\"message\":\"reason is required (at least 10 characters)\",\"details\":[]}",
"instance": "/api/v1/p2p-engine"
}400 FAILED_PRECONDITION — на сделке уже есть активная апелляция
(дублей веток нет: с OPEN/IN_PROGRESS/REOPENED повтор запрещён;
терминальные SATISFIED/REJECTED не блокируют новую):
{
"type": "about:blank",
"title": "Bad Request",
"status": 400,
"detail": "{\"code\":9,\"message\":\"deal 359fb026-6dd7-4f7a-bfa7-aa54a5afb6cb already has an active appeal (aa1fac74-33b3-41ce-ae47-445bc3dcd37d, status OPEN) — continue the existing thread\",\"details\":[]}",
"instance": "/api/v1/p2p-engine"
}404 NOT_FOUND — несуществующий deal_id (deal not found);
403 PERMISSION_DENIED — сделка другого мерчанта
(cannot access another merchant's resources).
Получение информации об апелляции
GET /api/v1/p2p/merchant/appeals/{appeal_id}Пример ответа (200 OK, живой ответ стейджа)
{
"appeal": {
"appeal_id": "aa1fac74-33b3-41ce-ae47-445bc3dcd37d",
"deal_id": "359fb026-6dd7-4f7a-bfa7-aa54a5afb6cb",
"status": "OPEN",
"reason": "E2E merchant appeals full-cycle test: payer claims transfer completed but escrow never confirmed",
"resolution_note": "",
"created_at": "2026-08-23T19:04:44Z",
"resolved_at": "",
"updated_at": "2026-08-23T19:04:44Z"
}
}Чужая апелляция → 403, несуществующая → 404 (appeal not found).
Список апелляций
GET /api/v1/p2p/merchant/appealsПараметры фильтрации (Query)
| Параметр | Тип | Описание |
|---|---|---|
status | string | Фильтр по canonical статусу: OPEN, IN_PROGRESS, REOPENED, SATISFIED, REJECTED. |
page_size | int32 | Строк на страницу (1–100, по умолчанию 20). |
page_token | string | Непрозрачный keyset-курсор из предыдущего ответа. Пустая строка = первая страница. |
Пример ответа (200 OK)
{
"data": [
{
"appeal_id": "aa1fac74-33b3-41ce-ae47-445bc3dcd37d",
"deal_id": "359fb026-6dd7-4f7a-bfa7-aa54a5afb6cb",
"status": "OPEN",
"reason": "E2E merchant appeals full-cycle test: payer claims transfer completed but escrow never confirmed",
"resolution_note": "",
"created_at": "2026-08-23T19:04:44Z",
"resolved_at": "",
"updated_at": "2026-08-23T19:04:44Z"
}
],
"next_page_token": ""
}В текущем релизе листинг пагинирован по канону AIP-158: keyset-курсор
(page_size 1–100 с умолчанием 20, next_page_token пуст = последняя
страница, битый непустой page_token → 400 invalid page_token).
Листинг скопирован по владельцу: апелляции чужих сделок в ответ не попадают.
Чат апелляции
В рамках апелляции мерчант может обмениваться текстовыми сообщениями с арбитром платформы. Доступно и из кабинета (JWT).
Отправка сообщения в чат
POST /api/v1/p2p/merchant/appeals/{appeal_id}/messagesПараметры запроса
| Поле | Тип | Обяз. | Описание |
|---|---|---|---|
body | string | Да | Текст сообщения (непустой — пустое body отклоняется с 400 body is required). |
file_ids | string[] | Нет | UUID загруженных файлов-доказательств. |
Пример запроса
{
"body": "Предоставляю выписку из банка для подтверждения списания.",
"file_ids": []
}Пример ответа (200 OK, живой ответ стейджа)
{
"message": {
"message_id": "a7b598e0-2074-4873-91ee-954ec588bcfc",
"appeal_id": "aa1fac74-33b3-41ce-ae47-445bc3dcd37d",
"sender_role": "MERCHANT",
"body": "Предоставляю выписку из банка для подтверждения списания.",
"file_ids": [],
"created_at": "2026-08-23T21:48:24Z"
}
}sender_role резолвится из аутентификации (MERCHANT для API/кабинета,
ADMIN — сообщения арбитра, TRADER — трейдера).
Получение истории сообщений
GET /api/v1/p2p/merchant/appeals/{appeal_id}/messagesПример ответа (200 OK)
{
"data": [
{
"message_id": "b33d77a0-0d3a-4422-b91c-7cb052a21aaa",
"appeal_id": "a82d77a0-0d3a-4422-b91c-7cb052a21def",
"sender_role": "MERCHANT",
"body": "Предоставляю выписку из банка.",
"file_ids": [],
"created_at": "2026-07-12T17:27:00Z"
},
{
"message_id": "b33d77a0-0d3a-4422-b91c-7cb052a21bbb",
"appeal_id": "a82d77a0-0d3a-4422-b91c-7cb052a21def",
"sender_role": "ADMIN",
"body": "Доказательство получено. Начинаем проверку.",
"file_ids": [],
"created_at": "2026-07-12T17:27:05Z"
}
],
"next_page_token": ""
}