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