Skip to content

adam-bates/toy-payments-engine

Repository files navigation

Toy Payments Engine 🧸💸🚒

Because even payment engines can be fun if they're written in Rust!

Toy Payments Engine, or TPE as I call it, is a pretend payments processing engine that tracks deposits and withdrawals for multiple clients, as well as disputes, resolutions, and charge-backs for transactions.

Disclaimer: Please don't use this in production :)

Contents

Usage 🚴

The easiest way to see it in action is to use the root-level example file:

cargo run -- transactions.csv

This should give an output that looks something like:

2022-09-02T21:32:00.437Z WARN [toy_payments_engine] Invalid withdrawal attempt: Cannot withdraw 3.0000 from client 2 when available amount is 2.0000
client,available,held,total,locked
1,1.5000,0.0000,1.5000,false
2,2.0000,0.0000,2.0000,false

Writing to a file ✍️

Don't worry! The logs are written to stderr, so we easily can direct our output into a file like so:

cargo run -- transactions.csv > accounts.csv

And now you can open ./accounts.csv to see:

client,available,held,total,locked
1,1.5000,0.0000,1.5000,false
2,2.0000,0.0000,2.0000,false

Testing 🧪

Running the test suite is as simple as:

cargo test

I know what you must be asking yourself ...

Oh, but great wise Adam, how does it all actually work?

Is that too cheesy of a title? Lol.

TL;DR:

1. Stream CSV rows
  a. Deserialize row to Transaction
  b. Append Transaction to Ledger
  c. Apply Ledger changes to Accounts

2. Build report of Accounts
  a. Write as CSV to stdout

Part 0: Assuptions

When processing transactions, it's not always clear if a transaction is valid, or how it should affect an account. I've made the following assuptions, which heavily affect the outcome of the program:

1. Transaction IDs are globally unique.

The following is considered invalid:

- Client 1, Transaction 1, Deposit 50
- Client 2, Transaction 1, Deposit 50

2. Withdrawals cannot be disputed.

Withdrawals are only valid if the client had enough available to withdrawal. Money comes in through deposits, so any disputes against a client having money they shouldn't, should be against the deposits.

3. Input amounts cannot be negative.

It doesn't make sense to deposit or withdrawal a negative value.

4. Maximum amount supported is: 922,337,203,685,477.5807

This number is explained below, but since this is a toy project, there's no need to waste memory by supporting more.

5. Bad transactions won't kill the application.

Whether its a deserialize issue, an overflow, or an invalid action due to business logic, the application will mark the transaction as invalid and move on to the next one.

Part 1: Input 🔠

The program expects to read a CSV file with the following structure.

Row:

Header Type Required Example
type TransactionType (see below) True deposit
client Unsigned 16-bit Integer True 123
tx Unsigned 32-bit Integer True 456
amount Money (see below) False 314.1592

TransactionType:

TransactionType Description
deposit Deposit amount to account
withdrawal Withdrawal amount from account
dispute Begin to dispute a transaction, moving amount in question into held funds
resolve Undo a transaction's dispute, moving amount in question out of held funds
chargeback Close a dispute and lock the account

Money:

Money is stored as a Signed 64-bit Integer representing hundredths-of-cents value.

ie. 314.1592 is stored as 3141592. This means the maximum value allowed is 922,337,203,685,477.5807, and the minimum value allowed is -922,337,203,685,477.5808.

Example File:

An example CSV file might look like:

type,       client,    tx,    amount
deposit,         1,     1,        10
withdrawal,      1,     2,      7.25
dispute,         1,     1,

Each line of the CSV is deserialized to an InputEvent.

Each InputEvent is parsed as a Transaction.

Part 2: Append to Ledger 🧾

Transactions are appended to our ledger, following WORM (Write Once, Read Many).

Part 3: Apply changes to Accounts ⚙️

We grab the AccountSnapshot for whichever client the transaction is for.

Then we apply new transactions to our snapshot, using our updated ledger.

Part 4: Report 📈

After all processing is completed, we loop through every AccountSnapshot, and build a report.

And, well ... that's it! 😁

Thanks for stopping by :) And feel free to reach out if you have any questions or concerns!

About

A pretend payments processing engine written in Rust.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages