# Checkout: Публичная T-Bank ссылка — параметры через Base64

## Зачем это нужно

Публичная T-Bank ссылка EasyPay (`https://pay.appload.tech/<slug>`) по умолчанию работает в **ad-hoc** режиме: вы шарите один URL, плательщик сам вводит email и телефон, цена фиксирована = `default_amount_rub` из конфига чекаута. Этого достаточно для простых сценариев — «оплатить курс», «оплатить услугу».

Когда вам нужно больше — pre-fill полей формы, фиксированная цена в коридоре, или передача своего идентификатора заказа для атрибуции — добавьте к URL query-параметр `?d=<base64url(JSON)>`. Кодируете payload у себя на стороне, фронтенд `pay.appload.tech` декодирует и предзаполняет форму. После оплаты ваш `order_id` возвращается обратно — в поле `Payment.Data.ep_order` вебхука и строкой `Order:` в Telegram-уведомлении.

## Как это работает

Использование сводится к трём шагам:


1. Соберите payload с нужными полями (`order_id`, `email`, `phone`, `amount_rub` — все опциональны).
2. Закодируйте его в **Base64URL** (см. примеры ниже).
3. Подставьте в URL: `https://pay.appload.tech/<slug>?d=<base64>`.

После успешной оплаты EasyPay присылает вам уведомление через настроенный канал (Telegram-триггер `tbank.transaction_confirmed` или webhook). Ваш `order_id` возвращается ровно в том виде, в котором вы его передали:

* **webhook** — поле `Payment.Data.ep_order` (структура тела: «Webhook EasyPay: структура данных платежей T-Bank»);
* **Telegram** — строка `Order:` в сообщении об оплате (наш внутренний `OrderID` в том же сообщении — это идентификатор попытки на нашей стороне, для сопоставления он не нужен).

## URL-контракт

```
https://pay.appload.tech/<slug>?d=<base64url(JSON)>
```

* `<slug>` — `product_slug` чекаута. Lowercase, регулярка `[a-z0-9][a-z0-9-]{1,62}`.
* `<base64url(JSON)>` — закодированный payload (см. ниже). **Опционален** — без `?d=` URL работает в ad-hoc режиме.

## Payload

```json
{
  "order_id":   "u_a1b2c3",
  "email":      "user@example.com",
  "phone":      "+79991234567",
  "amount_rub": 5990
}
```

| Поле | Обязательное | Описание |
|------|--------------|----------|
| `order_id` | Нет          | Ваш идентификатор заказа. Регулярка `[A-Za-z0-9_-]{1,64}`. После оплаты возвращается в `Payment.Data.ep_order` вебхука и в Telegram-уведомлении. Если не передан — EasyPay сгенерит `auto-<slug>-<ms>-<rand4>`. |
| `email` | Нет          | Pre-fill email на форме. Если задан — должен пройти валидацию (`^[^\s@]+@[^\s@]+\.[^\s@]+$`, ≤254 chars), иначе плательщик увидит ошибку при submit. |
| `phone` | Нет          | Pre-fill телефона. Формат `^\+?\d{11,15}$`. |
| `amount_rub` | Нет          | Точная цена в рублях. **Учитывается ТОЛЬКО если задан** `**order_id**` (anti-underpayment защита). Должен быть в коридоре `[min_amount_rub, max_amount_rub]` конфига. Без `order_id` сервер форсит `default_amount_rub`. |

**Anti-underpayment.** Если передаёте `amount_rub` без `order_id`, поле игнорируется и checkout сворачивается в ad-hoc (= default). Это сознательная защита: нестандартная цена должна быть привязана к конкретному заказу, иначе можно случайно дать скидку по битой ссылке.

**Поведение при ошибке декодинга.** Если `?d=` присутствует, но не парсится — фронтенд **покажет ошибку «Некорректная ссылка»** и не упадёт в ad-hoc. Это защита от молчаливого «проглатывания» битой ссылки с intended discount или order_id.

## Кодирование Base64URL

Алгоритм: сериализовать payload в JSON → UTF-8 → стандартный Base64 → URL-safe замены (`+` → `-`, `/` → `_`, padding `=` опционально).

JavaScript / Node:

```javascript
function encodePayload(payload) {
  return Buffer.from(JSON.stringify(payload), "utf8")
    .toString("base64")
    .replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}

const url = `https://pay.appload.tech/matrixon-colearn?d=${encodePayload({
  order_id:   "u_a1b2c3",
  email:      "user@example.com",
  amount_rub: 5990
})}`;
```

