Как хранится история переходов статусов платежей?
Что проверяют
Понимаете ли вы, что история статусов платежа это журнал для аудита и разбора споров, а не просто лог. Главное, что ищут: защиту от гонок и дублей, ведь статусы меняют и ваш код, и вебхуки провайдера. Следом спрашивают, что будет, если вебхук об успехе придёт раньше, чем «в обработке».
Как отвечать
Текущий статус лежит в payments, а каждый переход добавляется строкой в отдельную таблицу, например payment_status_transitions:
payment_id,from_status,to_status: оба статуса удобны для аудита и проверки, что переход был допустим;reasonиactor: кто изменил (система, оператор, провайдер) и почему, например код отказа банка;external_event_id: идентификатор события провайдера с уникальным индексом, повторный вебхук просто не вставится;created_at: время базы, а время события у провайдера храните отдельно, они расходятся.
Таблица только на добавление: UPDATE и DELETE запрещены правами роли приложения.
Смена статуса и запись в историю идут в одной транзакции. В Go с pgx это pool.Begin(ctx) и сразу defer tx.Rollback(ctx): после Commit откат ничего не делает. Сам переход защищается условием UPDATE payments SET status = $2 WHERE id = $1 AND status = $3, и если затронуто ноль строк, статус уже поменял кто-то другой. Допустимые переходы описывает конечный автомат, например map[Status][]Status, и переход из refunded в pending отклоняется ещё до базы.
При больших объёмах историю партиционируют по месяцам. Если о переходах должны узнать другие сервисы, событие пишут в outbox той же транзакцией.
Где спотыкаются
Обновляют статус без проверки текущего. Два параллельных обработчика читают pending, и один перезаписывает результат другого, в том числе финальный.
Не учитывают, что вебхуки приходят дважды и не по порядку. Без дедупликации и автомата статус откатывается из успешного обратно.
Пишут историю отдельным запросом после коммита или в горутине. При сбое между ними переход случился, а следа в журнале нет.