Платежи
Платежи — опциональный модуль: без настроенных провайдеров панель полностью функциональна, подписки выдаются админом вручную. Конкретный агрегатор не зашит в код: backend работает с интерфейсом, каждый агрегатор — адаптер. Включённых провайдеров может быть несколько одновременно — клиент выбирает способ оплаты в чекауте.
type PaymentProvider interface { CreateCheckout(ctx, req CheckoutRequest) (url string, externalID string, err error) ParseWebhook(payload []byte, headers http.Header) (PaymentEvent, error)}
type PaymentEvent struct { ExternalID string Status PaymentStatus // pending | paid | failed | refunded Amount decimal.Decimal Currency string}Всё специфичное для агрегатора — формат чекаута, схема вебхука, проверка подписи (HMAC-заголовок, allowlist IP) — внутри адаптера. Остальной backend видит только PaymentEvent.
sequenceDiagram
participant C as Клиент в кабинете
participant BE as Backend
participant P as Агрегатор
C->>BE: POST /portal/v1/checkout {plan_id, provider_id} + Idempotency-Key
BE->>P: CreateCheckout
BE->>BE: payments: pending
BE-->>C: checkout URL
C->>P: оплата
P->>BE: POST /webhooks/payments/{provider}
BE->>BE: адаптер → PaymentEvent → транзакция активации
BE-->>P: 200
Активация (при status = paid, provider-agnostic, одна транзакция):
payments.status → paid.- Подписка: указана в чекауте → продление
expires_at += plan.period; не указана → создание новой из плана (снапшот условий, Клиенты и подписки). suspended/expired→active, если причина устранена; инкремент ревизий затронутых нод.- Запись в
audit_log.
Идемпотентность
Section titled “Идемпотентность”Два независимых уровня:
- Чекаут:
Idempotency-Keyот клиента — повторный запрос (двойной клик, ретрай сети) возвращает тот же checkout URL, не создавая второйpayments. - Вебхуки:
UNIQUE (provider_id, external_id)+ проверка перехода статуса. Агрегаторы ретраят вебхуки; повторное событиеpaidпо уже обработанному платежу подтверждается (200) без повторной активации.
Вебхук с неизвестным external_id логируется и подтверждается — это события вне нашего флоу (созданные в дашборде агрегатора).
Возвраты
Section titled “Возвраты”refunded фиксируется в payments; автоматического отзыва подписки нет — решение (отключить, сократить срок) принимает админ. Автоматика здесь опаснее ручной обработки: сценарии возвратов разнородны (фрод, ошибка, goodwill).