ringkernel_accnet/models/
account.rs1use super::Decimal128;
7use rkyv::{Archive, Deserialize, Serialize};
8use uuid::Uuid;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Archive, Serialize, Deserialize)]
13#[archive(compare(PartialEq))]
14#[repr(u8)]
15pub enum AccountType {
16 Asset = 0,
19
20 Liability = 1,
23
24 Equity = 2,
27
28 Revenue = 3,
31
32 Expense = 4,
35
36 Contra = 5,
39}
40
41impl AccountType {
42 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, }
51 }
52
53 pub fn debit_increases(&self) -> bool {
55 matches!(self, AccountType::Asset | AccountType::Expense)
56 }
57
58 pub fn color(&self) -> [u8; 3] {
60 match self {
61 AccountType::Asset => [100, 149, 237], AccountType::Liability => [255, 99, 71], AccountType::Equity => [50, 205, 50], AccountType::Revenue => [255, 215, 0], AccountType::Expense => [255, 140, 0], AccountType::Contra => [148, 0, 211], }
68 }
69
70 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 pub fn icon(&self) -> char {
84 match self {
85 AccountType::Asset => '●', AccountType::Liability => '○', AccountType::Equity => '▣', AccountType::Revenue => '◆', AccountType::Expense => '◇', AccountType::Contra => '◐', }
92 }
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Archive, Serialize, Deserialize)]
97#[archive(compare(PartialEq))]
98#[repr(u8)]
99pub enum BalanceSide {
100 Debit = 0,
102 Credit = 1,
104}
105
106#[derive(Debug, Clone, Copy, Default, Archive, Serialize, Deserialize)]
108#[repr(C)]
109pub struct AccountSemantics {
110 pub flags: u32,
112 pub typical_frequency: f32,
114 pub avg_amount_scale: f32,
116}
117
118impl AccountSemantics {
119 pub const IS_CASH: u32 = 1 << 0;
121 pub const IS_RECEIVABLE: u32 = 1 << 1;
123 pub const IS_PAYABLE: u32 = 1 << 2;
125 pub const IS_REVENUE: u32 = 1 << 3;
127 pub const IS_EXPENSE: u32 = 1 << 4;
129 pub const IS_INVENTORY: u32 = 1 << 5;
131 pub const IS_VAT: u32 = 1 << 6;
133 pub const IS_SUSPENSE: u32 = 1 << 7;
135 pub const IS_INTERCOMPANY: u32 = 1 << 8;
137 pub const IS_DEPRECIATION: u32 = 1 << 9;
139 pub const IS_COGS: u32 = 1 << 10;
141 pub const IS_PAYROLL: u32 = 1 << 11;
143
144 pub fn is_cash(&self) -> bool {
146 self.flags & Self::IS_CASH != 0
147 }
148 pub fn is_suspense(&self) -> bool {
150 self.flags & Self::IS_SUSPENSE != 0
151 }
152 pub fn is_revenue(&self) -> bool {
154 self.flags & Self::IS_REVENUE != 0
155 }
156 pub fn is_expense(&self) -> bool {
158 self.flags & Self::IS_EXPENSE != 0
159 }
160}
161
162#[derive(Debug, Clone, Archive, Serialize, Deserialize)]
165#[repr(C, align(128))]
166pub struct AccountNode {
167 pub id: Uuid,
170 pub code_hash: u64,
172 pub index: u16,
174 pub account_type: AccountType,
176 pub class_id: u8,
178 pub subclass_id: u8,
180 pub _pad1: [u8; 3],
182
183 pub opening_balance: Decimal128,
190 pub closing_balance: Decimal128,
192
193 pub total_debits: Decimal128,
196 pub total_credits: Decimal128,
198
199 pub in_degree: u16,
202 pub out_degree: u16,
204 pub betweenness_centrality: f32,
206 pub pagerank: f32,
208 pub clustering_coefficient: f32,
210
211 pub suspense_score: f32,
214 pub risk_score: f32,
216 pub transaction_count: u32,
218
219 pub flags: AccountFlags,
222}
223
224#[derive(Debug, Clone, Copy, Default, Archive, Serialize, Deserialize)]
226#[repr(transparent)]
227pub struct AccountFlags(pub u32);
228
229impl AccountFlags {
230 pub const IS_SUSPENSE_ACCOUNT: u32 = 1 << 0;
232 pub const IS_CASH_ACCOUNT: u32 = 1 << 1;
234 pub const IS_REVENUE_ACCOUNT: u32 = 1 << 2;
236 pub const IS_EXPENSE_ACCOUNT: u32 = 1 << 3;
238 pub const IS_INTERCOMPANY: u32 = 1 << 4;
240 pub const FLAGGED_FOR_AUDIT: u32 = 1 << 5;
242 pub const HAS_GAAP_VIOLATION: u32 = 1 << 6;
244 pub const HAS_FRAUD_PATTERN: u32 = 1 << 7;
246 pub const IS_DORMANT: u32 = 1 << 8;
248 pub const HAS_ANOMALY: u32 = 1 << 9;
250
251 pub fn new() -> Self {
253 Self(0)
254 }
255
256 pub fn set(&mut self, flag: u32) {
258 self.0 |= flag;
259 }
260
261 pub fn clear(&mut self, flag: u32) {
263 self.0 &= !flag;
264 }
265
266 pub fn has(&self, flag: u32) -> bool {
268 self.0 & flag != 0
269 }
270}
271
272impl AccountNode {
273 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 pub fn net_change(&self) -> Decimal128 {
301 self.closing_balance - self.opening_balance
302 }
303
304 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 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 }
317 }
318
319 pub fn is_hub(&self) -> bool {
321 self.in_degree + self.out_degree > 10 || self.betweenness_centrality > 0.1
322 }
323}
324
325#[derive(Debug, Clone)]
327pub struct AccountMetadata {
328 pub code: String,
330 pub name: String,
332 pub description: String,
334 pub parent_id: Option<Uuid>,
336 pub semantics: AccountSemantics,
338}
339
340impl AccountMetadata {
341 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 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}