# Sandbox

Контур для самостоятельной отладки: можно пройти оба сценария целиком, не поднимая
банк, не отправляя SWIFT и не поднимая собственный обработчик вебхуков.

Включается переменной `SANDBOX_ENABLED=true`. В продуктиве всегда выключен —
запросы к `/v1/sandbox/*` вернут `404 SANDBOX_DISABLED`.

## Демо-партнёры

```bash
curl http://localhost:3000/v1/sandbox/partners -H "Authorization: Bearer $TOKEN"
```

| `partner_id` | Партнёр | Страна | Валюта | Сценарии |
|--------------|---------|--------|--------|----------|
| `PARTNER-777` | Grand Hotel Roma S.r.l. | IT | EUR | `INVOICE` |
| `PARTNER-512` | Dubai Luxury Transfers LLC | AE | AED | `INVOICE`, `PAYMENT_LINK` |
| `PARTNER-301` | Istanbul Fine Dining | TR | TRY | `PAYMENT_LINK` |

`PARTNER-301` удобен, чтобы проверить обработку `SCENARIO_NOT_SUPPORTED_BY_PARTNER`:
попробуйте создать для него поручение со сценарием `INVOICE`.

## Курсы валют

Курсы детерминированы, поэтому примеры воспроизводимы. Итоговый курс — базовый
плюс наценка `FX_MARKUP_PERCENT` (по умолчанию 1.5%), округлённый до копеек.

| Валюта | Базовый курс | Итоговый |
|--------|--------------|----------|
| EUR | 95.91 | 97.35 |
| USD | 88.50 | 89.83 |
| AED | 24.10 | 24.46 |
| TRY | 2.45 | 2.49 |
| CNY | 12.30 | 12.48 |

Также поддерживаются GBP, HKD, THB, SGD, JPY, MVR. Прочие валюты дадут
`422 CURRENCY_NOT_SUPPORTED` со списком доступных.

## Имитация оплаты клиента

Заменяет вебхук банковской выписки. Поручение проходит `CLIENT_PAID` →
`FX_EXECUTED` → `PARTNER_PAID` за один вызов.

```bash
curl -X POST http://localhost:3000/v1/sandbox/orders/$PA_ORDER_ID/simulate-client-payment \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{}'
```

Без `amount_rub` берётся ожидаемая сумма поручения. Чтобы проверить обработку
расхождения, передайте другую сумму — получите `422 PAYMENT_AMOUNT_MISMATCH`:

```bash
-d '{ "amount_rub": 100.00 }'
```

## Рублёвая ссылка `/pay`

После `POST /payment-details` в `payment_details.url` приходит адрес вида
`{API_BASE_URL}/pay/{pa_order_id}?uin=...`. В sandbox это живая страница
(не эквайринг): сумма, УИН, реквизиты и кнопка «Оплатить». Кнопка без токена
ЦСО вызывает ту же имитацию выписки (`POST /pay/{id}/simulate`).

```text
http://localhost:4000/pay/PA-ORD-2026-EDDAE05B?uin=CSO72890001
```

Страница отвечает `404 SANDBOX_DISABLED`, если `SANDBOX_ENABLED=false`.
В продуктиве URL остаётся контрактом для ЦСО; хостинг настоящей оплаты —
отдельный контур, не этот stub.

Тот же `/pay/{id}` открывается и для `INVOICE` (реквизиты + УИН), даже если
в ответе `payment_details.url` пустой: для 8.1 по умолчанию выдаётся
`bank_transfer`.

## Имитация подтверждения услуги

```bash
curl -X POST http://localhost:3000/v1/sandbox/orders/$PA_ORDER_ID/simulate-service-confirmation \
  -H "Authorization: Bearer $TOKEN"
```

`PARTNER_PAID` → `COMPLETED`.

## Приёмник вебхуков

Укажите его как `callback_url` при создании поручения — и увидите всё, что
отправляет ПА, включая результат проверки подписи.

```json
{ "callback_url": "http://localhost:3000/v1/sandbox/webhook-sink" }
```

```bash
curl http://localhost:3000/v1/sandbox/webhook-sink -H "Authorization: Bearer $TOKEN"
```

```json
{
  "items": [
    {
      "received_at": "2026-07-28T10:35:00.123Z",
      "event_id": "evt_a1b2c3d4e5f60718",
      "event_type": "order.status_changed",
      "signature": "sha256=8f7d3a1b...",
      "timestamp": "1785235500",
      "signature_valid": true,
      "body": { "spec_version": "1.0", "data": { "status": "AWAITING_PAYMENT" } }
    }
  ]
}
```

Очистить:

```bash
curl -X DELETE http://localhost:3000/v1/sandbox/webhook-sink -H "Authorization: Bearer $TOKEN"
```

## Подпись произвольного тела

Помогает отладить отправку входящих вебхуков в ПА: сервис вернёт заголовки,
которые нужно приложить к запросу.

```bash
curl -X POST http://localhost:3000/v1/sandbox/sign \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "source": "bank",
    "body": { "uin": "CSO72888421", "amount_rub": 146025.00 }
  }'
```

```json
{
  "headers": {
    "X-PA-Signature": "sha256=8f7d3a1b2c4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8",
    "X-PA-Timestamp": "1785235500"
  },
  "signed_body": "{\"uin\":\"CSO72888421\",\"amount_rub\":146025}"
}
```

Отправляйте **ровно** `signed_body` — пересборка JSON изменит байты и подпись
не сойдётся.

```bash
curl -X POST http://localhost:3000/v1/callbacks/bank/statement \
  -H 'Content-Type: application/json' \
  -H "X-PA-Signature: $SIG" \
  -H "X-PA-Timestamp: $TS" \
  -d "$SIGNED_BODY"
```

`source` выбирает секрет: `bank` для выписки, `partner` для событий партнёра.

## Что проверить перед боевым запуском

- [ ] Оба сценария проходят до `COMPLETED`
- [ ] Ваш обработчик проверяет подпись и отвечает `200` быстрее 10 секунд
- [ ] Повторная доставка одного `event_id` не создаёт дубль у вас
- [ ] Обработано расхождение суммы (`PAYMENT_AMOUNT_MISMATCH`)
- [ ] Обработан истёкший курс (`FX_RATE_EXPIRED`)
- [ ] Ретраи идут с тем же `Idempotency-Key`

Полный список — в [чек-листе перед продом](09-go-live.md).
