use super::{AccountType, Decimal128, HybridTimestamp};
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 SolvingMethod {
MethodA = 0,
MethodB = 1,
MethodC = 2,
MethodD = 3,
MethodE = 4,
Pending = 255,
}
impl SolvingMethod {
pub fn expected_ratio(&self) -> f64 {
match self {
SolvingMethod::MethodA => 0.6068,
SolvingMethod::MethodB => 0.1663,
SolvingMethod::MethodC => 0.11,
SolvingMethod::MethodD => 0.11,
SolvingMethod::MethodE => 0.0076,
SolvingMethod::Pending => 0.0,
}
}
pub fn base_confidence(&self) -> f32 {
match self {
SolvingMethod::MethodA => 1.0,
SolvingMethod::MethodB => 1.0, SolvingMethod::MethodC => 0.85,
SolvingMethod::MethodD => 1.0,
SolvingMethod::MethodE => 0.5, SolvingMethod::Pending => 0.0,
}
}
pub fn display_name(&self) -> &'static str {
match self {
SolvingMethod::MethodA => "A: 1-to-1",
SolvingMethod::MethodB => "B: n-to-n",
SolvingMethod::MethodC => "C: n-to-m",
SolvingMethod::MethodD => "D: Aggregate",
SolvingMethod::MethodE => "E: Decompose",
SolvingMethod::Pending => "Pending",
}
}
pub fn color(&self) -> [u8; 3] {
match self {
SolvingMethod::MethodA => [0, 200, 83], SolvingMethod::MethodB => [100, 181, 246], SolvingMethod::MethodC => [255, 193, 7], SolvingMethod::MethodD => [255, 152, 0], SolvingMethod::MethodE => [244, 67, 54], SolvingMethod::Pending => [158, 158, 158], }
}
}
#[derive(Debug, Clone, Archive, Serialize, Deserialize)]
#[repr(C, align(128))]
pub struct JournalEntry {
pub id: Uuid,
pub entity_id: Uuid,
pub document_number_hash: u64,
pub source_system_id: u32,
pub batch_id: u32,
pub posting_date: HybridTimestamp,
pub line_count: u16,
pub debit_line_count: u16,
pub credit_line_count: u16,
pub first_line_index: u16,
pub total_debits: Decimal128,
pub total_credits: Decimal128,
pub solving_method: SolvingMethod,
pub average_confidence: f32,
pub flow_count: u16,
pub _pad: u8,
pub flags: JournalEntryFlags,
pub _reserved: [u8; 12],
}
#[derive(Debug, Clone, Copy, Default, Archive, Serialize, Deserialize)]
#[repr(transparent)]
pub struct JournalEntryFlags(pub u32);
impl JournalEntryFlags {
pub const IS_BALANCED: u32 = 1 << 0;
pub const IS_TRANSFORMED: u32 = 1 << 1;
pub const HAS_DECOMPOSED_VALUES: u32 = 1 << 2;
pub const USES_HIGHER_AGGREGATE: u32 = 1 << 3;
pub const FLAGGED_FOR_AUDIT: u32 = 1 << 4;
pub const IS_REVERSING: u32 = 1 << 5;
pub const IS_RECURRING: u32 = 1 << 6;
pub const IS_ADJUSTMENT: u32 = 1 << 7;
pub const HAS_VAT: u32 = 1 << 8;
pub const IS_INTERCOMPANY: u32 = 1 << 9;
pub fn new() -> Self {
Self(Self::IS_BALANCED) }
pub fn is_balanced(&self) -> bool {
self.0 & Self::IS_BALANCED != 0
}
pub fn is_transformed(&self) -> bool {
self.0 & Self::IS_TRANSFORMED != 0
}
pub fn flagged_for_audit(&self) -> bool {
self.0 & Self::FLAGGED_FOR_AUDIT != 0
}
}
#[derive(Debug, Clone, Archive, Serialize, Deserialize)]
#[repr(C, align(64))]
pub struct JournalLineItem {
pub id: Uuid,
pub journal_entry_index: u32,
pub account_index: u16,
pub line_number: u16,
pub line_type: LineType,
pub _pad1: [u8; 3],
pub amount: Decimal128,
pub confidence: f32,
pub matched_line_index: u16,
pub flags: LineItemFlags,
pub _pad2: u8,
pub _reserved: [u8; 12],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Archive, Serialize, Deserialize)]
#[archive(compare(PartialEq))]
#[repr(u8)]
pub enum LineType {
Debit = 0,
Credit = 1,
}
impl LineType {
pub fn is_debit(&self) -> bool {
matches!(self, LineType::Debit)
}
pub fn is_credit(&self) -> bool {
matches!(self, LineType::Credit)
}
}
#[derive(Debug, Clone, Copy, Default, Archive, Serialize, Deserialize)]
#[repr(transparent)]
pub struct LineItemFlags(pub u8);
impl LineItemFlags {
pub const IS_SHADOW_BOOKING: u8 = 1 << 0;
pub const IS_HIGHER_AGGREGATE: u8 = 1 << 1;
pub const IS_VAT_LINE: u8 = 1 << 2;
pub const IS_ROUNDING_ADJUSTMENT: u8 = 1 << 3;
pub const IS_MATCHED: u8 = 1 << 4;
}
impl JournalEntry {
pub fn new(id: Uuid, entity_id: Uuid, posting_date: HybridTimestamp) -> Self {
Self {
id,
entity_id,
document_number_hash: 0,
source_system_id: 0,
batch_id: 0,
posting_date,
line_count: 0,
debit_line_count: 0,
credit_line_count: 0,
first_line_index: 0,
total_debits: Decimal128::ZERO,
total_credits: Decimal128::ZERO,
solving_method: SolvingMethod::Pending,
average_confidence: 0.0,
flow_count: 0,
_pad: 0,
flags: JournalEntryFlags::new(),
_reserved: [0; 12],
}
}
pub fn is_balanced(&self) -> bool {
(self.total_debits.to_f64() - self.total_credits.to_f64()).abs() < 0.01
}
pub fn determine_method(&self) -> SolvingMethod {
if self.debit_line_count == 1 && self.credit_line_count == 1 {
SolvingMethod::MethodA
} else if self.debit_line_count == self.credit_line_count {
SolvingMethod::MethodB
} else {
SolvingMethod::MethodC
}
}
}
impl JournalLineItem {
pub fn debit(account_index: u16, amount: Decimal128, line_number: u16) -> Self {
Self {
id: Uuid::new_v4(),
journal_entry_index: 0,
account_index,
line_number,
line_type: LineType::Debit,
_pad1: [0; 3],
amount,
confidence: 1.0,
matched_line_index: u16::MAX,
flags: LineItemFlags(0),
_pad2: 0,
_reserved: [0; 12],
}
}
pub fn credit(account_index: u16, amount: Decimal128, line_number: u16) -> Self {
Self {
id: Uuid::new_v4(),
journal_entry_index: 0,
account_index,
line_number,
line_type: LineType::Credit,
_pad1: [0; 3],
amount,
confidence: 1.0,
matched_line_index: u16::MAX,
flags: LineItemFlags(0),
_pad2: 0,
_reserved: [0; 12],
}
}
pub fn is_debit(&self) -> bool {
self.line_type.is_debit()
}
pub fn is_credit(&self) -> bool {
self.line_type.is_credit()
}
pub fn is_matched(&self) -> bool {
self.matched_line_index != u16::MAX
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum BookingPatternType {
CashReceipt = 0,
CashPayment = 1,
SalesRevenue = 2,
Purchase = 3,
Payroll = 4,
Depreciation = 5,
Accrual = 6,
Reversal = 7,
Intercompany = 8,
VatSettlement = 9,
BankReconciliation = 10,
Unknown = 255,
}
impl BookingPatternType {
pub fn expected_debit_type(&self) -> Option<AccountType> {
match self {
BookingPatternType::CashReceipt => Some(AccountType::Asset), BookingPatternType::CashPayment => Some(AccountType::Liability), BookingPatternType::SalesRevenue => Some(AccountType::Asset), BookingPatternType::Purchase => Some(AccountType::Expense),
BookingPatternType::Payroll => Some(AccountType::Expense),
BookingPatternType::Depreciation => Some(AccountType::Expense),
BookingPatternType::Accrual => Some(AccountType::Expense),
_ => None,
}
}
pub fn expected_credit_type(&self) -> Option<AccountType> {
match self {
BookingPatternType::CashReceipt => Some(AccountType::Revenue),
BookingPatternType::CashPayment => Some(AccountType::Asset), BookingPatternType::SalesRevenue => Some(AccountType::Revenue),
BookingPatternType::Purchase => Some(AccountType::Liability), BookingPatternType::Payroll => Some(AccountType::Asset), BookingPatternType::Depreciation => Some(AccountType::Contra), BookingPatternType::Accrual => Some(AccountType::Liability),
_ => None,
}
}
pub fn confidence_boost(&self) -> f32 {
match self {
BookingPatternType::CashReceipt => 0.20,
BookingPatternType::CashPayment => 0.20,
BookingPatternType::SalesRevenue => 0.15,
BookingPatternType::Purchase => 0.15,
BookingPatternType::Payroll => 0.25,
BookingPatternType::Depreciation => 0.25,
_ => 0.10,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_journal_entry_size() {
let size = std::mem::size_of::<JournalEntry>();
assert!(
size >= 128,
"JournalEntry should be at least 128 bytes, got {}",
size
);
assert!(
size.is_multiple_of(128),
"JournalEntry should be 128-byte aligned, got {}",
size
);
}
#[test]
fn test_line_item_size() {
let size = std::mem::size_of::<JournalLineItem>();
assert!(
size >= 64,
"JournalLineItem should be at least 64 bytes, got {}",
size
);
assert!(
size.is_multiple_of(64),
"JournalLineItem should be 64-byte aligned, got {}",
size
);
}
#[test]
fn test_method_determination() {
let mut entry = JournalEntry::new(Uuid::new_v4(), Uuid::new_v4(), HybridTimestamp::now());
entry.debit_line_count = 1;
entry.credit_line_count = 1;
assert_eq!(entry.determine_method(), SolvingMethod::MethodA);
entry.debit_line_count = 3;
entry.credit_line_count = 3;
assert_eq!(entry.determine_method(), SolvingMethod::MethodB);
entry.debit_line_count = 2;
entry.credit_line_count = 5;
assert_eq!(entry.determine_method(), SolvingMethod::MethodC);
}
}