use chrono::NaiveDate;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BeneficialOwner {
pub ubo_id: Uuid,
pub name: String,
pub date_of_birth: Option<NaiveDate>,
pub country_of_residence: String,
pub citizenship_country: Option<String>,
#[serde(with = "datasynth_core::serde_decimal")]
pub ownership_percentage: Decimal,
pub control_type: ControlType,
pub is_direct: bool,
pub intermediary_entity: Option<IntermediaryEntity>,
pub is_pep: bool,
pub is_sanctioned: bool,
pub verification_status: VerificationStatus,
pub verification_date: Option<NaiveDate>,
pub source_of_wealth: Option<String>,
pub is_true_ubo: bool,
pub is_hidden: bool,
}
impl BeneficialOwner {
pub fn new(ubo_id: Uuid, name: &str, country: &str, ownership_percentage: Decimal) -> Self {
Self {
ubo_id,
name: name.to_string(),
date_of_birth: None,
country_of_residence: country.to_string(),
citizenship_country: Some(country.to_string()),
ownership_percentage,
control_type: ControlType::OwnershipInterest,
is_direct: true,
intermediary_entity: None,
is_pep: false,
is_sanctioned: false,
verification_status: VerificationStatus::Verified,
verification_date: None,
source_of_wealth: None,
is_true_ubo: true,
is_hidden: false,
}
}
pub fn with_intermediary(mut self, intermediary: IntermediaryEntity) -> Self {
self.is_direct = false;
self.intermediary_entity = Some(intermediary);
self
}
pub fn as_pep(mut self) -> Self {
self.is_pep = true;
self
}
pub fn as_nominee(mut self) -> Self {
self.is_true_ubo = false;
self
}
pub fn as_hidden(mut self) -> Self {
self.is_hidden = true;
self.is_true_ubo = false;
self
}
pub fn calculate_risk_score(&self) -> u8 {
let mut score = 0.0;
let ownership_f64: f64 = self.ownership_percentage.try_into().unwrap_or(0.0);
if ownership_f64 >= 25.0 {
score += 20.0;
} else if ownership_f64 >= 10.0 {
score += 10.0;
}
score += self.control_type.risk_weight() * 10.0;
if !self.is_direct {
score += 15.0;
}
if self.is_pep {
score += 25.0;
}
if self.is_sanctioned {
score += 50.0;
}
if self.verification_status != VerificationStatus::Verified {
score += 10.0;
}
if self.is_hidden {
score += 30.0;
}
score.min(100.0) as u8
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ControlType {
#[default]
OwnershipInterest,
VotingRights,
BoardControl,
ManagementControl,
ContractualControl,
FamilyRelationship,
TrustArrangement,
NomineeArrangement,
}
impl ControlType {
pub fn risk_weight(&self) -> f64 {
match self {
Self::OwnershipInterest => 1.0,
Self::VotingRights => 1.1,
Self::BoardControl | Self::ManagementControl => 1.2,
Self::ContractualControl => 1.4,
Self::FamilyRelationship => 1.3,
Self::TrustArrangement => 1.6,
Self::NomineeArrangement => 2.0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum VerificationStatus {
#[default]
Verified,
PartiallyVerified,
Pending,
UnableToVerify,
Expired,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IntermediaryEntity {
pub entity_id: Uuid,
pub name: String,
pub entity_type: IntermediaryType,
pub jurisdiction: String,
#[serde(with = "datasynth_core::serde_decimal")]
pub ownership_percentage: Decimal,
pub is_shell: bool,
pub registration_number: Option<String>,
}
impl IntermediaryEntity {
pub fn new(
entity_id: Uuid,
name: &str,
entity_type: IntermediaryType,
jurisdiction: &str,
ownership_percentage: Decimal,
) -> Self {
Self {
entity_id,
name: name.to_string(),
entity_type,
jurisdiction: jurisdiction.to_string(),
ownership_percentage,
is_shell: false,
registration_number: None,
}
}
pub fn as_shell(mut self) -> Self {
self.is_shell = true;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum IntermediaryType {
HoldingCompany,
SPV,
Trust,
Foundation,
LimitedPartnership,
LLC,
Other,
}
impl IntermediaryType {
pub fn risk_weight(&self) -> f64 {
match self {
Self::HoldingCompany => 1.2,
Self::SPV => 1.5,
Self::Trust => 1.6,
Self::Foundation => 1.5,
Self::LimitedPartnership => 1.3,
Self::LLC => 1.2,
Self::Other => 1.4,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OwnershipChain {
pub ultimate_owner: BeneficialOwner,
pub intermediaries: Vec<IntermediaryEntity>,
pub total_layers: u8,
#[serde(with = "datasynth_core::serde_decimal")]
pub effective_ownership: Decimal,
}
impl OwnershipChain {
pub fn new(owner: BeneficialOwner) -> Self {
let effective = owner.ownership_percentage;
Self {
ultimate_owner: owner,
intermediaries: Vec::new(),
total_layers: 1,
effective_ownership: effective,
}
}
pub fn add_intermediary(&mut self, intermediary: IntermediaryEntity) {
let intermediary_pct: f64 = intermediary
.ownership_percentage
.try_into()
.unwrap_or(100.0);
let current_effective: f64 = self.effective_ownership.try_into().unwrap_or(0.0);
self.effective_ownership =
Decimal::from_f64_retain(current_effective * intermediary_pct / 100.0)
.unwrap_or(Decimal::ZERO);
self.intermediaries.push(intermediary);
self.total_layers += 1;
}
pub fn complexity_score(&self) -> u8 {
let mut score = (self.total_layers as f64 - 1.0) * 10.0;
for intermediary in &self.intermediaries {
score += intermediary.entity_type.risk_weight() * 5.0;
if intermediary.is_shell {
score += 20.0;
}
}
if !self.ultimate_owner.is_true_ubo {
score += 30.0;
}
score.min(100.0) as u8
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_beneficial_owner() {
let owner = BeneficialOwner::new(Uuid::new_v4(), "John Doe", "US", Decimal::from(50));
assert!(owner.is_true_ubo);
assert!(owner.is_direct);
}
#[test]
fn test_ownership_chain() {
let owner = BeneficialOwner::new(Uuid::new_v4(), "John Doe", "US", Decimal::from(100));
let mut chain = OwnershipChain::new(owner);
let holding = IntermediaryEntity::new(
Uuid::new_v4(),
"Holding Co Ltd",
IntermediaryType::HoldingCompany,
"KY",
Decimal::from(80),
);
chain.add_intermediary(holding);
assert_eq!(chain.total_layers, 2);
assert!(chain.effective_ownership < Decimal::from(100));
}
#[test]
fn test_risk_scoring() {
let base_owner = BeneficialOwner::new(Uuid::new_v4(), "Jane Doe", "US", Decimal::from(30));
let base_score = base_owner.calculate_risk_score();
let pep_owner =
BeneficialOwner::new(Uuid::new_v4(), "Minister Smith", "US", Decimal::from(30))
.as_pep();
let pep_score = pep_owner.calculate_risk_score();
assert!(pep_score > base_score);
}
}