ringkernel_accnet/models/
flow.rs1use super::{Decimal128, HybridTimestamp, SolvingMethod};
7use rkyv::{Archive, Deserialize, Serialize};
8use uuid::Uuid;
9
10#[derive(Debug, Clone, Archive, Serialize, Deserialize)]
13#[repr(C, align(64))]
14pub struct TransactionFlow {
15 pub source_account_index: u16,
18 pub target_account_index: u16,
20
21 pub amount: Decimal128,
24
25 pub journal_entry_id: Uuid,
28 pub debit_line_index: u16,
30 pub credit_line_index: u16,
32
33 pub timestamp: HybridTimestamp,
36
37 pub confidence: f32,
40 pub method_used: SolvingMethod,
42 pub flags: FlowFlags,
44 pub _pad: [u8; 2],
46
47 pub _reserved: [u8; 8],
50}
51
52#[derive(Debug, Clone, Copy, Default, Archive, Serialize, Deserialize)]
54#[repr(transparent)]
55pub struct FlowFlags(pub u8);
56
57impl FlowFlags {
58 pub const HAS_SHADOW_BOOKINGS: u8 = 1 << 0;
60 pub const USES_HIGHER_AGGREGATE: u8 = 1 << 1;
62 pub const FLAGGED_FOR_AUDIT: u8 = 1 << 2;
64 pub const IS_REVERSAL: u8 = 1 << 3;
66 pub const IS_CIRCULAR: u8 = 1 << 4;
68 pub const IS_ANOMALOUS: u8 = 1 << 5;
70 pub const IS_GAAP_VIOLATION: u8 = 1 << 6;
72 pub const IS_FRAUD_PATTERN: u8 = 1 << 7;
74
75 pub fn new() -> Self {
77 Self(0)
78 }
79
80 pub fn has(&self, flag: u8) -> bool {
82 self.0 & flag != 0
83 }
84
85 pub fn set(&mut self, flag: u8) {
87 self.0 |= flag;
88 }
89
90 pub fn clear(&mut self, flag: u8) {
92 self.0 &= !flag;
93 }
94}
95
96impl TransactionFlow {
97 pub fn new(
99 source: u16,
100 target: u16,
101 amount: Decimal128,
102 journal_entry_id: Uuid,
103 timestamp: HybridTimestamp,
104 ) -> Self {
105 Self {
106 source_account_index: source,
107 target_account_index: target,
108 amount,
109 journal_entry_id,
110 debit_line_index: 0,
111 credit_line_index: 0,
112 timestamp,
113 confidence: 1.0,
114 method_used: SolvingMethod::MethodA,
115 flags: FlowFlags::new(),
116 _pad: [0; 2],
117 _reserved: [0; 8],
118 }
119 }
120
121 #[allow(clippy::too_many_arguments)]
123 pub fn with_provenance(
124 source: u16,
125 target: u16,
126 amount: Decimal128,
127 journal_entry_id: Uuid,
128 debit_line_index: u16,
129 credit_line_index: u16,
130 timestamp: HybridTimestamp,
131 method: SolvingMethod,
132 confidence: f32,
133 ) -> Self {
134 Self {
135 source_account_index: source,
136 target_account_index: target,
137 amount,
138 journal_entry_id,
139 debit_line_index,
140 credit_line_index,
141 timestamp,
142 confidence,
143 method_used: method,
144 flags: FlowFlags::new(),
145 _pad: [0; 2],
146 _reserved: [0; 8],
147 }
148 }
149
150 pub fn is_self_loop(&self) -> bool {
152 self.source_account_index == self.target_account_index
153 }
154
155 pub fn is_high_confidence(&self) -> bool {
157 self.confidence >= 0.9
158 }
159
160 pub fn is_anomalous(&self) -> bool {
162 self.flags.has(FlowFlags::IS_ANOMALOUS)
163 || self.flags.has(FlowFlags::IS_CIRCULAR)
164 || self.flags.has(FlowFlags::IS_FRAUD_PATTERN)
165 || self.flags.has(FlowFlags::IS_GAAP_VIOLATION)
166 }
167}
168
169#[derive(Debug, Clone, Default)]
172pub struct AggregatedFlow {
173 pub source: u16,
175 pub target: u16,
177 pub total_amount: f64,
179 pub transaction_count: u32,
181 pub avg_confidence: f32,
183 pub first_timestamp: HybridTimestamp,
185 pub last_timestamp: HybridTimestamp,
187 pub method_counts: [u32; 5], pub flagged_count: u32,
191}
192
193impl AggregatedFlow {
194 pub fn new(source: u16, target: u16) -> Self {
196 Self {
197 source,
198 target,
199 ..Default::default()
200 }
201 }
202
203 pub fn add(&mut self, flow: &TransactionFlow) {
205 self.total_amount += flow.amount.to_f64();
206 self.transaction_count += 1;
207
208 let n = self.transaction_count as f32;
210 self.avg_confidence = self.avg_confidence * (n - 1.0) / n + flow.confidence / n;
211
212 if self.transaction_count == 1 {
214 self.first_timestamp = flow.timestamp;
215 self.last_timestamp = flow.timestamp;
216 } else {
217 if flow.timestamp < self.first_timestamp {
218 self.first_timestamp = flow.timestamp;
219 }
220 if flow.timestamp > self.last_timestamp {
221 self.last_timestamp = flow.timestamp;
222 }
223 }
224
225 let method_idx = flow.method_used as usize;
227 if method_idx < 5 {
228 self.method_counts[method_idx] += 1;
229 }
230
231 if flow.is_anomalous() {
233 self.flagged_count += 1;
234 }
235 }
236
237 pub fn dominant_method(&self) -> SolvingMethod {
239 let max_idx = self
240 .method_counts
241 .iter()
242 .enumerate()
243 .max_by_key(|(_, &count)| count)
244 .map(|(idx, _)| idx)
245 .unwrap_or(0);
246
247 match max_idx {
248 0 => SolvingMethod::MethodA,
249 1 => SolvingMethod::MethodB,
250 2 => SolvingMethod::MethodC,
251 3 => SolvingMethod::MethodD,
252 4 => SolvingMethod::MethodE,
253 _ => SolvingMethod::MethodA,
254 }
255 }
256
257 pub fn risk_score(&self) -> f32 {
259 let flag_ratio = self.flagged_count as f32 / self.transaction_count.max(1) as f32;
260 let confidence_factor = 1.0 - self.avg_confidence;
261 0.6 * flag_ratio + 0.4 * confidence_factor
262 }
263}
264
265#[derive(Debug, Clone, Copy, PartialEq, Eq)]
267pub enum FlowDirection {
268 Inflow,
270 Outflow,
272 Both,
274}
275
276#[derive(Debug, Clone, Copy)]
278pub struct GraphEdge {
279 pub from: u16,
281 pub to: u16,
283 pub weight: f64,
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290
291 #[test]
292 fn test_transaction_flow_size() {
293 let size = std::mem::size_of::<TransactionFlow>();
294 assert!(
295 size >= 64,
296 "TransactionFlow should be at least 64 bytes, got {}",
297 size
298 );
299 assert!(
300 size.is_multiple_of(64),
301 "TransactionFlow should be 64-byte aligned, got {}",
302 size
303 );
304 }
305
306 #[test]
307 fn test_aggregated_flow() {
308 let mut agg = AggregatedFlow::new(0, 1);
309
310 let flow1 = TransactionFlow::new(
311 0,
312 1,
313 Decimal128::from_f64(100.0),
314 Uuid::new_v4(),
315 HybridTimestamp::now(),
316 );
317
318 let mut flow2 = TransactionFlow::new(
319 0,
320 1,
321 Decimal128::from_f64(200.0),
322 Uuid::new_v4(),
323 HybridTimestamp::now(),
324 );
325 flow2.method_used = SolvingMethod::MethodB;
326
327 agg.add(&flow1);
328 agg.add(&flow2);
329
330 assert_eq!(agg.transaction_count, 2);
331 assert!((agg.total_amount - 300.0).abs() < 0.01);
332 let dominant = agg.dominant_method();
334 assert!(dominant == SolvingMethod::MethodA || dominant == SolvingMethod::MethodB);
335 }
336
337 #[test]
338 fn test_flow_flags() {
339 let mut flow = TransactionFlow::new(
340 0,
341 1,
342 Decimal128::from_f64(100.0),
343 Uuid::new_v4(),
344 HybridTimestamp::now(),
345 );
346
347 assert!(!flow.is_anomalous());
348
349 flow.flags.set(FlowFlags::IS_CIRCULAR);
350 assert!(flow.is_anomalous());
351 }
352}