Skip to main content

ringkernel_txmon/types/
customer.rs

1//! Customer risk profile types.
2
3use bytemuck::Zeroable;
4
5/// Customer risk level classification.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
7#[repr(u8)]
8pub enum CustomerRiskLevel {
9    /// Low risk - standard monitoring sufficient.
10    #[default]
11    Low = 0,
12    /// Medium risk - enhanced monitoring recommended.
13    Medium = 1,
14    /// High risk - enhanced due diligence required.
15    High = 2,
16    /// Prohibited - customer cannot be onboarded.
17    Prohibited = 3,
18}
19
20impl CustomerRiskLevel {
21    /// Get display name.
22    pub fn name(&self) -> &'static str {
23        match self {
24            CustomerRiskLevel::Low => "Low",
25            CustomerRiskLevel::Medium => "Medium",
26            CustomerRiskLevel::High => "High",
27            CustomerRiskLevel::Prohibited => "Prohibited",
28        }
29    }
30
31    /// Convert from u8.
32    pub fn from_u8(v: u8) -> Option<Self> {
33        match v {
34            0 => Some(CustomerRiskLevel::Low),
35            1 => Some(CustomerRiskLevel::Medium),
36            2 => Some(CustomerRiskLevel::High),
37            3 => Some(CustomerRiskLevel::Prohibited),
38            _ => None,
39        }
40    }
41}
42
43impl std::fmt::Display for CustomerRiskLevel {
44    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        write!(f, "{}", self.name())
46    }
47}
48
49/// 128-byte GPU-aligned customer risk profile.
50///
51/// Contains KYC/AML risk information for a customer.
52#[derive(Debug, Clone, Copy)]
53#[repr(C, align(128))]
54pub struct CustomerRiskProfile {
55    /// Unique customer identifier.
56    pub customer_id: u64, // 8 bytes, offset 0
57    /// Overall risk level (cast to CustomerRiskLevel).
58    pub risk_level: u8, // 1 byte, offset 8
59    /// Numeric risk score (0-100).
60    pub risk_score: u8, // 1 byte, offset 9
61    /// Country code (ISO 3166-1 numeric).
62    pub country_code: u16, // 2 bytes, offset 10
63    /// Is Politically Exposed Person.
64    pub is_pep: u8, // 1 byte, offset 12
65    /// Requires Enhanced Due Diligence.
66    pub requires_edd: u8, // 1 byte, offset 13
67    /// Has adverse media mentions.
68    pub has_adverse_media: u8, // 1 byte, offset 14
69    /// Geographic risk component (0-100).
70    pub geographic_risk: u8, // 1 byte, offset 15
71    /// Business/industry risk component (0-100).
72    pub business_risk: u8, // 1 byte, offset 16
73    /// Behavioral risk component (0-100).
74    pub behavioral_risk: u8, // 1 byte, offset 17
75    /// Padding for alignment.
76    _padding1: [u8; 2], // 2 bytes, offset 18-19
77    /// Total transaction count (lifetime).
78    pub transaction_count: u32, // 4 bytes, offset 20
79    /// Total alert count (lifetime).
80    pub alert_count: u32, // 4 bytes, offset 24
81    /// Velocity window transaction count (current window).
82    pub velocity_count: u32, // 4 bytes, offset 28
83    /// Custom amount threshold for this customer (cents, 0 = use default).
84    pub amount_threshold: u64, // 8 bytes, offset 32
85    /// Custom velocity threshold (0 = use default).
86    pub velocity_threshold: u32, // 4 bytes, offset 40
87    /// Padding for alignment.
88    _padding2: u32, // 4 bytes, offset 44
89    /// Allowed destination countries (bitmask, bit N = country code N allowed).
90    pub allowed_destinations: u64, // 8 bytes, offset 48
91    /// Average monthly transaction volume (cents).
92    pub avg_monthly_volume: u64, // 8 bytes, offset 56
93    /// Last transaction timestamp (Unix epoch ms).
94    pub last_transaction_ts: u64, // 8 bytes, offset 64
95    /// Account creation timestamp (Unix epoch ms).
96    pub created_ts: u64, // 8 bytes, offset 72
97    /// Reserved for future use.
98    _reserved: [u8; 48], // 48 bytes, offset 80-127
99}
100
101// Manual implementations since derive(Pod) is strict about padding
102unsafe impl bytemuck::Zeroable for CustomerRiskProfile {}
103unsafe impl bytemuck::Pod for CustomerRiskProfile {}
104
105const _: () = assert!(std::mem::size_of::<CustomerRiskProfile>() == 128);
106
107impl CustomerRiskProfile {
108    /// Create a new customer profile with default settings.
109    pub fn new(customer_id: u64, country_code: u16) -> Self {
110        Self {
111            customer_id,
112            risk_level: CustomerRiskLevel::Low as u8,
113            risk_score: 10,
114            country_code,
115            is_pep: 0,
116            requires_edd: 0,
117            has_adverse_media: 0,
118            geographic_risk: 10,
119            business_risk: 10,
120            behavioral_risk: 10,
121            _padding1: [0; 2],
122            transaction_count: 0,
123            alert_count: 0,
124            velocity_count: 0,
125            amount_threshold: 0,
126            velocity_threshold: 0,
127            _padding2: 0,
128            allowed_destinations: !0, // All countries allowed by default
129            avg_monthly_volume: 0,
130            last_transaction_ts: 0,
131            created_ts: 0,
132            _reserved: [0; 48],
133        }
134    }
135
136    /// Get the risk level enum.
137    pub fn risk_level(&self) -> CustomerRiskLevel {
138        CustomerRiskLevel::from_u8(self.risk_level).unwrap_or_default()
139    }
140
141    /// Check if customer is PEP.
142    pub fn is_pep(&self) -> bool {
143        self.is_pep != 0
144    }
145
146    /// Check if customer requires EDD.
147    pub fn requires_edd(&self) -> bool {
148        self.requires_edd != 0
149    }
150
151    /// Check if customer has adverse media.
152    pub fn has_adverse_media(&self) -> bool {
153        self.has_adverse_media != 0
154    }
155
156    /// Check if customer is high risk.
157    pub fn is_high_risk(&self) -> bool {
158        self.risk_level() >= CustomerRiskLevel::High || self.risk_score >= 70
159    }
160
161    /// Check if a destination country is allowed.
162    pub fn is_destination_allowed(&self, country_code: u16) -> bool {
163        if country_code >= 64 {
164            // Countries beyond bitmask range are allowed by default
165            true
166        } else {
167            (self.allowed_destinations >> country_code) & 1 == 1
168        }
169    }
170
171    /// Increment velocity count.
172    pub fn increment_velocity(&mut self) {
173        self.velocity_count = self.velocity_count.saturating_add(1);
174    }
175
176    /// Reset velocity count (called at start of new window).
177    pub fn reset_velocity(&mut self) {
178        self.velocity_count = 0;
179    }
180
181    /// Increment transaction count.
182    pub fn increment_transactions(&mut self) {
183        self.transaction_count = self.transaction_count.saturating_add(1);
184    }
185
186    /// Increment alert count.
187    pub fn increment_alerts(&mut self) {
188        self.alert_count = self.alert_count.saturating_add(1);
189    }
190}
191
192impl Default for CustomerRiskProfile {
193    fn default() -> Self {
194        Self::zeroed()
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201
202    #[test]
203    fn test_profile_size() {
204        assert_eq!(std::mem::size_of::<CustomerRiskProfile>(), 128);
205    }
206
207    #[test]
208    fn test_destination_allowed() {
209        let mut profile = CustomerRiskProfile::new(1, 1);
210        profile.allowed_destinations = 0b1111; // Only countries 0-3 allowed
211
212        assert!(profile.is_destination_allowed(0));
213        assert!(profile.is_destination_allowed(1));
214        assert!(profile.is_destination_allowed(2));
215        assert!(profile.is_destination_allowed(3));
216        assert!(!profile.is_destination_allowed(4));
217        assert!(!profile.is_destination_allowed(10));
218        // Countries beyond 63 are always allowed
219        assert!(profile.is_destination_allowed(100));
220    }
221}