Coding questions · Stripe
Compute transaction fees from a CSV string
Evaluates CSV parsing, numeric conversions and rounding, map lookups with fallbacks, transactional fee computation and per-merchant aggregation in the Coding & Algorithms category for Data Engineer roles.
You are given a CSV string representing a list of payment transactions. Write a function that parses the CSV and returns a CSV string with the total processing fee per merchant.
Input CSV
The input has a header and the following columns:
transaction_id(string)merchant_id(string) — the “person” you should aggregate fees forstatus(string) — one of:payment_completed,payment_pending,payment_failed,refund_completed,dispute_won,dispute_lost
payment_provider(string) — e.g.,card,bank_transfer,paypalbuyer_country(string, ISO-2) — e.g.,US,DE,FRcurrency(string, ISO-3) — e.g.,USD,EURamount(decimal) — transaction amount in the givencurrency
Assume:
- The CSV may contain extra whitespace; you should handle it.
- All amounts are non-negative.
Fee rules
You are also given (as in-memory dictionaries/maps):
-
base_rate_by_provider[payment_provider] -> decimalUsed for all statuses exceptpayment_completed. -
completed_rate_by_provider_and_country[(payment_provider, buyer_country)] -> decimalUsed only whenstatus = 'payment_completed'. -
fx_to_usd[currency] -> decimalMultiply an amount incurrencyby this factor to convert to USD.
Fee calculation per transaction:
- If
status = 'refund_completed': fee is 0 (no refund fee). - If
status = 'payment_completed':- Convert
amountto USD:amount_usd = amount * fx_to_usd[currency]. fee_usd = amount_usd * completed_rate + 0.30wherecompleted_ratecomes fromcompleted_rate_by_provider_and_country.
- Convert
- For all other statuses (
payment_pending,payment_failed,dispute_won,dispute_lost):- Convert
amountto USD:amount_usd = amount * fx_to_usd[currency]. fee_usd = amount_usd * base_rate + 0.30wherebase_ratecomes frombase_rate_by_provider.
- Convert
Lookups and fallbacks:
- If
(payment_provider, buyer_country)is missing for a completed payment, fall back tobase_rate_by_provider[payment_provider]. - If a
payment_provideris unknown, treat its rate as0(still apply the$0.30fixed fee for non-refunds). - If
currencyis unknown, assumefx_to_usd[currency] = 1.0.
Required output
Return a CSV string with header:
merchant_id,total_fee_usd
Where total_fee_usd is the sum of fee_usd across that merchant’s transactions, rounded to 2 decimals. Order rows by merchant_id ascending.
What to implement
Implement a function (in a language of your choice):
compute_fees_per_merchant(csv_str, base_rate_by_provider, completed_rate_by_provider_and_country, fx_to_usd) -> csv_str
Discuss time complexity and how you’d test edge cases (e.g., empty input, malformed rows, missing lookup keys).
Part 1: Parse a transaction CSV string and compute each user's fees
Parse a CSV of payment transactions and compute the total processing fee owed by each user, returned in integer cents.
What to implement
Implement solution(csv_string). The input is a single CSV string; return a dictionary mapping each user to their total fee, expressed as a whole number of cents (an integer).
Input format
When non-empty, the CSV begins with the exact header line:
user,status,payment_provider,amount
Each subsequent line is one transaction with these fields:
user— the user the transaction belongs to.status— one ofpayment_completed,payment_failed,payment_pending,refund_completed,dispute_lost,dispute_won.payment_provider— one ofcard,bank_transfer,wallet.amount— the transaction amount in US dollars, a non-negative decimal with at most 2 digits after the decimal point.
Fee rules
Compute the fee for each transaction based on its status:
-
payment_completed,payment_failed,payment_pending— the fee is a percentage ofamountplus a fixed $0.30:fee = amount × provider_rate + 0.30(USD)where
provider_ratedepends on the payment provider:provider rate card2.9% bank_transfer1.0% wallet1.5% -
dispute_lost— a flat $15.00 fee. -
refund_completed— $0 (no fee). -
dispute_won— $0 (no fee).
Rounding and accumulation
- Round each transaction's fee to the nearest cent using half-up rounding, then convert it to an integer number of cents before adding it to that user's running total.
- A user's returned value is the sum of their per-transaction cent fees.
Output and edge cases
- Return a dictionary mapping
user→ total fee in cents (integer). - Include every user that appears in the CSV, even if their total fee is
0(for example, a user with only arefund_completedordispute_wontransaction still appears, mapped to0). - Ignore blank lines (and any row with no user).
- If the input string is empty or whitespace-only, return an empty dictionary
{}.
Examples
A card payment_completed of 100.00 produces 100.00 × 0.029 + 0.30 = 3.20 USD → 320 cents.
A wallet payment_completed of 0.99 produces 0.99 × 0.015 + 0.30 = 0.31485 USD, which rounds half-up to 0.31 USD → 31 cents.
Constraints
- 0 <= number of transactions <= 100000
- The CSV has the exact header user,status,payment_provider,amount when non-empty
- amount is a non-negative decimal number with at most 2 digits after the decimal point
- status is one of payment_completed, payment_failed, payment_pending, refund_completed, dispute_lost, dispute_won
- payment_provider is one of card, bank_transfer, wallet
Hints
- Use the CSV header to access fields by name instead of relying on column positions.
- Avoid floating-point errors by using Decimal or by converting each rounded fee into cents before summing.
Approach
Approach: single pass with exact decimal money math
The core idea is a linear scan over the CSV rows, accumulating each user's fee in integer cents so the returned dictionary never carries floating-point error.
Why Decimal, not float? Fees like 0.99 * 0.015 + 0.30 (= 0.31485) must round half-up to a cent. Binary floats can't represent these decimals exactly, so the code uses Python's Decimal with ROUND_HALF_UP to compute each fee, then converts to an integer number of cents via to_cents ((value * 100).quantize(Decimal("1"), ROUND_HALF_UP)).
Steps:
- Empty guard — if the input is empty or whitespace-only, return
{}immediately. - Parse —
csv.DictReader(StringIO(csv_string))reads the header row once and yields each subsequent row as a dict keyed byuser/status/payment_provider/amount. Blank lines yield empty/Nonefields and are skipped (rows with an emptyuserare ignored). - Register every user —
totals.setdefault(user, 0)guarantees a user appears in the output even when all their fees are 0 (e.g. a lonerefund_completed). - Fee rules by status:
payment_completed/failed/pending→amount * provider_rate + 0.30, rounded to cents and added.dispute_lost→ flat1500cents.refund_completed/dispute_won→ no charge (user still registered in step 3).- anything else raises
ValueError(defensive; constraints forbid it).
- Return
dict(sorted(totals.items()))for deterministic, user-sorted output.
Correctness rests on registering the user before branching on status, doing all rounding per-transaction (matching the spec's "round before adding"), and using Decimal end-to-end so the cent totals are exact.
- Space complexity: O(u), where u is the number of distinct users — the
totalsdictionary holds one integer per user. CSV rows are streamed one at a time by DictReader, so no extra O(n) buffer of parsed rows is retained.
Part 2: Compute fees in USD for completed transactions using country-specific rates
Compute the total payment-processing fee charged to each user, in whole USD cents, given a CSV of transactions and a currency-to-USD exchange-rate table.
Function
solution(csv_string, exchange_rates)
Inputs
csv_string— a CSV string whose header (when non-empty) is exactly:user,status,payment_provider,buyer_country,currency,amountexchange_rates— a dictionary whereexchange_rates[currency]is the USD value of 1 unit of that currency.
Output
Return a dictionary mapping each user to their total fee in USD cents (an integer).
Every user that appears in the CSV must be present in the result — even if their total fee is 0. If csv_string is empty or contains only whitespace, return {}. (Key order in the returned dictionary does not matter.)
How to process each transaction
1. Convert the amount to USD:
converted_usd = amount * exchange_rates[currency]
2. Compute the transaction fee in USD based on status:
payment_completed
Fee = converted_usd * rate + 0.30, where rate depends on the provider and buyer_country:
| Provider | US / CA | GB | DE / FR / AT | All other countries |
|---|---|---|---|---|
card | 2.9% | 2.5% | 2.3% | 3.1% |
bank_transfer | 1.2% | 1.1% | 1.0% | 1.4% |
wallet | 1.8% | 1.8% | 1.8% | 1.8% |
(wallet is 1.8% for any country.)
payment_failed and payment_pending
Use the provider-only (country-independent) rates from Part 1 on the converted USD amount:
| Provider | Rate |
|---|---|
card | 2.9% |
bank_transfer | 1.0% |
wallet | 1.5% |
Fee = converted_usd * rate + 0.30.
Other statuses
refund_completed→ fee = 0 USDdispute_won→ fee = 0 USDdispute_lost→ fee = 15.00 USD flat (no conversion, no percentage, no fixed add-on)
3. Round and accumulate: Round each transaction's fee to the nearest cent using half-up rounding (e.g. 0.005 rounds up to 0.01), then add the resulting integer cents to that user's running total. Round per transaction — before adding to the total, not after summing.
Notes
- Skip any row whose
userfield is empty (after trimming surrounding whitespace), including fully blank lines. - Even users whose only transactions are
refund_completed,dispute_won, or otherwise sum to0must still appear in the result with a total of0.
Constraints
0 <= number of transactions <= 100000- The CSV has the exact header
user,status,payment_provider,buyer_country,currency,amountwhen non-empty. exchange_ratescontains every currency used in the CSV.amountis a non-negative decimal number.payment_provideris one ofcard,bank_transfer,wallet.statusis one ofpayment_completed,payment_failed,payment_pending,refund_completed,dispute_lost,dispute_won.
Examples
Example 1
Input: ("user,status,payment_provider,buyer_country,currency,amount\nalice,payment_completed,card,DE,EUR,100.00\nbob,payment_pending,bank_transfer,US,USD,50.00\nalice,dispute_lost,wallet,GB,GBP,80.00", {"USD": 1.0, "EUR": 1.10, "GBP": 1.25})
Output: {"alice": 1783, "bob": 80}
Explanation: alice: 100 EUR -> 110 USD, DE card rate 2.3%, so fee is 110 * 0.023 + 0.30 = 2.83 => 283 cents, plus 1500 cents for dispute_lost. bob: 50 USD pending bank transfer uses Part 1 logic, 50 * 1.0% + 0.30 = 0.80 => 80 cents.
Example 2
Input: ("user,status,payment_provider,buyer_country,currency,amount\nu1,payment_completed,card,JP,JPY,10000\nu1,payment_completed,wallet,FR,EUR,10.00\nu2,refund_completed,card,US,USD,9.99", {"USD": 1.0, "EUR": 1.10, "JPY": 0.0068})
Output: {"u1": 291, "u2": 0}
Explanation: 10000 JPY -> 68 USD, JP card uses the default 3.1% rate, so fee is 68 * 0.031 + 0.30 = 2.408 => 241 cents. 10 EUR -> 11 USD, wallet completed fee is 11 * 1.8% + 0.30 = 0.498 => 50 cents. Total for u1 is 291.
Hints
- Use one lookup table for currency conversion and another lookup table for the payment_completed country-specific rates.
- Only payment_completed changes from Part 1. The other statuses keep the same logic after converting the amount to USD.
Approach
The solution computes per-user fees in a single pass over the CSV, using Python's Decimal everywhere so currency math and half-up rounding are exact (floats would mis-round at the cent boundary).
Setup. If csv_string is empty or whitespace, it returns {}. Otherwise it builds two static rate tables: base_rates (provider-only rates for failed/pending), and completed_rates, a provider → country → rate map where each provider has a "*" entry for "all other countries". FX rates are converted to Decimal once via Decimal(str(rate)) (stringifying first avoids inheriting float noise).
Per-row processing. A csv.DictReader yields one dict per line. After stripping fields, rows with an empty user are skipped; every other user is registered with totals.setdefault(user, 0) so they appear even with a 0 total. Then it branches on status:
dispute_lost: add a flat 1500 cents.refund_completed/dispute_won: contribute 0 (user still recorded).- Otherwise convert:
amount_usd = Decimal(amount) * fx[currency].payment_completed: pick the rate withcountry_table.get(country, country_table["*"])so unknown countries fall back to*; fee =amount_usd * rate + 0.30.payment_failed/payment_pending: fee =amount_usd * base_rates[provider] + 0.30.
Rounding. to_cents multiplies by 100 and quantizes with ROUND_HALF_UP, so each transaction is rounded to a whole cent before being summed — matching the spec exactly (rounding the running total would differ).
Finally dict(sorted(totals.items())) returns users in sorted order. Correctness rests on the * fallback, per-transaction rounding, and exact Decimal arithmetic.
- Time complexity: O(n), where n is the number of CSV rows. Each row does O(1) dict lookups and one Decimal arithmetic/quantize. The final sort costs O(u log u) over u distinct users (u <= n), which is dominated by, or comparable to, the O(n) scan.
- Space complexity: O(u + c), where u is the number of distinct users (the
totalsmap) and c is the number of currencies in thefxmap. The rate tables are constant size; the CSV is streamed row by row via DictReader, so no O(n) buffering of parsed rows.