# Runbook: включение онлайн-оплаты ЮKassa на проде liderra.ru (ИП)

**Owner:** Дмитрий (владелец) + Claude-оператор | **Frequency:** один раз (go-live), затем — справочник
**Last Updated:** 2026-06-26 | **Last Run:** 2026-06-26 (шаги 1–4а выполнены; осталось флаг+тест — см. History)
**Кодовая фраза стены:** «роутер-наставник».

> ⚠️ **ПДн/секреты:** в этом файле НЕТ реальных реквизитов ИП, `shopId` и ключей. Реальные значения — в `моя/Карточка ИП.docx` (локально, НЕ в git) и в кабинете ЮKassa. В команды подставляются вручную на месте.

---

## Purpose

Перевести пополнение баланса клиента с текущей **мгновенной заглушки** (зачисление без реальной оплаты, `billing_yookassa_enabled = OFF`) на **реальную онлайн-оплату через ЮKassa** на ИП Кондратьева П.Д.

**Код оплаты уже написан и под тестами** — этот ранбук про настройку и операции, не про разработку. Включение состоит из: реквизиты продавца → ключи шлюза → webhook/IP → флаг → проверка живым платежом.

**Когда запускать:** когда у владельца на руках боевой `shopId` и секретный ключ из личного кабинета ЮKassa (шаг 1). До этого момента включать нечего.

---

## Prerequisites

- [ ] **Боевой магазин ЮKassa открыт на ИП** → есть `shopId` + секретный ключ (шаг 1; делает владелец на yookassa.ru).
- [ ] Доступ к проду: `ssh liderra-prod '<cmd>'` (бастион gitea-VM, алиас в `~/.ssh/config`).
- [ ] Доступ к админ-зоне (nginx HTTP Basic Auth, файл `.htpasswd-admin` на проде).
- [ ] Реквизиты ИП под рукой (`моя/Карточка ИП.docx`): наименование, ИНН, ОГРНИП, ОКПО, адрес, банк/р.с/БИК/корсчёт, режим НДС (для ИП на УСН — `usn_6` или `usn_15`).
- [ ] Свежий бэкап прод-БД (см. Rollback). Эталон отката деплоя: `/home/ubuntu/pre-deploy-20260623-162331.dump`.
- [ ] **GO владельца** на каждое изменение прода (правило CLAUDE.md §1.4 — необратимое только с явного разрешения).

---

## Procedure

