Quick Overview

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.

Compute transaction fees from a CSV string

Company: Stripe

Role: Data Engineer

Category: Coding & Algorithms

Difficulty: hard

Interview Round: Technical Screen

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 for - `status` (string) — one of: - `payment_completed`, `payment_pending`, `payment_failed`, - `refund_completed`, - `dispute_won`, `dispute_lost` - `payment_provider` (string) — e.g., `card`, `bank_transfer`, `paypal` - `buyer_country` (string, ISO-2) — e.g., `US`, `DE`, `FR` - `currency` (string, ISO-3) — e.g., `USD`, `EUR` - `amount` (decimal) — transaction amount in the given `currency` 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): 1) `base_rate_by_provider[payment_provider] -> decimal` Used for **all statuses except** `payment_completed`. 2) `completed_rate_by_provider_and_country[(payment_provider, buyer_country)] -> decimal` Used **only when** `status = 'payment_completed'`. 3) `fx_to_usd[currency] -> decimal` Multiply an amount in `currency` by 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 `amount` to USD: `amount_usd = amount * fx_to_usd[currency]`. - `fee_usd = amount_usd * completed_rate + 0.30` where `completed_rate` comes from `completed_rate_by_provider_and_country`. - For all other statuses (`payment_pending`, `payment_failed`, `dispute_won`, `dispute_lost`): - Convert `amount` to USD: `amount_usd = amount * fx_to_usd[currency]`. - `fee_usd = amount_usd * base_rate + 0.30` where `base_rate` comes from `base_rate_by_provider`. Lookups and fallbacks: - If `(payment_provider, buyer_country)` is missing for a completed payment, fall back to `base_rate_by_provider[payment_provider]`. - If a `payment_provider` is unknown, treat its rate as `0` (still apply the `$0.30` fixed fee for non-refunds). - If `currency` is unknown, assume `fx_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).

Overview: 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.

Read the full Stripe Data Engineer interview experience this question came from

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 of `payment_completed`, `payment_failed`, `payment_pending`, `refund_completed`, `dispute_lost`, `dispute_won`. - **`payment_provider`** — one of `card`, `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 of `amount` plus a fixed **$0.30**: `fee = amount × provider_rate + 0.30` (USD) where `provider_rate` depends on the payment provider: | provider | rate | |---|---| | `card` | 2.9% | | `bank_transfer` | 1.0% | | `wallet` | 1.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 a `refund_completed` or `dispute_won` transaction still appears, mapped to `0`). - **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

Examples

Input: "user,status,payment_provider,amount\nalice,payment_completed,card,100.00\nbob,payment_failed,bank_transfer,50.00\nalice,refund_completed,card,20.00\nbob,dispute_lost,wallet,80.00"

Expected Output: {"alice": 320, "bob": 1580}

Explanation: alice pays 100.00 * 2.9% + 0.30 = 3.20 => 320 cents. Her refund adds 0. bob pays 50.00 * 1.0% + 0.30 = 0.80 => 80 cents, plus a lost dispute fee of 1500 cents.

Input: "user,status,payment_provider,amount\nalice,payment_pending,wallet,10.00\nalice,payment_completed,wallet,0.99\ncarol,dispute_won,card,42.00"

Expected Output: {"alice": 76, "carol": 0}

Explanation: alice pays 45 cents for 10.00 via wallet and 31 cents for 0.99 via wallet after rounding, totaling 76. carol appears in the CSV, so she is included with fee 0.

Hints

  1. Use the CSV header to access fields by name instead of relying on column positions.
  2. Avoid floating-point errors by using Decimal or by converting each rounded fee into cents before summing.

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,amount` - **`exchange_rates`** — a dictionary where `exchange_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 USD** - `dispute_won` → fee = **0 USD** - `dispute_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 `user` field is empty** (after trimming surrounding whitespace), including fully blank lines. - Even users whose only transactions are `refund_completed`, `dispute_won`, or otherwise sum to `0` must still appear in the result with a total of `0`. ## Constraints - `0 <= number of transactions <= 100000` - The CSV has the exact header `user,status,payment_provider,buyer_country,currency,amount` when non-empty. - `exchange_rates` contains every currency used in the CSV. - `amount` is a non-negative decimal number. - `payment_provider` is one of `card`, `bank_transfer`, `wallet`. - `status` is one of `payment_completed`, `payment_failed`, `payment_pending`, `refund_completed`, `dispute_lost`, `dispute_won`.

Constraints

  • 0 <= number of transactions <= 100000
  • The CSV has the exact header user,status,payment_provider,buyer_country,currency,amount when non-empty
  • exchange_rates contains every currency used in the CSV
  • amount is a non-negative decimal number
  • payment_provider is one of card, bank_transfer, wallet
  • status is one of payment_completed, payment_failed, payment_pending, refund_completed, dispute_lost, dispute_won

Examples

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})

Expected Output: {"alice": 1783, "bob": 80}

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})

Expected Output: {"u1": 291, "u2": 0}

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 with country_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 `totals` map) and c is the number of currencies in the `fx` map. The rate tables are constant size; the CSV is streamed row by row via DictReader, so no O(n) buffering of parsed rows.

Hints

  1. Use one lookup table for currency conversion and another lookup table for the payment_completed country-specific rates.
  2. Only payment_completed changes from Part 1. The other statuses keep the same logic after converting the amount to USD.

Community answers

Answer by Maximus

#include using namespace std; class Solution { public: unordered_map solution(const string& csvString) { unordered_map mp; unordered_map providerRate({{"card",0.029},{"bank_transfer",0.01},{"wallet",0.015}}); stringstream ss(csvString); vector temp; string str; while(getline(ss,str,'\n')){ temp.push_back(str); } for(int i=1;i<temp.size();i++){ stringstream ss(temp[i]); string user,status,paymentProvider,amount; getline(ss,user,','); getline(ss,status,','); getline(ss,paymentProvider,','); getline(ss,amount,','); if(user.empty()) continue; if(status == "payment_completed" || status == "payment_failed" || status == "payment_pending"){ mp[user] += round(100 ((stod(amount) providerRate[paymentProvider]) + 0.3)); }else if(status == "dispute_lost"){ mp[user] += round(100 * 15.0); }else{ mp[user] += 0; } } return mp; } };

Loading coding console...

Show the approach

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:

  1. Empty guard — if the input is empty or whitespace-only, return {} immediately.
  2. Parsecsv.DictReader(StringIO(csv_string)) reads the header row once and yields each subsequent row as a dict keyed by user/status/payment_provider/amount. Blank lines yield empty/None fields and are skipped (rows with an empty user are ignored).
  3. Register every usertotals.setdefault(user, 0) guarantees a user appears in the output even when all their fees are 0 (e.g. a lone refund_completed).
  4. Fee rules by status:
    • payment_completed/failed/pendingamount * provider_rate + 0.30, rounded to cents and added.
    • dispute_lost → flat 1500 cents.
    • refund_completed/dispute_won → no charge (user still registered in step 3).
    • anything else raises ValueError (defensive; constraints forbid it).
  5. 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 `totals` dictionary 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.