koprogo_api/application/ports/journal_entry_repository.rs
1// Application Port: Journal Entry Repository
2//
3// CREDITS & ATTRIBUTION:
4// This implementation is inspired by the Noalyss project (https://gitlab.com/noalyss/noalyss)
5// Noalyss is a free accounting software for Belgian and French accounting
6// License: GPL-2.0-or-later (GNU General Public License version 2 or later)
7// Copyright: (C) 1989, 1991 Free Software Foundation, Inc.
8// Copyright: Dany De Bontridder <dany@alchimerys.eu>
9
10use crate::domain::entities::{JournalEntry, JournalEntryLine};
11use async_trait::async_trait;
12use chrono::{DateTime, Utc};
13use rust_decimal::Decimal;
14use std::collections::HashMap;
15use uuid::Uuid;
16
17/// Repository port for journal entries (double-entry bookkeeping)
18///
19/// This trait defines operations for managing accounting journal entries
20/// inspired by Noalyss' jrn/jrnx table structure.
21#[async_trait]
22pub trait JournalEntryRepository: Send + Sync {
23 /// Create a new journal entry with its lines
24 ///
25 /// # Arguments
26 /// - `entry`: The journal entry to create (must be balanced)
27 ///
28 /// # Returns
29 /// - `Ok(JournalEntry)` with generated IDs and timestamps
30 /// - `Err(String)` if validation fails or database error
31 ///
32 /// # Database Constraints
33 /// - Triggers validate that total debits = total credits
34 /// - Foreign keys validate account codes exist
35 async fn create(&self, entry: &JournalEntry) -> Result<JournalEntry, String>;
36
37 /// Find all journal entries for an organization
38 ///
39 /// Returns entries ordered by entry_date DESC.
40 async fn find_by_organization(
41 &self,
42 organization_id: Uuid,
43 ) -> Result<Vec<JournalEntry>, String>;
44
45 /// Find journal entries linked to a specific expense
46 ///
47 /// Returns all entries that were auto-generated from this expense.
48 async fn find_by_expense(&self, expense_id: Uuid) -> Result<Vec<JournalEntry>, String>;
49
50 /// Find journal entries for a date range
51 ///
52 /// Useful for generating period reports (income statement).
53 async fn find_by_date_range(
54 &self,
55 organization_id: Uuid,
56 start_date: DateTime<Utc>,
57 end_date: DateTime<Utc>,
58 ) -> Result<Vec<JournalEntry>, String>;
59
60 /// Calculate account balances from journal entry lines
61 ///
62 /// This replaces the old method of calculating balances directly from expenses.
63 ///
64 /// # Arguments
65 /// - `organization_id`: Organization to calculate for
66 ///
67 /// # Returns
68 /// - `HashMap<account_code, balance>` where:
69 /// - Assets/Expenses: balance = debits - credits
70 /// - Liabilities/Revenue: balance = credits - debits
71 ///
72 /// # Example
73 /// ```ignore
74 /// {
75 /// "6100": 5000.0, // Utilities expense
76 /// "4110": 1050.0, // VAT recoverable
77 /// "4400": -6050.0, // Suppliers payable (negative = liability)
78 /// "5500": 6050.0 // Bank (after payment)
79 /// }
80 /// ```
81 async fn calculate_account_balances(
82 &self,
83 organization_id: Uuid,
84 ) -> Result<HashMap<String, Decimal>, String>;
85
86 /// Calculate account balances for a specific period
87 ///
88 /// Same as `calculate_account_balances` but filtered by entry_date.
89 async fn calculate_account_balances_for_period(
90 &self,
91 organization_id: Uuid,
92 start_date: DateTime<Utc>,
93 end_date: DateTime<Utc>,
94 ) -> Result<HashMap<String, Decimal>, String>;
95
96 /// Get all journal entry lines for an account
97 ///
98 /// Useful for displaying account ledgers (grand-livre).
99 async fn find_lines_by_account(
100 &self,
101 organization_id: Uuid,
102 account_code: &str,
103 ) -> Result<Vec<JournalEntryLine>, String>;
104
105 /// Validate that an entry is balanced (debits = credits)
106 ///
107 /// This is a safety check before persisting. Database triggers also enforce this.
108 async fn validate_balance(&self, entry_id: Uuid) -> Result<bool, String>;
109
110 /// Calculate account balances for a specific building
111 ///
112 /// Filters journal entries by those linked to expenses/contributions for the building.
113 async fn calculate_account_balances_for_building(
114 &self,
115 organization_id: Uuid,
116 building_id: Uuid,
117 ) -> Result<HashMap<String, Decimal>, String>;
118
119 /// Calculate account balances for a specific building and period
120 async fn calculate_account_balances_for_building_and_period(
121 &self,
122 organization_id: Uuid,
123 building_id: Uuid,
124 start_date: DateTime<Utc>,
125 end_date: DateTime<Utc>,
126 ) -> Result<HashMap<String, Decimal>, String>;
127
128 /// Create a manual journal entry with multiple lines
129 async fn create_manual_entry(
130 &self,
131 entry: &JournalEntry,
132 lines: &[JournalEntryLine],
133 ) -> Result<(), String>;
134
135 /// List journal entries with filters
136 #[allow(clippy::too_many_arguments)]
137 async fn list_entries(
138 &self,
139 organization_id: Uuid,
140 building_id: Option<Uuid>,
141 journal_type: Option<String>,
142 start_date: Option<DateTime<Utc>>,
143 end_date: Option<DateTime<Utc>>,
144 limit: i64,
145 offset: i64,
146 ) -> Result<Vec<JournalEntry>, String>;
147
148 /// Find a journal entry by ID
149 async fn find_by_id(
150 &self,
151 entry_id: Uuid,
152 organization_id: Uuid,
153 ) -> Result<JournalEntry, String>;
154
155 /// Find all lines for a journal entry
156 async fn find_lines_by_entry(
157 &self,
158 entry_id: Uuid,
159 organization_id: Uuid,
160 ) -> Result<Vec<JournalEntryLine>, String>;
161
162 /// Delete a journal entry and its lines
163 async fn delete_entry(&self, entry_id: Uuid, organization_id: Uuid) -> Result<(), String>;
164}