Overview

For TCC (Tokenization Card) cards, the system automatically charges a monthly fee. The amount is taken from the BIN configuration (monthlyFee). When the BIN value is 0, no fee is charged.

How it works

When a TCC card is issued, a Temporal workflow ManageTokenizationMonthlyFees starts with an ID of the form monthly-fees-<card_id>.

The workflow follows this loop:

  1. Load card data — fetches the card, its card_created_at, and the fee from the BIN config.
  2. Backfill missed fees — if the card is older than one month, the workflow charges all missed monthly fees.
  3. Sleep until the next due date — the workflow sleeps until the next charge date (one month after card_created_at).
  4. Monthly cycle — every month it checks the card status:
    • If the card is closed or close_pending — the workflow exits.
    • Otherwise it charges the fee and sleeps until the following month.

Charge date

The date is anchored to card_created_at. A card created on January 15 is charged on February 15, March 15, and so on.

Idempotency

Each charge has a unique key: <issueTx>:<YYYY-MM-DD>. Duplicates are impossible — the database rejects a repeated transaction.

DB transactions

Every charge writes 3 rows to client_account_transactions:

Type Subtype Idempotency suffix Description
pending issue_fee :hold Hold the fee amount
fee issue_fee :issue-fee Charge the fee
deposit issue_fee :fee-credit Credit the fee account

Post-deploy verification

1. Fee cadence check

Returns every active TCC card with its last fee date and a status flag:

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;

What to look for:

  • NO_FEES — card is active but no fee has ever been charged. Verify the workflow is running.
  • OVERDUE — last fee was more than 31 days ago. The workflow may have stopped.
  • OK — all good.

2. Duplicate check

If workflows were restarted, make sure there are no duplicates:

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;

Expected result: empty — there should be no duplicates.

3. Reversal check

If restart_monthly_fees --reconcile-fees was run, count the reversals:

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. Running-workflow check

Through the Temporal CLI:

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

The number of running workflows should match the number of active TCC cards:

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

5. Fee history for a single card

For a detailed per-card check:

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;

Logging

The workflow writes structured logs to Temporal. Key messages:

Message Context
monthly fees workflow started Workflow start: card_id, issue_tx, card_created_at
backfilling missed fees Backfill: count, date range
charging monthly fee Each charge: card_id, fee_date, amount
skipping zero monthly fee BIN with zero fee
sleeping until next charge Wait: next_charge, sleep_duration
card closed, stopping monthly fees Exit: card_id, status

For the logs of a specific workflow use the Temporal UI or CLI:

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

Restart utility

The cmd/restart_monthly_fees tool can:

  • Restart every monthly-fee workflow (terminate + start).
  • Roll back duplicated transactions (--reconcile-fees).
  • Run in dry-run mode to preview (--dry-run).
# Preview only
go run ./cmd/restart_monthly_fees -cfg configs/configs.app.yaml --dry-run

# Restart workflows + roll back duplicates
go run ./cmd/restart_monthly_fees -cfg configs/configs.app.yaml --reconcile-fees

# Filtered by date range
go run ./cmd/restart_monthly_fees -cfg configs/configs.app.yaml --reconcile-fees --from 2025-01-01 --to 2025-06-01

FAQ

Card is active but the fee is not being charged

  1. Check the workflow is running: tctl workflow show --workflow_id "monthly-fees-<card_id>".
  2. Check the BIN — monthlyFee may be 0.
  3. Check that card_created_at is correct (not in the future).

Double charge
Impossible during normal operation thanks to idempotency. If you do see a duplicate, run restart_monthly_fees --reconcile-fees --dry-run for diagnosis.

Workflow exited with an error
Check the Temporal UI logs. Typical causes:

  • fee account missing — no fee account for the client / provider / currency combination.
  • DB error — usually transient; the workflow retries automatically.