A CLI program in Rust that processes a CSV stream of transactions and outputs final client account states.
This implementation processes financial transactions including deposits, withdrawals, and a complete dispute lifecycle (dispute → resolve → chargeback). The system maintains account states with precise decimal arithmetic and enforces business rules around locked accounts and insufficient funds.
- Transaction Types: Deposits, withdrawals, disputes, resolves, and chargebacks
- Decimal Precision: Uses
rust_decimalfor exact fixed-point arithmetic (4 decimal places) - Streaming Processing: Processes CSV row-by-row without loading entire file into memory
- Robust Error Handling: Continues processing on row-level errors, fails fast on fatal errors
- Account Locking: Accounts become frozen after chargeback
- Dispute Lifecycle: Only deposits are disputable
cargo run -- <input.csv> > accounts.csvcargo run -- sample/transactions.csv > output.csvCSV file with the following columns:
type: Transaction type (deposit, withdrawal, dispute, resolve, chargeback)client: Client ID (u16)tx: Transaction ID (u32, globally unique)amount: Decimal amount with up to 4 decimal places (required for deposit/withdrawal only)
Example:
type, client, tx, amount
deposit, 1, 1, 100.0
deposit, 2, 2, 200.0
withdrawal, 1, 3, 50.0
dispute, 1, 1,
resolve, 1, 1,CSV with the following columns:
client: Client IDavailable: Available funds (4 decimal places)held: Held funds due to disputes (4 decimal places)total: Total funds (available + held, 4 decimal places)locked: Boolean indicating if account is frozen
Example:
client,available,held,total,locked
1,50.0000,0.0000,50.0000,false
2,200.0000,0.0000,200.0000,false- Adds funds to the client's available balance
- Records transaction for future disputes
- Duplicate transaction IDs are ignored (first-write-wins)
- Deducts funds from available balance
- Fails silently if insufficient funds
- Not disputable
- Only applies to deposits
- Moves funds from
availabletoheld - Total balance remains unchanged
- Must be from the same client who made the deposit
- Releases disputed funds back to
available - Can only resolve transactions in disputed state
- Total balance remains unchanged
- Finalizes dispute by removing funds from
heldandtotal - Locks the account permanently
- All subsequent transactions for locked accounts are ignored
The specification explicitly states that only deposits can be disputed. This makes sense from a business logic perspective: disputes reverse incoming funds, not outgoing withdrawals.
Uses rust_decimal crate to avoid floating-point precision issues. All monetary values are exact.
- Fatal errors (exit non-zero): Cannot open file, missing CSV headers
- Row-level errors (logged to stderr, continue): Invalid transaction types, missing amounts, parsing failures, business rule violations
- Errors are logged with row numbers for debugging
total = available + heldat all times- No negative balances
- Once
locked = true, all transactions are rejected
Assumes chronological ordering in input file (later rows occur after earlier rows).
- Streaming CSV processing (O(1) memory per row)
- In-memory storage: O(C) for clients, O(D) for deposit transactions
- Transaction IDs are stored in HashMap (not array-indexed)
Run unit tests:
cargo testTests cover:
- Basic deposits and withdrawals
- Insufficient funds handling
- Dispute → resolve lifecycle
- Dispute → chargeback → account locking
- Cross-client dispute attempts (should fail)
- Whitespace and decimal formatting
- Duplicate transaction IDs
- Precision with 4 decimal places
- Invalid transaction types
cargo build --releaseThe optimized binary will be at target/release/toy_payments_engine.
.
├── Cargo.toml # Dependencies and project metadata
├── src/
│ ├── main.rs # CLI: argument parsing, CSV I/O
│ └── lib.rs # Engine core: business logic, testable
├── tests/
│ └── basic.rs # Integration tests
├── sample/
│ └── transactions.csv # Sample input file
└── README.md # This file
The program handles various error conditions gracefully:
- Unknown transaction type: Row skipped, logged to stderr
- Missing amount: For deposit/withdrawal, row skipped
- Invalid amount: Negative or non-parsable, row skipped
- Too many decimal places: More than 4, row skipped
- Insufficient funds: Withdrawal fails silently
- Duplicate transaction ID: Second occurrence ignored
- Invalid dispute: Wrong client, wrong state, or non-existent tx ignored
- Locked account: All transactions rejected
- Transaction IDs are globally unique across all clients
- Input file is in chronological order
- Only deposits are disputable
- After chargeback, accounts remain permanently locked
- CSV parsing is flexible (whitespace tolerance)
- Output can be in any client order (implementation sorts by client ID)
Possible extensions not implemented:
- Multiple currencies/assets per client
- Concurrent processing with per-client partitioning
- Persistent transaction journal for replay
- Rich metrics and telemetry
- Configurable rounding strategies
This is a toy implementation for demonstration purposes.