1use super::{AccountType, Decimal128, HybridTimestamp};
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 SolvingMethod {
16 MethodA = 0,
19
20 MethodB = 1,
24
25 MethodC = 2,
29
30 MethodD = 3,
34
35 MethodE = 4,
39
40 Pending = 255,
42}
43
44impl SolvingMethod {
45 pub fn expected_ratio(&self) -> f64 {
47 match self {
48 SolvingMethod::MethodA => 0.6068,
49 SolvingMethod::MethodB => 0.1663,
50 SolvingMethod::MethodC => 0.11,
51 SolvingMethod::MethodD => 0.11,
52 SolvingMethod::MethodE => 0.0076,
53 SolvingMethod::Pending => 0.0,
54 }
55 }
56
57 pub fn base_confidence(&self) -> f32 {
59 match self {
60 SolvingMethod::MethodA => 1.0,
61 SolvingMethod::MethodB => 1.0, SolvingMethod::MethodC => 0.85,
63 SolvingMethod::MethodD => 1.0,
64 SolvingMethod::MethodE => 0.5, SolvingMethod::Pending => 0.0,
66 }
67 }
68
69 pub fn display_name(&self) -> &'static str {
71 match self {
72 SolvingMethod::MethodA => "A: 1-to-1",
73 SolvingMethod::MethodB => "B: n-to-n",
74 SolvingMethod::MethodC => "C: n-to-m",
75 SolvingMethod::MethodD => "D: Aggregate",
76 SolvingMethod::MethodE => "E: Decompose",
77 SolvingMethod::Pending => "Pending",
78 }
79 }
80
81 pub fn color(&self) -> [u8; 3] {
83 match self {
84 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], }
91 }
92}
93
94#[derive(Debug, Clone, Archive, Serialize, Deserialize)]
97#[repr(C, align(128))]
98pub struct JournalEntry {
99 pub id: Uuid,
102 pub entity_id: Uuid,
104
105 pub document_number_hash: u64,
108 pub source_system_id: u32,
110 pub batch_id: u32,
112
113 pub posting_date: HybridTimestamp,
116
117 pub line_count: u16,
120 pub debit_line_count: u16,
122 pub credit_line_count: u16,
124 pub first_line_index: u16,
126
127 pub total_debits: Decimal128,
130 pub total_credits: Decimal128,
132
133 pub solving_method: SolvingMethod,
136 pub average_confidence: f32,
138 pub flow_count: u16,
140 pub _pad: u8,
142
143 pub flags: JournalEntryFlags,
146
147 pub _reserved: [u8; 12],
150}
151
152#[derive(Debug, Clone, Copy, Default, Archive, Serialize, Deserialize)]
154#[repr(transparent)]
155pub struct JournalEntryFlags(pub u32);
156
157impl JournalEntryFlags {
158 pub const IS_BALANCED: u32 = 1 << 0;
160 pub const IS_TRANSFORMED: u32 = 1 << 1;
162 pub const HAS_DECOMPOSED_VALUES: u32 = 1 << 2;
164 pub const USES_HIGHER_AGGREGATE: u32 = 1 << 3;
166 pub const FLAGGED_FOR_AUDIT: u32 = 1 << 4;
168 pub const IS_REVERSING: u32 = 1 << 5;
170 pub const IS_RECURRING: u32 = 1 << 6;
172 pub const IS_ADJUSTMENT: u32 = 1 << 7;
174 pub const HAS_VAT: u32 = 1 << 8;
176 pub const IS_INTERCOMPANY: u32 = 1 << 9;
178
179 pub fn new() -> Self {
181 Self(Self::IS_BALANCED) }
183
184 pub fn is_balanced(&self) -> bool {
186 self.0 & Self::IS_BALANCED != 0
187 }
188 pub fn is_transformed(&self) -> bool {
190 self.0 & Self::IS_TRANSFORMED != 0
191 }
192 pub fn flagged_for_audit(&self) -> bool {
194 self.0 & Self::FLAGGED_FOR_AUDIT != 0
195 }
196}
197
198#[derive(Debug, Clone, Archive, Serialize, Deserialize)]
201#[repr(C, align(64))]
202pub struct JournalLineItem {
203 pub id: Uuid,
206 pub journal_entry_index: u32,
208
209 pub account_index: u16,
212 pub line_number: u16,
214 pub line_type: LineType,
216 pub _pad1: [u8; 3],
218
219 pub amount: Decimal128,
222
223 pub confidence: f32,
226 pub matched_line_index: u16,
228 pub flags: LineItemFlags,
230 pub _pad2: u8,
232
233 pub _reserved: [u8; 12],
236}
237
238#[derive(Debug, Clone, Copy, PartialEq, Eq, Archive, Serialize, Deserialize)]
240#[archive(compare(PartialEq))]
241#[repr(u8)]
242pub enum LineType {
243 Debit = 0,
245 Credit = 1,
247}
248
249impl LineType {
250 pub fn is_debit(&self) -> bool {
252 matches!(self, LineType::Debit)
253 }
254 pub fn is_credit(&self) -> bool {
256 matches!(self, LineType::Credit)
257 }
258}
259
260#[derive(Debug, Clone, Copy, Default, Archive, Serialize, Deserialize)]
262#[repr(transparent)]
263pub struct LineItemFlags(pub u8);
264
265impl LineItemFlags {
266 pub const IS_SHADOW_BOOKING: u8 = 1 << 0;
268 pub const IS_HIGHER_AGGREGATE: u8 = 1 << 1;
270 pub const IS_VAT_LINE: u8 = 1 << 2;
272 pub const IS_ROUNDING_ADJUSTMENT: u8 = 1 << 3;
274 pub const IS_MATCHED: u8 = 1 << 4;
276}
277
278impl JournalEntry {
279 pub fn new(id: Uuid, entity_id: Uuid, posting_date: HybridTimestamp) -> Self {
281 Self {
282 id,
283 entity_id,
284 document_number_hash: 0,
285 source_system_id: 0,
286 batch_id: 0,
287 posting_date,
288 line_count: 0,
289 debit_line_count: 0,
290 credit_line_count: 0,
291 first_line_index: 0,
292 total_debits: Decimal128::ZERO,
293 total_credits: Decimal128::ZERO,
294 solving_method: SolvingMethod::Pending,
295 average_confidence: 0.0,
296 flow_count: 0,
297 _pad: 0,
298 flags: JournalEntryFlags::new(),
299 _reserved: [0; 12],
300 }
301 }
302
303 pub fn is_balanced(&self) -> bool {
305 (self.total_debits.to_f64() - self.total_credits.to_f64()).abs() < 0.01
306 }
307
308 pub fn determine_method(&self) -> SolvingMethod {
310 if self.debit_line_count == 1 && self.credit_line_count == 1 {
311 SolvingMethod::MethodA
312 } else if self.debit_line_count == self.credit_line_count {
313 SolvingMethod::MethodB
314 } else {
315 SolvingMethod::MethodC
316 }
317 }
318}
319
320impl JournalLineItem {
321 pub fn debit(account_index: u16, amount: Decimal128, line_number: u16) -> Self {
323 Self {
324 id: Uuid::new_v4(),
325 journal_entry_index: 0,
326 account_index,
327 line_number,
328 line_type: LineType::Debit,
329 _pad1: [0; 3],
330 amount,
331 confidence: 1.0,
332 matched_line_index: u16::MAX,
333 flags: LineItemFlags(0),
334 _pad2: 0,
335 _reserved: [0; 12],
336 }
337 }
338
339 pub fn credit(account_index: u16, amount: Decimal128, line_number: u16) -> Self {
341 Self {
342 id: Uuid::new_v4(),
343 journal_entry_index: 0,
344 account_index,
345 line_number,
346 line_type: LineType::Credit,
347 _pad1: [0; 3],
348 amount,
349 confidence: 1.0,
350 matched_line_index: u16::MAX,
351 flags: LineItemFlags(0),
352 _pad2: 0,
353 _reserved: [0; 12],
354 }
355 }
356
357 pub fn is_debit(&self) -> bool {
359 self.line_type.is_debit()
360 }
361
362 pub fn is_credit(&self) -> bool {
364 self.line_type.is_credit()
365 }
366
367 pub fn is_matched(&self) -> bool {
369 self.matched_line_index != u16::MAX
370 }
371}
372
373#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
375#[repr(u8)]
376pub enum BookingPatternType {
377 CashReceipt = 0,
379 CashPayment = 1,
381 SalesRevenue = 2,
383 Purchase = 3,
385 Payroll = 4,
387 Depreciation = 5,
389 Accrual = 6,
391 Reversal = 7,
393 Intercompany = 8,
395 VatSettlement = 9,
397 BankReconciliation = 10,
399 Unknown = 255,
401}
402
403impl BookingPatternType {
404 pub fn expected_debit_type(&self) -> Option<AccountType> {
406 match self {
407 BookingPatternType::CashReceipt => Some(AccountType::Asset), BookingPatternType::CashPayment => Some(AccountType::Liability), BookingPatternType::SalesRevenue => Some(AccountType::Asset), BookingPatternType::Purchase => Some(AccountType::Expense),
411 BookingPatternType::Payroll => Some(AccountType::Expense),
412 BookingPatternType::Depreciation => Some(AccountType::Expense),
413 BookingPatternType::Accrual => Some(AccountType::Expense),
414 _ => None,
415 }
416 }
417
418 pub fn expected_credit_type(&self) -> Option<AccountType> {
420 match self {
421 BookingPatternType::CashReceipt => Some(AccountType::Revenue),
422 BookingPatternType::CashPayment => Some(AccountType::Asset), BookingPatternType::SalesRevenue => Some(AccountType::Revenue),
424 BookingPatternType::Purchase => Some(AccountType::Liability), BookingPatternType::Payroll => Some(AccountType::Asset), BookingPatternType::Depreciation => Some(AccountType::Contra), BookingPatternType::Accrual => Some(AccountType::Liability),
428 _ => None,
429 }
430 }
431
432 pub fn confidence_boost(&self) -> f32 {
434 match self {
435 BookingPatternType::CashReceipt => 0.20,
436 BookingPatternType::CashPayment => 0.20,
437 BookingPatternType::SalesRevenue => 0.15,
438 BookingPatternType::Purchase => 0.15,
439 BookingPatternType::Payroll => 0.25,
440 BookingPatternType::Depreciation => 0.25,
441 _ => 0.10,
442 }
443 }
444}
445
446#[cfg(test)]
447mod tests {
448 use super::*;
449
450 #[test]
451 fn test_journal_entry_size() {
452 let size = std::mem::size_of::<JournalEntry>();
453 assert!(
454 size >= 128,
455 "JournalEntry should be at least 128 bytes, got {}",
456 size
457 );
458 assert!(
459 size.is_multiple_of(128),
460 "JournalEntry should be 128-byte aligned, got {}",
461 size
462 );
463 }
464
465 #[test]
466 fn test_line_item_size() {
467 let size = std::mem::size_of::<JournalLineItem>();
468 assert!(
469 size >= 64,
470 "JournalLineItem should be at least 64 bytes, got {}",
471 size
472 );
473 assert!(
474 size.is_multiple_of(64),
475 "JournalLineItem should be 64-byte aligned, got {}",
476 size
477 );
478 }
479
480 #[test]
481 fn test_method_determination() {
482 let mut entry = JournalEntry::new(Uuid::new_v4(), Uuid::new_v4(), HybridTimestamp::now());
483
484 entry.debit_line_count = 1;
486 entry.credit_line_count = 1;
487 assert_eq!(entry.determine_method(), SolvingMethod::MethodA);
488
489 entry.debit_line_count = 3;
491 entry.credit_line_count = 3;
492 assert_eq!(entry.determine_method(), SolvingMethod::MethodB);
493
494 entry.debit_line_count = 2;
496 entry.credit_line_count = 5;
497 assert_eq!(entry.determine_method(), SolvingMethod::MethodC);
498 }
499}