rtm_core/processor/
accounting_system.rs

1use std::collections::{HashMap, HashSet};
2
3use crate::{
4    models::{AccountingOperation, Amount, ClientId, Transaction, TransactionId, TransactionKind},
5    processor::ClientAccountState,
6};
7
8use super::{ClientAccount, TransactionError};
9
10#[derive(Debug)]
11#[must_use]
12pub struct AccountingSystem {
13    client_accounts: HashMap<ClientId, ClientAccount>,
14    seen_transactions: HashSet<TransactionId>,
15}
16
17impl AccountingSystem {
18    pub fn new() -> Self {
19        Self {
20            client_accounts: HashMap::new(),
21            seen_transactions: HashSet::new(),
22        }
23    }
24
25    /// Runs a transaction over the existing accounting system state.
26    ///
27    /// # Errors
28    ///
29    /// For especific errors see [`TransactionError`].
30    #[allow(clippy::missing_panics_doc)]
31    pub fn run_operation(&mut self, operation: AccountingOperation) -> Result<(), TransactionError> {
32        let client_id = operation.client_id();
33
34        let client_account = self
35            .client_accounts
36            .entry(client_id)
37            .or_insert_with(|| ClientAccount::new(client_id));
38
39        if client_account.state == ClientAccountState::Locked {
40            return Err(TransactionError::AccountLocked { client_id: client_id });
41        }
42
43        macro_rules! referred_transaction {
44            ($transaction_id: expr) => {{
45                let transaction_id = ($transaction_id).clone();
46                if let Some(tr) = client_account.transactions.get(&transaction_id) {
47                    tr
48                } else {
49                    return Err(TransactionError::TransactionDoesNotExist {
50                        ref_id: transaction_id,
51                    });
52                }
53            }};
54        }
55
56        match operation {
57            AccountingOperation::Transaction { transaction } => {
58                let amount = normalize_amount(&transaction);
59                let new_amount = client_account.available_balance.clone() + amount;
60                if new_amount < Amount::zero() {
61                    return Err(TransactionError::InsufficientFunds {
62                        cause_id: transaction.id(),
63                    });
64                }
65
66                let transaction_id = transaction.id();
67                if self.seen_transactions.contains(&transaction_id) {
68                    return Err(TransactionError::DuplicateTransaction {
69                        cause_id: transaction_id,
70                    });
71                }
72                self.seen_transactions.insert(transaction_id);
73
74                client_account.available_balance = new_amount;
75                client_account.transactions.insert(transaction_id, transaction);
76            }
77            AccountingOperation::Dispute {
78                client_id,
79                ref_id: transaction_id,
80            } => {
81                let referred_transaction = referred_transaction!(transaction_id);
82
83                if client_id != referred_transaction.client_id() {
84                    return Err(TransactionError::CrossClientTransaction);
85                }
86
87                if !client_account.disputed_transactions.insert(transaction_id) {
88                    return Err(TransactionError::TransactionAlreadyDisputed { ref_id: transaction_id });
89                }
90
91                let amount = normalize_amount(referred_transaction);
92
93                client_account.held_balance += amount.clone();
94                client_account.available_balance -= amount;
95            }
96            AccountingOperation::Resolve {
97                client_id,
98                ref_id: transaction_id,
99            } => {
100                let referred_transaction = referred_transaction!(transaction_id);
101                if client_id != referred_transaction.client_id() {
102                    return Err(TransactionError::CrossClientTransaction);
103                }
104
105                if !client_account.disputed_transactions.remove(&transaction_id) {
106                    return Err(TransactionError::TransactionNotDisputed { ref_id: transaction_id });
107                }
108
109                let amount = normalize_amount(referred_transaction);
110
111                client_account.held_balance -= amount.clone();
112                client_account.available_balance += amount;
113            }
114            AccountingOperation::Chargeback {
115                client_id,
116                ref_id: transaction_id,
117            } => {
118                let referred_transaction = referred_transaction!(transaction_id);
119                if client_id != referred_transaction.client_id() {
120                    return Err(TransactionError::CrossClientTransaction);
121                }
122
123                if !client_account.disputed_transactions.remove(&transaction_id) {
124                    return Err(TransactionError::TransactionNotDisputed { ref_id: transaction_id });
125                }
126
127                let amount = normalize_amount(referred_transaction);
128
129                client_account.held_balance -= amount.clone();
130                client_account.state = ClientAccountState::Locked;
131            }
132        }
133        Ok(())
134    }
135
136    /// Iterates over all currently tracked client accounts.
137    pub fn iter_accounts(&self) -> impl Iterator<Item = &ClientAccount> {
138        self.client_accounts.values()
139    }
140}
141
142impl Default for AccountingSystem {
143    fn default() -> Self {
144        Self::new()
145    }
146}
147
148fn normalize_amount(referred_transaction: &Transaction) -> Amount {
149    let amount = referred_transaction.amount().clone();
150    match referred_transaction.kind() {
151        TransactionKind::Deposit => amount,
152        TransactionKind::Withdrawal => -amount,
153    }
154}