use super::Decimal128;
use rkyv::{Archive, Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Archive, Serialize, Deserialize)]
#[archive(compare(PartialEq))]
#[repr(u8)]
pub enum AccountType {
Asset = 0,
Liability = 1,
Equity = 2,
Revenue = 3,
Expense = 4,
Contra = 5,
}
impl AccountType {
pub fn normal_balance(&self) -> BalanceSide {
match self {
AccountType::Asset | AccountType::Expense => BalanceSide::Debit,
AccountType::Liability | AccountType::Equity | AccountType::Revenue => {
BalanceSide::Credit
}
AccountType::Contra => BalanceSide::Credit, }
}
pub fn debit_increases(&self) -> bool {
matches!(self, AccountType::Asset | AccountType::Expense)
}
pub fn color(&self) -> [u8; 3] {
match self {
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], }
}
pub fn display_name(&self) -> &'static str {
match self {
AccountType::Asset => "Asset",
AccountType::Liability => "Liability",
AccountType::Equity => "Equity",
AccountType::Revenue => "Revenue",
AccountType::Expense => "Expense",
AccountType::Contra => "Contra",
}
}
pub fn icon(&self) -> char {
match self {
AccountType::Asset => '●', AccountType::Liability => '○', AccountType::Equity => '▣', AccountType::Revenue => '◆', AccountType::Expense => '◇', AccountType::Contra => '◐', }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Archive, Serialize, Deserialize)]
#[archive(compare(PartialEq))]
#[repr(u8)]
pub enum BalanceSide {
Debit = 0,
Credit = 1,
}
#[derive(Debug, Clone, Copy, Default, Archive, Serialize, Deserialize)]
#[repr(C)]
pub struct AccountSemantics {
pub flags: u32,
pub typical_frequency: f32,
pub avg_amount_scale: f32,
}
impl AccountSemantics {
pub const IS_CASH: u32 = 1 << 0;
pub const IS_RECEIVABLE: u32 = 1 << 1;
pub const IS_PAYABLE: u32 = 1 << 2;
pub const IS_REVENUE: u32 = 1 << 3;
pub const IS_EXPENSE: u32 = 1 << 4;
pub const IS_INVENTORY: u32 = 1 << 5;
pub const IS_VAT: u32 = 1 << 6;
pub const IS_SUSPENSE: u32 = 1 << 7;
pub const IS_INTERCOMPANY: u32 = 1 << 8;
pub const IS_DEPRECIATION: u32 = 1 << 9;
pub const IS_COGS: u32 = 1 << 10;
pub const IS_PAYROLL: u32 = 1 << 11;
pub fn is_cash(&self) -> bool {
self.flags & Self::IS_CASH != 0
}
pub fn is_suspense(&self) -> bool {
self.flags & Self::IS_SUSPENSE != 0
}
pub fn is_revenue(&self) -> bool {
self.flags & Self::IS_REVENUE != 0
}
pub fn is_expense(&self) -> bool {
self.flags & Self::IS_EXPENSE != 0
}
}
#[derive(Debug, Clone, Archive, Serialize, Deserialize)]
#[repr(C, align(128))]
pub struct AccountNode {
pub id: Uuid,
pub code_hash: u64,
pub index: u16,
pub account_type: AccountType,
pub class_id: u8,
pub subclass_id: u8,
pub _pad1: [u8; 3],
pub opening_balance: Decimal128,
pub closing_balance: Decimal128,
pub total_debits: Decimal128,
pub total_credits: Decimal128,
pub in_degree: u16,
pub out_degree: u16,
pub betweenness_centrality: f32,
pub pagerank: f32,
pub clustering_coefficient: f32,
pub suspense_score: f32,
pub risk_score: f32,
pub transaction_count: u32,
pub flags: AccountFlags,
}
#[derive(Debug, Clone, Copy, Default, Archive, Serialize, Deserialize)]
#[repr(transparent)]
pub struct AccountFlags(pub u32);
impl AccountFlags {
pub const IS_SUSPENSE_ACCOUNT: u32 = 1 << 0;
pub const IS_CASH_ACCOUNT: u32 = 1 << 1;
pub const IS_REVENUE_ACCOUNT: u32 = 1 << 2;
pub const IS_EXPENSE_ACCOUNT: u32 = 1 << 3;
pub const IS_INTERCOMPANY: u32 = 1 << 4;
pub const FLAGGED_FOR_AUDIT: u32 = 1 << 5;
pub const HAS_GAAP_VIOLATION: u32 = 1 << 6;
pub const HAS_FRAUD_PATTERN: u32 = 1 << 7;
pub const IS_DORMANT: u32 = 1 << 8;
pub const HAS_ANOMALY: u32 = 1 << 9;
pub fn new() -> Self {
Self(0)
}
pub fn set(&mut self, flag: u32) {
self.0 |= flag;
}
pub fn clear(&mut self, flag: u32) {
self.0 &= !flag;
}
pub fn has(&self, flag: u32) -> bool {
self.0 & flag != 0
}
}
impl AccountNode {
pub fn new(id: Uuid, account_type: AccountType, index: u16) -> Self {
Self {
id,
code_hash: 0,
index,
account_type,
class_id: 0,
subclass_id: 0,
_pad1: [0; 3],
opening_balance: Decimal128::ZERO,
closing_balance: Decimal128::ZERO,
total_debits: Decimal128::ZERO,
total_credits: Decimal128::ZERO,
in_degree: 0,
out_degree: 0,
betweenness_centrality: 0.0,
pagerank: 0.0,
clustering_coefficient: 0.0,
suspense_score: 0.0,
risk_score: 0.0,
transaction_count: 0,
flags: AccountFlags::new(),
}
}
pub fn net_change(&self) -> Decimal128 {
self.closing_balance - self.opening_balance
}
pub fn total_activity(&self) -> Decimal128 {
Decimal128::from_f64(self.total_debits.to_f64().abs() + self.total_credits.to_f64().abs())
}
pub fn balance_ratio(&self) -> f64 {
let activity = self.total_activity().to_f64();
if activity > 0.0 {
self.closing_balance.to_f64().abs() / activity
} else {
1.0 }
}
pub fn is_hub(&self) -> bool {
self.in_degree + self.out_degree > 10 || self.betweenness_centrality > 0.1
}
}
#[derive(Debug, Clone)]
pub struct AccountMetadata {
pub code: String,
pub name: String,
pub description: String,
pub parent_id: Option<Uuid>,
pub semantics: AccountSemantics,
}
impl AccountMetadata {
pub fn new(code: impl Into<String>, name: impl Into<String>) -> Self {
Self {
code: code.into(),
name: name.into(),
description: String::new(),
parent_id: None,
semantics: AccountSemantics::default(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_account_type_normal_balance() {
assert_eq!(AccountType::Asset.normal_balance(), BalanceSide::Debit);
assert_eq!(AccountType::Liability.normal_balance(), BalanceSide::Credit);
assert_eq!(AccountType::Revenue.normal_balance(), BalanceSide::Credit);
assert_eq!(AccountType::Expense.normal_balance(), BalanceSide::Debit);
}
#[test]
fn test_decimal128_arithmetic() {
let a = Decimal128::from_f64(100.50);
let b = Decimal128::from_f64(25.25);
let sum = a + b;
assert!((sum.to_f64() - 125.75).abs() < 0.01);
}
#[test]
fn test_account_node_size() {
let size = std::mem::size_of::<AccountNode>();
assert!(
size >= 128,
"AccountNode should be at least 128 bytes, got {}",
size
);
assert!(
size.is_multiple_of(128),
"AccountNode should be 128-byte aligned, got {}",
size
);
}
}