1use super::{
11 AccountFlags, AccountMetadata, AccountNode, AccountType, AggregatedFlow, FlowDirection,
12 GraphEdge, HybridTimestamp, TransactionFlow,
13};
14use rkyv::{Archive, Deserialize, Serialize};
15use std::collections::HashMap;
16use uuid::Uuid;
17
18#[derive(Debug, Clone, Default)]
20pub struct NetworkStatistics {
21 pub node_count: usize,
23 pub edge_count: usize,
25 pub unique_edge_count: usize,
27 pub density: f64,
29 pub avg_degree: f64,
31 pub max_degree: u32,
33 pub component_count: usize,
35 pub total_flow_amount: f64,
37 pub avg_confidence: f64,
39 pub suspense_account_count: usize,
41 pub gaap_violation_count: usize,
43 pub fraud_pattern_count: usize,
45 pub last_updated: HybridTimestamp,
47}
48
49#[derive(Debug, Clone)]
51pub struct AccountingNetwork {
52 pub id: Uuid,
54 pub entity_id: Uuid,
56 pub fiscal_year: u16,
58 pub fiscal_period: u8,
60
61 pub accounts: Vec<AccountNode>,
63 pub account_metadata: HashMap<u16, AccountMetadata>,
65
66 pub flows: Vec<TransactionFlow>,
68 pub aggregated_flows: HashMap<(u16, u16), AggregatedFlow>,
70
71 pub adjacency_out: Vec<Vec<(u16, usize)>>,
73 pub adjacency_in: Vec<Vec<(u16, usize)>>,
75
76 pub statistics: NetworkStatistics,
78
79 pub period_start: HybridTimestamp,
81 pub period_end: HybridTimestamp,
83}
84
85impl AccountingNetwork {
86 pub fn new(entity_id: Uuid, fiscal_year: u16, fiscal_period: u8) -> Self {
88 Self {
89 id: Uuid::new_v4(),
90 entity_id,
91 fiscal_year,
92 fiscal_period,
93 accounts: Vec::new(),
94 account_metadata: HashMap::new(),
95 flows: Vec::new(),
96 aggregated_flows: HashMap::new(),
97 adjacency_out: Vec::new(),
98 adjacency_in: Vec::new(),
99 statistics: NetworkStatistics::default(),
100 period_start: HybridTimestamp::zero(),
101 period_end: HybridTimestamp::zero(),
102 }
103 }
104
105 pub fn add_account(&mut self, mut account: AccountNode, metadata: AccountMetadata) -> u16 {
107 let index = self.accounts.len() as u16;
108 account.index = index;
109 self.accounts.push(account);
110 self.account_metadata.insert(index, metadata);
111 self.adjacency_out.push(Vec::new());
112 self.adjacency_in.push(Vec::new());
113 self.statistics.node_count = self.accounts.len();
114 index
115 }
116
117 pub fn add_flow(&mut self, flow: TransactionFlow) {
119 let flow_index = self.flows.len();
120 let source = flow.source_account_index;
121 let target = flow.target_account_index;
122
123 if (source as usize) < self.adjacency_out.len() {
125 self.adjacency_out[source as usize].push((target, flow_index));
126 }
127 if (target as usize) < self.adjacency_in.len() {
128 self.adjacency_in[target as usize].push((source, flow_index));
129 }
130
131 let key = (source, target);
133 self.aggregated_flows
134 .entry(key)
135 .or_insert_with(|| AggregatedFlow::new(source, target))
136 .add(&flow);
137
138 let flow_amount = flow.amount;
140 if (source as usize) < self.accounts.len() {
141 let acc = &mut self.accounts[source as usize];
142 acc.out_degree = acc.out_degree.saturating_add(1);
143 acc.transaction_count = acc.transaction_count.saturating_add(1);
144 acc.total_debits = acc.total_debits + flow_amount;
146 acc.closing_balance = acc.closing_balance - flow_amount;
147 }
148 if (target as usize) < self.accounts.len() {
149 let acc = &mut self.accounts[target as usize];
150 acc.in_degree = acc.in_degree.saturating_add(1);
151 acc.transaction_count = acc.transaction_count.saturating_add(1);
152 acc.total_credits = acc.total_credits + flow_amount;
154 acc.closing_balance = acc.closing_balance + flow_amount;
155 }
156
157 if self.period_start.physical == 0 || flow.timestamp < self.period_start {
159 self.period_start = flow.timestamp;
160 }
161 if flow.timestamp > self.period_end {
162 self.period_end = flow.timestamp;
163 }
164
165 self.flows.push(flow);
166 self.statistics.edge_count = self.flows.len();
167 self.statistics.unique_edge_count = self.aggregated_flows.len();
168 }
169
170 pub fn incorporate_flows(&mut self, flows: &[TransactionFlow]) {
172 for flow in flows {
173 self.add_flow(flow.clone());
174 }
175 self.update_statistics();
176 }
177
178 pub fn update_statistics(&mut self) {
180 let n = self.accounts.len();
181 let e = self.aggregated_flows.len();
182
183 self.statistics.density = if n > 1 {
185 e as f64 / (n * (n - 1)) as f64
186 } else {
187 0.0
188 };
189
190 let total_degree: u32 = self
192 .accounts
193 .iter()
194 .map(|a| a.in_degree as u32 + a.out_degree as u32)
195 .sum();
196 self.statistics.avg_degree = if n > 0 {
197 total_degree as f64 / n as f64
198 } else {
199 0.0
200 };
201
202 self.statistics.max_degree = self
204 .accounts
205 .iter()
206 .map(|a| a.in_degree as u32 + a.out_degree as u32)
207 .max()
208 .unwrap_or(0);
209
210 let mut total_amount = 0.0;
212 let mut total_confidence = 0.0;
213 for flow in &self.flows {
214 total_amount += flow.amount.to_f64().abs();
215 total_confidence += flow.confidence as f64;
216 }
217 self.statistics.total_flow_amount = total_amount;
218 self.statistics.avg_confidence = if !self.flows.is_empty() {
219 total_confidence / self.flows.len() as f64
220 } else {
221 0.0
222 };
223
224 self.statistics.suspense_account_count = self
226 .accounts
227 .iter()
228 .filter(|a| a.flags.has(AccountFlags::IS_SUSPENSE_ACCOUNT))
229 .count();
230 self.statistics.gaap_violation_count = self
231 .accounts
232 .iter()
233 .filter(|a| a.flags.has(AccountFlags::HAS_GAAP_VIOLATION))
234 .count();
235 self.statistics.fraud_pattern_count = self
236 .accounts
237 .iter()
238 .filter(|a| a.flags.has(AccountFlags::HAS_FRAUD_PATTERN))
239 .count();
240
241 self.statistics.last_updated = HybridTimestamp::now();
242 }
243
244 pub fn neighbors(&self, account_index: u16, direction: FlowDirection) -> Vec<u16> {
246 let mut result = Vec::new();
247 let idx = account_index as usize;
248
249 match direction {
250 FlowDirection::Outflow => {
251 if idx < self.adjacency_out.len() {
252 for &(target, _) in &self.adjacency_out[idx] {
253 if !result.contains(&target) {
254 result.push(target);
255 }
256 }
257 }
258 }
259 FlowDirection::Inflow => {
260 if idx < self.adjacency_in.len() {
261 for &(source, _) in &self.adjacency_in[idx] {
262 if !result.contains(&source) {
263 result.push(source);
264 }
265 }
266 }
267 }
268 FlowDirection::Both => {
269 result.extend(self.neighbors(account_index, FlowDirection::Outflow));
270 for neighbor in self.neighbors(account_index, FlowDirection::Inflow) {
271 if !result.contains(&neighbor) {
272 result.push(neighbor);
273 }
274 }
275 }
276 }
277
278 result
279 }
280
281 pub fn edges(&self) -> Vec<GraphEdge> {
283 self.aggregated_flows
284 .iter()
285 .map(|(&(from, to), agg)| GraphEdge {
286 from,
287 to,
288 weight: agg.total_amount,
289 })
290 .collect()
291 }
292
293 pub fn get_account(&self, index: u16) -> Option<&AccountNode> {
295 self.accounts.get(index as usize)
296 }
297
298 pub fn get_metadata(&self, index: u16) -> Option<&AccountMetadata> {
300 self.account_metadata.get(&index)
301 }
302
303 pub fn find_by_code(&self, code: &str) -> Option<u16> {
305 for (idx, meta) in &self.account_metadata {
306 if meta.code == code {
307 return Some(*idx);
308 }
309 }
310 None
311 }
312
313 pub fn find_by_name(&self, name: &str) -> Vec<u16> {
315 let lower_name = name.to_lowercase();
316 self.account_metadata
317 .iter()
318 .filter(|(_, meta)| meta.name.to_lowercase().contains(&lower_name))
319 .map(|(&idx, _)| idx)
320 .collect()
321 }
322
323 pub fn accounts_by_type(&self, account_type: AccountType) -> Vec<u16> {
325 self.accounts
326 .iter()
327 .filter(|a| a.account_type == account_type)
328 .map(|a| a.index)
329 .collect()
330 }
331
332 pub fn calculate_pagerank(&mut self, damping: f64, iterations: usize) {
335 let n = self.accounts.len();
336 if n == 0 {
337 return;
338 }
339
340 let mut pagerank = vec![1.0 / n as f64; n];
341 let mut new_pagerank = vec![0.0; n];
342
343 for _ in 0..iterations {
344 new_pagerank.fill((1.0 - damping) / n as f64);
345
346 for (i, account) in self.accounts.iter().enumerate() {
347 let out_degree = account.out_degree as usize;
348 if out_degree > 0 {
349 let contribution = damping * pagerank[i] / out_degree as f64;
350 for &(target, _) in &self.adjacency_out[i] {
351 new_pagerank[target as usize] += contribution;
352 }
353 } else {
354 let contribution = damping * pagerank[i] / n as f64;
356 for pr in new_pagerank.iter_mut() {
357 *pr += contribution;
358 }
359 }
360 }
361
362 std::mem::swap(&mut pagerank, &mut new_pagerank);
363 }
364
365 for (i, account) in self.accounts.iter_mut().enumerate() {
367 account.pagerank = pagerank[i] as f32;
368 }
369 }
370
371 pub fn snapshot(&self) -> NetworkSnapshot {
373 NetworkSnapshot {
374 timestamp: HybridTimestamp::now(),
375 node_count: self.accounts.len(),
376 edge_count: self.flows.len(),
377 unique_edges: self.aggregated_flows.len(),
378 total_flow: self.statistics.total_flow_amount,
379 avg_confidence: self.statistics.avg_confidence,
380 suspense_count: self.statistics.suspense_account_count,
381 violation_count: self.statistics.gaap_violation_count,
382 fraud_count: self.statistics.fraud_pattern_count,
383 }
384 }
385
386 pub fn compute_pagerank(&self, iterations: usize, damping: f64) -> Vec<f64> {
389 let n = self.accounts.len();
390 if n == 0 {
391 return Vec::new();
392 }
393
394 let mut pagerank = vec![1.0 / n as f64; n];
395 let mut new_pagerank = vec![0.0; n];
396
397 for _ in 0..iterations {
398 new_pagerank.fill((1.0 - damping) / n as f64);
399
400 for (i, account) in self.accounts.iter().enumerate() {
401 let out_degree = account.out_degree as usize;
402 if out_degree > 0 {
403 let contribution = damping * pagerank[i] / out_degree as f64;
404 for &(target, _) in &self.adjacency_out[i] {
405 new_pagerank[target as usize] += contribution;
406 }
407 } else {
408 let contribution = damping * pagerank[i] / n as f64;
410 for pr in new_pagerank.iter_mut() {
411 *pr += contribution;
412 }
413 }
414 }
415
416 std::mem::swap(&mut pagerank, &mut new_pagerank);
417 }
418
419 pagerank
420 }
421}
422
423#[derive(Debug, Clone)]
425pub struct NetworkSnapshot {
426 pub timestamp: HybridTimestamp,
428 pub node_count: usize,
430 pub edge_count: usize,
432 pub unique_edges: usize,
434 pub total_flow: f64,
436 pub avg_confidence: f64,
438 pub suspense_count: usize,
440 pub violation_count: usize,
442 pub fraud_count: usize,
444}
445
446#[derive(Debug, Clone, Copy, Archive, Serialize, Deserialize)]
449#[repr(C, align(128))]
450pub struct GpuNetworkHeader {
451 pub network_id: u64,
453 pub entity_id: u64,
455 pub fiscal_year: u16,
457 pub fiscal_period: u8,
459 pub account_count: u8,
461 pub flow_count: u32,
463 pub _pad1: [u8; 4],
465 pub period_start: HybridTimestamp,
467 pub period_end: HybridTimestamp,
469 pub density: f32,
471 pub avg_degree: f32,
473 pub _reserved: [u8; 72],
475}
476
477impl From<&AccountingNetwork> for GpuNetworkHeader {
478 fn from(network: &AccountingNetwork) -> Self {
479 Self {
480 network_id: network.id.as_u128() as u64,
481 entity_id: network.entity_id.as_u128() as u64,
482 fiscal_year: network.fiscal_year,
483 fiscal_period: network.fiscal_period,
484 account_count: network.accounts.len().min(256) as u8,
485 flow_count: network.flows.len() as u32,
486 _pad1: [0; 4],
487 period_start: network.period_start,
488 period_end: network.period_end,
489 density: network.statistics.density as f32,
490 avg_degree: network.statistics.avg_degree as f32,
491 _reserved: [0; 72],
492 }
493 }
494}
495
496#[cfg(test)]
497mod tests {
498 use super::*;
499 use crate::models::Decimal128;
500
501 fn create_test_network() -> AccountingNetwork {
502 let mut network = AccountingNetwork::new(Uuid::new_v4(), 2024, 1);
503
504 let cash = AccountNode::new(Uuid::new_v4(), AccountType::Asset, 0);
506 let ar = AccountNode::new(Uuid::new_v4(), AccountType::Asset, 1);
507 let revenue = AccountNode::new(Uuid::new_v4(), AccountType::Revenue, 2);
508
509 network.add_account(cash, AccountMetadata::new("1100", "Cash"));
510 network.add_account(ar, AccountMetadata::new("1200", "Accounts Receivable"));
511 network.add_account(revenue, AccountMetadata::new("4000", "Sales Revenue"));
512
513 let flow1 = TransactionFlow::new(
515 2, 1, Decimal128::from_f64(1000.0),
518 Uuid::new_v4(),
519 HybridTimestamp::now(),
520 );
521 let flow2 = TransactionFlow::new(
522 1, 0, Decimal128::from_f64(1000.0),
525 Uuid::new_v4(),
526 HybridTimestamp::now(),
527 );
528
529 network.add_flow(flow1);
530 network.add_flow(flow2);
531
532 network
533 }
534
535 #[test]
536 fn test_network_creation() {
537 let network = create_test_network();
538 assert_eq!(network.accounts.len(), 3);
539 assert_eq!(network.flows.len(), 2);
540 assert_eq!(network.aggregated_flows.len(), 2);
541 }
542
543 #[test]
544 fn test_neighbors() {
545 let network = create_test_network();
546
547 let in_neighbors = network.neighbors(1, FlowDirection::Inflow);
551 let out_neighbors = network.neighbors(1, FlowDirection::Outflow);
552
553 assert!(in_neighbors.contains(&2));
554 assert!(out_neighbors.contains(&0));
555 }
556
557 #[test]
558 fn test_pagerank() {
559 let mut network = create_test_network();
560 network.calculate_pagerank(0.85, 20);
561
562 let cash_pr = network.accounts[0].pagerank;
564 let revenue_pr = network.accounts[2].pagerank;
565 assert!(cash_pr > revenue_pr);
566 }
567
568 #[test]
569 fn test_gpu_header_size() {
570 let size = std::mem::size_of::<GpuNetworkHeader>();
571 assert!(
572 size >= 128,
573 "GpuNetworkHeader should be at least 128 bytes, got {}",
574 size
575 );
576 assert!(
577 size.is_multiple_of(128),
578 "GpuNetworkHeader should be 128-byte aligned, got {}",
579 size
580 );
581 }
582}