Skip to main content

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}