Skip to main content

datasynth_core/models/
journal_entry.rs

1//! Journal Entry data structures for General Ledger postings.
2//!
3//! This module defines the core journal entry structures that form the basis
4//! of double-entry bookkeeping. Each journal entry consists of a header and
5//! one or more line items that must balance (total debits = total credits).
6
7use chrono::{DateTime, NaiveDate, Utc};
8use rust_decimal::Decimal;
9use serde::{Deserialize, Serialize};
10use uuid::Uuid;
11
12use super::anomaly::FraudType;
13use super::approval::ApprovalWorkflow;
14
15/// Source of a journal entry transaction.
16///
17/// Distinguishes between manual human entries and automated system postings,
18/// which is critical for audit trail analysis and fraud detection.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
20#[serde(rename_all = "snake_case")]
21pub enum TransactionSource {
22    /// Manual entry by human user during working hours
23    #[default]
24    Manual,
25    /// Automated system posting (interfaces, batch jobs, EDI)
26    Automated,
27    /// Recurring scheduled posting (depreciation, amortization)
28    Recurring,
29    /// Reversal of a previous entry
30    Reversal,
31    /// Period-end adjustment entry
32    Adjustment,
33    /// Statistical posting (memo only, no financial impact)
34    Statistical,
35}
36
37// Note: FraudType is defined in anomaly.rs and re-exported from mod.rs
38// Use `crate::models::FraudType` for fraud type classification.
39
40/// Business process that originated the transaction.
41///
42/// Aligns with standard enterprise process frameworks for process mining
43/// and analytics integration.
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
45#[serde(rename_all = "UPPERCASE")]
46pub enum BusinessProcess {
47    /// Order-to-Cash: sales, billing, accounts receivable
48    O2C,
49    /// Procure-to-Pay: purchasing, accounts payable
50    P2P,
51    /// Record-to-Report: GL, consolidation, reporting
52    #[default]
53    R2R,
54    /// Hire-to-Retire: payroll, HR accounting
55    H2R,
56    /// Acquire-to-Retire: fixed assets, depreciation
57    A2R,
58    /// Source-to-Contract: sourcing, supplier qualification, RFx
59    S2C,
60    /// Manufacturing: production orders, quality, cycle counts
61    #[serde(rename = "MFG")]
62    Mfg,
63    /// Banking operations: KYC/AML, accounts, transactions
64    #[serde(rename = "BANK")]
65    Bank,
66    /// Audit engagement lifecycle
67    #[serde(rename = "AUDIT")]
68    Audit,
69    /// Treasury operations
70    Treasury,
71    /// Tax accounting
72    Tax,
73    /// Intercompany transactions
74    Intercompany,
75}
76
77/// Document type classification for journal entries.
78///
79/// Standard SAP-compatible document type codes.
80#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
81pub struct DocumentType {
82    /// Two-character document type code (e.g., "SA", "KR", "DR")
83    pub code: String,
84    /// Human-readable description
85    pub description: String,
86    /// Associated business process
87    pub business_process: BusinessProcess,
88    /// Is this a reversal document type
89    pub is_reversal: bool,
90}
91
92impl DocumentType {
93    /// Standard GL account document
94    pub fn gl_account() -> Self {
95        Self {
96            code: "SA".to_string(),
97            description: "G/L Account Document".to_string(),
98            business_process: BusinessProcess::R2R,
99            is_reversal: false,
100        }
101    }
102
103    /// Vendor invoice
104    pub fn vendor_invoice() -> Self {
105        Self {
106            code: "KR".to_string(),
107            description: "Vendor Invoice".to_string(),
108            business_process: BusinessProcess::P2P,
109            is_reversal: false,
110        }
111    }
112
113    /// Customer invoice
114    pub fn customer_invoice() -> Self {
115        Self {
116            code: "DR".to_string(),
117            description: "Customer Invoice".to_string(),
118            business_process: BusinessProcess::O2C,
119            is_reversal: false,
120        }
121    }
122
123    /// Vendor payment
124    pub fn vendor_payment() -> Self {
125        Self {
126            code: "KZ".to_string(),
127            description: "Vendor Payment".to_string(),
128            business_process: BusinessProcess::P2P,
129            is_reversal: false,
130        }
131    }
132
133    /// Customer payment
134    pub fn customer_payment() -> Self {
135        Self {
136            code: "DZ".to_string(),
137            description: "Customer Payment".to_string(),
138            business_process: BusinessProcess::O2C,
139            is_reversal: false,
140        }
141    }
142
143    /// Asset posting
144    pub fn asset_posting() -> Self {
145        Self {
146            code: "AA".to_string(),
147            description: "Asset Posting".to_string(),
148            business_process: BusinessProcess::A2R,
149            is_reversal: false,
150        }
151    }
152
153    /// Payroll posting
154    pub fn payroll() -> Self {
155        Self {
156            code: "PR".to_string(),
157            description: "Payroll Document".to_string(),
158            business_process: BusinessProcess::H2R,
159            is_reversal: false,
160        }
161    }
162}
163
164/// Header information for a journal entry document.
165///
166/// Contains all metadata about the posting including timing, user, and
167/// organizational assignment.
168#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct JournalEntryHeader {
170    /// Unique identifier for this journal entry (UUID v7 for time-ordering)
171    pub document_id: Uuid,
172
173    /// Company code this entry belongs to
174    pub company_code: String,
175
176    /// Fiscal year (4-digit)
177    pub fiscal_year: u16,
178
179    /// Fiscal period (1-12, or 13-16 for special periods)
180    pub fiscal_period: u8,
181
182    /// Posting date (when the entry affects the books)
183    pub posting_date: NaiveDate,
184
185    /// Document date (date on source document)
186    pub document_date: NaiveDate,
187
188    /// Entry timestamp (when created in system)
189    pub created_at: DateTime<Utc>,
190
191    /// Document type code
192    pub document_type: String,
193
194    /// Transaction currency (ISO 4217)
195    pub currency: String,
196
197    /// Exchange rate to local currency (1.0 if same currency)
198    pub exchange_rate: Decimal,
199
200    /// Reference document number (external reference)
201    pub reference: Option<String>,
202
203    /// Header text/description
204    pub header_text: Option<String>,
205
206    /// User who created the entry
207    pub created_by: String,
208
209    /// User persona classification for behavioral analysis
210    pub user_persona: String,
211
212    /// Transaction source (manual vs automated)
213    pub source: TransactionSource,
214
215    /// Business process reference
216    pub business_process: Option<BusinessProcess>,
217
218    /// Ledger (0L = Leading Ledger)
219    pub ledger: String,
220
221    /// Is this entry part of a fraud scenario
222    pub is_fraud: bool,
223
224    /// Fraud type if applicable
225    pub fraud_type: Option<FraudType>,
226
227    // --- Anomaly Tracking Fields ---
228    /// Whether this entry has an injected anomaly
229    #[serde(default)]
230    pub is_anomaly: bool,
231
232    /// Unique anomaly identifier for label linkage
233    #[serde(default)]
234    pub anomaly_id: Option<String>,
235
236    /// Type of anomaly if applicable (serialized enum name)
237    #[serde(default)]
238    pub anomaly_type: Option<String>,
239
240    /// Simulation batch ID for traceability
241    pub batch_id: Option<Uuid>,
242
243    // --- Internal Controls / SOX Compliance Fields ---
244    /// Internal control IDs that apply to this transaction
245    #[serde(default)]
246    pub control_ids: Vec<String>,
247
248    /// Whether this is a SOX-relevant transaction
249    #[serde(default)]
250    pub sox_relevant: bool,
251
252    /// Control status for this transaction
253    #[serde(default)]
254    pub control_status: super::internal_control::ControlStatus,
255
256    /// Whether a Segregation of Duties violation occurred
257    #[serde(default)]
258    pub sod_violation: bool,
259
260    /// Type of SoD conflict if violation occurred
261    #[serde(default)]
262    pub sod_conflict_type: Option<super::sod::SodConflictType>,
263
264    // --- Approval Workflow ---
265    /// Approval workflow for high-value transactions
266    #[serde(default)]
267    pub approval_workflow: Option<ApprovalWorkflow>,
268
269    // --- OCPM (Object-Centric Process Mining) Traceability ---
270    /// OCPM event IDs that triggered this journal entry
271    #[serde(default)]
272    pub ocpm_event_ids: Vec<Uuid>,
273
274    /// OCPM object IDs involved in this journal entry
275    #[serde(default)]
276    pub ocpm_object_ids: Vec<Uuid>,
277
278    /// OCPM case ID for process instance tracking
279    #[serde(default)]
280    pub ocpm_case_id: Option<Uuid>,
281}
282
283impl JournalEntryHeader {
284    /// Create a new journal entry header with default values.
285    pub fn new(company_code: String, posting_date: NaiveDate) -> Self {
286        Self {
287            document_id: Uuid::now_v7(),
288            company_code,
289            fiscal_year: posting_date.year() as u16,
290            fiscal_period: posting_date.month() as u8,
291            posting_date,
292            document_date: posting_date,
293            created_at: Utc::now(),
294            document_type: "SA".to_string(),
295            currency: "USD".to_string(),
296            exchange_rate: Decimal::ONE,
297            reference: None,
298            header_text: None,
299            created_by: "SYSTEM".to_string(),
300            user_persona: "automated_system".to_string(),
301            source: TransactionSource::Automated,
302            business_process: Some(BusinessProcess::R2R),
303            ledger: "0L".to_string(),
304            is_fraud: false,
305            fraud_type: None,
306            // Anomaly tracking
307            is_anomaly: false,
308            anomaly_id: None,
309            anomaly_type: None,
310            batch_id: None,
311            // Internal Controls / SOX fields
312            control_ids: Vec::new(),
313            sox_relevant: false,
314            control_status: super::internal_control::ControlStatus::default(),
315            sod_violation: false,
316            sod_conflict_type: None,
317            // Approval workflow
318            approval_workflow: None,
319            // OCPM traceability
320            ocpm_event_ids: Vec::new(),
321            ocpm_object_ids: Vec::new(),
322            ocpm_case_id: None,
323        }
324    }
325
326    /// Create a new journal entry header with a deterministic document ID.
327    ///
328    /// Used for reproducible generation where the document ID is derived
329    /// from a seed and counter.
330    pub fn with_deterministic_id(
331        company_code: String,
332        posting_date: NaiveDate,
333        document_id: Uuid,
334    ) -> Self {
335        Self {
336            document_id,
337            company_code,
338            fiscal_year: posting_date.year() as u16,
339            fiscal_period: posting_date.month() as u8,
340            posting_date,
341            document_date: posting_date,
342            created_at: Utc::now(),
343            document_type: "SA".to_string(),
344            currency: "USD".to_string(),
345            exchange_rate: Decimal::ONE,
346            reference: None,
347            header_text: None,
348            created_by: "SYSTEM".to_string(),
349            user_persona: "automated_system".to_string(),
350            source: TransactionSource::Automated,
351            business_process: Some(BusinessProcess::R2R),
352            ledger: "0L".to_string(),
353            is_fraud: false,
354            fraud_type: None,
355            // Anomaly tracking
356            is_anomaly: false,
357            anomaly_id: None,
358            anomaly_type: None,
359            batch_id: None,
360            // Internal Controls / SOX fields
361            control_ids: Vec::new(),
362            sox_relevant: false,
363            control_status: super::internal_control::ControlStatus::default(),
364            sod_violation: false,
365            sod_conflict_type: None,
366            // Approval workflow
367            approval_workflow: None,
368            // OCPM traceability
369            ocpm_event_ids: Vec::new(),
370            ocpm_object_ids: Vec::new(),
371            ocpm_case_id: None,
372        }
373    }
374}
375
376use chrono::Datelike;
377
378/// Individual line item within a journal entry.
379///
380/// Each line represents a debit or credit posting to a specific GL account.
381/// Line items must be balanced within a journal entry (sum of debits = sum of credits).
382#[derive(Debug, Clone, Serialize, Deserialize)]
383pub struct JournalEntryLine {
384    /// Parent document ID (matches header)
385    pub document_id: Uuid,
386
387    /// Line item number within document (1-based)
388    pub line_number: u32,
389
390    /// GL account number
391    pub gl_account: String,
392
393    /// Account code (alias for gl_account for compatibility)
394    #[serde(default)]
395    pub account_code: String,
396
397    /// Account description (for display)
398    #[serde(default)]
399    pub account_description: Option<String>,
400
401    /// Debit amount in transaction currency (positive or zero)
402    #[serde(with = "rust_decimal::serde::str")]
403    pub debit_amount: Decimal,
404
405    /// Credit amount in transaction currency (positive or zero)
406    #[serde(with = "rust_decimal::serde::str")]
407    pub credit_amount: Decimal,
408
409    /// Amount in local/company currency
410    #[serde(with = "rust_decimal::serde::str")]
411    pub local_amount: Decimal,
412
413    /// Amount in group currency (for consolidation)
414    #[serde(default, with = "rust_decimal::serde::str_option")]
415    pub group_amount: Option<Decimal>,
416
417    /// Cost center assignment
418    pub cost_center: Option<String>,
419
420    /// Profit center assignment
421    pub profit_center: Option<String>,
422
423    /// Segment for segment reporting
424    pub segment: Option<String>,
425
426    /// Functional area
427    pub functional_area: Option<String>,
428
429    /// Line item text/description
430    pub line_text: Option<String>,
431
432    /// Text field (alias for line_text for compatibility)
433    #[serde(default)]
434    pub text: Option<String>,
435
436    /// Reference field
437    #[serde(default)]
438    pub reference: Option<String>,
439
440    /// Value date (for interest calculations)
441    #[serde(default)]
442    pub value_date: Option<NaiveDate>,
443
444    /// Tax code
445    pub tax_code: Option<String>,
446
447    /// Tax amount
448    #[serde(default, with = "rust_decimal::serde::str_option")]
449    pub tax_amount: Option<Decimal>,
450
451    /// Assignment field (for account assignment)
452    pub assignment: Option<String>,
453
454    /// Reference to offsetting account (for network generation)
455    pub offsetting_account: Option<String>,
456
457    /// Is this posting to a suspense/clearing account
458    pub is_suspense: bool,
459
460    /// Trading partner company code (for intercompany)
461    pub trading_partner: Option<String>,
462
463    /// Quantity (for quantity-based postings)
464    #[serde(default, with = "rust_decimal::serde::str_option")]
465    pub quantity: Option<Decimal>,
466
467    /// Unit of measure
468    pub unit_of_measure: Option<String>,
469
470    /// Unit (alias for unit_of_measure for compatibility)
471    #[serde(default)]
472    pub unit: Option<String>,
473
474    /// Project code
475    #[serde(default)]
476    pub project_code: Option<String>,
477}
478
479impl JournalEntryLine {
480    /// Create a new debit line item.
481    pub fn debit(document_id: Uuid, line_number: u32, gl_account: String, amount: Decimal) -> Self {
482        Self {
483            document_id,
484            line_number,
485            gl_account: gl_account.clone(),
486            account_code: gl_account,
487            account_description: None,
488            debit_amount: amount,
489            credit_amount: Decimal::ZERO,
490            local_amount: amount,
491            group_amount: None,
492            cost_center: None,
493            profit_center: None,
494            segment: None,
495            functional_area: None,
496            line_text: None,
497            text: None,
498            reference: None,
499            value_date: None,
500            tax_code: None,
501            tax_amount: None,
502            assignment: None,
503            offsetting_account: None,
504            is_suspense: false,
505            trading_partner: None,
506            quantity: None,
507            unit_of_measure: None,
508            unit: None,
509            project_code: None,
510        }
511    }
512
513    /// Create a new credit line item.
514    pub fn credit(
515        document_id: Uuid,
516        line_number: u32,
517        gl_account: String,
518        amount: Decimal,
519    ) -> Self {
520        Self {
521            document_id,
522            line_number,
523            gl_account: gl_account.clone(),
524            account_code: gl_account,
525            account_description: None,
526            debit_amount: Decimal::ZERO,
527            credit_amount: amount,
528            local_amount: -amount,
529            group_amount: None,
530            cost_center: None,
531            profit_center: None,
532            segment: None,
533            functional_area: None,
534            line_text: None,
535            text: None,
536            reference: None,
537            value_date: None,
538            tax_code: None,
539            tax_amount: None,
540            assignment: None,
541            offsetting_account: None,
542            is_suspense: false,
543            trading_partner: None,
544            quantity: None,
545            unit_of_measure: None,
546            unit: None,
547            project_code: None,
548        }
549    }
550
551    /// Check if this is a debit posting.
552    pub fn is_debit(&self) -> bool {
553        self.debit_amount > Decimal::ZERO
554    }
555
556    /// Check if this is a credit posting.
557    pub fn is_credit(&self) -> bool {
558        self.credit_amount > Decimal::ZERO
559    }
560
561    /// Get the signed amount (positive for debit, negative for credit).
562    pub fn signed_amount(&self) -> Decimal {
563        self.debit_amount - self.credit_amount
564    }
565
566    // Convenience accessors for compatibility
567
568    /// Get the account code (alias for gl_account).
569    #[allow(clippy::misnamed_getters)]
570    pub fn account_code(&self) -> &str {
571        &self.gl_account
572    }
573
574    /// Get the account description (currently returns empty string as not stored).
575    pub fn account_description(&self) -> &str {
576        // Account descriptions are typically looked up from CoA, not stored per line
577        ""
578    }
579}
580
581impl Default for JournalEntryLine {
582    fn default() -> Self {
583        Self {
584            document_id: Uuid::nil(),
585            line_number: 0,
586            gl_account: String::new(),
587            account_code: String::new(),
588            account_description: None,
589            debit_amount: Decimal::ZERO,
590            credit_amount: Decimal::ZERO,
591            local_amount: Decimal::ZERO,
592            group_amount: None,
593            cost_center: None,
594            profit_center: None,
595            segment: None,
596            functional_area: None,
597            line_text: None,
598            text: None,
599            reference: None,
600            value_date: None,
601            tax_code: None,
602            tax_amount: None,
603            assignment: None,
604            offsetting_account: None,
605            is_suspense: false,
606            trading_partner: None,
607            quantity: None,
608            unit_of_measure: None,
609            unit: None,
610            project_code: None,
611        }
612    }
613}
614
615/// Complete journal entry with header and line items.
616///
617/// Represents a balanced double-entry bookkeeping transaction where
618/// total debits must equal total credits.
619#[derive(Debug, Clone, Serialize, Deserialize)]
620pub struct JournalEntry {
621    /// Header with document metadata
622    pub header: JournalEntryHeader,
623    /// Line items (debit and credit postings)
624    pub lines: Vec<JournalEntryLine>,
625}
626
627impl JournalEntry {
628    /// Create a new journal entry with header and empty lines.
629    pub fn new(header: JournalEntryHeader) -> Self {
630        Self {
631            header,
632            lines: Vec::new(),
633        }
634    }
635
636    /// Create a new journal entry with basic parameters (convenience constructor).
637    ///
638    /// This is a simplified constructor for backwards compatibility that creates
639    /// a journal entry with the specified document number, company code, posting date,
640    /// and description.
641    pub fn new_simple(
642        _document_number: String,
643        company_code: String,
644        posting_date: NaiveDate,
645        description: String,
646    ) -> Self {
647        let mut header = JournalEntryHeader::new(company_code, posting_date);
648        header.header_text = Some(description);
649        Self {
650            header,
651            lines: Vec::new(),
652        }
653    }
654
655    /// Add a line item to the journal entry.
656    pub fn add_line(&mut self, line: JournalEntryLine) {
657        self.lines.push(line);
658    }
659
660    /// Get the total debit amount.
661    pub fn total_debit(&self) -> Decimal {
662        self.lines.iter().map(|l| l.debit_amount).sum()
663    }
664
665    /// Get the total credit amount.
666    pub fn total_credit(&self) -> Decimal {
667        self.lines.iter().map(|l| l.credit_amount).sum()
668    }
669
670    /// Check if the journal entry is balanced (debits = credits).
671    pub fn is_balanced(&self) -> bool {
672        self.total_debit() == self.total_credit()
673    }
674
675    /// Get the out-of-balance amount (should be zero for valid entries).
676    pub fn balance_difference(&self) -> Decimal {
677        self.total_debit() - self.total_credit()
678    }
679
680    /// Get the number of line items.
681    pub fn line_count(&self) -> usize {
682        self.lines.len()
683    }
684
685    /// Check if the line count is even.
686    pub fn has_even_line_count(&self) -> bool {
687        self.lines.len().is_multiple_of(2)
688    }
689
690    /// Get the count of debit and credit lines.
691    pub fn debit_credit_counts(&self) -> (usize, usize) {
692        let debits = self.lines.iter().filter(|l| l.is_debit()).count();
693        let credits = self.lines.iter().filter(|l| l.is_credit()).count();
694        (debits, credits)
695    }
696
697    /// Check if debit and credit line counts are equal.
698    pub fn has_equal_debit_credit_counts(&self) -> bool {
699        let (d, c) = self.debit_credit_counts();
700        d == c
701    }
702
703    /// Get unique GL accounts used in this entry.
704    pub fn unique_accounts(&self) -> Vec<&str> {
705        let mut accounts: Vec<&str> = self.lines.iter().map(|l| l.gl_account.as_str()).collect();
706        accounts.sort();
707        accounts.dedup();
708        accounts
709    }
710
711    /// Check if any line posts to a suspense account.
712    pub fn has_suspense_posting(&self) -> bool {
713        self.lines.iter().any(|l| l.is_suspense)
714    }
715
716    // Convenience accessors for header fields
717
718    /// Get the company code.
719    pub fn company_code(&self) -> &str {
720        &self.header.company_code
721    }
722
723    /// Get the document number (document_id as string).
724    pub fn document_number(&self) -> String {
725        self.header.document_id.to_string()
726    }
727
728    /// Get the posting date.
729    pub fn posting_date(&self) -> NaiveDate {
730        self.header.posting_date
731    }
732
733    /// Get the document date.
734    pub fn document_date(&self) -> NaiveDate {
735        self.header.document_date
736    }
737
738    /// Get the fiscal year.
739    pub fn fiscal_year(&self) -> u16 {
740        self.header.fiscal_year
741    }
742
743    /// Get the fiscal period.
744    pub fn fiscal_period(&self) -> u8 {
745        self.header.fiscal_period
746    }
747
748    /// Get the currency.
749    pub fn currency(&self) -> &str {
750        &self.header.currency
751    }
752
753    /// Check if this entry is marked as fraud.
754    pub fn is_fraud(&self) -> bool {
755        self.header.is_fraud
756    }
757
758    /// Check if this entry has a SOD violation.
759    pub fn has_sod_violation(&self) -> bool {
760        self.header.sod_violation
761    }
762
763    /// Get the description (header text).
764    pub fn description(&self) -> Option<&str> {
765        self.header.header_text.as_deref()
766    }
767
768    /// Set the description (header text).
769    pub fn set_description(&mut self, description: String) {
770        self.header.header_text = Some(description);
771    }
772}
773
774#[cfg(test)]
775#[allow(clippy::unwrap_used)]
776mod tests {
777    use super::*;
778
779    #[test]
780    fn test_balanced_entry() {
781        let header = JournalEntryHeader::new(
782            "1000".to_string(),
783            NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
784        );
785        let mut entry = JournalEntry::new(header);
786
787        entry.add_line(JournalEntryLine::debit(
788            entry.header.document_id,
789            1,
790            "100000".to_string(),
791            Decimal::from(1000),
792        ));
793        entry.add_line(JournalEntryLine::credit(
794            entry.header.document_id,
795            2,
796            "200000".to_string(),
797            Decimal::from(1000),
798        ));
799
800        assert!(entry.is_balanced());
801        assert_eq!(entry.line_count(), 2);
802        assert!(entry.has_even_line_count());
803        assert!(entry.has_equal_debit_credit_counts());
804    }
805
806    #[test]
807    fn test_unbalanced_entry() {
808        let header = JournalEntryHeader::new(
809            "1000".to_string(),
810            NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
811        );
812        let mut entry = JournalEntry::new(header);
813
814        entry.add_line(JournalEntryLine::debit(
815            entry.header.document_id,
816            1,
817            "100000".to_string(),
818            Decimal::from(1000),
819        ));
820        entry.add_line(JournalEntryLine::credit(
821            entry.header.document_id,
822            2,
823            "200000".to_string(),
824            Decimal::from(500),
825        ));
826
827        assert!(!entry.is_balanced());
828        assert_eq!(entry.balance_difference(), Decimal::from(500));
829    }
830}