← Все проекты уровня 10
Уровень 10 · Связь с другими сервисамиВариант C

Симулятор платежей

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

Техническое задание

Модель данных и назначение

Создайте Go REST API симулятора платежей, подключённого только к локальному эмулятору; реальные платежи запрещены. PostgreSQL с миграциями хранит состояние. Переменная окружения API_TOKENS содержит JSON-отображение Bearer-токенов в owner_id; token передаётся в Authorization: Bearer, owner_id никогда не принимается в теле. Запрос без токена или с неизвестным токеном возвращает 401. Order содержит id, owner_id, amount_minor, currency=RUB, state, idempotency_key, provider_reference, created_at, updated_at. amount_minor — целое от 1 до 1,000,000,000,000 копеек. Payment хранит order_id, provider, provider_payment_id, state и timestamps. WebhookEvent хранит provider_event_id, signature_valid, received_at и результат обработки; уникальность задаётся парой (provider, provider_event_id).

API, проверки и эксплуатация

POST /api/orders требует Bearer-токен и принимает amountMinor и idempotencyKey, создаёт заказ с кодом 201. Ключ уникален в пределах owner_id, длиной 1–128 символов. Сумма от 1 до 1,000,000,000,000 копеек, валюта всегда RUB; повтор того же ключа с той же суммой возвращает существующий заказ с кодом 200, с другой суммой — 409 idempotency_conflict. POST /api/orders/{id}/pay разрешён владельцу, запускает эмулятор не более чем на 60 секунд и переводит created в processing; ответ 202, повтор не запускает вторую оплату. GET /api/orders/{id} требует Bearer-токен и возвращает состояние владельцу; существующий чужой заказ — 403, отсутствующий id — 404. POST /api/orders/{id}/cancel переводит только created в cancelled и отвечает 200; для других состояний — 409. Подтверждённое событие переводит processing в paid, отклонённое — в failed; paid, failed и cancelled терминальны. Повторные и пришедшие не по порядку события сохраняются как ignored и не создают второй эффект.

POST /api/webhooks/fake-provider принимает необработанное тело запроса с полями eventId, orderId, providerPaymentId, sequence, outcome, amountMinor, currency и providerReference, а также подпись HMAC-SHA256. Подпись проверяется сравнением за постоянное время по отдельному FAKE_PROVIDER_WEBHOOK_SECRET из окружения; тело ограничено 1 MiB, неизвестные поля и outcome кроме approved/declined дают 400. Bearer API token для webhook не используется. Неверная подпись — 401 без записи бизнес-события; валидное неизвестное/невалидное событие — 400; валидный дубль — 200 duplicate=true. При успешной проверке событие, маркер дедупликации, переход платежа и заказа и одна запись эффекта фиксируются одной транзакцией. Повторная доставка события с тем же идентификатором не создаёт повторный побочный эффект. Событие с порядковым номером, который не превышает последний принятый, сохраняется как ignored и не меняет текущее состояние. Подтверждённый платёж должен точно равняться order.amount_minor и иметь currency=RUB; частичные платежи запрещены. Ссылка провайдера, amountMinor и currency сверяются с заказом; несовпадение даёт 409 payment_mismatch без изменения заказа. Эмулятор задаёт approved/declined/тайм-аут и порядковые номера событий; порядковый номер должен строго расти, иначе событие сохраняется как ignored. Вызов ограничен 60 секундами и учитывает отмену контекста. Состояние processing и эффект хранятся в PostgreSQL транзакционно; после перезапуска незавершённая оплата безопасно сверяется с эмулятором без повторного эффекта. Используйте контекстные тайм-ауты SQL и индексы по ключу идемпотентности и событию. Неверный ввод даёт 400, отсутствующая запись — 404, конфликт состояния — 409, ошибка эмулятора — 502. Не выводите секреты, подписи, токены или платёжные данные в логи. README указывает, что провайдер тестовый.

Структура программы

В internal/httpapi проверяются токен и подпись; internal/payments задаёт суммы и переходы; internal/provider содержит эмулятор; internal/workers восстанавливает оплаты. main.go связывает компоненты; ctx передаётся исполнителю, os.Exit не используется.

Критерии готовности

Ожидаемый результат

Поставка

  • Запускаемый Go REST API с PostgreSQL-миграциями использует только тестовый эмулятор провайдера; README фиксирует суммы в копейках RUB, HMAC и состояния заказа.
  • Настроенные Bearer-токены API привязаны к ownerID, а отдельный webhook secret хранится в окружении.
  • Офлайн-тесты подменяют тестовый эмулятор провайдера и используют локальную временную базу; реальный платёжный сервис не вызывается.

Приёмка

  • Повтор ключа и суммы возвращает один заказ, повтор ключа с другой суммой возвращает 409. Заказ можно отменить только до запуска оплаты.
  • Успешный webhook с валидной подписью меняет processing на paid и создаёт ровно один эффект.
  • Повтор события с тем же идентификатором и событие меньшей sequence не меняют повторно paid; неверная подпись возвращает 401 без изменений.
  • Подтверждённая сумма должна совпадать с order.amount_minor; частичный платёж и сумма выше 1e12 копеек отклоняются.
  • Чужой Bearer-токен получает 403, тайм-аут эмулятора провайдера ограничен 60 секундами, завершённый заказ не запускает вторую оплату.
  • README показывает зависимости; денежные правила и переходы тестируются без HTTP/БД, webhook и PostgreSQL-сценарии — отдельно с эмулятором.