> Как исправлять застрявшие платежи вручную при наличии реконсайл-джобы (Go)

Уровень: senior · Роль: backend · Язык: Go · Категория: Технические вопросы

Компании: InDrive

Стек: Go

> Пример ответа

Короткий ответ

Реконсайл-джоба автоматически исправляет застрявшие платежи, но бывают случаи, когда требуется ручное вмешательство: джоба не может разрешить конфликт, данные не сходятся, или есть подозрение на баг. Основной подход - сначала разобраться, почему джоба не справилась, затем либо скорректировать данные и перезапустить обработку, либо выполнить компенсирующее действие (refund, повторный charge) через админ-интерфейс или скрипт. Важно не дублировать логику джобы и не менять статусы напрямую в обход бизнес-правил.

Подробное объяснение

Реконсайл-джоба - это фоновый процесс, который сверяет локальные платежи с данными платежного провайдера и приводит их к консистентному состоянию. Она обрабатывает типовые кейсы: pending слишком долго, статус не совпадает, нет подтверждения от провайдера.

Ручное исправление нужно, когда:

  • джоба не может определить корректный статус (например, провайдер вернул неоднозначный ответ);
  • данные в БД повреждены или устарели (например, нет внешнего ID);
  • джоба пропустила платеж из-за бага или ретраев;
  • требуется бизнес-решение (например, форс-мажор, клиент звонит и говорит, что оплата прошла, а у нас pending).

Алгоритм ручного исправления:

  1. Найти платеж по ID или внешнему ID.
  2. Проверить текущий статус, историю переходов, логи джобы.
  3. Определить, почему джоба не обработала: ошибка, отсутствие данных, конфликт статусов.
  4. Исправить первопричину (обновить внешний ID, поправить данные провайдера).
  5. Выполнить повторный запуск реконсайла для этого платежа (через админ-эндпоинт или ручной триггер).
  6. Если повторный запуск не помогает - выполнить компенсирующее действие вручную: refund, завершить как failed, или вручную выставить confirmed, если есть подтверждение от провайдера.

Критично: ручное вмешательство должно логироваться, идемпотентно, и не должно конфликтовать с джобой (например, если джоба сейчас обрабатывает этот платеж - нужно подождать или заблокировать его).

На практике

В Go-сервисе обычно есть админ-эндпоинт или CLI-утилита для таких операций. Типичный flow:

  • POST /admin/payments/{id}/reconcile - принудительный запуск реконсайла для одного платежа;
  • POST /admin/payments/{id}/force-status - установка статуса в обход джобы, но с проверкой прав и записью в audit log;
  • скрипт, который находит платежи в статусе pending старше N часов и выводит причины, почему джоба их не обработала.

Важно: force-status не должен вызываться напрямую из БД (UPDATE ... SET status = 'confirmed'), потому что это ломает инварианты (например, не обновляется updated_at, не пишется событие, не отправляется вебхук). Лучше использовать сервисный слой с методами ConfirmPayment, FailPayment, RefundPayment.

Также стоит предусмотреть механизм блокировки: если джоба сейчас обрабатывает платеж, ручная операция должна либо подождать, либо отменить текущую обработку (через distributed lock или версионирование строки).

Пример кода

GO
type PaymentService struct {
repo PaymentRepository
reconciler Reconciler
audit AuditLogger
}
// ForceReconcile запускает реконсайл для конкретного платежа вручную.
func (s *PaymentService) ForceReconcile(ctx context.Context, paymentID string) error {
payment, err := s.repo.GetByID(ctx, paymentID)
if err != nil {
return fmt.Errorf("get payment: %w", err)
}
// Проверяем, что платеж не обрабатывается джобой прямо сейчас.
if payment.LockedBy != "" {
return ErrPaymentLocked
}
// Запускаем реконсайл через общий интерфейс - ту же логику, что и джоба.
if err := s.reconciler.ReconcileOne(ctx, payment); err != nil {
return fmt.Errorf("reconcile: %w", err)
}
s.audit.Log(ctx, "manual_reconcile", payment.ID, map[string]string{
"triggered_by": "admin",
})
return nil
}
// ForceStatus устанавливает статус вручную, но через сервисный метод.
func (s *PaymentService) ForceStatus(ctx context.Context, paymentID string, newStatus Status, reason string) error {
payment, err := s.repo.GetByID(ctx, paymentID)
if err != nil {
return err
}
if !canTransition(payment.Status, newStatus) {
return ErrInvalidTransition
}
// Вызываем доменный метод, а не пишем в БД напрямую.
if err := payment.ChangeStatus(ctx, newStatus, reason); err != nil {
return err
}
if err := s.repo.Save(ctx, payment); err != nil {
return err
}
s.audit.Log(ctx, "manual_status_change", payment.ID, map[string]string{
"from": string(payment.Status),
"to": string(newStatus),
"reason": reason,
})
return nil
}

Как отвечать на собеседовании

Начни с того, что реконсайл-джоба - это основной механизм, и ручное вмешательство - исключение. Опиши процесс диагностики: сначала смотрим логи джобы, потом данные платежа, потом провайдера. Подчеркни, что нельзя менять статус напрямую в БД - только через сервисный слой. Упомяни идемпотентность и аудит. Если спросят про конкретный кейс - приведи пример: платеж завис в pending, джоба не может получить ответ от провайдера, мы вручную запускаем реконсайл через админ-эндпоинт, и если провайдер подтверждает - статус становится confirmed, иначе - failed.

Что проверяет интервьюер

  • Понимание разницы между автоматической и ручной обработкой.
  • Умение проектировать безопасные админ-операции (аудит, блокировки, идемпотентность).
  • Знание доменных инвариантов платежной системы.
  • Умение работать с конкурентностью (джоба и ручное действие одновременно).
  • Понимание, когда ручное вмешательство оправдано, а когда это костыль.

Типичные ошибки

  • Прямое обновление статуса через SQL - ломает события, вебхуки, аудит.
  • Не проверять, обрабатывает ли джоба платеж в данный момент - гонка.
  • Дублировать логику реконсайла вручную вместо вызова общего метода.
  • Не логировать причину ручного изменения - потом невозможно разобраться.
  • Считать, что ручное исправление - это норма, а не исключение; интервьюер ждет, что ты сначала попытаешься понять, почему джоба не сработала.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью