ringkernel_accnet/models/
account.rs

1//! Account node representation for the accounting network graph.
2//!
3//! Each account is a node in the directed graph, with edges representing
4//! monetary flows between accounts.
5
6use super::Decimal128;
7use rkyv::{Archive, Deserialize, Serialize};
8use uuid::Uuid;
9
10/// The five fundamental account types in double-entry bookkeeping.
11/// Each has a "normal balance" side (debit or credit).
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Archive, Serialize, Deserialize)]
13#[archive(compare(PartialEq))]
14#[repr(u8)]
15pub enum AccountType {
16    /// Assets: What the company owns (Cash, Inventory, Equipment)
17    /// Normal balance: Debit (increases with debits)
18    Asset = 0,
19
20    /// Liabilities: What the company owes (Accounts Payable, Loans)
21    /// Normal balance: Credit (increases with credits)
22    Liability = 1,
23
24    /// Equity: Owner's stake (Common Stock, Retained Earnings)
25    /// Normal balance: Credit
26    Equity = 2,
27
28    /// Revenue: Income from operations (Sales, Service Revenue)
29    /// Normal balance: Credit
30    Revenue = 3,
31
32    /// Expense: Costs of operations (Salaries, Rent, Utilities)
33    /// Normal balance: Debit
34    Expense = 4,
35
36    /// Contra accounts: Offset their parent account type
37    /// (Accumulated Depreciation, Sales Returns)
38    Contra = 5,
39}
40
41impl AccountType {
42    /// Returns the normal balance side for this account type.
43    pub fn normal_balance(&self) -> BalanceSide {
44        match self {
45            AccountType::Asset | AccountType::Expense => BalanceSide::Debit,
46            AccountType::Liability | AccountType::Equity | AccountType::Revenue => {
47                BalanceSide::Credit
48            }
49            AccountType::Contra => BalanceSide::Credit, // Usually contra-asset
50        }
51    }
52
53    /// Returns true if debits increase this account's balance.
54    pub fn debit_increases(&self) -> bool {
55        matches!(self, AccountType::Asset | AccountType::Expense)
56    }
57
58    /// Returns a color for visualization.
59    pub fn color(&self) -> [u8; 3] {
60        match self {
61            AccountType::Asset => [100, 149, 237],   // Cornflower blue
62            AccountType::Liability => [255, 99, 71], // Tomato red
63            AccountType::Equity => [50, 205, 50],    // Lime green
64            AccountType::Revenue => [255, 215, 0],   // Gold
65            AccountType::Expense => [255, 140, 0],   // Dark orange
66            AccountType::Contra => [148, 0, 211],    // Dark violet
67        }
68    }
69
70    /// Returns a display name.
71    pub fn display_name(&self) -> &'static str {
72        match self {
73            AccountType::Asset => "Asset",
74            AccountType::Liability => "Liability",
75            AccountType::Equity => "Equity",
76            AccountType::Revenue => "Revenue",
77            AccountType::Expense => "Expense",
78            AccountType::Contra => "Contra",
79        }
80    }
81
82    /// Returns an icon character for visualization.
83    pub fn icon(&self) -> char {
84        match self {
85            AccountType::Asset => '●',     // Solid circle
86            AccountType::Liability => '○', // Empty circle
87            AccountType::Equity => '▣',    // Square with fill
88            AccountType::Revenue => '◆',   // Diamond
89            AccountType::Expense => '◇',   // Empty diamond
90            AccountType::Contra => '◐',    // Half-filled circle
91        }
92    }
93}
94
95/// Which side of the accounting equation.
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Archive, Serialize, Deserialize)]
97#[archive(compare(PartialEq))]
98#[repr(u8)]
99pub enum BalanceSide {
100    /// Debit side (left side of T-account).
101    Debit = 0,
102    /// Credit side (right side of T-account).
103    Credit = 1,
104}
105
106/// Semantic flags for account behavior analysis.
107#[derive(Debug, Clone, Copy, Default, Archive, Serialize, Deserialize)]
108#[repr(C)]
109pub struct AccountSemantics {
110    /// Bit flags for semantic roles
111    pub flags: u32,
112    /// Typical transactions per month
113    pub typical_frequency: f32,
114    /// Log-scale average transaction size (0-100)
115    pub avg_amount_scale: f32,
116}
117
118impl AccountSemantics {
119    /// Flag: Cash or cash equivalent account.
120    pub const IS_CASH: u32 = 1 << 0;
121    /// Flag: Accounts receivable.
122    pub const IS_RECEIVABLE: u32 = 1 << 1;
123    /// Flag: Accounts payable.
124    pub const IS_PAYABLE: u32 = 1 << 2;
125    /// Flag: Revenue account.
126    pub const IS_REVENUE: u32 = 1 << 3;
127    /// Flag: Expense account.
128    pub const IS_EXPENSE: u32 = 1 << 4;
129    /// Flag: Inventory account.
130    pub const IS_INVENTORY: u32 = 1 << 5;
131    /// Flag: VAT/sales tax account.
132    pub const IS_VAT: u32 = 1 << 6;
133    /// Flag: Suspense/clearing account.
134    pub const IS_SUSPENSE: u32 = 1 << 7;
135    /// Flag: Intercompany account.
136    pub const IS_INTERCOMPANY: u32 = 1 << 8;
137    /// Flag: Depreciation account.
138    pub const IS_DEPRECIATION: u32 = 1 << 9;
139    /// Flag: Cost of goods sold account.
140    pub const IS_COGS: u32 = 1 << 10;
141    /// Flag: Payroll account.
142    pub const IS_PAYROLL: u32 = 1 << 11;
143
144    /// Check if this is a cash account.
145    pub fn is_cash(&self) -> bool {
146        self.flags & Self::IS_CASH != 0
147    }
148    /// Check if this is a suspense account.
149    pub fn is_suspense(&self) -> bool {
150        self.flags & Self::IS_SUSPENSE != 0
151    }
152    /// Check if this is a revenue account.
153    pub fn is_revenue(&self) -> bool {
154        self.flags & Self::IS_REVENUE != 0
155    }
156    /// Check if this is an expense account.
157    pub fn is_expense(&self) -> bool {
158        self.flags & Self::IS_EXPENSE != 0
159    }
160}
161
162/// A single account node in the accounting network.
163/// GPU-aligned to 128 bytes for efficient memory access.
164#[derive(Debug, Clone, Archive, Serialize, Deserialize)]
165#[repr(C, align(128))]
166pub struct AccountNode {
167    // === Identity (32 bytes) ===
168    /// Unique identifier
169    pub id: Uuid,
170    /// Account code hash (for fast lookup)
171    pub code_hash: u64,
172    /// Index in the network's account array (0-255 for GPU)
173    pub index: u16,
174    /// Account type classification
175    pub account_type: AccountType,
176    /// Account class ID (for hierarchy)
177    pub class_id: u8,
178    /// Account subclass ID
179    pub subclass_id: u8,
180    /// Padding for alignment
181    pub _pad1: [u8; 3],
182
183    // === Account metadata (variable, stored separately) ===
184    // code: String - stored in auxiliary structure
185    // name: String - stored in auxiliary structure
186
187    // === Balances (32 bytes) ===
188    /// Balance at period start
189    pub opening_balance: Decimal128,
190    /// Balance at period end
191    pub closing_balance: Decimal128,
192
193    // === Activity (32 bytes) ===
194    /// Total debit activity in period
195    pub total_debits: Decimal128,
196    /// Total credit activity in period
197    pub total_credits: Decimal128,
198
199    // === Graph metrics (16 bytes) ===
200    /// Number of incoming edges (accounts that flow TO this account)
201    pub in_degree: u16,
202    /// Number of outgoing edges (accounts that flow FROM this account)
203    pub out_degree: u16,
204    /// Betweenness centrality (0.0 - 1.0)
205    pub betweenness_centrality: f32,
206    /// PageRank score
207    pub pagerank: f32,
208    /// Clustering coefficient
209    pub clustering_coefficient: f32,
210
211    // === Analysis results (12 bytes) ===
212    /// Suspense account confidence (0.0 - 1.0)
213    pub suspense_score: f32,
214    /// Risk score from anomaly detection
215    pub risk_score: f32,
216    /// Transaction count in period
217    pub transaction_count: u32,
218
219    // === Flags (4 bytes) ===
220    /// Account property flags.
221    pub flags: AccountFlags,
222}
223
224/// Bit flags for account properties.
225#[derive(Debug, Clone, Copy, Default, Archive, Serialize, Deserialize)]
226#[repr(transparent)]
227pub struct AccountFlags(pub u32);
228
229impl AccountFlags {
230    /// Flag: Identified as a suspense account.
231    pub const IS_SUSPENSE_ACCOUNT: u32 = 1 << 0;
232    /// Flag: Cash or cash equivalent.
233    pub const IS_CASH_ACCOUNT: u32 = 1 << 1;
234    /// Flag: Revenue account.
235    pub const IS_REVENUE_ACCOUNT: u32 = 1 << 2;
236    /// Flag: Expense account.
237    pub const IS_EXPENSE_ACCOUNT: u32 = 1 << 3;
238    /// Flag: Intercompany account.
239    pub const IS_INTERCOMPANY: u32 = 1 << 4;
240    /// Flag: Flagged for audit review.
241    pub const FLAGGED_FOR_AUDIT: u32 = 1 << 5;
242    /// Flag: Has GAAP violation.
243    pub const HAS_GAAP_VIOLATION: u32 = 1 << 6;
244    /// Flag: Involved in fraud pattern.
245    pub const HAS_FRAUD_PATTERN: u32 = 1 << 7;
246    /// Flag: Dormant account (no recent activity).
247    pub const IS_DORMANT: u32 = 1 << 8;
248    /// Flag: Has detected anomaly.
249    pub const HAS_ANOMALY: u32 = 1 << 9;
250
251    /// Create a new empty flags instance.
252    pub fn new() -> Self {
253        Self(0)
254    }
255
256    /// Set a flag.
257    pub fn set(&mut self, flag: u32) {
258        self.0 |= flag;
259    }
260
261    /// Clear a flag.
262    pub fn clear(&mut self, flag: u32) {
263        self.0 &= !flag;
264    }
265
266    /// Check if a flag is set.
267    pub fn has(&self, flag: u32) -> bool {
268        self.0 & flag != 0
269    }
270}
271
272impl AccountNode {
273    /// Create a new account node with default values.
274    pub fn new(id: Uuid, account_type: AccountType, index: u16) -> Self {
275        Self {
276            id,
277            code_hash: 0,
278            index,
279            account_type,
280            class_id: 0,
281            subclass_id: 0,
282            _pad1: [0; 3],
283            opening_balance: Decimal128::ZERO,
284            closing_balance: Decimal128::ZERO,
285            total_debits: Decimal128::ZERO,
286            total_credits: Decimal128::ZERO,
287            in_degree: 0,
288            out_degree: 0,
289            betweenness_centrality: 0.0,
290            pagerank: 0.0,
291            clustering_coefficient: 0.0,
292            suspense_score: 0.0,
293            risk_score: 0.0,
294            transaction_count: 0,
295            flags: AccountFlags::new(),
296        }
297    }
298
299    /// Calculate the net change for this period.
300    pub fn net_change(&self) -> Decimal128 {
301        self.closing_balance - self.opening_balance
302    }
303
304    /// Calculate total activity (debits + credits).
305    pub fn total_activity(&self) -> Decimal128 {
306        Decimal128::from_f64(self.total_debits.to_f64().abs() + self.total_credits.to_f64().abs())
307    }
308
309    /// Calculate balance ratio (balance / activity) - low = suspense indicator.
310    pub fn balance_ratio(&self) -> f64 {
311        let activity = self.total_activity().to_f64();
312        if activity > 0.0 {
313            self.closing_balance.to_f64().abs() / activity
314        } else {
315            1.0 // No activity = not suspense
316        }
317    }
318
319    /// Check if this account has high centrality (hub in the network).
320    pub fn is_hub(&self) -> bool {
321        self.in_degree + self.out_degree > 10 || self.betweenness_centrality > 0.1
322    }
323}
324
325/// Auxiliary structure for account string data.
326#[derive(Debug, Clone)]
327pub struct AccountMetadata {
328    /// Account code (e.g., "1100")
329    pub code: String,
330    /// Account name (e.g., "Cash and Cash Equivalents")
331    pub name: String,
332    /// Description
333    pub description: String,
334    /// Parent account ID for hierarchy
335    pub parent_id: Option<Uuid>,
336    /// Semantic properties
337    pub semantics: AccountSemantics,
338}
339
340impl AccountMetadata {
341    /// Create new account metadata with code and name.
342    pub fn new(code: impl Into<String>, name: impl Into<String>) -> Self {
343        Self {
344            code: code.into(),
345            name: name.into(),
346            description: String::new(),
347            parent_id: None,
348            semantics: AccountSemantics::default(),
349        }
350    }
351}
352
353#[cfg(test)]
354mod tests {
355    use super::*;
356
357    #[test]
358    fn test_account_type_normal_balance() {
359        assert_eq!(AccountType::Asset.normal_balance(), BalanceSide::Debit);
360        assert_eq!(AccountType::Liability.normal_balance(), BalanceSide::Credit);
361        assert_eq!(AccountType::Revenue.normal_balance(), BalanceSide::Credit);
362        assert_eq!(AccountType::Expense.normal_balance(), BalanceSide::Debit);
363    }
364
365    #[test]
366    fn test_decimal128_arithmetic() {
367        let a = Decimal128::from_f64(100.50);
368        let b = Decimal128::from_f64(25.25);
369        let sum = a + b;
370        assert!((sum.to_f64() - 125.75).abs() < 0.01);
371    }
372
373    #[test]
374    fn test_account_node_size() {
375        // Ensure GPU alignment (may be larger with rkyv metadata)
376        let size = std::mem::size_of::<AccountNode>();
377        assert!(
378            size >= 128,
379            "AccountNode should be at least 128 bytes, got {}",
380            size
381        );
382        assert!(
383            size.is_multiple_of(128),
384            "AccountNode should be 128-byte aligned, got {}",
385            size
386        );
387    }
388}