Skip to main content

datasynth_banking/models/
account.rs

1//! Banking account model for KYC/AML simulation.
2
3use chrono::{DateTime, NaiveDate, Utc};
4use datasynth_core::models::banking::{AccountFeatures, AccountStatus, BankAccountType};
5use rust_decimal::Decimal;
6use serde::{Deserialize, Serialize};
7use uuid::Uuid;
8
9/// A bank account with full metadata.
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct BankAccount {
12    /// Unique account identifier
13    pub account_id: Uuid,
14    /// Account number (masked for output)
15    pub account_number: String,
16    /// Account type
17    pub account_type: BankAccountType,
18    /// Primary owner customer ID
19    pub primary_owner_id: Uuid,
20    /// Joint owner customer IDs
21    pub joint_owner_ids: Vec<Uuid>,
22    /// Account status
23    pub status: AccountStatus,
24    /// Account currency (ISO 4217)
25    pub currency: String,
26    /// Account opening date
27    pub opening_date: NaiveDate,
28    /// Account closing date (if closed)
29    pub closing_date: Option<NaiveDate>,
30    /// Current balance
31    #[serde(with = "rust_decimal::serde::str")]
32    pub current_balance: Decimal,
33    /// Available balance (may differ due to holds)
34    #[serde(with = "rust_decimal::serde::str")]
35    pub available_balance: Decimal,
36    /// Account features/capabilities
37    pub features: AccountFeatures,
38    /// IBAN (for international accounts)
39    pub iban: Option<String>,
40    /// BIC/SWIFT code
41    pub swift_bic: Option<String>,
42    /// Routing number (for US accounts)
43    pub routing_number: Option<String>,
44    /// Branch code
45    pub branch_code: Option<String>,
46    /// Interest rate (for savings/CD)
47    pub interest_rate: Option<Decimal>,
48    /// Overdraft limit
49    #[serde(with = "rust_decimal::serde::str")]
50    pub overdraft_limit: Decimal,
51    /// Last activity timestamp
52    pub last_activity: Option<DateTime<Utc>>,
53    /// Days dormant (calculated field)
54    pub days_dormant: u32,
55    /// Is this a nominee account
56    pub is_nominee: bool,
57    /// Linked card numbers (masked)
58    pub linked_cards: Vec<String>,
59    /// Purpose of account (declared)
60    pub declared_purpose: Option<String>,
61    /// Source account for linked funding
62    pub funding_source_account: Option<Uuid>,
63
64    // Ground truth labels
65    /// Whether this is a mule account
66    pub is_mule_account: bool,
67    /// Whether this is a funnel account
68    pub is_funnel_account: bool,
69    /// Associated case ID for suspicious activity
70    pub case_id: Option<String>,
71}
72
73impl BankAccount {
74    /// Create a new account.
75    pub fn new(
76        account_id: Uuid,
77        account_number: String,
78        account_type: BankAccountType,
79        primary_owner_id: Uuid,
80        currency: &str,
81        opening_date: NaiveDate,
82    ) -> Self {
83        let features = match account_type {
84            BankAccountType::Checking => AccountFeatures::retail_standard(),
85            BankAccountType::BusinessOperating => AccountFeatures::business_standard(),
86            _ => AccountFeatures::default(),
87        };
88
89        Self {
90            account_id,
91            account_number,
92            account_type,
93            primary_owner_id,
94            joint_owner_ids: Vec::new(),
95            status: AccountStatus::Active,
96            currency: currency.to_string(),
97            opening_date,
98            closing_date: None,
99            current_balance: Decimal::ZERO,
100            available_balance: Decimal::ZERO,
101            features,
102            iban: None,
103            swift_bic: None,
104            routing_number: None,
105            branch_code: None,
106            interest_rate: None,
107            overdraft_limit: Decimal::ZERO,
108            last_activity: None,
109            days_dormant: 0,
110            is_nominee: false,
111            linked_cards: Vec::new(),
112            declared_purpose: None,
113            funding_source_account: None,
114            is_mule_account: false,
115            is_funnel_account: false,
116            case_id: None,
117        }
118    }
119
120    /// Check if account can process transactions.
121    pub fn can_transact(&self) -> bool {
122        self.status.allows_transactions()
123    }
124
125    /// Check if account has sufficient funds for debit.
126    pub fn has_sufficient_funds(&self, amount: Decimal) -> bool {
127        self.available_balance + self.overdraft_limit >= amount
128    }
129
130    /// Apply a debit (outgoing transaction).
131    pub fn apply_debit(&mut self, amount: Decimal, timestamp: DateTime<Utc>) -> bool {
132        if !self.has_sufficient_funds(amount) {
133            return false;
134        }
135        self.current_balance -= amount;
136        self.available_balance -= amount;
137        self.last_activity = Some(timestamp);
138        self.days_dormant = 0;
139        true
140    }
141
142    /// Apply a credit (incoming transaction).
143    pub fn apply_credit(&mut self, amount: Decimal, timestamp: DateTime<Utc>) {
144        self.current_balance += amount;
145        self.available_balance += amount;
146        self.last_activity = Some(timestamp);
147        self.days_dormant = 0;
148    }
149
150    /// Place a hold on funds.
151    pub fn place_hold(&mut self, amount: Decimal) {
152        self.available_balance -= amount;
153    }
154
155    /// Release a hold on funds.
156    pub fn release_hold(&mut self, amount: Decimal) {
157        self.available_balance += amount;
158    }
159
160    /// Close the account.
161    pub fn close(&mut self, close_date: NaiveDate) {
162        self.status = AccountStatus::Closed;
163        self.closing_date = Some(close_date);
164    }
165
166    /// Freeze the account.
167    pub fn freeze(&mut self) {
168        self.status = AccountStatus::Frozen;
169    }
170
171    /// Mark as dormant.
172    pub fn mark_dormant(&mut self, days: u32) {
173        self.days_dormant = days;
174        if days > 365 {
175            self.status = AccountStatus::Dormant;
176        }
177    }
178
179    /// Add a joint owner.
180    pub fn add_joint_owner(&mut self, owner_id: Uuid) {
181        if !self.joint_owner_ids.contains(&owner_id) {
182            self.joint_owner_ids.push(owner_id);
183        }
184    }
185
186    /// Get all owner IDs (primary + joint).
187    pub fn all_owner_ids(&self) -> Vec<Uuid> {
188        let mut owners = vec![self.primary_owner_id];
189        owners.extend(&self.joint_owner_ids);
190        owners
191    }
192
193    /// Calculate risk score for the account.
194    pub fn calculate_risk_score(&self) -> u8 {
195        let mut score = self.account_type.risk_weight() * 30.0;
196
197        // Status risk
198        score += self.status.risk_indicator() * 20.0;
199
200        // Feature risk
201        if self.features.international_transfers {
202            score += 10.0;
203        }
204        if self.features.wire_transfers {
205            score += 5.0;
206        }
207        if self.features.cash_deposits {
208            score += 5.0;
209        }
210
211        // Ground truth
212        if self.is_mule_account {
213            score += 50.0;
214        }
215        if self.is_funnel_account {
216            score += 40.0;
217        }
218
219        score.min(100.0) as u8
220    }
221}
222
223/// Account holder summary for reporting.
224#[derive(Debug, Clone, Serialize, Deserialize)]
225pub struct AccountHolder {
226    /// Customer ID
227    pub customer_id: Uuid,
228    /// Holder type
229    pub holder_type: AccountHolderType,
230    /// Ownership percentage (for joint accounts)
231    pub ownership_percent: Option<u8>,
232    /// Date added as holder
233    pub added_date: NaiveDate,
234}
235
236/// Type of account holder.
237#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
238#[serde(rename_all = "snake_case")]
239pub enum AccountHolderType {
240    /// Primary account owner
241    Primary,
242    /// Joint owner with full rights
243    JointOwner,
244    /// Authorized signer (no ownership)
245    AuthorizedSigner,
246    /// Beneficiary
247    Beneficiary,
248    /// Power of attorney
249    PowerOfAttorney,
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255
256    #[test]
257    fn test_account_creation() {
258        let account = BankAccount::new(
259            Uuid::new_v4(),
260            "****1234".to_string(),
261            BankAccountType::Checking,
262            Uuid::new_v4(),
263            "USD",
264            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
265        );
266
267        assert!(account.can_transact());
268        assert_eq!(account.current_balance, Decimal::ZERO);
269    }
270
271    #[test]
272    fn test_account_transactions() {
273        let mut account = BankAccount::new(
274            Uuid::new_v4(),
275            "****1234".to_string(),
276            BankAccountType::Checking,
277            Uuid::new_v4(),
278            "USD",
279            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
280        );
281
282        let now = Utc::now();
283
284        // Credit
285        account.apply_credit(Decimal::from(1000), now);
286        assert_eq!(account.current_balance, Decimal::from(1000));
287
288        // Debit
289        assert!(account.apply_debit(Decimal::from(500), now));
290        assert_eq!(account.current_balance, Decimal::from(500));
291
292        // Insufficient funds
293        assert!(!account.apply_debit(Decimal::from(1000), now));
294    }
295
296    #[test]
297    fn test_account_freeze() {
298        let mut account = BankAccount::new(
299            Uuid::new_v4(),
300            "****1234".to_string(),
301            BankAccountType::Checking,
302            Uuid::new_v4(),
303            "USD",
304            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
305        );
306
307        account.freeze();
308        assert!(!account.can_transact());
309    }
310
311    #[test]
312    fn test_joint_owners() {
313        let mut account = BankAccount::new(
314            Uuid::new_v4(),
315            "****1234".to_string(),
316            BankAccountType::Checking,
317            Uuid::new_v4(),
318            "USD",
319            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
320        );
321
322        let joint_owner = Uuid::new_v4();
323        account.add_joint_owner(joint_owner);
324
325        let owners = account.all_owner_ids();
326        assert_eq!(owners.len(), 2);
327    }
328}