232 lines
7.2 KiB
Rust
232 lines
7.2 KiB
Rust
//! Define all supported transactions.
|
|
use crate::{
|
|
core::{ClientId, TxAmount, TxId},
|
|
ParseError,
|
|
};
|
|
|
|
use serde::Deserialize;
|
|
|
|
/// A generic [Transaction].
|
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize)]
|
|
#[serde(try_from = "TransactionRecord")]
|
|
pub enum Transaction {
|
|
Deposit(Deposit),
|
|
Withdrawal(Withdrawal),
|
|
Dispute(Dispute),
|
|
Resolve(Resolve),
|
|
Chargeback(Chargeback),
|
|
}
|
|
|
|
impl Transaction {
|
|
/// Build a [csv::ReaderBuilder] configured to read a CSV formatted [Transaction] stream.
|
|
pub fn configured_csv_reader_builder() -> csv::ReaderBuilder {
|
|
let mut builder = csv::ReaderBuilder::new();
|
|
builder
|
|
// Expect header input
|
|
.has_headers(true)
|
|
// Allow whitespace
|
|
.trim(csv::Trim::All)
|
|
// Allow trailing fields to be omitted
|
|
.flexible(true);
|
|
builder
|
|
}
|
|
}
|
|
|
|
// A type used to deserialize [Transaction] from an input CSV stream.
|
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize)]
|
|
struct TransactionRecord<'a> {
|
|
#[serde(rename = "type")]
|
|
type_: &'a str,
|
|
client: ClientId,
|
|
tx: TxId,
|
|
amount: Option<TxAmount>,
|
|
}
|
|
|
|
impl TryFrom<TransactionRecord<'_>> for Transaction {
|
|
type Error = ParseError;
|
|
|
|
fn try_from(value: TransactionRecord<'_>) -> Result<Self, Self::Error> {
|
|
let TransactionRecord {
|
|
type_,
|
|
client,
|
|
tx,
|
|
amount,
|
|
} = value;
|
|
|
|
let transaction = match type_ {
|
|
"deposit" => {
|
|
let amount = amount.ok_or(ParseError::MissingAmount)?;
|
|
Transaction::Deposit(Deposit { client, tx, amount })
|
|
}
|
|
"withdrawal" => {
|
|
let amount = amount.ok_or(ParseError::MissingAmount)?;
|
|
Transaction::Withdrawal(Withdrawal { client, tx, amount })
|
|
}
|
|
"dispute" => Transaction::Dispute(Dispute { client, tx }),
|
|
"resolve" => Transaction::Resolve(Resolve { client, tx }),
|
|
"chargeback" => Transaction::Chargeback(Chargeback { client, tx }),
|
|
_ => return Err(ParseError::UnknownTx(type_.into())),
|
|
};
|
|
Ok(transaction)
|
|
}
|
|
}
|
|
|
|
/// Deposit funds into an account, i.e: increase its balance by the amount given.
|
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
|
pub struct Deposit {
|
|
pub client: ClientId,
|
|
pub tx: TxId,
|
|
pub amount: TxAmount,
|
|
}
|
|
|
|
/// Withdraw funds from an account, i.e: the opposite of a [Deposit]. It is not allowed to withdraw
|
|
/// more than is available on the given account, and should result in a no-op.
|
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
|
pub struct Withdrawal {
|
|
pub client: ClientId,
|
|
pub tx: TxId,
|
|
pub amount: TxAmount,
|
|
}
|
|
|
|
/// Hold funds for an erroneous transaction that should be reversed. Extract the amount of funds
|
|
/// corresponding to the given transaction into a held funds envelop by transfering it from their
|
|
/// available funds. If the given transaction does not exist, this results in a no-op.
|
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
|
pub struct Dispute {
|
|
pub client: ClientId,
|
|
pub tx: TxId,
|
|
}
|
|
|
|
/// Resolve a [Dispute] in favor of the client: move the held funds for the diputed transaction
|
|
/// back to the available funds. If either the given transaction does not exist, or is not
|
|
/// disputed, this results in a no-op.
|
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
|
pub struct Resolve {
|
|
pub client: ClientId,
|
|
pub tx: TxId,
|
|
}
|
|
|
|
/// Resolve [Dispute] by withdrawing held funds. The held funds are decreased by the amount of the
|
|
/// transaction. An account which succesffully executed a chargeback is subsequently frozen. If
|
|
/// either the transaction does not exist, or is not disputed, this results in a no-op and the
|
|
/// account is *not* frozen.
|
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
|
pub struct Chargeback {
|
|
pub client: ClientId,
|
|
pub tx: TxId,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use super::*;
|
|
use fpdec::{Dec, Decimal};
|
|
|
|
fn parse_transaction(input: &str) -> Transaction {
|
|
let rdr = Transaction::configured_csv_reader_builder().from_reader(input.as_bytes());
|
|
rdr.into_deserialize().next().unwrap().unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn deserialize_deposit() {
|
|
let data = "type,client,tx,amount\ndeposit,1,2,3.0";
|
|
assert_eq!(
|
|
parse_transaction(data),
|
|
Transaction::Deposit(Deposit {
|
|
client: ClientId(1),
|
|
tx: TxId(2),
|
|
amount: TxAmount(Dec!(3.0))
|
|
}),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn deserialize_withdrawal() {
|
|
let data = "type,client,tx,amount\nwithdrawal,1,2,3.0";
|
|
assert_eq!(
|
|
parse_transaction(data),
|
|
Transaction::Withdrawal(Withdrawal {
|
|
client: ClientId(1),
|
|
tx: TxId(2),
|
|
amount: TxAmount(Dec!(3.0))
|
|
}),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn deserialize_dispute() {
|
|
let data = "type,client,tx,amount\ndispute,1,2";
|
|
assert_eq!(
|
|
parse_transaction(data),
|
|
Transaction::Dispute(Dispute {
|
|
client: ClientId(1),
|
|
tx: TxId(2),
|
|
}),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn deserialize_resolve() {
|
|
let data = "type,client,tx,amount\nresolve,1,2";
|
|
assert_eq!(
|
|
parse_transaction(data),
|
|
Transaction::Resolve(Resolve {
|
|
client: ClientId(1),
|
|
tx: TxId(2),
|
|
}),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn deserialize_chargeback() {
|
|
let data = "type,client,tx,amount\nchargeback,1,2";
|
|
assert_eq!(
|
|
parse_transaction(data),
|
|
Transaction::Chargeback(Chargeback {
|
|
client: ClientId(1),
|
|
tx: TxId(2),
|
|
}),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn deserialize_transactions() {
|
|
let data = concat!(
|
|
"type,client,tx,amount\n",
|
|
"deposit, 1, 2, 12.0000\n",
|
|
"withdrawal , 3,4, 42.27 \n",
|
|
"dispute, 5 , 6, \n",
|
|
" resolve,7,8,\n",
|
|
"chargeback,9,10",
|
|
);
|
|
let rdr = Transaction::configured_csv_reader_builder().from_reader(data.as_bytes());
|
|
let transactions: Result<Vec<Transaction>, _> = rdr.into_deserialize().collect();
|
|
assert_eq!(
|
|
transactions.unwrap(),
|
|
vec![
|
|
Transaction::Deposit(Deposit {
|
|
client: ClientId(1),
|
|
tx: TxId(2),
|
|
amount: TxAmount(Dec!(12.0000)),
|
|
}),
|
|
Transaction::Withdrawal(Withdrawal {
|
|
client: ClientId(3),
|
|
tx: TxId(4),
|
|
amount: TxAmount(Dec!(42.27)),
|
|
}),
|
|
Transaction::Dispute(Dispute {
|
|
client: ClientId(5),
|
|
tx: TxId(6),
|
|
}),
|
|
Transaction::Resolve(Resolve {
|
|
client: ClientId(7),
|
|
tx: TxId(8),
|
|
}),
|
|
Transaction::Chargeback(Chargeback {
|
|
client: ClientId(9),
|
|
tx: TxId(10),
|
|
}),
|
|
]
|
|
);
|
|
}
|
|
}
|