Skip to content

Fee calculation

A SQL-driven port of FeesCalculatorService. For each fee_type in (IN, OUT), find the single most specific active rule.

Matching: specificity ranking

sql
select *,
  (case when business_id @> array[:business_id] then 0 else 1 end
 + case when risk_level @> array[:risk_level] then 0 else 1 end
 + case when card_brand @> array[:card_brand] then 0 else 1 end) as _rank
from fee_rules
where rule_type = :rule_type
  and status = 'active'
  and currency_code = :currency_code
  and (acquirer is null or acquirer @> array[:acquirer])
  and (business_id is null or business_id @> array[:business_id])
  and (payment_method_id is null or payment_method_id @> array[:pmid])
  -- ... country_code, risk_level, card_brand with the same null-or-match pattern
order by _rank asc
limit 1;

Tonder's Mongo $or/$exists null handling becomes Postgres (col is null or col @> array[:val]). Specificity ranking is the _rank expression. Missing either IN or OUT rule → error E0013. There is a matching simulator in the Admin Panel.

Formulas (port verbatim)

The tonder side label is renamed to platform in the response, since Vecnet is the platform. Use the decimal library for all of this.

CategoryFormula
PAYMENT / APMSfee = max(amount×rate/100 + fixed_fee, minimum_fee); iva = fee×iva_rate/100; rr = amount×hold_reserve_percentage/100; net = amount − fee − iva − rr. International vs domestic selects inter/intra rates; APMS is always domestic.
REFUNDfee = refund_fee; net = amount + fee + iva (merchant absorbs fees on top).
DISPUTEfee = chargeback_fee; if Wonnet = fee + iva, else net = amount + fee + iva.
VOIDall zero; net = amount.
WITHDRAWALfee = max(amount×rate/100 + fixed_fee, minimum_fee); net = amount (fees posted as separate entries).
TOPUPhardcoded default rule (0 rates/fees, fixed_days_delay), no rule lookup.

Fees feed the posting function, which records them as DEBIT/CREDIT entries across the platform revenue/expense and VAT accounts.

Vecnet — Build Spec v0.2 · Obsidian Terminal