Python:

```python
import base64, json

def encode_payload(payload):
    raw = json.dumps(payload, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
    return base64.urlsafe_b64encode(raw).decode().rstrip("=")

url = f"https://pay.appload.tech/matrixon-colearn?d={encode_payload({
    'order_id':   'u_a1b2c3',
    'email':      'user@example.com',
    'amount_rub': 5990,
})}"
```

## Как параметры возвращаются обратно

После оплаты вам приходит уведомление на ваш канал. В теле вебхука поле `Payment.Data.ep_order` — ровно то, что вы положили в `order_id` внутри `?d=`.

На вашей стороне:


1. Получите свой идентификатор из `Payment.Data.ep_order`.
2. Если `order_id` вы не передавали, EasyPay сгенерировал свой — такой начинается с `auto-`:

```javascript
if (order_id.startsWith("auto-")) {
  // идентификатор сгенерировали мы — вашего заказа за ним нет
  return null;
}
```

## Полный пример

**Сценарий.** Отправить персональную ссылку по заказу `A-1043` клиенту `user@example.com` по цене 5990 ₽ (default по чекауту — 9990 ₽).

```javascript
const slug   = "my-product";
const payload = { order_id: "A-1043", email: "user@example.com", amount_rub: 5990 };
const dParam  = encodePayload(payload);

const url = `https://pay.appload.tech/${slug}?d=${dParam}`;
// Шлёте url клиенту → клиент оплачивает →
// EasyPay присылает вебхук с Payment.Data.ep_order = "A-1043" →
// находите у себя заказ A-1043 и отмечаете его оплаченным.
```

## Ограничения

* **Размер** `**order_id**`**:** ≤64 символа, charset `[A-Za-z0-9_-]`. Если ваш идентификатор длиннее — храните у себя короткий ключ-ссылку на заказ.
* **Schema payload фиксированная** — только 4 поля (`order_id`, `email`, `phone`, `amount_rub`). Свои ключи в payload игнорируются.
* **Anti-underpayment:** `amount_rub` без `order_id` всегда игнорируется. Хотите кастомную цену — обязательно передавайте и `order_id`.
* **Anti-corrupt link:** битый `?d=` (не декодится) → ошибка на странице, не degradation в ad-hoc.
* **Безопасность.** `order_id` уезжает в T-Bank отдельным параметром платежа (виден в их кабинете), приходит к вам в уведомлении и хранится в БД EasyPay. Не кладите туда секреты — только публичный контекст: ID, источники, флаги. Если нужно прокинуть что-то чувствительное — держите у себя по короткому ключу.
* **Идемпотентность вебхука.** У платежей по публичной ссылке `Payment.Data.ep_payment_uuid` приходит `null` — он существует только у платежей, созданных через API с `idempotency_key`. Ключ дедупликации повторных доставок здесь — `Payment.OrderId`.
* **Идемпотентность ссылки.** Тот же `order_id` с теми же `(slug, amount, email, phone)` → replay, вернётся та же T-Bank ссылка. Тот же `order_id` с другими полями → `idempotency_conflict`. Если планируете retry — держите `order_id` стабильным для одной попытки.

## Ошибки

Все ошибки возвращаются с **HTTP 200** + `success: false`.

| `error_code` | Когда |
|------------|-------|
| `invalid_slug` | `<slug>` в URL не матчит регулярку `[a-z0-9][a-z0-9-]{1,62}`. |
| `invalid_order_id` | `order_id` длиннее 64 символов или содержит символы вне `[A-Za-z0-9_-]` (например `+`, `/`, `=` из стандартного Base64 вместо URL-safe). |
| `invalid_email` / `invalid_phone` | Email/телефон не прошёл валидацию при submit. |
| `invalid_amount` / `amount_out_of_range` | `amount_rub` ≤0 или вне коридора `[min, max]` конфига. |
| `idempotency_conflict` | `order_id` уже использован с другими `(slug, amount, email, phone)`. Сгенерируйте новый. |
| `init_in_progress` / `init_pending_stale` | Параллельный запрос с тем же `order_id` ещё обрабатывается. Подождите 60 сек и повторите. |
| `order_id_already_used` | Прошлая попытка с этим `order_id` завершилась `init_failed`. Используйте новый. |
| `product_not_found` | Чекаута с таким slug нет, либо статус не `active`. |

Дополнительно фронтенд показывает страницу «Некорректная ссылка» при невалидном slug или нераспарсенном `?d=`.