use super::{Decimal128, HybridTimestamp, SolvingMethod};
use rkyv::{Archive, Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Archive, Serialize, Deserialize)]
#[repr(C, align(64))]
pub struct TransactionFlow {
pub source_account_index: u16,
pub target_account_index: u16,
pub amount: Decimal128,
pub journal_entry_id: Uuid,
pub debit_line_index: u16,
pub credit_line_index: u16,
pub timestamp: HybridTimestamp,
pub confidence: f32,
pub method_used: SolvingMethod,
pub flags: FlowFlags,
pub _pad: [u8; 2],
pub _reserved: [u8; 8],
}
#[derive(Debug, Clone, Copy, Default, Archive, Serialize, Deserialize)]
#[repr(transparent)]
pub struct FlowFlags(pub u8);
impl FlowFlags {
pub const HAS_SHADOW_BOOKINGS: u8 = 1 << 0;
pub const USES_HIGHER_AGGREGATE: u8 = 1 << 1;
pub const FLAGGED_FOR_AUDIT: u8 = 1 << 2;
pub const IS_REVERSAL: u8 = 1 << 3;
pub const IS_CIRCULAR: u8 = 1 << 4;
pub const IS_ANOMALOUS: u8 = 1 << 5;
pub const IS_GAAP_VIOLATION: u8 = 1 << 6;
pub const IS_FRAUD_PATTERN: u8 = 1 << 7;
pub fn new() -> Self {
Self(0)
}
pub fn has(&self, flag: u8) -> bool {
self.0 & flag != 0
}
pub fn set(&mut self, flag: u8) {
self.0 |= flag;
}
pub fn clear(&mut self, flag: u8) {
self.0 &= !flag;
}
}
impl TransactionFlow {
pub fn new(
source: u16,
target: u16,
amount: Decimal128,
journal_entry_id: Uuid,
timestamp: HybridTimestamp,
) -> Self {
Self {
source_account_index: source,
target_account_index: target,
amount,
journal_entry_id,
debit_line_index: 0,
credit_line_index: 0,
timestamp,
confidence: 1.0,
method_used: SolvingMethod::MethodA,
flags: FlowFlags::new(),
_pad: [0; 2],
_reserved: [0; 8],
}
}
#[allow(clippy::too_many_arguments)]
pub fn with_provenance(
source: u16,
target: u16,
amount: Decimal128,
journal_entry_id: Uuid,
debit_line_index: u16,
credit_line_index: u16,
timestamp: HybridTimestamp,
method: SolvingMethod,
confidence: f32,
) -> Self {
Self {
source_account_index: source,
target_account_index: target,
amount,
journal_entry_id,
debit_line_index,
credit_line_index,
timestamp,
confidence,
method_used: method,
flags: FlowFlags::new(),
_pad: [0; 2],
_reserved: [0; 8],
}
}
pub fn is_self_loop(&self) -> bool {
self.source_account_index == self.target_account_index
}
pub fn is_high_confidence(&self) -> bool {
self.confidence >= 0.9
}
pub fn is_anomalous(&self) -> bool {
self.flags.has(FlowFlags::IS_ANOMALOUS)
|| self.flags.has(FlowFlags::IS_CIRCULAR)
|| self.flags.has(FlowFlags::IS_FRAUD_PATTERN)
|| self.flags.has(FlowFlags::IS_GAAP_VIOLATION)
}
}
#[derive(Debug, Clone, Default)]
pub struct AggregatedFlow {
pub source: u16,
pub target: u16,
pub total_amount: f64,
pub transaction_count: u32,
pub avg_confidence: f32,
pub first_timestamp: HybridTimestamp,
pub last_timestamp: HybridTimestamp,
pub method_counts: [u32; 5], pub flagged_count: u32,
}
impl AggregatedFlow {
pub fn new(source: u16, target: u16) -> Self {
Self {
source,
target,
..Default::default()
}
}
pub fn add(&mut self, flow: &TransactionFlow) {
self.total_amount += flow.amount.to_f64();
self.transaction_count += 1;
let n = self.transaction_count as f32;
self.avg_confidence = self.avg_confidence * (n - 1.0) / n + flow.confidence / n;
if self.transaction_count == 1 {
self.first_timestamp = flow.timestamp;
self.last_timestamp = flow.timestamp;
} else {
if flow.timestamp < self.first_timestamp {
self.first_timestamp = flow.timestamp;
}
if flow.timestamp > self.last_timestamp {
self.last_timestamp = flow.timestamp;
}
}
let method_idx = flow.method_used as usize;
if method_idx < 5 {
self.method_counts[method_idx] += 1;
}
if flow.is_anomalous() {
self.flagged_count += 1;
}
}
pub fn dominant_method(&self) -> SolvingMethod {
let max_idx = self
.method_counts
.iter()
.enumerate()
.max_by_key(|(_, &count)| count)
.map(|(idx, _)| idx)
.unwrap_or(0);
match max_idx {
0 => SolvingMethod::MethodA,
1 => SolvingMethod::MethodB,
2 => SolvingMethod::MethodC,
3 => SolvingMethod::MethodD,
4 => SolvingMethod::MethodE,
_ => SolvingMethod::MethodA,
}
}
pub fn risk_score(&self) -> f32 {
let flag_ratio = self.flagged_count as f32 / self.transaction_count.max(1) as f32;
let confidence_factor = 1.0 - self.avg_confidence;
0.6 * flag_ratio + 0.4 * confidence_factor
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FlowDirection {
Inflow,
Outflow,
Both,
}
#[derive(Debug, Clone, Copy)]
pub struct GraphEdge {
pub from: u16,
pub to: u16,
pub weight: f64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_transaction_flow_size() {
let size = std::mem::size_of::<TransactionFlow>();
assert!(
size >= 64,
"TransactionFlow should be at least 64 bytes, got {}",
size
);
assert!(
size.is_multiple_of(64),
"TransactionFlow should be 64-byte aligned, got {}",
size
);
}
#[test]
fn test_aggregated_flow() {
let mut agg = AggregatedFlow::new(0, 1);
let flow1 = TransactionFlow::new(
0,
1,
Decimal128::from_f64(100.0),
Uuid::new_v4(),
HybridTimestamp::now(),
);
let mut flow2 = TransactionFlow::new(
0,
1,
Decimal128::from_f64(200.0),
Uuid::new_v4(),
HybridTimestamp::now(),
);
flow2.method_used = SolvingMethod::MethodB;
agg.add(&flow1);
agg.add(&flow2);
assert_eq!(agg.transaction_count, 2);
assert!((agg.total_amount - 300.0).abs() < 0.01);
let dominant = agg.dominant_method();
assert!(dominant == SolvingMethod::MethodA || dominant == SolvingMethod::MethodB);
}
#[test]
fn test_flow_flags() {
let mut flow = TransactionFlow::new(
0,
1,
Decimal128::from_f64(100.0),
Uuid::new_v4(),
HybridTimestamp::now(),
);
assert!(!flow.is_anomalous());
flow.flags.set(FlowFlags::IS_CIRCULAR);
assert!(flow.is_anomalous());
}
}