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 /// Écritures rattachées à une quote-part de copropriétaire.
51 ///
52 /// Sert l'idempotence de l'écriture d'encaissement : une quote-part peut
53 /// être soldée par la voie interface (`mark-paid`) OU par la réussite d'un
54 /// paiement du module `/payments`. Sans ce contrôle, un même encaissement
55 /// débiterait la banque deux fois.
56 async fn find_by_contribution(
57 &self,
58 contribution_id: Uuid,
59 ) -> Result<Vec<JournalEntry>, String>;
60
61 /// Find journal entries for a date range
62 ///
63 /// Useful for generating period reports (income statement).
64 async fn find_by_date_range(
65 &self,
66 organization_id: Uuid,
67 start_date: DateTime<Utc>,
68 end_date: DateTime<Utc>,
69 ) -> Result<Vec<JournalEntry>, String>;
70
71 /// Calculate account balances from journal entry lines
72 ///
73 /// This replaces the old method of calculating balances directly from expenses.
74 ///
75 /// # Arguments
76 /// - `organization_id`: Organization to calculate for
77 ///
78 /// # Returns
79 /// - `HashMap<account_code, balance>` where:
80 /// - Assets/Expenses: balance = debits - credits
81 /// - Liabilities/Revenue: balance = credits - debits
82 ///
83 /// # Example
84 /// ```ignore
85 /// {
86 /// "6100": 5000.0, // Utilities expense
87 /// "4110": 1050.0, // VAT recoverable
88 /// "4400": -6050.0, // Suppliers payable (negative = liability)
89 /// "5500": 6050.0 // Bank (after payment)
90 /// }
91 /// ```
92 async fn calculate_account_balances(
93 &self,
94 organization_id: Uuid,
95 ) -> Result<HashMap<String, Decimal>, String>;
96
97 /// Calculate account balances for a specific period
98 ///
99 /// Same as `calculate_account_balances` but filtered by entry_date.
100 async fn calculate_account_balances_for_period(
101 &self,
102 organization_id: Uuid,
103 start_date: DateTime<Utc>,
104 end_date: DateTime<Utc>,
105 ) -> Result<HashMap<String, Decimal>, String>;
106
107 /// Get all journal entry lines for an account
108 ///
109 /// Useful for displaying account ledgers (grand-livre).
110 async fn find_lines_by_account(
111 &self,
112 organization_id: Uuid,
113 account_code: &str,
114 ) -> Result<Vec<JournalEntryLine>, String>;
115
116 /// Validate that an entry is balanced (debits = credits)
117 ///
118 /// This is a safety check before persisting. Database triggers also enforce this.
119 async fn validate_balance(&self, entry_id: Uuid) -> Result<bool, String>;
120
121 /// Calculate account balances for a specific building
122 ///
123 /// Filters journal entries by those linked to expenses/contributions for the building.
124 async fn calculate_account_balances_for_building(
125 &self,
126 organization_id: Uuid,
127 building_id: Uuid,
128 ) -> Result<HashMap<String, Decimal>, String>;
129
130 /// Calculate account balances for a specific building and period
131 async fn calculate_account_balances_for_building_and_period(
132 &self,
133 organization_id: Uuid,
134 building_id: Uuid,
135 start_date: DateTime<Utc>,
136 end_date: DateTime<Utc>,
137 ) -> Result<HashMap<String, Decimal>, String>;
138
139 /// Create a manual journal entry with multiple lines
140 async fn create_manual_entry(
141 &self,
142 entry: &JournalEntry,
143 lines: &[JournalEntryLine],
144 ) -> Result<(), String>;
145
146 /// List journal entries with filters
147 #[allow(clippy::too_many_arguments)]
148 async fn list_entries(
149 &self,
150 organization_id: Uuid,
151 building_id: Option<Uuid>,
152 journal_type: Option<String>,
153 start_date: Option<DateTime<Utc>>,
154 end_date: Option<DateTime<Utc>>,
155 limit: i64,
156 offset: i64,
157 ) -> Result<Vec<JournalEntry>, String>;
158
159 /// Find a journal entry by ID
160 async fn find_by_id(
161 &self,
162 entry_id: Uuid,
163 organization_id: Uuid,
164 ) -> Result<JournalEntry, String>;
165
166 /// Find all lines for a journal entry
167 async fn find_lines_by_entry(
168 &self,
169 entry_id: Uuid,
170 organization_id: Uuid,
171 ) -> Result<Vec<JournalEntryLine>, String>;
172
173 /// Delete a journal entry and its lines
174 async fn delete_entry(&self, entry_id: Uuid, organization_id: Uuid) -> Result<(), String>;
175}