Перейти к содержимому

Баланс пользователя

Баланс пользователя — опциональный внутренний кошелёк Minishop. Пользователь может пополнить его через обычного платёжного провайдера и затем полностью или частично оплачивать подписку, продление, докупку трафика и устройств. Средства учитываются в неизменяемом журнале операций, а не в одном перезаписываемом поле пользователя.

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

Откройте Система → Настройки → Общие → Баланс пользователя. Настройки доступны как overrides поверх .env:

Переменная Назначение
USER_BALANCE_ENABLED Показывает баланс в личном кабинете и разрешает пополнение и оплату из него. По умолчанию выключен.
USER_BALANCE_CURRENCY Код валюты баланса. Пустое значение наследует валюту платежей из DEFAULT_CURRENCY_SYMBOL.
USER_BALANCE_TOPUP_MIN_AMOUNT Минимальная сумма одного пользовательского пополнения.
USER_BALANCE_TOPUP_MAX_AMOUNT Максимальная сумма одного пользовательского пополнения. Не может быть меньше минимальной.
USER_BALANCE_TOPUP_PRESETS JSON-массив быстрых сумм, например [500, 1000, 2000, 5000]. Значения вне диапазона отбрасываются.

Валюта нормализуется к верхнему регистру. Для известных валют учитывается их реальная точность: например, RUB/USD/EUR имеют две дробные цифры, а валюты без дробной части работают целыми единицами. Интерфейс показывает локализованный символ, когда он известен, и код валюты в остальных случаях.

После смены USER_BALANCE_CURRENCY старые записи журнала не конвертируются автоматически. Они остаются в исходной валюте, а интерфейс и новые операции работают с выбранной текущей валютой. Поздний callback уже созданного пополнения зачисляет деньги в валюте исходного счёта, чтобы смена настройки не привела к потере оплаченных средств. Перед сменой валюты всё равно рекомендуется завершить открытые платежи и зафиксировать порядок переноса остатков отдельно.

  1. Пользователь нажимает кнопку пополнения на главной странице или в настройках личного кабинета.
  2. Вводит сумму либо выбирает один из настроенных пресетов.
  3. Выбирает доступного внешнего платёжного провайдера.
  4. Minishop создаёт обычный Payment с режимом продажи balance_topup.
  5. После подтверждённого callback сумма один раз записывается в журнал баланса и становится доступной для покупок. Подписка при этом не активируется и не продлевается.

Telegram Stars, обычный и партнёрский баланс не предлагаются как способы пополнения: это исключает циклические операции и смешение денежных контуров. Провайдер также должен поддерживать выбранную валюту и сумму. Ограничение частоты запросов применяется до создания платежа.

Повторный callback безопасен: платеж блокируется на время финализации, завершённый платеж повторно не проводится, а запись пополнения имеет ключ user-balance-payment-topup:<payment_id>. Успешное пополнение появляется в Система → Логи с пользователем, суммой, валютой, провайдером и ID платежа; те же основные поля пишутся в технический лог backend. Если включены LOG_PAYMENTS и LOG_CHAT_ID, после успешного коммита в платёжную тему/чат Telegram отправляется отдельное уведомление «Пополнен баланс пользователя» с теми же реквизитами. Оно не содержит фиктивного периода подписки и не отправляется при повторном callback.

Refund или reversal успешного пополнения создаёт отдельную отрицательную запись и не меняет исходную. Если пользователь уже успел потратить деньги, итоговый баланс может стать отрицательным. Новые списания и ручные операции, требующие доступных средств, блокируются до погашения долга.

На checkout пользователь включает оплату с баланса. Если одновременно доступны обычный и партнёрский источники, по умолчанию выбирается обычный баланс, а источник можно изменить через выпадающее меню.

  • Если выбранный баланс покрывает всю серверную котировку, создаётся внутренний успешный Payment для аудита и запускается обычная финализация покупки без внешнего провайдера.
  • При частичной оплате провайдеру передаётся остаток. Minishop сохраняет полную стоимость и долю, зарезервированную на выбранном балансе.
  • Если у провайдера есть минимальная внешняя сумма, баланс применяется только так, чтобы остаток не оказался ниже этого минимума.
  • Валюта checkout должна совпадать с валютой выбранного баланса.
  • Во время создания платежа пользователь и остаток блокируются транзакцией. Параллельные запросы не могут дважды потратить одну и ту же сумму.
  • Для отменённого, истёкшего или неуспешного внешнего платежа резерв возвращается отдельной ledger-записью. Если поздний callback всё же завершил оплату, возврат помечается неактивным и списание остаётся в силе.
  • Фоновая сверка возвращает зависшие резервы полностью оплаченных с баланса покупок, если процесс завершения аварийно остановился, и повторно проверяет терминальные платежи без записи возврата.

Правила партнёрского баланса, включая hold, отрицательные reversal и доступность оплаты, описаны в документации партнёрской программы.

Когда функция включена, текущий обычный баланс показывается в основной информации карточки пользователя и отдельной колонке списка пользователей. При включённой партнёрской программе рядом показывается партнёрский остаток в валюте обычного баланса. Список получает остатки пакетными агрегатами для текущей страницы и не выполняет отдельный запрос на каждого пользователя.

В карточке пользователя раздел управления балансом позволяет:

  • добавить, списать или установить точную сумму обычного баланса;
  • оставить обязательную для операционного процесса причину/комментарий (поле технически допускает пустое значение и тогда записывается стандартная причина);
  • конвертировать средства между обычным и партнёрским балансом активного партнёра;
  • кнопкой Макс подставить весь доступный остаток выбранного источника;
  • просмотреть последние операции неизменяемого журнала.

Списание и конвертация не допускают сумму выше актуального остатка. Проверка в интерфейсе нужна для удобства, но окончательная проверка выполняется сервером после блокировки пользователя и партнёрского профиля. Ключ идемпотентности защищает повторную отправку формы.

При конвертации действуют дополнительные правила:

  • нужен активный партнёрский профиль; paused/closed-профиль не подходит;
  • обе стороны используют USER_BALANCE_CURRENCY, поэтому смешивать валюты одной операцией нельзя;
  • при переводе партнёрских средств в обычные сначала расходуется невыводимая часть партнёрского остатка, затем выводимая;
  • перевод обычного баланса в партнёрский создаёт невыводимую партнёрскую запись;
  • обе ledger-записи создаются в одной транзакции: частично выполненной конвертации быть не может.

Выключение функции скрывает обычный баланс и блокирует новые пользовательские пополнения и оплаты, но не удаляет историю и остатки. Администратор по-прежнему видит журнал в карточке пользователя и может подготовить остатки к повторному включению. Не удаляйте ledger-записи вручную и не исправляйте итог прямым SQL — используйте корректировку с причиной.

Для проверки инцидента сопоставляйте:

  1. запись Payment и её sale_mode, статус, сумму, валюту и провайдера;
  2. запись журнала баланса по reference_type=payment и reference_id=<payment_id>;
  3. событие balance_topup_succeeded в админских логах;
  4. callback и технические сообщения провайдера в backend.

Если платёж успешен у провайдера, но баланс не зачислен, не добавляйте сумму вручную до проверки user-balance-payment-topup:<payment_id>: повторная ручная корректировка поверх позднего retry приведёт к двойному зачислению.