Мобильное приложение кошелек: где ломается логика платежей
Пик нагрузки в кошельковом приложении — не открытие экрана, а момент подтверждения операции. Пользователь жмёт «оплатить», сеть на секунду пропадает, запрос уходит повторно — и вот уже два одинаковых списания в истории. Именно на таких сценариях ломается логика платежей: рассинхрон баланса при параллельных операциях, зависшие транзакции, которые висят в статусе «в обработке» сутками, и двойные списания при обрыве связи. Каждый из этих сбоев стоит доверия: пользователь видит неверный остаток один раз — и удаляет приложение.
Корень проблемы в том, что мобильное приложение кошелек — это тонкий клиент, который живёт в нестабильной сети, а деньги двигаются на сервере. Если клиент сам решает, когда повторить запрос, а сервер не различает повтор от новой операции, баланс расходится. Отсюда классика: списание прошло, ответ до клиента не дошёл, клиент отправил повтор — и операция исполнилась дважды.
Второй источник сбоев — параллельность. Пользователь платит в одном окне и одновременно получает перевод в другом; два потока читают один и тот же остаток, оба считают его достаточным, оба списывают. Без блокировок и единой точки учёта баланс уезжает даже без обрывов связи.
Третий слой — статусы транзакций. Платёж ушёл в банк-эквайер, но подтверждение задержалось: в приложении он «завис», в бэк-офисе уже прошёл. Пока нет сверки и идемпотентных ключей на каждую операцию, такие расхождения копятся и всплывают в поддержке пачками.
Разбираем, как это закрывается на уровне разработки платежного приложения — от идемпотентности запросов до сверки состояний и восстановления зависших операций.
Архитектура кошелька: реестр операций, резервирование, сверка
Реестр операций как единственный источник правды
Баланс кошелька нельзя хранить как одно изменяемое поле: при параллельных списаниях и повторах запроса он неизбежно расходится с историей. Поэтому учёт строим на неизменяемом реестре операций — каждая запись фиксирует сумму, тип, статус и связанный идентификатор, а текущий баланс выводится из журнала. Любая операция получает ключ идемпотентности: повторный запрос с тем же ключом не создаёт вторую запись, а возвращает результат первой. Это убирает двойные списания при обрыве связи, когда клиент не получил ответ и повторил запрос.
Резервирование средств и сверка транзакций
Списание проходит в две фазы: сначала сумма резервируется, затем подтверждается или снимается по факту ответа платёжного провайдера. Резерв виден в балансе отдельной строкой, поэтому пользователь не тратит зарезервированное дважды. Фоновая сверка транзакций сверяет реестр с выписками провайдера по расписанию и помечает расхождения — зависшие операции закрываются автоматически, а не через ручной разбор. Доля зависших платежей падает с процентов до долей, а восстановление после сбоя занимает минуты, потому что состояние восстанавливается из журнала, а не из памяти сервиса.
Что выбирают под нагрузку
| Вариант | Когда подходит | Ограничения |
|---|---|---|
| Реестр операций + резервы | Высокая частота списаний, несколько провайдеров | Нужна дисциплина по ключам идемпотентности |
| Прямое изменение баланса | Простые сценарии без параллельных операций | Двойные списания при повторах, ручная сверка |
| Внешний платёжный сервис «под ключ» | Быстрый запуск, минимум своей логики | Слабее контроль над сверкой и откатами |
Схему учёта строим на транзакционном ядре с журналом операций — так состояние кошелька остаётся воспроизводимым, а каждая транзакция прослеживается до источника. Подход отработан на платёжных системах уровня PostgreSQL с гарантиями целостности на уровне хранилища.
Опишите вашу схему учёта и провайдеров — вернёмся с оценкой и планом по архитектуре.
Этапы разработки кошелькового приложения: от аудита до передачи
Разработка мобильного приложения кошелек — это в первую очередь проект про деньги и доверие, а не про экраны. Ошибка в модели операций или в обработке повторного запроса стоит дороже, чем весь остальной код: пользователь видит списанные дважды средства, а поддержка разбирает инцидент вручную. Поэтому этапы разработки кошелька строятся вокруг платежного ядра, а интерфейс подключается к уже проверенной логике.
Ниже — маршрут, по которому мы ведём проект. Каждый этап заканчивается артефактом, который можно проверить, а не устным «готово».
- Аудит текущей схемы. Смотрим, как операции проходят сейчас: где хранится баланс, кто источник истины, какие провайдеры и банки уже подключены, как обрабатываются возвраты и отмены. На выходе — карта потоков и список узких мест.
- Проектирование модели операций. Фиксируем состояния платежа (создан, в обработке, подтверждён, отклонён, возвращён), правила идемпотентности и порядок сверки. Это ядро: от него зависит, как поведёт себя приложение при обрыве связи.
- Внедрение платежного модуля. Подключаем эквайринг, пополнение, вывод и переводы между пользователями, настраиваем вебхуки и повторные попытки. Каждый сценарий получает понятный код ошибки, а не «что-то пошло не так».
- Проектирование операций и лимитов. Логируем каждое движение средств, настраиваем лимиты, антифрод-правила и роли доступа к админ-панели. История операций должна сходиться с данными провайдера до копейки.
- Тестирование платежей на реальных сценариях. Прогоняем успешные и провальные кейсы: недостаток средств, таймаут провайдера, двойное нажатие, возврат, отмена на середине. Отдельно — проверка на слабом канале и при перезапуске приложения.
- Передача и документация. Отдаём исходники, схемы потоков, описание интеграций, инструкции по развёртыванию и чеклист для поддержки. Команда заказчика может развивать продукт без нас.
Схема из шести этапов даёт предсказуемый результат: платежное ядро отделено от интерфейса, а каждое изменение проверяется на тестовом контуре до выхода к пользователям. Опишите вашу задачу — вернёмся с оценкой и планом по этапам.
Чем отличается предоплаченный кошелек от расчетного счета в приложении?
Предоплаченный кошелек и расчетный счет в приложении — это две принципиально разные модели хранения средств, и от выбора зависит вся логика мобильного приложения кошелька: как формируются проводки, где проходит граница между балансом пользователя и деньгами на стороне партнера, какие операции вообще допустимы.
В предоплаченном кошельке пользователь сначала вносит средства на счет эмитента, и только потом тратит их из баланса. Приложение оперирует не деньгами напрямую, а обязательством: баланс — это запись во внутреннем реестре, которую эмитент обязан погасить при операции. Отсюда жесткие ограничения — лимиты на пополнение и остаток, запрет на переводы третьим лицам сверх установленного порога, обязательная идентификация при превышении уровней.
Для приложения это означает, что каждая операция проходит через резервирование суммы, а состояние баланса нужно синхронизировать с реестром эмитента, а не считать его источником истины.
Расчетный счет в приложении устроен иначе: средства лежат на банковском счете клиента, а приложение — это канал доступа к нему. Здесь баланс всегда сверяется с банковским ядром, операции инициируются через платежный шлюз, а лимиты и правила диктует банк, а не продукт. Логика приложения усложняется: нужно обрабатывать асинхронные подтверждения, откаты при недостатке средств и статусы, которые приходят с задержкой.
Возвраты в кошельке — самый чувствительный участок. В предоплаченной модели возврат зачисляется обратно на баланс кошелька, и приложение должно корректно обработать частичный возврат, отмену и повторное зачисление, не создавая двойных записей. В модели расчетного счета возврат идет на банковский счет, а приложение лишь отображает статус — но обязано показать пользователю, куда именно придут деньги и когда.
Практический вывод для архитектуры: смешивать обе модели в одном продукте без четкого разделения реестров нельзя — это приводит к расхождениям баланса и спорным операциям. На этапе проектирования мы фиксируем, кто эмитент, где хранится истина по балансу и как проходят возвраты, — и уже под это строим слой операций и сверок.
Кейс: как мы убрали двойные списания и зависшие платежи
Заказчик пришёл с классической болью платёжного продукта: в мобильном приложении кошелёк при обрыве сети или повторном нажатии «Оплатить» операция уходила в банк дважды, а часть переводов зависала в промежуточном статусе. Разбор инцидентов вёлся вручную по логам, поддержка тонула в обращениях, а баланс пользователя и фактическое состояние счёта расходились.
Мы начали с аудита платёжного контура и нашли корень: клиент инициировал списание, но не имел сквозного идентификатора операции, который переживал бы разрыв соединения и повторную отправку. Без него любой ретрай на стороне приложения или сети создавал новую транзакцию вместо продолжения существующей.
Что изменили
Внедрили идемпотентный реестр операций: каждая попытка платежа получает уникальный ключ, который генерируется на устройстве и сохраняется локально до подтверждения от платёжного шлюза. Повторная отправка с тем же ключом не создаёт новую операцию, а возвращает текущий статус уже начатой — двойное списание становится невозможным на уровне модели данных.
Добавили конечный автомат статусов с явными переходами и таймаутами: «зависшая» операция по истечении окна ожидания автоматически уходит в сверку с банком, а не остаётся в неопределённости. Фоновый воркер доразбирает такие случаи без участия человека.
Метрики до и после
| Метрика | До | После |
|---|---|---|
| Доля зависших операций | высокая, разбор вручную | единичные случаи, автосверка |
| Время ручного разбора инцидента | десятки минут на операцию | минуты, только исключения |
| Обращения в поддержку по платежам | регулярный поток | снизились кратно |
| Двойные списания | повторялись при ретраях | устранены на уровне реестра |
Отдельно закрыли наблюдаемость: каждая операция получила сквозной идентификатор в логах и трассировке, поэтому инцидент теперь разбирается по одной строке, а не по выгрузке за сутки. Поддержка видит статус платежа в реальном времени, а не переспрашивает инженеров.
Идемпотентность — не украшение, а базовое требование к любому платёжному приложению: без неё ретраи и нестабильная сеть гарантированно порождают расхождения. Мы проектируем такие контуры так, чтобы корректность не зависела от качества связи на стороне пользователя.
Совместимость, миграция и откат: что обсудить до старта
Миграция платёжного модуля — самый рискованный участок проекта: ядро уже держит балансы, лимиты и историю операций, и любая рассинхронизация с ним видна пользователю мгновенно. Поэтому до старта мы фиксируем контракт обмена с ядром, схему переноса данных и точку, из которой возможен откат изменений без потери уже проведённых транзакций.
Что если текущее ядро не поддерживает нужные методы? Тогда совместимость закрывается адаптером на стороне сервиса: приложение не меняет контракт, а прослойка приводит вызовы к тому, что ядро умеет. Такой слой изолирует доработки и не требует правок в проде ядра.
Как переносятся данные кошелька? Через поэтапную сверку: сначала теневая запись в новое хранилище, затем сравнение контрольных сумм по балансам и истории, и только после совпадения — переключение чтения. Откат на этом шаге — это возврат указателя на прежний источник, без выката старой сборки.
Что если релиз окажется проблемным? Держим обратную совместимость форматов и флаги включения: новая логика гасится удалённо, приложение продолжает работать на прежнем контракте. Пользователь не теряет доступ к балансу даже при откате.
Как обеспечивается безопасность доступов при передаче?
Доступы выдаются по принципу минимально необходимого: отдельные учётные записи под каждую среду, ключи с ограниченным сроком, журналирование всех вызовов к ядру. При завершении работ доступы отзываются, а ключи ротируются — это фиксируется в акте.
На руки заказчик получает:
- схему интеграции приложения с ядром и описание контракта обмена;
- регламент миграции данных с точками сверки и критериями переключения;
- процедуру отката изменений с пошаговым сценарием;
- матрицу доступов и политику ротации ключей;
- документацию по кошельку: описание модулей, сценариев и граничных случаев;
- набор проверок для приёмки — что и как тестируется перед выкатом;
- журнал известных ограничений и план их закрытия.
Опишите вашу задачу — вернёмся с оценкой и планом: разберём стыковку с текущим ядром, объём переноса данных и порядок отката.







