Руководство по операциям с выплатами (для админов)
Внутреннее — только для операторов
Эта страница документирует Admin → Payout Management для администраторов платформы FundlyHub. Она описывает инструменты, которые перемещают реальные деньги и пишут в финансовый реестр (ledger). Прочитайте правила безопасности денег перед запуском чего-либо. Деструктивные действия требуют права manage_system_settings. Названия элементов интерфейса (UI) приведены на английском — именно так они отображаются в админ-панели.
Payout Management находится на админ-странице Payout Management и имеет три вкладки: Transfers (реестр по каждому пожертвованию), Creator Payouts (выплаты Stripe на банковские счета создателей) и Connected Accounts. Это руководство сосредоточено на вкладке Transfers, где происходят релизы и ремедиация.
Модель статусов переводов на основе доказательств
Каждая строка пожертвования на вкладке Transfers несёт один из пяти бейджей статуса. Статус выводится из доказательств, а не из одного флага — строка считается «деньги переведены» только тогда, когда Stripe это реально подтверждает. Это ключевой инвариант, который не даёт платформе платить дважды или молча «терять» средства.
| Бейдж | Значение | Готова к релизу? |
|---|---|---|
| Held | Средства удерживаются платформой, и соответствующее списание прошло расчёт (settled), и у создателя есть активный connected account. Готова к релизу сейчас. | Да |
| Pending settlement | Удерживается, но списание ещё не прошло расчёт. Станет Held автоматически, как только Stripe его рассчитает. | Ещё нет |
| Transferred | Существует аудит-строка реестра с реальным tr_… — Stripe доказуемо переместил деньги. | Готово |
| Direct | Destination charge: Stripe перевёл деньги создателю в момент списания. Отдельный перевод не нужен. | Готово |
| Anomaly | Красный бейдж. Реестр утверждает, что деньги переведены, но ничто в Stripe это не подтверждает (класс бага #1126). Требует ремедиации. | Нужна проверка |
Почему «Held» состоит из двух частей: строка считается готовой к релизу только когда списание прошло расчёт и connected account создателя активен (has_connected_account && charges_enabled). Рассчитанные, но не онбордингованные средства отслеживаются отдельно как Held — awaiting onboarding — деньги в безопасности, их просто нельзя перевести, пока создатель не завершит Stripe Connect.
Фильтр статуса наверху вкладки Transfers позволяет ограничить таблицу любым одним статусом (Held, Pending settlement, Transferred, Direct, Anomaly).
KPI-плитки
Над таблицей расположены четыре основные плитки. Первые три кликабельны — клик ведёт на вкладку Transfers, отфильтрованную по этому статусу.
| Плитка | Что считает | Действие по клику |
|---|---|---|
| Ready to release | Held-строки, прошедшие расчёт и с активным connected account — переводимы прямо сейчас. Подсказка показывает чистую сумму. | Фильтрует Transfers по held |
| Awaiting settlement | Pending-settlement строки — релизятся автоматически после расчёта. | Фильтрует Transfers по pending_settlement |
| Anomalies | Строки, чей реестр утверждает движение денег без доказательств Stripe. При 0 подсказка показывает «ledger clean». | Фильтрует Transfers по anomaly |
| Connected accounts | Всего connected accounts, в подсказке N verified · M pending setup. | — |
Под плитками вспомогательная полоса несёт метрики меньшего веса:
- Monthly volume — переводы за этот месяц
- Platform fees — чаевые за этот месяц
- Held — awaiting onboarding — рассчитанные удержанные средства, которые платформа пока не может перевести, потому что создатель не подключил Stripe (чистая сумма в подсказке)
- Transferred — подтверждённые Stripe + direct, за всё время
- Total donations — за всё время
Сама метка вкладки Transfers показывает живые бейджи: N ready (warning) и N anomalies (error), когда эти счётчики ненулевые.
Меню Actions
Выпадающее меню Actions (вверху справа на вкладке Transfers) защищено правом manage_system_settings. В нём четыре пункта.
Release N ready ($X)
Создаёт реальные переводы Stripe для каждой Held-строки, готовой к релизу. Поведение:
- Проходит по каждому создателю с удержанными средствами и запускает путь релиза по каждому пожертвованию для тех, чей connected account активен. Создатели без активного connected account пропускаются (считаются отдельно, не как ошибки).
- Каждый перевод запускается по одному на пожертвование с установленным
source_transaction, поэтому каждое пожертвование отображается отдельной строкой в connected account создателя. - Переводы — за вычетом комиссий (net): сумма в реестре Held уже является чистой суммой к выплате создателю.
- Адоптировать, а не платить дважды: перед созданием перевода путь релиза проверяет в Stripe наличие уже существующего живого перевода для этого списания (через
charge.transferили per-donationtransfer_group). Если такой уже есть, он его адоптирует — пишет аудит-строку и переключает запись реестра без нового движения денег — вместо создания второго перевода. - Строки, чьё списание ещё не прошло расчёт, отклоняются Stripe и остаются Held; webhook повторит их при следующем
balance.available.
Диалог подтверждения резюмирует объём перед запуском. После прогона таблица и счётчики KPI перечитываются с сервера (без оптимистичных переключений — бейдж меняется, только если обновление реестра в БД действительно зафиксировалось).
Remediate anomalies
Открывает диалог Remediate transfer anomalies. Это инструмент восстановления для Anomaly-строк — реестр говорит о движении денег, но доказательств Stripe нет. Он никогда не создаёт переводы Stripe.
Рабочий процесс: Detect → проверка → Execute (с Attest как операторским переопределением):
Creator user ID (необязательно). Пусто — сканирует всю платформу на аномалии. Ограничение одним создателем также включает ещё удерживаемые строки этого создателя, чтобы подтверждённые ручные выплаты можно было засвидетельствовать (attest).
Detect — только чтение. Проверяет каждую аномалию против Stripe и классифицирует её. Проверьте чипы классификации:
Чип Значение Поведение Execute restore_to_held Доказуемо «застряло» — перевода Stripe не найдено. Восстанавливается в held → снова попадает в обычный пайплайн релиза. already_transferred Реальный перевод Stripe существует; отсутствовала только аудит-строка. Аудит-строка дописывается (деньги не движутся). direct_confirmed Подтверждённый destination charge. Помечается как direct (деньги не движутся). subscription_excluded Запись подписки, исключена из этого пути. Без изменений. held_unproven Всё ещё held, без доказательств Stripe в любую сторону — только attest. Execute не трогает. attested Помечено оператором как оплачено вне платформы. — error Ошибка Stripe/БД по этой строке; печатается под строкой. Ничего не изменено. — Execute — restore N ($X) + backfill M — применяет не-attest классификации: восстанавливает недоказанные не-held строки в Held, дописывает аудит-строки для подтверждённых переводов, помечает подтверждённые destination charges. Переводы Stripe не создаются. Диалог остаётся открытым, показывая классификацию после Execute (чтобы ошибки записи не прятались за тостом успеха).
Attest paid off-platform — N ($X) — операторское переопределение, показывается только когда выбран создатель и есть кандидаты на attest. Помечает пожертвования создателя как оплаченные БЕЗ доказательств Stripe. Используйте только когда вы лично проверили, что деньги дошли до создателя (например, ручной перевод через дашборд Stripe или банковская выплата). Записывает выплату как доказательство без движения денег и запрашивает причину для аудит-лога.
Restore против Attest — правило двойной оплаты
Execute-restore возвращает недоказанные строки в Held, чтобы обычный пайплайн их оплатил. Attest фиксирует, что они уже оплачены вне платформы, и денег не двигает. НИКОГДА не делайте Execute-restore для создателя, которому вы уже заплатили вручную — это оплатит дважды. Если вы не на 100% уверены, что деньги уже переместились, attest — неправильный инструмент; если вы на 100% уверены, что переместились, restore — неправильный инструмент. В сомнениях — отмените и сначала проверьте в дашборде Stripe.
Reconcile held with Stripe
Проходит по каждой Held-строке и спрашивает Stripe, существует ли уже живой перевод для её списания — разрешается из любого источника доказательств: charge.transfer (эпоха source-transaction) или per-donation transfer_group (donation-<id>, эпоха plain-transfer). Если найден живой, нереверсированный перевод, он его адоптирует: пишет аудит-строку и переключает запись в Transferred без движения денег.
Это инструмент восстановления для «Stripe заплатил создателю, но откат БД оставил строку Held». Безопасен для запуска в любое время — он только адоптирует существующие переводы, никогда не создаёт новые. Тост показывает checked / reconciled (совпало с существующими переводами) / genuinely held / skipped / errors.
Backfill settlement data
Старые удержанные пожертвования предшествуют settlement-вебхукам, поэтому их stripe_available_on равно NULL — они вечно отображаются как Pending settlement, и путь релиза их пропускает. Это действие подтягивает реальное stripe_available_on из Stripe для таких строк и чинит ложные пометки destination-charge. Идемпотентно — безопасно перезапускать. Тост сообщает, сколько из отсутствующих строк было дозаполнено и сколько ещё осталось.
Дерево решений
Строка показывает Anomaly → Откройте Remediate anomalies, запустите Detect (ограничьте создателем, если знаете его). Проверьте чипы: строки already_transferred / direct_confirmed нужно лишь Execute для дозаполнения доказательств (деньги не движутся). Строки restore_to_held действительно «застряли» — Execute восстановит их в Held, чтобы пайплайн их оплатил, если только у вас нет доказательств, что создателю уже заплатили вручную, — тогда Attest. Никогда не восстанавливайте строку для создателя, которому вы заплатили вручную.
Строка показывает Held, но создатель говорит, что ему заплатили → Не релизьте. Сначала запустите Reconcile held with Stripe — если в Stripe уже есть живой перевод для этого списания, reconcile его адоптирует, и строка переключится в Transferred без движения денег. Если reconcile ничего не нашёл, но вы лично проверили выплату вне платформы, ограничьте Remediate anomalies этим создателем и сделайте Attest paid off-platform с указанием причины. Релиз вместо этого приведёт к двойной оплате.
Pending settlement многомесячной давности → Строка, вероятно, предшествует settlement-вебхукам и имеет NULL дату расчёта. Запустите Backfill settlement data, чтобы подтянуть stripe_available_on из Stripe. После дозаполнения и расчёта она перейдёт в Held и станет готовой к релизу (или релизнется автоматически на следующем прогоне).
Правила безопасности денег
Прочитайте перед запуском любого действия
- Каждое переключение статуса требует доказательств. Строка переключается в Transferred/Direct только при наличии реального
tr_…Stripe,charge.transfer, совпадающегоtransfer_groupили аудированного операторского attestation. Ни один флаг не переключается только по метаданным. - Ремедиация не двигает деньги. Detect — только чтение; Execute, Reconcile и Backfill только адоптируют существующие переводы, дозаполняют аудит-строки или помечают подтверждённые состояния. Ни один из них никогда не вызывает
transfers.create. Только Release N ready создаёт переводы Stripe. - Защита точного совпадения. Execute и Attest несут числа этого прогона Detect (
expectedCandidates/expectedRestoreCents) в серверную защиту точного совпадения. Если реестр изменился между Detect и Execute, сервер возвращает 409 → «State changed — re-run Detect». При 409 не повторяйте вслепую — перезапустите Detect и проверьте свежую классификацию перед действием. - Никогда не платите дважды. Не делайте Execute-restore для создателя, которому уже заплатили вручную. Используйте Attest для подтверждённых выплат вне платформы; Restore — только для действительно «застрявших» средств.
- Release адоптирует перед оплатой. Путь релиза проверяет в Stripe уже существующий живой перевод и адоптирует его вместо создания второго — но эта подстраховка не заменяет правила выше.
Вопросы по конкретной строке или зависшему прогону ремедиации? Напишите на tech@cyty.io с ID пожертвования и текстом тоста Detect/Execute.