use chrono::{DateTime, NaiveDate, Utc};
use datasynth_core::models::banking::{AccountFeatures, AccountStatus, BankAccountType};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BankAccount {
pub account_id: Uuid,
pub account_number: String,
pub account_type: BankAccountType,
pub primary_owner_id: Uuid,
pub joint_owner_ids: Vec<Uuid>,
pub status: AccountStatus,
pub currency: String,
pub opening_date: NaiveDate,
pub closing_date: Option<NaiveDate>,
#[serde(with = "rust_decimal::serde::str")]
pub current_balance: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub available_balance: Decimal,
pub features: AccountFeatures,
pub iban: Option<String>,
pub swift_bic: Option<String>,
pub routing_number: Option<String>,
pub branch_code: Option<String>,
pub interest_rate: Option<Decimal>,
#[serde(with = "rust_decimal::serde::str")]
pub overdraft_limit: Decimal,
pub last_activity: Option<DateTime<Utc>>,
pub days_dormant: u32,
pub is_nominee: bool,
pub linked_cards: Vec<String>,
pub declared_purpose: Option<String>,
pub funding_source_account: Option<Uuid>,
pub gl_account: Option<String>,
pub is_mule_account: bool,
pub is_funnel_account: bool,
pub case_id: Option<String>,
}
impl BankAccount {
pub fn new(
account_id: Uuid,
account_number: String,
account_type: BankAccountType,
primary_owner_id: Uuid,
currency: &str,
opening_date: NaiveDate,
) -> Self {
let features = match account_type {
BankAccountType::Checking => AccountFeatures::retail_standard(),
BankAccountType::BusinessOperating => AccountFeatures::business_standard(),
_ => AccountFeatures::default(),
};
Self {
account_id,
account_number,
account_type,
primary_owner_id,
joint_owner_ids: Vec::new(),
status: AccountStatus::Active,
currency: currency.to_string(),
opening_date,
closing_date: None,
current_balance: Decimal::ZERO,
available_balance: Decimal::ZERO,
features,
iban: None,
swift_bic: None,
routing_number: None,
branch_code: None,
interest_rate: None,
overdraft_limit: Decimal::ZERO,
last_activity: None,
days_dormant: 0,
is_nominee: false,
linked_cards: Vec::new(),
declared_purpose: None,
funding_source_account: None,
is_mule_account: false,
is_funnel_account: false,
case_id: None,
gl_account: None,
}
}
pub fn can_transact(&self) -> bool {
self.status.allows_transactions()
}
pub fn has_sufficient_funds(&self, amount: Decimal) -> bool {
self.available_balance + self.overdraft_limit >= amount
}
pub fn apply_debit(&mut self, amount: Decimal, timestamp: DateTime<Utc>) -> bool {
if !self.has_sufficient_funds(amount) {
return false;
}
self.current_balance -= amount;
self.available_balance -= amount;
self.last_activity = Some(timestamp);
self.days_dormant = 0;
true
}
pub fn apply_credit(&mut self, amount: Decimal, timestamp: DateTime<Utc>) {
self.current_balance += amount;
self.available_balance += amount;
self.last_activity = Some(timestamp);
self.days_dormant = 0;
}
pub fn place_hold(&mut self, amount: Decimal) {
self.available_balance -= amount;
}
pub fn release_hold(&mut self, amount: Decimal) {
self.available_balance += amount;
}
pub fn close(&mut self, close_date: NaiveDate) {
self.status = AccountStatus::Closed;
self.closing_date = Some(close_date);
}
pub fn freeze(&mut self) {
self.status = AccountStatus::Frozen;
}
pub fn mark_dormant(&mut self, days: u32) {
self.days_dormant = days;
if days > 365 {
self.status = AccountStatus::Dormant;
}
}
pub fn add_joint_owner(&mut self, owner_id: Uuid) {
if !self.joint_owner_ids.contains(&owner_id) {
self.joint_owner_ids.push(owner_id);
}
}
pub fn all_owner_ids(&self) -> Vec<Uuid> {
let mut owners = vec![self.primary_owner_id];
owners.extend(&self.joint_owner_ids);
owners
}
pub fn calculate_risk_score(&self) -> u8 {
let mut score = self.account_type.risk_weight() * 30.0;
score += self.status.risk_indicator() * 20.0;
if self.features.international_transfers {
score += 10.0;
}
if self.features.wire_transfers {
score += 5.0;
}
if self.features.cash_deposits {
score += 5.0;
}
if self.is_mule_account {
score += 50.0;
}
if self.is_funnel_account {
score += 40.0;
}
score.min(100.0) as u8
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AccountHolder {
pub customer_id: Uuid,
pub holder_type: AccountHolderType,
pub ownership_percent: Option<u8>,
pub added_date: NaiveDate,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AccountHolderType {
Primary,
JointOwner,
AuthorizedSigner,
Beneficiary,
PowerOfAttorney,
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_account_creation() {
let account = BankAccount::new(
Uuid::new_v4(),
"****1234".to_string(),
BankAccountType::Checking,
Uuid::new_v4(),
"USD",
NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
);
assert!(account.can_transact());
assert_eq!(account.current_balance, Decimal::ZERO);
}
#[test]
fn test_account_transactions() {
let mut account = BankAccount::new(
Uuid::new_v4(),
"****1234".to_string(),
BankAccountType::Checking,
Uuid::new_v4(),
"USD",
NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
);
let now = Utc::now();
account.apply_credit(Decimal::from(1000), now);
assert_eq!(account.current_balance, Decimal::from(1000));
assert!(account.apply_debit(Decimal::from(500), now));
assert_eq!(account.current_balance, Decimal::from(500));
assert!(!account.apply_debit(Decimal::from(1000), now));
}
#[test]
fn test_account_freeze() {
let mut account = BankAccount::new(
Uuid::new_v4(),
"****1234".to_string(),
BankAccountType::Checking,
Uuid::new_v4(),
"USD",
NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
);
account.freeze();
assert!(!account.can_transact());
}
#[test]
fn test_joint_owners() {
let mut account = BankAccount::new(
Uuid::new_v4(),
"****1234".to_string(),
BankAccountType::Checking,
Uuid::new_v4(),
"USD",
NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
);
let joint_owner = Uuid::new_v4();
account.add_joint_owner(joint_owner);
let owners = account.all_owner_ids();
assert_eq!(owners.len(), 2);
}
}