Skip to content

Fee Calculation Flow

Fee engine that matches rules by acquirer/country/payment method and calculates fees per transaction type.


Overview

AttributeValue
Called byOrchestratorService.calculateFees() (real-time) and BulkReprocessProcessorService (bulk)
Servicesrc/service/FeesCalculatorService.ts
Interfacesrc/repository/IFeesCalculatorService.ts
Data sourceMongoDB financesFeeRules collection

The fee engine queries MongoDB for matching fee rules (one per fee_type: IN and OUT), then dispatches to a type-specific calculator. It handles 5 transaction types with different formulas and business rules.


Flow Diagram


Input Schema

IFeesCalculatorRequest.body is one of:

IFeeCalculatorPayInRequest (PAYMENT, APMS_PAYMENT, REFUND, DISPUTE, VOID)

FieldTypeRequiredDescription
business_idstringyesMerchant identifier
amountnumberyesTransaction amount
fee_typestring[]yesAlways ["IN", "OUT"]
detail_typeDetailTypeEnumyesEvent type
transaction_statusTransactionStatusEnumyesSuccess, Won, etc.
currency_codestringyesISO currency code
acquirerstringyesAcquirer identifier
payment_method_idnumberyesPayment method ID
issuing_country_idnumbernoIssuing country (for international detection)
card_brandstringnoVisa, Mastercard, etc.

IFeeCalculatorWithdrawalRequest (WITHDRAWAL, TOPUP)

FieldTypeRequiredDescription
business_idstringyesMerchant identifier
amountnumberyesTransaction amount
fee_typestring[]yesAlways ["IN", "OUT"]
detail_typeDetailTypeEnumyesWITHDRAWAL or TOPUP
transaction_statusTransactionStatusEnumyes
currency_codestringyes
acquirerstringyes
methodstringyesTransfer method
issuing_country_idnumberno

Fee Rule Matching

Pipeline (per fee_type)

buildMatchPipelineRule() constructs a MongoDB aggregation pipeline against financesFeeRules:

PayIn match conditions:

business_id   ∈ [rule.business_id] OR rule.business_id is null
status        = active
acquirer      = acquirer
payment_method_id = payment_method_id
fee_type      = fee_type (IN or OUT)
currency_code = currency_code
country_code  ∈ business.country_code OR null
risk_level    ∈ [rule.risk_level] OR null
card_brand    = card_brand OR rule.card_brand missing/null

Withdrawal match conditions:

business_id   ∈ [rule.business_id] OR null
status        = active
acquirer      = acquirer
transaction_type = extract(detail_type)   e.g. WITHDRAWAL, TOPUP
method        = method
fee_type      = fee_type
currency_code = currency_code
country_code  ∈ business.country_code OR null
risk_level    ∈ [rule.risk_level] OR null

Ranking (specificity)

Rules are ranked by a _rank score — lower score = higher priority (more specific):

ConditionScore contribution
business_id matches (not null)0
business_id is null (generic rule)1
risk_level matches0
risk_level is null1
card_brand matches0
card_brand is null1

The pipeline sorts ascending on _rank and takes $limit: 1. The most specific rule wins.

NULL field handling

Fields like business_id and risk_level can be null in MongoDB (generic rules). The pipeline uses $or with $exists checks to handle missing or null values without filtering them out:

json
{ "$or": [
  { "business_id": { "$in": [businessId] } },
  { "business_id": null },
  { "business_id": { "$exists": false } }
]}

Fee Calculation Formulas

PAYMENT / APMS_PAYMENT — calculatePaymentFees()

International detection:

  • APMS_PAYMENT → always isInternational = false
  • Others → isInternational = issuingCountryId !== business.country_id

Rate selection:

  • isInternational = true → use inter_transaction_rate, inter_transaction_fee
  • isInternational = false → use intra_transaction_rate, intra_transaction_fee

Per side (tonder / acquirer):

transaction_rate   = amount × rate / 100
transaction_fee    = fixed_fee
fee_amount         = max(transaction_rate + transaction_fee, minimum_fee)
iva_amount         = fee_amount × iva_rate / 100
rolling_reserve    = amount × hold_reserve_percentage / 100
net_amount         = amount - fee_amount - iva_amount - rolling_reserve_amount

REFUND — calculateRefundFees()

transaction_rate   = 0
fee_amount         = refund_fee (fixed)
iva_amount         = fee_amount × iva_rate / 100
rolling_reserve    = 0
net_amount         = amount + fee_amount + iva_amount   ← refund added back

DISPUTE — calculateDisputeFees()

transaction_rate   = 0
fee_amount         = chargeback_fee (fixed)
iva_amount         = fee_amount × iva_rate / 100
rolling_reserve    = 0

if transactionStatus = "Won":
  net_amount = fee_amount + iva_amount          ← only fees charged (merchant won)
else:
  net_amount = amount + fee_amount + iva_amount ← full amount + fees (merchant lost)

VOID — calculateVoidFees()

All rates and fees = 0
net_amount = amount   ← void has no charges

WITHDRAWAL / TOPUP — calculateWithdrawalFees()

Always intra (no international variant)
transaction_rate   = amount × intra_transaction_rate / 100
fee_amount         = max(transaction_rate + intra_transaction_fee, minimum_fee)
iva_amount         = fee_amount × iva_rate / 100
rolling_reserve    = 0
net_amount         = amount   ← withdrawal amount unchanged; fees deducted separately

TOPUP — default rules

TOPUP uses hardcoded default rules (no MongoDB lookup):

All rates = 0, all fees = 0
settlement_policy = fixed_days_delay

Output Structure

IFeesCalculatorResponse:

FieldDescription
tonderTonder-side fee breakdown
tonder.fee_amountTonder fee
tonder.iva_amountIVA on tonder fee
tonder.net_amountNet after tonder fees
tonder.rolling_reserve_amountReserve held
acquirerAcquirer-side fee breakdown (same structure)
acquirer.fee_amountAcquirer fee
acquirer.iva_amountIVA on acquirer fee
acquirer.net_amountNet after acquirer fees
fee_rulesThe matched IN and OUT fee rule records
isInternationalWhether the transaction was classified as international

Error Codes

CodeCondition
E0012Business not found in MongoDB
E0013Fee rule not found for the requested fee_type combination

Non-Obvious Behaviors

BehaviorDetail
APMS_PAYMENT always intrabitso, oxxopay, mercadopago — detail_type mapped to APMS_PAYMENT. These are always isInternational = false regardless of issuing country.
TOPUP skips rule matchingTOPUP gets a hardcoded default rule (0% rates, fixed_days_delay settlement). No MongoDB query for fee rules.
minimum_fee clampfee_amount = max(rate + fixed, minimum_fee). If computed fee is below minimum, minimum applies.
Null fields use $or/$existsMongoDB fee rules can have null business_id (generic rules). Pipeline uses $or to match both null and the actual value.
fee_type: ["IN", "OUT"]Always requests both sides. The engine runs two separate MongoDB aggregations and must find rules for both. Missing either throws E0013.
Refund net is positivenet_amount = amount + fee + iva. Refunds add fees on top of the refunded amount (customer gets full amount back; merchant absorbs fees).
Dispute WONWhen merchant wins, net_amount = fee + iva only — transaction amount not recovered in this step (handled separately).
Void zeroes everythingNo rates, no fees, no IVA, no rolling reserve. net_amount = amount.
Withdrawal net = amountFor withdrawals, fees are tracked but not deducted from net_amount at this stage — they are handled as separate debit entries in accounting.

Vecnet — Build Spec v0.2 · Obsidian Terminal