1use async_trait::async_trait;
36use rabia_core::smr::StateMachine;
37use serde::{Deserialize, Serialize};
38use std::collections::HashMap;
39
40#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
42pub struct Account {
43 pub account_id: String,
45 pub balance: i64,
47 pub created_at: u64,
49 pub last_transaction_at: u64,
51 pub transaction_count: u64,
53}
54
55impl Account {
56 pub fn new(account_id: String, initial_balance: i64) -> Self {
57 let now = std::time::SystemTime::now()
58 .duration_since(std::time::UNIX_EPOCH)
59 .unwrap_or_default()
60 .as_millis() as u64;
61
62 Self {
63 account_id,
64 balance: initial_balance,
65 created_at: now,
66 last_transaction_at: now,
67 transaction_count: 0,
68 }
69 }
70
71 pub fn update_balance(&mut self, new_balance: i64) {
72 self.balance = new_balance;
73 self.last_transaction_at = std::time::SystemTime::now()
74 .duration_since(std::time::UNIX_EPOCH)
75 .unwrap_or_default()
76 .as_millis() as u64;
77 self.transaction_count += 1;
78 }
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
83pub struct Transaction {
84 pub transaction_id: String,
86 pub from_account: Option<String>,
88 pub to_account: Option<String>,
90 pub amount: i64,
92 pub timestamp: u64,
94 pub transaction_type: TransactionType,
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
99pub enum TransactionType {
100 Deposit,
101 Withdrawal,
102 Transfer,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
107pub enum BankingCommand {
108 CreateAccount {
110 account_id: String,
111 initial_balance: i64,
112 },
113 Deposit { account_id: String, amount: i64 },
115 Withdraw { account_id: String, amount: i64 },
117 Transfer {
119 from_account: String,
120 to_account: String,
121 amount: i64,
122 },
123 GetBalance { account_id: String },
125 GetAccount { account_id: String },
127 ListAccounts,
129 GetTransactionHistory {
131 account_id: Option<String>,
132 limit: Option<usize>,
133 },
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
138pub struct BankingResponse {
139 pub success: bool,
141 pub data: Option<BankingData>,
143 pub error: Option<String>,
145 pub transaction_id: Option<String>,
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
150pub enum BankingData {
151 Account(Account),
152 Balance(i64),
153 Accounts(Vec<Account>),
154 Transactions(Vec<Transaction>),
155}
156
157impl BankingResponse {
158 pub fn success(data: Option<BankingData>) -> Self {
159 Self {
160 success: true,
161 data,
162 error: None,
163 transaction_id: None,
164 }
165 }
166
167 pub fn success_with_transaction(data: Option<BankingData>, transaction_id: String) -> Self {
168 Self {
169 success: true,
170 data,
171 error: None,
172 transaction_id: Some(transaction_id),
173 }
174 }
175
176 pub fn error(message: String) -> Self {
177 Self {
178 success: false,
179 data: None,
180 error: Some(message),
181 transaction_id: None,
182 }
183 }
184}
185
186#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
188pub struct BankingState {
189 pub accounts: HashMap<String, Account>,
191 pub transactions: Vec<Transaction>,
193 pub operation_count: u64,
195}
196
197#[derive(Debug, Clone)]
199pub struct BankingSMR {
200 state: BankingState,
201}
202
203impl BankingSMR {
204 pub fn new() -> Self {
206 Self {
207 state: BankingState::default(),
208 }
209 }
210
211 pub fn account_count(&self) -> usize {
213 self.state.accounts.len()
214 }
215
216 pub fn transaction_count(&self) -> usize {
218 self.state.transactions.len()
219 }
220
221 pub fn operation_count(&self) -> u64 {
223 self.state.operation_count
224 }
225
226 pub fn total_value(&self) -> i64 {
228 self.state
229 .accounts
230 .values()
231 .map(|account| account.balance)
232 .sum()
233 }
234
235 fn generate_transaction_id() -> String {
236 uuid::Uuid::new_v4().to_string()
237 }
238
239 fn validate_amount(amount: i64) -> Result<(), String> {
240 if amount <= 0 {
241 return Err("Amount must be positive".to_string());
242 }
243 if amount > 1_000_000_000 {
244 return Err("Amount exceeds maximum limit".to_string());
246 }
247 Ok(())
248 }
249
250 fn validate_account_id(account_id: &str) -> Result<(), String> {
251 if account_id.is_empty() {
252 return Err("Account ID cannot be empty".to_string());
253 }
254 if account_id.len() > 50 {
255 return Err("Account ID too long".to_string());
256 }
257 Ok(())
258 }
259}
260
261impl Default for BankingSMR {
262 fn default() -> Self {
263 Self::new()
264 }
265}
266
267#[async_trait]
268impl StateMachine for BankingSMR {
269 type Command = BankingCommand;
270 type Response = BankingResponse;
271 type State = BankingState;
272
273 async fn apply_command(&mut self, command: Self::Command) -> Self::Response {
274 self.state.operation_count += 1;
275
276 match command {
277 BankingCommand::CreateAccount {
278 account_id,
279 initial_balance,
280 } => {
281 if let Err(e) = Self::validate_account_id(&account_id) {
282 return BankingResponse::error(e);
283 }
284
285 if initial_balance < 0 {
286 return BankingResponse::error(
287 "Initial balance cannot be negative".to_string(),
288 );
289 }
290
291 if self.state.accounts.contains_key(&account_id) {
292 return BankingResponse::error("Account already exists".to_string());
293 }
294
295 let account = Account::new(account_id.clone(), initial_balance);
296 self.state.accounts.insert(account_id, account.clone());
297
298 BankingResponse::success(Some(BankingData::Account(account)))
299 }
300
301 BankingCommand::Deposit { account_id, amount } => {
302 if let Err(e) = Self::validate_amount(amount) {
303 return BankingResponse::error(e);
304 }
305
306 let account = match self.state.accounts.get_mut(&account_id) {
307 Some(account) => account,
308 None => return BankingResponse::error("Account not found".to_string()),
309 };
310
311 match account.balance.checked_add(amount) {
312 Some(new_balance) => {
313 account.update_balance(new_balance);
314
315 let transaction_id = Self::generate_transaction_id();
316 let transaction = Transaction {
317 transaction_id: transaction_id.clone(),
318 from_account: None,
319 to_account: Some(account_id),
320 amount,
321 timestamp: std::time::SystemTime::now()
322 .duration_since(std::time::UNIX_EPOCH)
323 .unwrap_or_default()
324 .as_millis() as u64,
325 transaction_type: TransactionType::Deposit,
326 };
327 self.state.transactions.push(transaction);
328
329 BankingResponse::success_with_transaction(
330 Some(BankingData::Balance(new_balance)),
331 transaction_id,
332 )
333 }
334 None => BankingResponse::error("Deposit would cause overflow".to_string()),
335 }
336 }
337
338 BankingCommand::Withdraw { account_id, amount } => {
339 if let Err(e) = Self::validate_amount(amount) {
340 return BankingResponse::error(e);
341 }
342
343 let account = match self.state.accounts.get_mut(&account_id) {
344 Some(account) => account,
345 None => return BankingResponse::error("Account not found".to_string()),
346 };
347
348 if account.balance < amount {
349 return BankingResponse::error("Insufficient funds".to_string());
350 }
351
352 let new_balance = account.balance - amount;
353 account.update_balance(new_balance);
354
355 let transaction_id = Self::generate_transaction_id();
356 let transaction = Transaction {
357 transaction_id: transaction_id.clone(),
358 from_account: Some(account_id),
359 to_account: None,
360 amount,
361 timestamp: std::time::SystemTime::now()
362 .duration_since(std::time::UNIX_EPOCH)
363 .unwrap_or_default()
364 .as_millis() as u64,
365 transaction_type: TransactionType::Withdrawal,
366 };
367 self.state.transactions.push(transaction);
368
369 BankingResponse::success_with_transaction(
370 Some(BankingData::Balance(new_balance)),
371 transaction_id,
372 )
373 }
374
375 BankingCommand::Transfer {
376 from_account,
377 to_account,
378 amount,
379 } => {
380 if let Err(e) = Self::validate_amount(amount) {
381 return BankingResponse::error(e);
382 }
383
384 if from_account == to_account {
385 return BankingResponse::error("Cannot transfer to same account".to_string());
386 }
387
388 let from_balance = match self.state.accounts.get(&from_account) {
390 Some(account) => account.balance,
391 None => return BankingResponse::error("Source account not found".to_string()),
392 };
393
394 if !self.state.accounts.contains_key(&to_account) {
395 return BankingResponse::error("Destination account not found".to_string());
396 }
397
398 if from_balance < amount {
399 return BankingResponse::error("Insufficient funds".to_string());
400 }
401
402 let from_account_ref = self.state.accounts.get_mut(&from_account).unwrap();
404 from_account_ref.update_balance(from_balance - amount);
405
406 let to_account_ref = self.state.accounts.get_mut(&to_account).unwrap();
407 let to_new_balance = to_account_ref.balance + amount;
408 to_account_ref.update_balance(to_new_balance);
409
410 let transaction_id = Self::generate_transaction_id();
411 let transaction = Transaction {
412 transaction_id: transaction_id.clone(),
413 from_account: Some(from_account),
414 to_account: Some(to_account),
415 amount,
416 timestamp: std::time::SystemTime::now()
417 .duration_since(std::time::UNIX_EPOCH)
418 .unwrap_or_default()
419 .as_millis() as u64,
420 transaction_type: TransactionType::Transfer,
421 };
422 self.state.transactions.push(transaction);
423
424 BankingResponse::success_with_transaction(None, transaction_id)
425 }
426
427 BankingCommand::GetBalance { account_id } => {
428 match self.state.accounts.get(&account_id) {
429 Some(account) => {
430 BankingResponse::success(Some(BankingData::Balance(account.balance)))
431 }
432 None => BankingResponse::error("Account not found".to_string()),
433 }
434 }
435
436 BankingCommand::GetAccount { account_id } => {
437 match self.state.accounts.get(&account_id) {
438 Some(account) => {
439 BankingResponse::success(Some(BankingData::Account(account.clone())))
440 }
441 None => BankingResponse::error("Account not found".to_string()),
442 }
443 }
444
445 BankingCommand::ListAccounts => {
446 let accounts: Vec<Account> = self.state.accounts.values().cloned().collect();
447 BankingResponse::success(Some(BankingData::Accounts(accounts)))
448 }
449
450 BankingCommand::GetTransactionHistory { account_id, limit } => {
451 let mut transactions: Vec<Transaction> = if let Some(account_id) = account_id {
452 self.state
453 .transactions
454 .iter()
455 .filter(|tx| {
456 tx.from_account.as_ref() == Some(&account_id)
457 || tx.to_account.as_ref() == Some(&account_id)
458 })
459 .cloned()
460 .collect()
461 } else {
462 self.state.transactions.clone()
463 };
464
465 transactions.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
467
468 if let Some(limit) = limit {
470 transactions.truncate(limit);
471 }
472
473 BankingResponse::success(Some(BankingData::Transactions(transactions)))
474 }
475 }
476 }
477
478 fn get_state(&self) -> Self::State {
479 self.state.clone()
480 }
481
482 fn set_state(&mut self, state: Self::State) {
483 self.state = state;
484 }
485
486 fn serialize_state(&self) -> Vec<u8> {
487 bincode::serialize(&self.state).unwrap_or_default()
488 }
489
490 fn deserialize_state(&mut self, data: &[u8]) -> Result<(), Box<dyn std::error::Error>> {
491 self.state = bincode::deserialize(data)?;
492 Ok(())
493 }
494
495 async fn apply_commands(&mut self, commands: Vec<Self::Command>) -> Vec<Self::Response> {
496 let mut responses = Vec::with_capacity(commands.len());
497 for command in commands {
498 responses.push(self.apply_command(command).await);
499 }
500 responses
501 }
502
503 fn is_deterministic(&self) -> bool {
504 true
505 }
506}
507
508#[cfg(test)]
509mod tests {
510 use super::*;
511
512 #[tokio::test]
513 async fn test_banking_account_creation() {
514 let mut bank = BankingSMR::new();
515
516 let response = bank
517 .apply_command(BankingCommand::CreateAccount {
518 account_id: "alice".to_string(),
519 initial_balance: 1000,
520 })
521 .await;
522
523 assert!(response.success);
524 assert_eq!(bank.account_count(), 1);
525
526 let response = bank
528 .apply_command(BankingCommand::CreateAccount {
529 account_id: "alice".to_string(),
530 initial_balance: 500,
531 })
532 .await;
533
534 assert!(!response.success);
535 assert!(response.error.as_ref().unwrap().contains("already exists"));
536 }
537
538 #[tokio::test]
539 async fn test_banking_deposit_withdraw() {
540 let mut bank = BankingSMR::new();
541
542 bank.apply_command(BankingCommand::CreateAccount {
544 account_id: "alice".to_string(),
545 initial_balance: 1000,
546 })
547 .await;
548
549 let response = bank
551 .apply_command(BankingCommand::Deposit {
552 account_id: "alice".to_string(),
553 amount: 500,
554 })
555 .await;
556
557 assert!(response.success);
558 assert!(response.transaction_id.is_some());
559
560 let response = bank
562 .apply_command(BankingCommand::GetBalance {
563 account_id: "alice".to_string(),
564 })
565 .await;
566
567 if let Some(BankingData::Balance(balance)) = response.data {
568 assert_eq!(balance, 1500);
569 } else {
570 panic!("Expected balance data");
571 }
572
573 let response = bank
575 .apply_command(BankingCommand::Withdraw {
576 account_id: "alice".to_string(),
577 amount: 200,
578 })
579 .await;
580
581 assert!(response.success);
582
583 let response = bank
585 .apply_command(BankingCommand::Withdraw {
586 account_id: "alice".to_string(),
587 amount: 2000,
588 })
589 .await;
590
591 assert!(!response.success);
592 assert!(response
593 .error
594 .as_ref()
595 .unwrap()
596 .contains("Insufficient funds"));
597 }
598
599 #[tokio::test]
600 async fn test_banking_transfer() {
601 let mut bank = BankingSMR::new();
602
603 bank.apply_command(BankingCommand::CreateAccount {
605 account_id: "alice".to_string(),
606 initial_balance: 1000,
607 })
608 .await;
609
610 bank.apply_command(BankingCommand::CreateAccount {
611 account_id: "bob".to_string(),
612 initial_balance: 500,
613 })
614 .await;
615
616 let response = bank
618 .apply_command(BankingCommand::Transfer {
619 from_account: "alice".to_string(),
620 to_account: "bob".to_string(),
621 amount: 300,
622 })
623 .await;
624
625 assert!(response.success);
626 assert!(response.transaction_id.is_some());
627
628 let alice_response = bank
630 .apply_command(BankingCommand::GetBalance {
631 account_id: "alice".to_string(),
632 })
633 .await;
634 let bob_response = bank
635 .apply_command(BankingCommand::GetBalance {
636 account_id: "bob".to_string(),
637 })
638 .await;
639
640 if let Some(BankingData::Balance(alice_balance)) = alice_response.data {
641 assert_eq!(alice_balance, 700);
642 }
643
644 if let Some(BankingData::Balance(bob_balance)) = bob_response.data {
645 assert_eq!(bob_balance, 800);
646 }
647
648 let response = bank
650 .apply_command(BankingCommand::Transfer {
651 from_account: "alice".to_string(),
652 to_account: "bob".to_string(),
653 amount: 1000,
654 })
655 .await;
656
657 assert!(!response.success);
658 assert!(response
659 .error
660 .as_ref()
661 .unwrap()
662 .contains("Insufficient funds"));
663 }
664
665 #[tokio::test]
666 async fn test_banking_state_serialization() {
667 let mut bank = BankingSMR::new();
668
669 bank.apply_command(BankingCommand::CreateAccount {
671 account_id: "alice".to_string(),
672 initial_balance: 1000,
673 })
674 .await;
675
676 bank.apply_command(BankingCommand::Deposit {
677 account_id: "alice".to_string(),
678 amount: 500,
679 })
680 .await;
681
682 let serialized = bank.serialize_state();
684 assert!(!serialized.is_empty());
685
686 let mut new_bank = BankingSMR::new();
688 new_bank.deserialize_state(&serialized).unwrap();
689
690 assert_eq!(new_bank.account_count(), 1);
692 assert_eq!(new_bank.transaction_count(), 1);
693 assert_eq!(new_bank.operation_count(), bank.operation_count());
694
695 let response = new_bank
696 .apply_command(BankingCommand::GetBalance {
697 account_id: "alice".to_string(),
698 })
699 .await;
700
701 if let Some(BankingData::Balance(balance)) = response.data {
702 assert_eq!(balance, 1500);
703 }
704 }
705
706 #[tokio::test]
707 async fn test_banking_transaction_history() {
708 let mut bank = BankingSMR::new();
709
710 bank.apply_command(BankingCommand::CreateAccount {
712 account_id: "alice".to_string(),
713 initial_balance: 1000,
714 })
715 .await;
716
717 bank.apply_command(BankingCommand::CreateAccount {
718 account_id: "bob".to_string(),
719 initial_balance: 500,
720 })
721 .await;
722
723 bank.apply_command(BankingCommand::Deposit {
725 account_id: "alice".to_string(),
726 amount: 200,
727 })
728 .await;
729
730 bank.apply_command(BankingCommand::Transfer {
731 from_account: "alice".to_string(),
732 to_account: "bob".to_string(),
733 amount: 300,
734 })
735 .await;
736
737 let response = bank
739 .apply_command(BankingCommand::GetTransactionHistory {
740 account_id: None,
741 limit: None,
742 })
743 .await;
744
745 if let Some(BankingData::Transactions(transactions)) = response.data {
746 assert_eq!(transactions.len(), 2);
747 }
748
749 let response = bank
751 .apply_command(BankingCommand::GetTransactionHistory {
752 account_id: Some("alice".to_string()),
753 limit: None,
754 })
755 .await;
756
757 if let Some(BankingData::Transactions(transactions)) = response.data {
758 assert_eq!(transactions.len(), 2); }
760 }
761}