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:
- Load card data — fetches the card, its
card_created_at, and the fee from the BIN config. - Backfill missed fees — if the card is older than one month, the workflow charges all missed monthly fees.
- Sleep until the next due date — the workflow sleeps until the next charge date (one month after
card_created_at). - Monthly cycle — every month it checks the card status:
- If the card is
closedorclose_pending— the workflow exits. - Otherwise it charges the fee and sleeps until the following month.
- If the card is
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
- Check the workflow is running:
tctl workflow show --workflow_id "monthly-fees-<card_id>". - Check the BIN —
monthlyFeemay be0. - Check that
card_created_atis 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.