rtm_core/processor/
accounting_system.rs1use 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 #[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 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}