Skip to main content

ringkernel_txmon/factory/
generator.rs

1//! Transaction generator for synthetic workloads.
2
3use super::{AccountGenerator, TransactionPattern};
4use crate::types::{CustomerRiskLevel, CustomerRiskProfile, Transaction};
5use rand::prelude::*;
6use rand::rngs::SmallRng;
7use std::collections::HashMap;
8
9/// Transaction factory state.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
11pub enum FactoryState {
12    /// Factory is stopped.
13    #[default]
14    Stopped,
15    /// Factory is running and generating transactions.
16    Running,
17    /// Factory is paused.
18    Paused,
19}
20
21impl std::fmt::Display for FactoryState {
22    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23        match self {
24            FactoryState::Stopped => write!(f, "Stopped"),
25            FactoryState::Running => write!(f, "Running"),
26            FactoryState::Paused => write!(f, "Paused"),
27        }
28    }
29}
30
31/// Configuration for the transaction generator.
32#[derive(Debug, Clone)]
33pub struct GeneratorConfig {
34    /// Target transactions per second.
35    pub transactions_per_second: u32,
36    /// Number of simulated customer accounts.
37    pub customer_count: u32,
38    /// Percentage of suspicious transactions (0-100).
39    pub suspicious_rate: u8,
40    /// Batch size for transaction generation.
41    pub batch_size: u32,
42}
43
44impl Default for GeneratorConfig {
45    fn default() -> Self {
46        Self {
47            transactions_per_second: 100,
48            customer_count: 1000,
49            suspicious_rate: 5,
50            batch_size: 64,
51        }
52    }
53}
54
55/// Transaction generator that produces synthetic transaction workloads.
56pub struct TransactionGenerator {
57    config: GeneratorConfig,
58    rng: SmallRng,
59    customers: Vec<CustomerRiskProfile>,
60    customer_map: HashMap<u64, usize>,
61    next_tx_id: u64,
62}
63
64impl TransactionGenerator {
65    /// Create a new transaction generator with the given configuration.
66    pub fn new(config: GeneratorConfig) -> Self {
67        let mut acct_gen = AccountGenerator::new(42);
68        let customers = acct_gen.generate_customers(config.customer_count);
69
70        let customer_map: HashMap<u64, usize> = customers
71            .iter()
72            .enumerate()
73            .map(|(i, c)| (c.customer_id, i))
74            .collect();
75
76        Self {
77            config,
78            rng: SmallRng::seed_from_u64(12345),
79            customers,
80            customer_map,
81            next_tx_id: 1,
82        }
83    }
84
85    /// Get a reference to the configuration.
86    pub fn config(&self) -> &GeneratorConfig {
87        &self.config
88    }
89
90    /// Update the configuration.
91    pub fn set_config(&mut self, config: GeneratorConfig) {
92        self.config = config;
93    }
94
95    /// Get the number of customers.
96    pub fn customer_count(&self) -> usize {
97        self.customers.len()
98    }
99
100    /// Get a reference to all customers.
101    pub fn customers(&self) -> &[CustomerRiskProfile] {
102        &self.customers
103    }
104
105    /// Get a mutable reference to a customer by ID.
106    pub fn get_customer_mut(&mut self, customer_id: u64) -> Option<&mut CustomerRiskProfile> {
107        self.customer_map
108            .get(&customer_id)
109            .copied()
110            .and_then(|idx| self.customers.get_mut(idx))
111    }
112
113    /// Generate a batch of transactions and return with their customer profiles.
114    ///
115    /// Returns (transactions, corresponding profiles).
116    pub fn generate_batch(&mut self) -> (Vec<Transaction>, Vec<CustomerRiskProfile>) {
117        let batch_size = self.config.batch_size as usize;
118        let mut transactions = Vec::with_capacity(batch_size);
119        let mut profiles = Vec::with_capacity(batch_size);
120
121        let timestamp = std::time::SystemTime::now()
122            .duration_since(std::time::UNIX_EPOCH)
123            .unwrap()
124            .as_millis() as u64;
125
126        // Track which transactions are suspicious for velocity updates
127        let mut suspicious_tx_indices = Vec::new();
128
129        for i in 0..batch_size {
130            // Select a random customer
131            let customer_idx = self.rng.gen_range(0..self.customers.len());
132            let customer = &self.customers[customer_idx];
133
134            // Determine if this should be a suspicious transaction
135            let is_suspicious = self.config.suspicious_rate > 0
136                && self.rng.gen_range(0..100) < self.config.suspicious_rate;
137
138            // Choose pattern
139            let pattern = if is_suspicious {
140                TransactionPattern::random_suspicious(&mut self.rng)
141            } else {
142                TransactionPattern::random_normal(&mut self.rng)
143            };
144
145            // Generate transaction
146            let tx_id = self.next_tx_id;
147            self.next_tx_id += 1;
148
149            // Vary timestamp slightly within batch
150            let tx_timestamp = timestamp + i as u64;
151
152            let tx = pattern.generate(tx_id, customer, tx_timestamp, &mut self.rng);
153
154            transactions.push(tx);
155            profiles.push(*customer);
156
157            if is_suspicious {
158                suspicious_tx_indices.push(i);
159            }
160        }
161
162        // Only update velocity counts for suspicious transactions
163        // This prevents normal activity from triggering velocity alerts
164        for &idx in &suspicious_tx_indices {
165            let tx = &transactions[idx];
166            if let Some(customer) = self.get_customer_mut(tx.customer_id) {
167                customer.increment_velocity();
168            }
169        }
170
171        // Update transaction counts for all transactions (this doesn't affect alerts)
172        for tx in &transactions {
173            if let Some(customer) = self.get_customer_mut(tx.customer_id) {
174                customer.increment_transactions();
175                customer.last_transaction_ts = tx.timestamp;
176            }
177        }
178
179        (transactions, profiles)
180    }
181
182    /// Reset velocity counts for all customers (simulating new time window).
183    pub fn reset_velocity_window(&mut self) {
184        for customer in &mut self.customers {
185            customer.reset_velocity();
186        }
187    }
188
189    /// Get statistics about the customer base.
190    pub fn customer_stats(&self) -> CustomerStats {
191        let mut stats = CustomerStats {
192            total: self.customers.len(),
193            ..Default::default()
194        };
195
196        for c in &self.customers {
197            match c.risk_level() {
198                CustomerRiskLevel::Low => stats.low_risk += 1,
199                CustomerRiskLevel::Medium => stats.medium_risk += 1,
200                CustomerRiskLevel::High => stats.high_risk += 1,
201                CustomerRiskLevel::Prohibited => stats.prohibited += 1,
202            }
203            if c.is_pep() {
204                stats.pep += 1;
205            }
206            if c.requires_edd() {
207                stats.edd_required += 1;
208            }
209        }
210
211        stats
212    }
213}
214
215/// Statistics about the customer base.
216#[derive(Debug, Clone, Default)]
217pub struct CustomerStats {
218    /// Total number of customers.
219    pub total: usize,
220    /// Low risk customers.
221    pub low_risk: usize,
222    /// Medium risk customers.
223    pub medium_risk: usize,
224    /// High risk customers.
225    pub high_risk: usize,
226    /// Prohibited customers.
227    pub prohibited: usize,
228    /// Politically Exposed Persons.
229    pub pep: usize,
230    /// Customers requiring Enhanced Due Diligence.
231    pub edd_required: usize,
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237
238    #[test]
239    fn test_generate_batch() {
240        let config = GeneratorConfig {
241            batch_size: 100,
242            ..Default::default()
243        };
244        let mut gen = TransactionGenerator::new(config);
245
246        let (transactions, profiles) = gen.generate_batch();
247
248        assert_eq!(transactions.len(), 100);
249        assert_eq!(profiles.len(), 100);
250
251        // Check all transactions have valid customer IDs
252        for (tx, profile) in transactions.iter().zip(profiles.iter()) {
253            assert_eq!(tx.customer_id, profile.customer_id);
254        }
255    }
256
257    #[test]
258    fn test_suspicious_rate() {
259        let config = GeneratorConfig {
260            batch_size: 1000,
261            suspicious_rate: 10, // 10% suspicious
262            ..Default::default()
263        };
264        let mut gen = TransactionGenerator::new(config);
265
266        let (transactions, _) = gen.generate_batch();
267
268        // Count transactions with amounts near threshold (smurfing indicator)
269        let near_threshold = transactions
270            .iter()
271            .filter(|tx| tx.amount_cents >= 900_000 && tx.amount_cents < 1_000_000)
272            .count();
273
274        // Should have some suspicious transactions
275        assert!(near_threshold > 0);
276    }
277
278    #[test]
279    fn test_velocity_increment() {
280        let config = GeneratorConfig {
281            batch_size: 10,
282            customer_count: 1,
283            suspicious_rate: 100, // All transactions are suspicious
284            ..Default::default()
285        };
286        let mut gen = TransactionGenerator::new(config);
287
288        // Initial velocity should be 0
289        assert_eq!(gen.customers[0].velocity_count, 0);
290
291        // Generate batch (all suspicious, so all increment velocity)
292        let _ = gen.generate_batch();
293
294        // All transactions go to the same customer, all are suspicious
295        assert_eq!(gen.customers[0].velocity_count, 10);
296    }
297
298    #[test]
299    fn test_velocity_no_increment_normal() {
300        let config = GeneratorConfig {
301            batch_size: 10,
302            customer_count: 1,
303            suspicious_rate: 0, // No suspicious transactions
304            ..Default::default()
305        };
306        let mut gen = TransactionGenerator::new(config);
307
308        // Initial velocity should be 0
309        assert_eq!(gen.customers[0].velocity_count, 0);
310
311        // Generate batch (none suspicious, so no velocity increment)
312        let _ = gen.generate_batch();
313
314        // Velocity should still be 0 since no suspicious transactions
315        assert_eq!(gen.customers[0].velocity_count, 0);
316    }
317}