docs(billing): спека онлайн-пополнения баланса через ЮKassa Часть 1+2
Фундамент legal_entities/payment_gateways/saas_transactions + драйвер ЮKassa, поток create->redirect->webhook->автозачисление, рубильник-флаг billing_yookassa_enabled в админке, чек 54-ФЗ заложен и выключен. Часть 3 счёт на безнал - следующим циклом. Прод на заглушке до ООО Б-1. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Brain Status (auto-generated)
|
||||
|
||||
Last updated: 2026-06-22T16:20:13.048Z
|
||||
Last updated: 2026-06-22T16:26:01.609Z
|
||||
|
||||
| Контролёр | Состояние | Детали |
|
||||
|---|---|---|
|
||||
@@ -47,6 +47,9 @@ Last updated: 2026-06-22T16:20:13.048Z
|
||||
|
||||
| Время | Действие | Причина |
|
||||
|---|---|---|
|
||||
| 2026-06-22T16:24:44.007Z | bash:ls $TEMP/claude-economy-ae7348fc-4410-4d81-8546-4b57c2df3ad0.json 2>/dev/null && cat $TEMP/claude-economy-ae7348fc- | floor: опасная по содержанию команда без аварийного выхода — блок (правило 8); FLOOR-ESCAPE: bash:ls $TEMP/claude-econom |
|
||||
| 2026-06-22T16:22:00.158Z | bash:ls $TEMP/claude-economy-ae7348fc-4410-4d81-8546-4b57c2df3ad0.json 2>/dev/null && cat $TEMP/claude-economy-ae7348fc- | floor: опасная по содержанию команда без аварийного выхода — блок (правило 8); FLOOR-ESCAPE: bash:ls $TEMP/claude-econom |
|
||||
| 2026-06-22T16:20:44.486Z | bash:ls $TEMP/claude-economy-ae7348fc-4410-4d81-8546-4b57c2df3ad0.json 2>/dev/null && cat $TEMP/claude-economy-ae7348fc- | floor: опасная по содержанию команда без аварийного выхода — блок (правило 8); FLOOR-ESCAPE: bash:ls $TEMP/claude-econom |
|
||||
| 2026-06-22T16:19:05.841Z | bash:ls $TEMP/claude-economy-ae7348fc-4410-4d81-8546-4b57c2df3ad0.json 2>/dev/null && cat $TEMP/claude-economy-ae7348fc- | floor: опасная по содержанию команда без аварийного выхода — блок (правило 8); FLOOR-ESCAPE: bash:ls $TEMP/claude-econom |
|
||||
| 2026-06-22T16:17:41.809Z | bash:ls $TEMP/claude-economy-ae7348fc-4410-4d81-8546-4b57c2df3ad0.json 2>/dev/null && cat $TEMP/claude-economy-ae7348fc- | floor: опасная по содержанию команда без аварийного выхода — блок (правило 8); FLOOR-ESCAPE: bash:ls $TEMP/claude-econom |
|
||||
| 2026-06-22T16:17:18.508Z | bash:cat "$TEMP/claude-economy-c45f5b05-836d-48ba-bf47-ab3477fd20e5.json" 2>/dev/null \|\| echo "FILE_NOT_FOUND" | floor: опасная по содержанию команда без аварийного выхода — блок (правило 8); FLOOR-ESCAPE: bash:cat "$TEMP/claude-econ |
|
||||
@@ -54,9 +57,6 @@ Last updated: 2026-06-22T16:20:13.048Z
|
||||
| 2026-06-22T16:12:12.039Z | bash:ls $TEMP/claude-economy-ae7348fc-4410-4d81-8546-4b57c2df3ad0.json 2>/dev/null && cat $TEMP/claude-economy-ae7348fc- | floor: опасная по содержанию команда без аварийного выхода — блок (правило 8); FLOOR-ESCAPE: bash:ls $TEMP/claude-econom |
|
||||
| 2026-06-22T16:10:29.400Z | bash:cat "$TEMP/claude-economy-c45f5b05-836d-48ba-bf47-ab3477fd20e5.json" 2>/dev/null \|\| echo "FILE_NOT_FOUND" | floor: опасная по содержанию команда без аварийного выхода — блок (правило 8); FLOOR-ESCAPE: bash:cat "$TEMP/claude-econ |
|
||||
| 2026-06-22T16:07:20.953Z | bash:cat "$TEMP/claude-economy-c45f5b05-836d-48ba-bf47-ab3477fd20e5.json" 2>/dev/null \|\| echo "FILE_NOT_FOUND" | floor: опасная по содержанию команда без аварийного выхода — блок (правило 8); FLOOR-ESCAPE: bash:cat "$TEMP/claude-econ |
|
||||
| 2026-06-22T15:58:15.793Z | bash:ls $TEMP/claude-economy-c45f5b05-836d-48ba-bf47-ab3477fd20e5.json 2>/dev/null && cat $TEMP/claude-economy-c45f5b05- | floor: опасная по содержанию команда без аварийного выхода — блок (правило 8); FLOOR-ESCAPE: bash:ls $TEMP/claude-econom |
|
||||
| 2026-06-22T15:56:58.348Z | bash:tail -c 8000 "C:\Users\Administrator\.claude\projects\c--------------claude-brain\ae7348fc-4410-4d81-8546-4b57c2df3 | floor: опасная по содержанию команда без аварийного выхода — блок (правило 8); FLOOR-ESCAPE: bash:tail -c 8000 "C:\Users |
|
||||
| 2026-06-22T15:56:46.709Z | bash:ls $TEMP/claude-economy-ae7348fc-4410-4d81-8546-4b57c2df3ad0.json 2>/dev/null && cat $TEMP/claude-economy-ae7348fc- | floor: опасная по содержанию команда без аварийного выхода — блок (правило 8); FLOOR-ESCAPE: bash:ls $TEMP/claude-econom |
|
||||
|
||||
## Метрики (информационные, не алерты)
|
||||
|
||||
@@ -140,9 +140,9 @@ Episodes since last run: 542 / threshold: 10
|
||||
|
||||
| PID | Имя | CPU-время | Возраст |
|
||||
|---|---|---|---|
|
||||
| 6976 | Code | 2.59ч | 0.0ч |
|
||||
| 3440 | MsMpEng | 2.57ч | NaNч |
|
||||
| 14232 | msedge | 2.39ч | 0.0ч |
|
||||
| 6976 | Code | 2.60ч | NaNч |
|
||||
| 3440 | MsMpEng | 2.59ч | 0.0ч |
|
||||
| 14232 | msedge | 2.44ч | 0.0ч |
|
||||
|
||||
⚠️ Проверь, не «осиротевшие» ли это процессы от завершённых Claude-сессий.
|
||||
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
# Дизайн: Онлайн-пополнение баланса через ЮKassa (Часть 1 + Часть 2)
|
||||
|
||||
**Дата:** 2026-06-22
|
||||
**Статус:** утверждён заказчиком (брейншторм), готов к написанию плана
|
||||
**Автор:** Claude (под стеной «роутер-наставник»)
|
||||
**Кодовая фраза:** роутер-наставник
|
||||
|
||||
---
|
||||
|
||||
## 1. Цель и контекст
|
||||
|
||||
Сделать **реальный приём онлайн-оплаты** для пополнения рублёвого баланса тенанта
|
||||
через платёжный шлюз **ЮKassa** (карта / СБП-QR / SberPay / YooMoney), с
|
||||
автозачислением баланса по факту оплаты.
|
||||
|
||||
**Текущее состояние (что уже есть и работает на проде liderra.ru):**
|
||||
|
||||
- `POST /api/billing/topup` → `BillingController@topup` → `BillingTopupService` —
|
||||
**MVP-заглушка**: кредитует `tenants.balance_rub` мгновенно и пишет append-only
|
||||
строку `balance_transactions(type='topup')`. Реального платёжного шлюза нет.
|
||||
- Деньги считаются корректно: `bcmath`, `lockForUpdate` на строке тенанта,
|
||||
append-only ledger с hash-chain триггером (`audit_chain_hash`),
|
||||
`audit_block_mutation` запрещает UPDATE/DELETE.
|
||||
- Фронт: `BillingView.vue`, `TopupDialog`, `topup()` в `resources/js/api/billing.ts`.
|
||||
- В `db/schema.sql` платёжный домен **спроектирован** (`payment_gateways`,
|
||||
`legal_entities`, `saas_invoices`, `saas_invoice_items`, `saas_transactions`,
|
||||
`saas_upd_documents`, `tariff_subscriptions`), но **реально не построен** —
|
||||
нет ни миграций, ни моделей (в `app/Models/` их нет).
|
||||
|
||||
**Блокер Б-1:** боевые ключи ЮKassa и фискализация (54-ФЗ чеки) требуют
|
||||
зарегистрированного **ООО** (договор с кассой + ОФД). Поэтому «по-настоящему»
|
||||
(боевые деньги) включить нельзя до закрытия Б-1. Эта работа доводит интеграцию
|
||||
до состояния «полностью готова, отлажена на песочнице картами, включается ключами
|
||||
и флагом».
|
||||
|
||||
---
|
||||
|
||||
## 2. Объём (scope)
|
||||
|
||||
Декомпозиция на 3 части (решение заказчика 2026-06-22):
|
||||
|
||||
| Часть | Что | В этой спеке |
|
||||
|---|---|---|
|
||||
| **1. Фундамент** | Таблицы/модели `legal_entities`, `payment_gateways`, `saas_transactions`; интерфейс драйвера + `YooKassaDriver`; админ-ввод ключей | **ДА** |
|
||||
| **2. Онлайн-оплата** | Создание платежа → редирект → webhook → автозачисление; флаг вкл/выкл; чек в код | **ДА** |
|
||||
| **3. Счёт на безнал** | `saas_invoices` + PDF-счёт + ручное/полуавтоматическое зачисление | **НЕТ** (следующий цикл, отдельная спека) |
|
||||
|
||||
### Out of scope (явно НЕ делаем сейчас)
|
||||
|
||||
- Безналичная оплата по счёту (Часть 3).
|
||||
- Боевое подключение ключей ЮKassa и реальная фискализация (post-Б-1, шаг владельца).
|
||||
- Автосписание/рекуррентные платежи (сохранённые карты).
|
||||
- Возвраты (refunds) через UI — структуру под `chargeback_*` не трогаем.
|
||||
- Тарифные подписки (`tariff_subscriptions`) — отдельная тема.
|
||||
|
||||
---
|
||||
|
||||
## 3. Решение заказчика по вопросам брейншторма
|
||||
|
||||
1. **Касса:** ЮKassa (YooKassa). СБП-QR поддерживается (способ `sbp`, редирект-сценарий).
|
||||
2. **Чеки 54-ФЗ:** заложить в код (передаём состав чека в запрос ЮKassa),
|
||||
фактически включится после ООО+ОФД.
|
||||
3. **Пути оплаты:** онлайн + счёт на безнал — но в этой спеке только онлайн (Часть 1+2).
|
||||
4. **Рубильник:** флаг в `system_settings`, переключается в админке
|
||||
«SaaS-admin → Система → Настройки».
|
||||
|
||||
---
|
||||
|
||||
## 4. Архитектура
|
||||
|
||||
### 4.1. Поток онлайн-оплаты (флаг ВКЛ)
|
||||
|
||||
```
|
||||
Клиент: «Пополнить» N₽
|
||||
→ POST /api/billing/topup {amount_rub}
|
||||
→ создаём saas_transactions(status=pending, gateway=yookassa)
|
||||
→ YooKassaDriver->createPayment(amount, idempotenceKey, receipt?, returnUrl)
|
||||
→ ЮKassa возвращает {id, confirmation_url}
|
||||
→ сохраняем gateway_payment_id в saas_transactions
|
||||
→ ответ фронту: { confirmation_url }
|
||||
→ фронт редиректит клиента на confirmation_url
|
||||
→ клиент платит (карта / СБП-QR) на стороне ЮKassa
|
||||
→ клиент возвращается на return_url (BillingView с баннером «ожидаем подтверждение»)
|
||||
|
||||
ЮKassa (асинхронно):
|
||||
→ POST /api/billing/payment-webhook (event=payment.succeeded)
|
||||
→ проверка подлинности (см. §4.5)
|
||||
→ идемпотентность по webhook_dedup_keys / gateway_payment_id
|
||||
→ если succeeded и ещё не зачислено:
|
||||
DB::transaction + lockForUpdate:
|
||||
BillingTopupService->topup(tenantId, amountRub, userId=null)
|
||||
saas_transactions → succeeded, привязка balance_transaction_id
|
||||
→ 200 OK
|
||||
```
|
||||
|
||||
### 4.2. Поток при флаге ВЫКЛ (текущая заглушка, прод сегодня)
|
||||
|
||||
`POST /api/billing/topup` ведёт себя как сейчас: мгновенно зачисляет баланс через
|
||||
`BillingTopupService->topup()` и возвращает `{ transaction, balance_rub }`.
|
||||
**Контракт ответа фронту:** дискриминируется по наличию поля —
|
||||
`confirmation_url` (шлюз) ИЛИ `transaction`+`balance_rub` (заглушка). Фронт:
|
||||
есть `confirmation_url` → редирект; иначе → как сейчас (мгновенный успех).
|
||||
|
||||
### 4.3. Компоненты (изоляция и ответственность)
|
||||
|
||||
| Компонент | Ответственность | Зависит от |
|
||||
|---|---|---|
|
||||
| `PaymentGatewayDriver` (interface) | контракт: `createPayment()`, `parseWebhook()`, `verifyWebhook()` | — |
|
||||
| `YooKassaDriver` | реализация под API ЮKassa (HTTP, idempotence-key, разбор статуса) | HTTP-клиент, config из `payment_gateways` |
|
||||
| `PaymentGatewayManager` | выбор активного драйвера по `payment_gateways.is_active` + флагу | модели |
|
||||
| `BillingTopupService` (есть) | **не меняется** — зачисление баланса + ledger | Tenant, BalanceTransaction |
|
||||
| `OnlineTopupService` (новый) | оркестрация: создать saas_transactions + позвать драйвер | Driver, модели |
|
||||
| `PaymentWebhookController` (новый) | приём webhook, идемпотентность, вызов зачисления | Driver, OnlineTopupService, BillingTopupService |
|
||||
| `BillingController@topup` (есть) | развилка флаг вкл/выкл | OnlineTopupService / BillingTopupService |
|
||||
|
||||
Граница: `OnlineTopupService` ничего не знает про HTTP ЮKassa (это в драйвере);
|
||||
`BillingTopupService` ничего не знает про шлюз (только баланс). Webhook-контроллер
|
||||
не считает деньги сам — делегирует в `BillingTopupService`.
|
||||
|
||||
### 4.4. Данные
|
||||
|
||||
Создаём миграции под **уже спроектированную** схему (`db/schema.sql`), без отхода
|
||||
от неё (правка schema.sql + запись в `db/CHANGELOG_schema.md`, RLS-review). Минимум
|
||||
для Части 1+2:
|
||||
|
||||
- **`legal_entities`** — юрлицо (название, ИНН/КПП, адрес). Для песочницы — одна
|
||||
запись-заглушка; боевые реквизиты впишутся после ООО.
|
||||
- **`payment_gateways`** — `code='yookassa'`, `driver`, `legal_entity_id`,
|
||||
`config` (зашифрован `Crypt::encrypt`: `{shop_id, secret_key}`), `is_active`,
|
||||
`accepts_methods`, `min/max_amount_rub`.
|
||||
- **`saas_transactions`** — платёж: `tenant_id`, `gateway_id`, `gateway_payment_id`,
|
||||
`amount_rub`, `status` (`pending|succeeded|canceled`), `balance_transaction_id`
|
||||
(FK на зачисленную строку ledger, NULL до зачисления), таймстемпы. RLS под тенанта.
|
||||
|
||||
> Точная форма колонок берётся из существующего `db/schema.sql`; план сверит и при
|
||||
> необходимости допишет недостающие поля (например `idempotence_key`,
|
||||
> `return_url`), фиксируя это в `db/CHANGELOG_schema.md`.
|
||||
|
||||
Модели Eloquent: `LegalEntity`, `PaymentGateway`, `SaasTransaction`.
|
||||
|
||||
### 4.5. Безопасность
|
||||
|
||||
- **Ключи** ЮKassa — только `Crypt::encrypt` в `payment_gateways.config`; никогда в
|
||||
git/логах/Sentry. Ввод через отдельную защищённую админ-форму (не общий
|
||||
экран `system_settings`).
|
||||
- **Webhook подлинность:** ЮKassa не подписывает webhook HMAC-секретом, поэтому
|
||||
канонический способ — **не доверять телу webhook**, а по `object.id` из него
|
||||
сделать `GET /payments/{id}` к API ЮKassa и поверить статусу из ответа
|
||||
(server-to-server сверка).
|
||||
Дополнительно — ограничение по списку IP ЮKassa, если доступно на инфраструктуре.
|
||||
- **Идемпотентность:** запись в `webhook_dedup_keys` по `gateway_payment_id`;
|
||||
зачисление только если `saas_transactions.status != succeeded`. Повторный
|
||||
webhook → 200 OK без повторного зачисления.
|
||||
- **Деньги:** зачисление в одной DB-транзакции с `lockForUpdate` (как в
|
||||
`BillingTopupService`), append-only ledger неизменен.
|
||||
- **RLS:** `saas_transactions` под тенант-контекстом; webhook исполняется вне
|
||||
пользовательской сессии → зачисление под системной ролью с явным tenant scope
|
||||
по `saas_transactions.tenant_id` (паттерн `crm_supplier_worker`/джоб).
|
||||
- **Idempotence-key** при `createPayment` — чтобы повторный POST topup не создал
|
||||
два платежа в ЮKassa.
|
||||
|
||||
### 4.6. Чек 54-ФЗ (заложен, выключен)
|
||||
|
||||
В запросе `createPayment` формируем секцию `receipt` (email/телефон клиента, одна
|
||||
позиция «Пополнение баланса», ставка НДС, признак предмета/способа расчёта).
|
||||
Активируется только при подключённом ОФД (после ООО). Под флагом
|
||||
`billing_receipt_enabled` (по умолчанию выкл), чтобы песочница без ОФД не падала.
|
||||
|
||||
### 4.7. Флаги (system_settings)
|
||||
|
||||
| Ключ | Тип | По умолчанию | Смысл |
|
||||
|---|---|---|---|
|
||||
| `billing_yookassa_enabled` | bool | `false` | рубильник: заглушка ↔ реальный шлюз |
|
||||
| `billing_receipt_enabled` | bool | `false` | передавать ли чек 54-ФЗ в ЮKassa |
|
||||
|
||||
Оба правятся в «SaaS-admin → Система → Настройки» (`AdminSystemSettingsController`,
|
||||
с аудит-логом). Сидируются в `db/schema.sql` seed с дефолтом `false`.
|
||||
|
||||
---
|
||||
|
||||
## 5. UI / фронт
|
||||
|
||||
- **`TopupDialog`**: добавить пресеты сумм (1000 / 3000 / 5000 ₽) + произвольная
|
||||
(мин 100, как сейчас). При ответе с `confirmation_url` — редирект; иначе —
|
||||
текущее поведение (мгновенный успех).
|
||||
- **`BillingView`**: обработать возврат с `return_url` — баннер «Платёж
|
||||
обрабатывается, баланс обновится автоматически» + опрос/refresh кошелька.
|
||||
- **Админ:** форма ввода ключей ЮKassa (shopId + secret, маскированная) +
|
||||
переключатель `billing_yookassa_enabled` (уже через существующий экран настроек).
|
||||
|
||||
---
|
||||
|
||||
## 6. Тестирование (TDD)
|
||||
|
||||
**Backend (Pest):**
|
||||
- `OnlineTopupService`: создаёт `saas_transactions(pending)`, зовёт драйвер, хранит `gateway_payment_id`.
|
||||
- `YooKassaDriver`: на замоканных HTTP-ответах — создание платежа, разбор `succeeded/canceled`.
|
||||
- `PaymentWebhookController`: успех → зачисление; **повтор → без двойного зачисления**;
|
||||
поддельный/неизвестный платёж → отклонён; статус не `succeeded` → не зачисляет.
|
||||
- Флаг вкл/выкл: `topup` ведёт себя соответственно (редирект vs мгновенно).
|
||||
- Идемпотентность зачисления (двойной webhook, гонка).
|
||||
- RLS: тенант видит только свои `saas_transactions`.
|
||||
|
||||
**Frontend (Vitest):** `TopupDialog` (пресеты, редирект), `BillingView` (возврат).
|
||||
|
||||
**Ручная проверка:** песочница ЮKassa тестовой картой (полный цикл
|
||||
create→pay→webhook→зачисление). СБП-QR — только на боевом магазине (post-Б-1),
|
||||
код пишется сейчас.
|
||||
|
||||
---
|
||||
|
||||
## 7. Риски и решения
|
||||
|
||||
| Риск | Решение |
|
||||
|---|---|
|
||||
| Сломать рабочий прод-topup | Флаг по умолчанию `false` → прод живёт на заглушке |
|
||||
| Двойное зачисление при повторном webhook | `webhook_dedup_keys` + проверка `status` + транзакция |
|
||||
| Webhook нельзя проверить подписью | Server-to-server сверка `GET /payments/{id}` |
|
||||
| Песочница не тестирует СБП | Картой тестируем логику; СБП-код проверяется на боевом |
|
||||
| Отход от schema.sql | Миграции строго по schema + запись в CHANGELOG_schema + RLS-review |
|
||||
| Чек без ОФД роняет платёж | `billing_receipt_enabled=false` по умолчанию |
|
||||
|
||||
---
|
||||
|
||||
## 8. Критерии готовности (Часть 1+2)
|
||||
|
||||
- [ ] Миграции `legal_entities`, `payment_gateways`, `saas_transactions` + модели; schema.sql синхронен + CHANGELOG + RLS-review.
|
||||
- [ ] `PaymentGatewayDriver` + `YooKassaDriver` + менеджер; unit-тесты зелёные.
|
||||
- [ ] `OnlineTopupService` + развилка флага в `BillingController@topup`.
|
||||
- [ ] `PaymentWebhookController` идемпотентен; зачисление через `BillingTopupService`.
|
||||
- [ ] Флаги в `system_settings` + админ-форма ключей.
|
||||
- [ ] Фронт: пресеты + редирект + обработка возврата.
|
||||
- [ ] Pest + Vitest зелёные; ручной прогон на песочнице картой пройден.
|
||||
- [ ] Прод не затронут (флаг `false`); регрессия GREEN.
|
||||
|
||||
---
|
||||
|
||||
## 9. Следующий шаг
|
||||
|
||||
Спека → план реализации (skill `superpowers:writing-plans`), затем TDD-реализация
|
||||
под стеной «роутер-наставник». Часть 3 (счёт на безнал) — отдельным циклом.
|
||||
Reference in New Issue
Block a user