Ежемесячные комиссии (Monthly Fees) — руководство для поддержки

Общее описание

Для карт типа TCC (Tokenization Card) система автоматически списывает ежемесячную комиссию. Размер комиссии определяется конфигурацией BIN-а карты (поле monthlyFee). Если значение равно 0, комиссия не списывается.

Как это работает

При выпуске TCC-карты запускается Temporal-воркфлоу ManageTokenizationMonthlyFees с ID вида monthly-fees-<card_id>.

Воркфлоу работает по следующей логике:

  1. Получение данных карты — загружает карту из БД, определяет card_created_at и размер комиссии из BIN-конфигурации.
  2. Добор пропущенных комиссий (backfill) — если с момента создания карты прошло больше месяца, воркфлоу доначисляет все пропущенные комиссии за каждый месяц.
  3. Ожидание следующей даты — воркфлоу засыпает до следующей даты списания (ровно через месяц от card_created_at).
  4. Ежемесячный цикл — каждый месяц проверяет статус карты:
    • Если карта closed или close_pending — воркфлоу завершается.
    • Иначе списывает комиссию и засыпает до следующего месяца.

Дата списания

Дата привязана к card_created_at. Если карта создана 15 января, комиссии будут 15 февраля, 15 марта и т.д.

Идемпотентность

Каждое списание имеет уникальный ключ: <issueTx>:<YYYY-MM-DD>. Дублирование невозможно — БД отклонит повторную транзакцию.

Транзакции в БД

При каждом списании создаются 3 записи в client_account_transactions:

Тип Подтип Суффикс идемпотентности Описание
pending issue_fee :hold Холд суммы комиссии
fee issue_fee :issue-fee Списание комиссии
deposit issue_fee :fee-credit Зачисление на fee-аккаунт

Верификация после деплоя

1. Проверка каденции комиссий

Показывает все активные TCC-карты с датой последней комиссии и статусом:

SELECT
    c.card_id,
    c.card_created_at,
    c.card_created_at + INTERVAL '1 month' AS first_fee_date,
    last_fee.last_fee_at,
    CASE
        WHEN last_fee.last_fee_at IS NULL THEN 'NO_FEES'
        WHEN NOW() - last_fee.last_fee_at > INTERVAL '31 days' THEN 'OVERDUE'
        ELSE 'OK'
    END AS status
FROM cards c
LEFT JOIN LATERAL (
    SELECT MAX(client_account_transaction_created_at) AS last_fee_at
    FROM client_account_transactions
    WHERE client_account_transaction_card_id = c.card_id
      AND client_account_transaction_subtype = 'issue_fee'
      AND client_account_transaction_type = 'fee'
      AND client_account_transaction_idemp_key NOT LIKE 'monthly-fee-return:%'
) last_fee ON true
WHERE c.card_product = 'TCC'
  AND c.card_status NOT IN ('closed', 'close_pending')
ORDER BY status DESC, c.card_created_at;

Что искать:

  • NO_FEES — карта активна, но комиссии ни разу не списались. Проверьте, что воркфлоу запущен.
  • OVERDUE — последняя комиссия больше 31 дня назад. Возможно, воркфлоу остановился.
  • OK — всё в порядке.

2. Проверка дубликатов

Если были перезапуски воркфлоу, убедитесь, что нет дублей:

WITH fee_txs AS (
    SELECT
        client_account_transaction_card_id AS card_id,
        regexp_replace(
            client_account_transaction_idemp_key,
            ':(hold|issue-fee|fee-credit)$', ''
        ) AS base_key,
        client_account_transaction_type AS tx_type,
        COUNT(*) AS cnt
    FROM client_account_transactions
    WHERE client_account_transaction_subtype = 'issue_fee'
      AND client_account_transaction_idemp_key NOT LIKE 'monthly-fee-return:%'
    GROUP BY 1, 2, 3
)
SELECT card_id, base_key, tx_type, cnt
FROM fee_txs
WHERE cnt > 1
ORDER BY cnt DESC;

Ожидаемый результат: пустой — дубликатов быть не должно.

3. Проверка реверсов

Если запускался инструмент restart_monthly_fees --reconcile-fees, проверьте количество реверсов:

SELECT
    client_account_transaction_card_id AS card_id,
    COUNT(*) AS reversal_count,
    SUM(client_account_transaction_amount) AS total_reversed
FROM client_account_transactions
WHERE client_account_transaction_idemp_key LIKE 'monthly-fee-return:%'
GROUP BY client_account_transaction_card_id
ORDER BY reversal_count DESC;

4. Проверка запущенных воркфлоу

Через Temporal CLI:

tctl workflow list \
  --query "WorkflowType='ManageTokenizationMonthlyFees' AND ExecutionStatus='Running'" \
  --print_full_paged | wc -l

Количество запущенных воркфлоу должно совпадать с числом активных TCC-карт:

SELECT COUNT(*) AS active_tcc_cards
FROM cards
WHERE card_product = 'TCC'
  AND card_status NOT IN ('closed', 'close_pending');

5. История комиссий конкретной карты

Для детальной проверки по одной карте:

SELECT
    client_account_transaction_idemp_key AS idemp_key,
    client_account_transaction_type AS type,
    client_account_transaction_amount AS amount,
    client_account_transaction_created_at AS created_at
FROM client_account_transactions
WHERE client_account_transaction_card_id = '<CARD_ID>'
  AND client_account_transaction_subtype = 'issue_fee'
ORDER BY client_account_transaction_created_at;

Логирование

Воркфлоу пишет структурированные логи в Temporal. Ключевые сообщения:

Сообщение Контекст
monthly fees workflow started Старт воркфлоу: card_id, issue_tx, card_created_at
backfilling missed fees Доначисление: количество, диапазон дат
charging monthly fee Каждое списание: card_id, fee_date, amount
skipping zero monthly fee BIN с нулевой комиссией
sleeping until next charge Ожидание: next_charge, sleep_duration
card closed, stopping monthly fees Завершение: card_id, status

Для просмотра логов конкретного воркфлоу используйте Temporal UI или CLI:

tctl workflow show --workflow_id "monthly-fees-<card_id>"

Инструмент перезапуска

Утилита cmd/restart_monthly_fees позволяет:

  • Перезапустить все воркфлоу ежемесячных комиссий (terminate + start).
  • Откатить дублированные транзакции (--reconcile-fees).
  • Запустить в режиме dry-run для проверки (--dry-run).
# Только посмотреть, что будет сделано
go run ./cmd/restart_monthly_fees -cfg configs/configs.app.yaml --dry-run

# Перезапустить воркфлоу + откатить дубли
go run ./cmd/restart_monthly_fees -cfg configs/configs.app.yaml --reconcile-fees

# С фильтрацией по дате
go run ./cmd/restart_monthly_fees -cfg configs/configs.app.yaml --reconcile-fees --from 2025-01-01 --to 2025-06-01

Частые вопросы

Карта активна, но комиссия не списывается

  1. Проверьте, что воркфлоу запущен: tctl workflow show --workflow_id "monthly-fees-<card_id>".
  2. Проверьте BIN карты — возможно, monthlyFee = 0.
  3. Проверьте, что card_created_at корректна (не в будущем).

Двойное списание комиссии
Невозможно при нормальной работе из-за идемпотентности. Если видите дубликат — запустите restart_monthly_fees --reconcile-fees --dry-run для диагностики.

Воркфлоу завершился с ошибкой
Проверьте логи в Temporal UI. Типичные причины:

  • fee account missing — не настроен fee-аккаунт для клиента/провайдера/валюты.
  • Ошибка БД — временная, воркфлоу автоматически повторит.