Proof of Concept of a payment system in Rust for transaction processing and accounts maintenance.
- Single asset, multiple accounts.
- Command line friendly.
- Pre-validated CVS file input.
- Ignores invalid records.
Rejects invalid transactions:
- InvalidType,
- InsufficientFunds,
- IDNotFound,
- IconsistentWithValueHeld,
- InvalidInput,
- TargetTransactionAmountMissing,
- Bubbles processing errors.
- Extensible transaction types.
- Lossless numeric operations on
Amounttypes (via usingfraction::Decimalcargo package). - Streams over large input files.
The following transaction types are currently supported.
pub enum TransactionType {
Deposit,
Withdrawal,
Dispute,
Resolve,
Chargeback,
}Additional Design Notes Here.
The unit tests can be ran with multiple threads:
cargo test
cargo build --release
./target/release/integrator --help
integrator 1.0
Sebastian Sastre <[email protected]>
PoC payment system to demonstrate transactions processing and account maintenance using CSV files.
USAGE:
integrator <FILENAME>
ARGS:
<FILENAME> Defines the CSV filename to use as input.
OPTIONS:
-h, --help Print help information
-V, --version Print version information
-
The program models the payments processing using the aid of these objects:
App. The spot to start taking input and sending it to collaborators for processing and output generation.Transaction. The main model for the different types of operations to process. It can be instantiated from a parsed CSV record usingfrom_record.Transactions. It's a helper object for keeping a store support dedicated to transactions. If in the future the transactions have to change its support, that can be conveniently refactored only from there.Account. The accounts belong to the app object and they help to keep correct state of a client's account and process transactions.
-
I've used TDD for this program to ensure result correctness and at the same time to help me to incrementally add functionality detecting any regression as I need to introduce changes. So far there are 9 unit tests with what I think are self-evident, unambiguous names to the most fundamental functionality.
-
Amount,ClientIDandTransactionIDhave dedicated types to ensure correctness and allow a change from a single point in code in case of future type migrations. -
I've found cases where specs were vague or ambiguous to decide program behavior. I've added the
RejectedTransactionenum to handle detailed feedback and specificity when the account processes these:- When a withdrawal amount is greater than the accounts' available value, it will produce
Err(RejectedTransaction::InsufficientFounds). - Input is assumed to be valid but the code is defensive, so when a deposit or withdrawal transaction is found to not have an amount or it failed to parse a valid value, processing it will produce a
Err(RejectedTransaction::InvalidInput). - When transaction ids can't help to locate a transaction, processing will produce a
Err(RejectedTransaction::IDNotFound). - When the amount of a dispute is greater than the account's available value, processing it will produce
Err(RejectedTransaction::InsufficientFounds). - When a resolve transaction brings an amount that is greater than the held amount, processing will produce an
Err(RejectedTransaction::InconsistentWithValueHeld).
- When a withdrawal amount is greater than the accounts' available value, it will produce
-
The way the program reacts to all
RejectedTransactioncases is to not produce any output and silently move on processing the next transaction.
Executing:
cargo test
Will show:
running 9 tests
test tests::unit::can_parse_a_deposit_command ... ok
test tests::unit::can_parse_a_withdrawal_command ... ok
test tests::unit::can_parse_input_filename_from_command_line ... ok
test tests::unit::can_read_a_record_streamed_from_a_csv_input_file ... ok
test tests::unit::chargeback_decreases_held_and_total_balances_and_locks_account ... ok
test tests::unit::deposit_can_increase_account_balance ... ok
test tests::unit::dispute_increase_disputed_balance_and_maintain_total ... ok
test tests::unit::resolve_decrease_held_balances_increase_available_and_maintain_total ... ok
test tests::unit::withdrawal_can_decrease_account_balance ... ok
test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
In the input directory, I've added a handful of simple scenarios which incrementally cover the types of transactions.
For example:
cargo run -- input/scenario5.csv
Produces:
1,1.0000,0.0000,1.0000,false
2,2.0000,0.0000,2.0000,false
1,3.0000,0.0000,3.0000,false
1,1.5000,0.0000,1.5000,false
1,0.5000,1.0000,1.5000,false
1,3.5000,1.0000,4.5000,false
1,3.5000,0.0000,3.5000,true
Which is the output of having processed:
- Deposits,
- Withdrawals,
- Disputes,
- Resolution of one of the disputes,
- A chargeback of the other dispute and
- Rejecting a deposit after the chargeback made the account to get locked
v1.0.1->v1.0.2:- Deprecated and removed usage of globals.
- Added a bunch of testing scenarios.
- Building the CSV Reader setting it to not expect headers in the input file (preventing bug of not processing the first record).
- Removed output messages when rejecting unexpected or inconsistent transactions. Now they will be silently ignored.
fraction::Decimalis now the foundation of theAmounttype so operations can be made lossless (preventing error accumulation on balances) while output rendering can be show as per specs.