> Порядок шагов **строгий**. Если включить флаг (шаг 5) раньше настройки шлюза (шаги 2–3), пополнение упадёт в `503 «Платёжный шлюз не настроен»` ([BillingController.php:63](../../../app/app/Http/Controllers/Api/BillingController.php#L63)).

### Шаг 1: Открыть магазин в ЮKassa на ИП

```
1. Зайти на yookassa.ru → зарегистрировать магазин на ИП Кондратьев П.Д.
2. Пройти проверку, подписать договор, выбрать способы оплаты (карта, СБП).
3. В ЛК → «Интеграция» → получить shopId и секретный ключ (боевой, не тестовый).
4. Там же позже (шаг 4) указать URL для webhook.
```

**Expected result:** на руках боевой `shopId` (число) и секретный ключ (`live_...`).
**If it fails:** ЮKassa может запросить доп. документы по ИП — это вне нашего контура, ждём владельца. Без ключей дальше не идём.

---

### Шаг 2: Завести реквизиты продавца (ИП) в справочник `legal_entities`

UI для редактирования юрлиц-продавцов **нет** — заводим SQL-ом под superuser (таблица системная, без RLS). Реальные числа берём из `моя/Карточка ИП.docx`.

```bash
# На проде, под postgres-superuser (как и прочие ручные операции с системными таблицами):
ssh liderra-prod "sudo -u postgres psql -d liderra -v ON_ERROR_STOP=1"
```

Внутри psql (подставить реальные значения вместо <...>; для ИП `kpp` = NULL):

```sql
INSERT INTO legal_entities
  (code, name, short_name, legal_form, inn, kpp, ogrn, okpo,
   legal_address, bank_name, bank_account, bank_bik, bank_corr,
   director_name, vat_mode, is_active, is_default)
VALUES
  ('ip_kondratev', '<Полное наименование ИП>', '<Краткое>', 'IP',
   '<ИНН>', NULL, '<ОГРНИП>', '<ОКПО>',
   '<юр. адрес>', '<банк>', '<р/с>', '<БИК>', '<корсчёт>',
   '<ФИО предпринимателя>', 'usn_6', TRUE, TRUE);

-- Запомнить id для шага 3:
SELECT id, code, name FROM legal_entities WHERE code = 'ip_kondratev';
```

**Expected result:** одна строка, получен числовой `id` (например 1). `is_default = TRUE` (единственное активное юрлицо).
**If it fails:**

- `duplicate key ... code` — запись уже есть, не вставляем повторно, берём `id` существующей.
- `duplicate key ... idx_legal_entities_default` — уже есть другое юрлицо с `is_default=TRUE`; вставлять с `is_default=FALSE` либо снять флаг у старого.
- `vat_mode` CHECK — допустимы только `vat20/vat10/vat7/vat5/vat0/no_vat/usn_6/usn_15` ([schema.sql:300](../../../db/schema.sql#L300)).

---

### Шаг 3: Ввести ключи ЮKassa в шлюз (шифруются в БД)

Эндпоинт: `PUT /api/admin/payment-gateways/yookassa` ([routes/web.php:155](../../../app/routes/web.php#L155)). `config` шифруется `Crypt::encrypt` — ключи в БД и в ответе НЕ светятся ([AdminPaymentGatewayController.php:47](../../../app/app/Http/Controllers/Api/AdminPaymentGatewayController.php#L47)).

```bash
# С машины, имеющей доступ к админ-зоне (через nginx basic-auth admin:<pass>):
curl -u 'admin:<htpasswd-admin-pass>' \
  -X PUT 'https://liderra.ru/api/admin/payment-gateways/yookassa' \
  -H 'Content-Type: application/json' \
  -d '{
    "shop_id": "<SHOP_ID>",
    "secret_key": "<SECRET_KEY>",
    "is_active": true,
    "legal_entity_id": <id из шага 2>
  }'
```

**Expected result:** `{"status":"ok","code":"yookassa","is_active":true}`.
**If it fails:**

- `422 «Сначала заведите юридическое лицо»` — не сделан шаг 2 / не передан `legal_entity_id`.
- `401/403` от nginx — неверный admin-пароль basic-auth.
- Проверить, что ключ записан зашифрованным: `SELECT code, is_active, legal_entity_id, left(config,12) FROM payment_gateways WHERE code='yookassa';` — `config` должен быть нечитаемой строкой (`eyJpdiI6...`), не JSON.

---

### Шаг 4: Webhook URL + IP-allowlist

**4а. В кабинете ЮKassa** указать URL уведомлений (события `payment.succeeded`):

```
https://liderra.ru/api/webhook/payment
```

([routes/web.php:323](../../../app/routes/web.php#L323) — публичный, CSRF-exempt, подлинность через server-to-server сверку.)

**4б. На проде** прописать IP-подсети ЮKassa в `.env` (defense-in-depth; пусто = fail-open):

```bash
ssh liderra-prod "sudo -u www-data sed -n '/YOOKASSA_WEBHOOK_IPS/p' /var/www/liderra/.env"   # проверить, есть ли строка
# Если нет — добавить (значение — опубликованные подсети ЮKassa, сверить с актуальной докой ЮKassa):
# YOOKASSA_WEBHOOK_IPS=185.71.76.0/27,185.71.77.0/27,77.75.153.0/25,77.75.154.128/25,77.75.156.11,77.75.156.35,2a02:5180::/32
```

После правки `.env` — **пересобрать конфиг-кэш и потом рестартить php-fpm** (грабли opcache, см. ниже):

```bash
ssh liderra-prod "cd /var/www/liderra && sudo -u www-data php artisan config:cache && sudo systemctl restart php8.3-fpm"
```

**Expected result:** в `config('services.yookassa.webhook_ip_allowlist')` непустой массив; webhook с чужого IP → `200 {"status":"ignored"}` (no-op), со своего → обрабатывается.
**If it fails:** если webhook от ЮKassa стал игнорироваться — значит реальный IP ЮKassa не попал в список; временно очистить `YOOKASSA_WEBHOOK_IPS` (fail-open) и свериться с актуальной докой ЮKassa по подсетям.

> Список IP — defense-in-depth, НЕ единственная защита. Основная — server-to-server сверка статуса/суммы/валюты + идемпотентность ([PaymentWebhookController.php](../../../app/app/Http/Controllers/Api/PaymentWebhookController.php)). Даже при fail-open поддельный webhook не зачислит деньги.

---

### Шаг 5: Включить флаг `billing_yookassa_enabled` (ТОЛЬКО после 2–4)

```bash
ssh liderra-prod "sudo -u postgres psql -d liderra -v ON_ERROR_STOP=1 -c \"UPDATE system_settings SET value='true', updated_at=NOW() WHERE key='billing_yookassa_enabled';\""
# Проверить, что строка реально обновлена (1 row), а не отсутствует:
ssh liderra-prod "sudo -u postgres psql -d liderra -t -c \"SELECT key, value FROM system_settings WHERE key='billing_yookassa_enabled';\""
```

Если строки нет (`UPDATE 0`) — вставить:

```sql
INSERT INTO system_settings (key, value, updated_at) VALUES ('billing_yookassa_enabled','true',NOW())
ON CONFLICT (key) DO UPDATE SET value='true', updated_at=NOW();
```

После смены настройки — сбросить кэш настроек (если кэшируется) и пересобрать кэши ДО рестарта:

```bash
ssh liderra-prod "cd /var/www/liderra && sudo -u www-data php artisan cache:clear && sudo systemctl restart php8.3-fpm"
```

**Expected result:** `billing_yookassa_enabled = true`. Теперь `POST /api/billing/topup` возвращает `confirmation_url` вместо мгновенного зачисления ([BillingController.php:60](../../../app/app/Http/Controllers/Api/BillingController.php#L60)).
**If it fails:** см. Troubleshooting (503 = шлюз не активен).

---

### Шаг 6: Проверка живым платежом (минимальная сумма)

```
1. Войти клиентским аккаунтом (тестовый тенант, НЕ боевой client1).
2. Баланс → Пополнить → минимум 100 ₽ ([min 100 в валидации topup]).
3. Должен открыться confirmation_url ЮKassa → оплатить реальной картой.
4. После оплаты ЮKassa шлёт webhook → баланс пополняется.
```

**Expected result:**

- В `saas_transactions` появилась строка `pending` → стала `success` после webhook.
- Баланс тенанта вырос ровно на сумму платежа (ledger, копейка-в-копейку).
- Повторный/дублирующий webhook НЕ зачисляет второй раз (идемпотентность).

**If it fails:** см. Troubleshooting.

---

## Verification

- [ ] `SELECT value FROM system_settings WHERE key='billing_yookassa_enabled';` → `true`.
- [ ] `SELECT code, is_active, legal_entity_id FROM payment_gateways WHERE code='yookassa';` → `is_active=t`, `legal_entity_id` указывает на ИП.
- [ ] Тестовый платёж 100 ₽ прошёл: `saas_transactions` → `success`, баланс +100 ₽.
- [ ] Дубль webhook не задвоил баланс (повторно дернуть тот же webhook — баланс не изменился).
- [ ] Боевой клиент `client1` (tenant 2) НЕ затронут: баланс == был до начала (эталон 1 836 400 ₽ / 1013 сделок на момент деплоя 23.06).
- [ ] Снять снимок состояния → обновить `ПИЛОТ.md` («обнови пилот»).

---

## Troubleshooting

| Symptom | Likely Cause | Fix |
|---|---|---|
| `topup` → `503 Платёжный шлюз не настроен` | Флаг ВКЛ, но нет активного шлюза | Выполнить шаг 3 (ключи + `is_active=true`); проверить `payment_gateways` |
| `topup` → `503`, шлюз вроде есть | `activeGateway()` не находит активный | Убедиться `is_active=true` И ключи расшифровываются (`credentials()` не пустой — неверный `APP_KEY`/битый `Crypt`) |
| `PUT payment-gateways` → `422 заведите юрлицо` | Нет строки в `legal_entities` / нет `legal_entity_id` | Шаг 2, передать `legal_entity_id` |
| Webhook приходит, баланс не растёт | Сверка статуса не `succeeded` / валюта ≠ RUB / сумма не совпала | Логи `PaymentWebhookController`; проверить, что платёж в ЮKassa `succeeded` и в рублях |
| Webhook → `200 ignored` | IP не в allowlist ЛИБО валюта/сумма не сошлись | Свериться с IP ЮKassa (шаг 4б); проверить валюту RUB |
| Баланс задвоился | (не должно — есть идемпотентность) | Проверить `saas_transactions.status` переход `pending→success` атомарный; эскалация |
| Прод 500 после правки `.env`/кэшей | opcache держит старый кэш | Пересобрать кэши, **затем** рестарт php-fpm (порядок!); см. грабли |
| Клиент жалуется «списали, баланс не пополнился» | Webhook не дошёл/упал | Проверить доступность `https://liderra.ru/api/webhook/payment`, повторную отправку из ЛК ЮKassa, server-to-server сверку |

### Грабли прода (из деплоя 23.06)

- **composer** — только от root (`COMPOSER_ALLOW_SUPERUSER=1` + `chown vendor www-data`); этот ранбук composer не требует.
- **Миграции `crm_migrator`-owned таблиц** — вручную под `sudo -u postgres` + `INSERT INTO migrations`. (В этом ранбуке схема не меняется — миграций нет.)
- **Кэши пересобирать ДО рестарта php-fpm** (opcache подхватывает только после рестарта, идущего ПОСЛЕ пересборки).
- **`tinker <file>` виснет** — для проверок использовать самобутстрап PHP-скрипт от www-data (`require vendor/autoload + bootstrap/app + Kernel::bootstrap`), не tinker.
- **gitea — единственный бэкап** (github origin мёртв).

---

## Rollback

Откат мгновенный и безопасный — **выключить флаг**, поведение вернётся к мгновенной заглушке:

```bash
ssh liderra-prod "sudo -u postgres psql -d liderra -c \"UPDATE system_settings SET value='false', updated_at=NOW() WHERE key='billing_yookassa_enabled';\""
ssh liderra-prod "cd /var/www/liderra && sudo -u www-data php artisan cache:clear && sudo systemctl restart php8.3-fpm"
```

- Деньги уже принятых платежей остаются зачисленными (ledger необратим — это корректно).
- Шлюз можно деактивировать: `UPDATE payment_gateways SET is_active=false WHERE code='yookassa';`
- Полный откат БД (на крайний случай): `pg_restore` из `/home/ubuntu/pre-deploy-20260623-162331.dump` — **только с GO владельца**, затрёт всё с момента бэкапа.

---

## Escalation

| Situation | Contact | Method |
|---|---|---|
| ЮKassa не открывает магазин / запросила документы | ЮKassa поддержка | ЛК ЮKassa / договорной менеджер |
| Деньги списались у клиента, баланс не пополнился | Владелец + проверка webhook | Срочно: выключить флаг (Rollback), разобрать `saas_transactions` + логи |
| Подозрение на двойное списание/зачисление | Владелец | Стоп-флаг, аудит ledger, не трогать прод-БД без снимка |
| Любое необратимое действие на проде | Владелец | Явный GO перед выполнением |

---

## History

| Date | Run By | Notes |
|---|---|---|
| 2026-06-24 | Claude | Ранбук создан по итогам разведки кода оплаты. Код готов; ждёт `shopId`/ключ ЮKassa (шаг 1) от владельца. Не запускался. |
| 2026-06-26 | Claude + владелец | **Шаги 1–4а ВЫПОЛНЕНЫ.** Договор подписан (№НЭК.448000.01), магазин 1392092, ключ `live_…P8RI` выпущен. Заведены `legal_entities` ИП id=1 (`ip_kondratev`, vat_mode=usn_6 — уточнить) + `payment_gateways` yookassa id=2 active (config зашифрован, roundtrip ок) + webhook payment.succeeded в кабинете. **⚠️ ВАЖНАЯ ПОПРАВКА К РАНБУКУ:** после переезда на Managed PG (26.06) команды `sudo -u postgres psql -d liderra` бьют в СТАРУЮ локальную rollback-БД на VM, НЕ в живой кластер — данные приложения (legal_entities/payment_gateways/system_settings, шаги 2,3,5) заводить **через app/Eloquent** (бутстрап-скрипт под www-data, `/var/www/liderra/app/`) или psql к хосту кластера. **ОСТАЛОСЬ:** шаг 5 (флаг, в кластере), шаг 6 (живой тест), шаг 4б (IP-allowlist, отложен — сейчас fail-open). См. память `project-yookassa-online-payment-golive-2026-06-26`. |
