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.
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
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
Running the test suite is as simple as:
cargo test
I know what you must be asking yourself ...
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
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:
The following is considered invalid:
- Client 1, Transaction 1, Deposit 50
- Client 2, Transaction 1, Deposit 50
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.
It doesn't make sense to deposit or withdrawal a negative value.
This number is explained below, but since this is a toy project, there's no need to waste memory by supporting more.
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.
The program expects to read a CSV file with the following structure.
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 | 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 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
.
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.
-
Link to deserialization and parsing calls: src/main.rs:36-52
-
Link to InputEvent: src/tpe/input.rs:13
-
Link to Transaction: src/tpe/transaction.rs:6
Transactions are appended to our ledger, following WORM (Write Once, Read Many).
-
Link to append call: src/main.rs:56-59
-
Link to Ledger: src/tpe/ledger.rs:8
We grab the AccountSnapshot for whichever client the transaction is for.
Then we apply new transactions to our snapshot, using our updated ledger.
-
Link to apply call: src/main.rs:63-66
-
Link to apply logic: src/tpe/snapshots/account_snapshot.rs:103
After all processing is completed, we loop through every AccountSnapshot
, and build a report.
-
Building the report: src/tpe/snapshots/account_snapshots.rs:26
-
Serializing the report: src/main.rs:82
Thanks for stopping by :) And feel free to reach out if you have any questions or concerns!