cswork / libraryOpen practice workspace

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

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:

    providerrate
    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 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

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.

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. Parse — csv.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 user — totals.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/pending → amount * 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.

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:

ProviderUS / CAGBDE / FR / ATAll other countries
card2.9%2.5%2.3%3.1%
bank_transfer1.2%1.1%1.0%1.4%
wallet1.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:

ProviderRate
card2.9%
bank_transfer1.0%
wallet1.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.

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

  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.

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.