ringkernel_txmon/factory/
patterns.rs1use crate::types::{CustomerRiskProfile, Transaction, TransactionType};
4use rand::prelude::*;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum TransactionPattern {
9 NormalRetail,
11 HighValueWire,
13 VelocityBurst,
15 Smurfing,
17 GeographicAnomaly,
19 RoundTrip,
21}
22
23impl TransactionPattern {
24 pub fn generate(
26 &self,
27 transaction_id: u64,
28 customer: &CustomerRiskProfile,
29 timestamp: u64,
30 rng: &mut impl Rng,
31 ) -> Transaction {
32 let mut tx = Transaction::new(
33 transaction_id,
34 customer.customer_id,
35 0, customer.country_code,
37 timestamp,
38 );
39
40 match self {
41 TransactionPattern::NormalRetail => {
42 tx.amount_cents = rng.gen_range(1000..50_000);
44 tx.tx_type = TransactionType::Card as u8;
45 tx.country_code = customer.country_code; }
47
48 TransactionPattern::HighValueWire => {
49 tx.amount_cents = rng.gen_range(1_500_000..10_000_000);
51 tx.tx_type = TransactionType::Wire as u8;
52 if rng.gen::<f32>() < 0.3 {
54 tx.country_code = rng.gen_range(1..16);
55 }
56 }
57
58 TransactionPattern::VelocityBurst => {
59 tx.amount_cents = rng.gen_range(5000..50_000);
61 tx.tx_type = TransactionType::ACH as u8;
62 tx.country_code = customer.country_code;
63 }
64
65 TransactionPattern::Smurfing => {
66 let threshold = 1_000_000u64; let percentage = rng.gen_range(90..99) as u64;
69 tx.amount_cents = (threshold * percentage) / 100;
70 tx.tx_type = TransactionType::Cash as u8;
71 tx.country_code = customer.country_code;
72 }
73
74 TransactionPattern::GeographicAnomaly => {
75 tx.amount_cents = rng.gen_range(100_000..2_000_000);
77 tx.tx_type = TransactionType::Wire as u8;
78 loop {
80 tx.country_code = rng.gen_range(1..16);
81 if tx.country_code != customer.country_code {
82 break;
83 }
84 }
85 if rng.gen::<f32>() < 0.5 {
87 tx.country_code = if rng.gen::<bool>() { 7 } else { 6 };
88 }
89 }
90
91 TransactionPattern::RoundTrip => {
92 tx.amount_cents = rng.gen_range(500_000..5_000_000);
94 tx.tx_type = TransactionType::Internal as u8;
95 tx.destination_id = customer.customer_id; tx.flags |= 0x01; }
98 }
99
100 tx
101 }
102
103 pub fn random_normal(rng: &mut impl Rng) -> Self {
106 let roll: f32 = rng.gen();
109 if roll < 0.95 {
110 TransactionPattern::NormalRetail
111 } else {
112 TransactionPattern::NormalRetail
114 }
115 }
116
117 pub fn random_suspicious(rng: &mut impl Rng) -> Self {
119 let roll: f32 = rng.gen();
120 if roll < 0.40 {
121 TransactionPattern::Smurfing
122 } else if roll < 0.70 {
123 TransactionPattern::GeographicAnomaly
124 } else if roll < 0.90 {
125 TransactionPattern::VelocityBurst
126 } else {
127 TransactionPattern::RoundTrip
128 }
129 }
130}
131
132impl std::fmt::Display for TransactionPattern {
133 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134 match self {
135 TransactionPattern::NormalRetail => write!(f, "Normal Retail"),
136 TransactionPattern::HighValueWire => write!(f, "High Value Wire"),
137 TransactionPattern::VelocityBurst => write!(f, "Velocity Burst"),
138 TransactionPattern::Smurfing => write!(f, "Smurfing"),
139 TransactionPattern::GeographicAnomaly => write!(f, "Geographic Anomaly"),
140 TransactionPattern::RoundTrip => write!(f, "Round Trip"),
141 }
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148 use crate::factory::AccountGenerator;
149
150 #[test]
151 fn test_smurfing_under_threshold() {
152 let mut rng = SmallRng::seed_from_u64(42);
153 let mut acct_gen = AccountGenerator::new(42);
154 let customer = acct_gen.generate_customer();
155
156 for _ in 0..100 {
157 let tx = TransactionPattern::Smurfing.generate(1, &customer, 0, &mut rng);
158 assert!(tx.amount_cents < 1_000_000);
160 assert!(tx.amount_cents >= 900_000);
161 }
162 }
163
164 #[test]
165 fn test_geographic_anomaly_different_country() {
166 let mut rng = SmallRng::seed_from_u64(42);
167 let mut acct_gen = AccountGenerator::new(42);
168 let customer = acct_gen.generate_customer();
169
170 for _ in 0..100 {
171 let tx = TransactionPattern::GeographicAnomaly.generate(1, &customer, 0, &mut rng);
172 assert_ne!(tx.country_code, customer.country_code);
173 }
174 }
175}