Katkat

Validate transactions with risk rules and reporting

StripeSoftware Engineer · Technical Screen

Last updated

Implement an end-to-end transaction validation pipeline that parses raw CSV records, evaluates behavioral fraud detection rules, and outputs a formatted fixed-width audit report.

Problem Statement

You are tasked with building a transaction validation and fraud detection engine. Your engine processes a list of raw CSV transaction records, evaluates validation and risk rules, and generates a fixed-width audit summary.

Each CSV record contains the following comma-separated fields: transaction_id,user_id,amount_cents,currency,timestamp_iso

Validation & Risk Rules

Process each transaction against these rules in priority order. If a transaction violates multiple rules, report only the highest-priority reason:

  1. INVALID_FORMAT: The record does not have exactly 5 fields, amount_cents is not a positive integer (> 0), or timestamp_iso cannot be parsed as a valid ISO-8601 UTC timestamp (YYYY-MM-DDTHH:MM:SSZ).
  2. UNSUPPORTED_CURRENCY: The currency is not one of USD, EUR, GBP.
  3. HIGH_VELOCITY: The same user_id has already executed 2 or more approved transactions within the trailing 60-second window (inclusive: t - 60s <= prev_t <= t).
  4. AMOUNT_SPIKE: The amount_cents exceeds 3 times the user's historical approved average transaction amount prior to this transaction. (Note: Only applies if the user has at least 2 previously approved transactions).

If none of the rules trigger, mark the transaction as APPROVED and add it to the user's approved history.

Transactions are guaranteed to be presented in strictly ascending order by timestamp_iso.

Expected Output

Return a list of fixed-width formatted strings, one for each input record, with columns separated by two spaces (" "):

  • TX_ID: Left-aligned, width 10 (truncate if longer).
  • STATUS: Left-aligned, width 8 (APPROVED or DECLINED).
  • REASON: Left-aligned, width 20 (OK if approved, otherwise the rule name above).

Function Signature

def evaluate_transactions(records: list[str]) -> list[str]:
    pass

Constraints

  • Number of records: 1 <= N <= 10^4
  • transaction_id and user_id: alphanumeric strings up to 32 characters
  • amount_cents: integer up to 10^9

Example

records = [
    "tx_01,user_a,1000,USD,2023-01-01T00:00:00Z",
    "tx_02,user_a,1200,USD,2023-01-01T00:00:15Z",
    "tx_03,user_a,1100,USD,2023-01-01T00:00:30Z",
    "tx_04,user_a,5000,USD,2023-01-01T00:02:00Z",
]

output = evaluate_transactions(records)
# Output:
# [
#   "tx_01       APPROVED  OK                  ",
#   "tx_02       APPROVED  OK                  ",
#   "tx_03       DECLINED  HIGH_VELOCITY       ",
#   "tx_04       DECLINED  AMOUNT_SPIKE        "
# ]

Explanation: tx_01 and tx_02 are approved (avg = 1100). tx_03 occurs at 00:00:30Z, which is within 60s of tx_01 and tx_02 (2 previous approved), so it triggers HIGH_VELOCITY. tx_04 occurs at 00:02:00Z (velocity window cleared); however, 5000 > 3 * 1100 (3300), triggering AMOUNT_SPIKE.

Hints

Hint 1

Parse the timestamp to Unix epoch seconds or comparable integer timestamps to simplify sliding-window velocity checks.

Hint 2

Maintain state per `user_id` storing approved timestamps in a deque/list and a running sum and count of approved transaction amounts.

Hint 3

Use Python string formatting (e.g., `f"{tx_id[:10]:<10} {status:<8} {reason:<20}"`) to cleanly enforce exact column widths.

Follow-up questions

  • How would you handle out-of-order or late-arriving transaction streams without loading all records into memory?
  • If the historical average rule needed to be evaluated across a rolling 30-day window instead of all-time, what data structure would you use to maintain `O(1)` updates?
  • How would you scale this design if users make transactions concurrently across multiple worker instances?