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    /// FK → GL account number in the chart of accounts (e.g. "1010" for Cash at Bank).
64    /// `None` when the account has not yet been mapped to the general ledger.
65    pub gl_account: Option<String>,
66
67    // Ground truth labels
68    /// Whether this is a mule account
69    pub is_mule_account: bool,
70    /// Whether this is a funnel account
71    pub is_funnel_account: bool,
72    /// Associated case ID for suspicious activity
73    pub case_id: Option<String>,
74}
75
76impl BankAccount {
77    /// Create a new account.
78    pub fn new(
79        account_id: Uuid,
80        account_number: String,
81        account_type: BankAccountType,
82        primary_owner_id: Uuid,
83        currency: &str,
84        opening_date: NaiveDate,
85    ) -> Self {
86        let features = match account_type {
87            BankAccountType::Checking => AccountFeatures::retail_standard(),
88            BankAccountType::BusinessOperating => AccountFeatures::business_standard(),
89            _ => AccountFeatures::default(),
90        };
91
92        Self {
93            account_id,
94            account_number,
95            account_type,
96            primary_owner_id,
97            joint_owner_ids: Vec::new(),
98            status: AccountStatus::Active,
99            currency: currency.to_string(),
100            opening_date,
101            closing_date: None,
102            current_balance: Decimal::ZERO,
103            available_balance: Decimal::ZERO,
104            features,
105            iban: None,
106            swift_bic: None,
107            routing_number: None,
108            branch_code: None,
109            interest_rate: None,
110            overdraft_limit: Decimal::ZERO,
111            last_activity: None,
112            days_dormant: 0,
113            is_nominee: false,
114            linked_cards: Vec::new(),
115            declared_purpose: None,
116            funding_source_account: None,
117            is_mule_account: false,
118            is_funnel_account: false,
119            case_id: None,
120            gl_account: None,
121        }
122    }
123
124    /// Check if account can process transactions.
125    pub fn can_transact(&self) -> bool {
126        self.status.allows_transactions()
127    }
128
129    /// Check if account has sufficient funds for debit.
130    pub fn has_sufficient_funds(&self, amount: Decimal) -> bool {
131        self.available_balance + self.overdraft_limit >= amount
132    }
133
134    /// Apply a debit (outgoing transaction).
135    pub fn apply_debit(&mut self, amount: Decimal, timestamp: DateTime<Utc>) -> bool {
136        if !self.has_sufficient_funds(amount) {
137            return false;
138        }
139        self.current_balance -= amount;
140        self.available_balance -= amount;
141        self.last_activity = Some(timestamp);
142        self.days_dormant = 0;
143        true
144    }
145
146    /// Apply a credit (incoming transaction).
147    pub fn apply_credit(&mut self, amount: Decimal, timestamp: DateTime<Utc>) {
148        self.current_balance += amount;
149        self.available_balance += amount;
150        self.last_activity = Some(timestamp);
151        self.days_dormant = 0;
152    }
153
154    /// Place a hold on funds.
155    pub fn place_hold(&mut self, amount: Decimal) {
156        self.available_balance -= amount;
157    }
158
159    /// Release a hold on funds.
160    pub fn release_hold(&mut self, amount: Decimal) {
161        self.available_balance += amount;
162    }
163
164    /// Close the account.
165    pub fn close(&mut self, close_date: NaiveDate) {
166        self.status = AccountStatus::Closed;
167        self.closing_date = Some(close_date);
168    }
169
170    /// Freeze the account.
171    pub fn freeze(&mut self) {
172        self.status = AccountStatus::Frozen;
173    }
174
175    /// Mark as dormant.
176    pub fn mark_dormant(&mut self, days: u32) {
177        self.days_dormant = days;
178        if days > 365 {
179            self.status = AccountStatus::Dormant;
180        }
181    }
182
183    /// Add a joint owner.
184    pub fn add_joint_owner(&mut self, owner_id: Uuid) {
185        if !self.joint_owner_ids.contains(&owner_id) {
186            self.joint_owner_ids.push(owner_id);
187        }
188    }
189
190    /// Get all owner IDs (primary + joint).
191    pub fn all_owner_ids(&self) -> Vec<Uuid> {
192        let mut owners = vec![self.primary_owner_id];
193        owners.extend(&self.joint_owner_ids);
194        owners
195    }
196
197    /// Calculate risk score for the account.
198    pub fn calculate_risk_score(&self) -> u8 {
199        let mut score = self.account_type.risk_weight() * 30.0;
200
201        // Status risk
202        score += self.status.risk_indicator() * 20.0;
203
204        // Feature risk
205        if self.features.international_transfers {
206            score += 10.0;
207        }
208        if self.features.wire_transfers {
209            score += 5.0;
210        }
211        if self.features.cash_deposits {
212            score += 5.0;
213        }
214
215        // Ground truth
216        if self.is_mule_account {
217            score += 50.0;
218        }
219        if self.is_funnel_account {
220            score += 40.0;
221        }
222
223        score.min(100.0) as u8
224    }
225}
226
227/// Account holder summary for reporting.
228#[derive(Debug, Clone, Serialize, Deserialize)]
229pub struct AccountHolder {
230    /// Customer ID
231    pub customer_id: Uuid,
232    /// Holder type
233    pub holder_type: AccountHolderType,
234    /// Ownership percentage (for joint accounts)
235    pub ownership_percent: Option<u8>,
236    /// Date added as holder
237    pub added_date: NaiveDate,
238}
239
240/// Type of account holder.
241#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
242#[serde(rename_all = "snake_case")]
243pub enum AccountHolderType {
244    /// Primary account owner
245    Primary,
246    /// Joint owner with full rights
247    JointOwner,
248    /// Authorized signer (no ownership)
249    AuthorizedSigner,
250    /// Beneficiary
251    Beneficiary,
252    /// Power of attorney
253    PowerOfAttorney,
254}
255
256#[cfg(test)]
257#[allow(clippy::unwrap_used)]
258mod tests {
259    use super::*;
260
261    #[test]
262    fn test_account_creation() {
263        let account = BankAccount::new(
264            Uuid::new_v4(),
265            "****1234".to_string(),
266            BankAccountType::Checking,
267            Uuid::new_v4(),
268            "USD",
269            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
270        );
271
272        assert!(account.can_transact());
273        assert_eq!(account.current_balance, Decimal::ZERO);
274    }
275
276    #[test]
277    fn test_account_transactions() {
278        let mut account = BankAccount::new(
279            Uuid::new_v4(),
280            "****1234".to_string(),
281            BankAccountType::Checking,
282            Uuid::new_v4(),
283            "USD",
284            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
285        );
286
287        let now = Utc::now();
288
289        // Credit
290        account.apply_credit(Decimal::from(1000), now);
291        assert_eq!(account.current_balance, Decimal::from(1000));
292
293        // Debit
294        assert!(account.apply_debit(Decimal::from(500), now));
295        assert_eq!(account.current_balance, Decimal::from(500));
296
297        // Insufficient funds
298        assert!(!account.apply_debit(Decimal::from(1000), now));
299    }
300
301    #[test]
302    fn test_account_freeze() {
303        let mut account = BankAccount::new(
304            Uuid::new_v4(),
305            "****1234".to_string(),
306            BankAccountType::Checking,
307            Uuid::new_v4(),
308            "USD",
309            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
310        );
311
312        account.freeze();
313        assert!(!account.can_transact());
314    }
315
316    #[test]
317    fn test_joint_owners() {
318        let mut account = BankAccount::new(
319            Uuid::new_v4(),
320            "****1234".to_string(),
321            BankAccountType::Checking,
322            Uuid::new_v4(),
323            "USD",
324            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
325        );
326
327        let joint_owner = Uuid::new_v4();
328        account.add_joint_owner(joint_owner);
329
330        let owners = account.all_owner_ids();
331        assert_eq!(owners.len(), 2);
332    }
333}