koprogo_api/application/ports/account_repository.rs
1// Application Port: AccountRepository
2//
3// CREDITS & ATTRIBUTION:
4// This repository interface 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//
10// The repository pattern provides abstraction over the Belgian PCMN (Plan Comptable Minimum Normalisé)
11// data access layer, following Noalyss' approach to account management.
12
13use crate::domain::entities::{Account, AccountType};
14use async_trait::async_trait;
15use uuid::Uuid;
16
17/// Repository trait for managing accounts in the Belgian accounting plan
18///
19/// This port defines the contract for account persistence operations.
20/// Implementations must handle:
21/// - Multi-tenancy (organization_id filtering)
22/// - Hierarchical account relationships (parent_code)
23/// - Account code uniqueness within organization
24///
25/// Inspired by Noalyss Acc_Plan_SQL and Tmp_Pcmn_SQL classes
26/// See: include/database/acc_plan_sql.class.php in Noalyss repository
27#[async_trait]
28pub trait AccountRepository: Send + Sync {
29 /// Create a new account in the chart of accounts
30 ///
31 /// # Arguments
32 /// * `account` - Account to create
33 ///
34 /// # Returns
35 /// - `Ok(Account)` - Created account with database-generated ID
36 /// - `Err(String)` - Error message if creation fails
37 ///
38 /// # Errors
39 /// - Duplicate account code within organization
40 /// - Parent account code does not exist
41 /// - Database constraint violation
42 async fn create(&self, account: &Account) -> Result<Account, String>;
43
44 /// Find account by ID
45 ///
46 /// # Arguments
47 /// * `id` - Account ID
48 ///
49 /// # Returns
50 /// - `Ok(Some(Account))` - Account found
51 /// - `Ok(None)` - Account not found
52 /// - `Err(String)` - Database error
53 async fn find_by_id(&self, id: Uuid) -> Result<Option<Account>, String>;
54
55 /// Find account by code within an organization
56 ///
57 /// # Arguments
58 /// * `code` - Account code (e.g., "700", "604001")
59 /// * `organization_id` - Organization ID
60 ///
61 /// # Returns
62 /// - `Ok(Some(Account))` - Account found
63 /// - `Ok(None)` - Account not found
64 /// - `Err(String)` - Database error
65 async fn find_by_code(
66 &self,
67 code: &str,
68 organization_id: Uuid,
69 ) -> Result<Option<Account>, String>;
70
71 /// Find all accounts for an organization
72 ///
73 /// # Arguments
74 /// * `organization_id` - Organization ID
75 ///
76 /// # Returns
77 /// - `Ok(Vec<Account>)` - All accounts for the organization (can be empty)
78 /// - `Err(String)` - Database error
79 ///
80 /// Note: Results are ordered by code ASC for hierarchical display
81 async fn find_by_organization(&self, organization_id: Uuid) -> Result<Vec<Account>, String>;
82
83 /// Find accounts by type within an organization
84 ///
85 /// # Arguments
86 /// * `account_type` - Account type (Asset, Liability, Expense, Revenue, OffBalance)
87 /// * `organization_id` - Organization ID
88 ///
89 /// # Returns
90 /// - `Ok(Vec<Account>)` - Matching accounts (can be empty)
91 /// - `Err(String)` - Database error
92 ///
93 /// Useful for generating financial reports:
94 /// - Balance sheet: Asset + Liability accounts
95 /// - Income statement: Expense + Revenue accounts
96 async fn find_by_type(
97 &self,
98 account_type: AccountType,
99 organization_id: Uuid,
100 ) -> Result<Vec<Account>, String>;
101
102 /// Find child accounts of a parent account
103 ///
104 /// # Arguments
105 /// * `parent_code` - Parent account code (e.g., "60" to find "600", "604", etc.)
106 /// * `organization_id` - Organization ID
107 ///
108 /// # Returns
109 /// - `Ok(Vec<Account>)` - Direct children of the parent account (can be empty)
110 /// - `Err(String)` - Database error
111 ///
112 /// Note: Returns only direct children, not all descendants
113 async fn find_by_parent_code(
114 &self,
115 parent_code: &str,
116 organization_id: Uuid,
117 ) -> Result<Vec<Account>, String>;
118
119 /// Find accounts that can be used directly in transactions
120 ///
121 /// # Arguments
122 /// * `organization_id` - Organization ID
123 ///
124 /// # Returns
125 /// - `Ok(Vec<Account>)` - Accounts with direct_use = true (can be empty)
126 /// - `Err(String)` - Database error
127 ///
128 /// Summary accounts (direct_use = false) cannot be used in journal entries
129 async fn find_direct_use_accounts(&self, organization_id: Uuid)
130 -> Result<Vec<Account>, String>;
131
132 /// Search accounts by code pattern
133 ///
134 /// # Arguments
135 /// * `code_pattern` - SQL LIKE pattern (e.g., "60%", "604%")
136 /// * `organization_id` - Organization ID
137 ///
138 /// # Returns
139 /// - `Ok(Vec<Account>)` - Matching accounts (can be empty)
140 /// - `Err(String)` - Database error
141 ///
142 /// Useful for finding all accounts in a class or sub-class:
143 /// - "6%" - All expenses (class 6)
144 /// - "60%" - All class 60 expenses
145 /// - "604%" - All accounts under 604
146 async fn search_by_code_pattern(
147 &self,
148 code_pattern: &str,
149 organization_id: Uuid,
150 ) -> Result<Vec<Account>, String>;
151
152 /// Update an existing account
153 ///
154 /// # Arguments
155 /// * `account` - Account with updated fields
156 ///
157 /// # Returns
158 /// - `Ok(Account)` - Updated account
159 /// - `Err(String)` - Error message if update fails
160 ///
161 /// # Errors
162 /// - Account not found
163 /// - Code change would create duplicate
164 /// - Parent code does not exist
165 /// - Database constraint violation
166 async fn update(&self, account: &Account) -> Result<Account, String>;
167
168 /// Delete an account
169 ///
170 /// # Arguments
171 /// * `id` - Account ID
172 ///
173 /// # Returns
174 /// - `Ok(())` - Account deleted successfully
175 /// - `Err(String)` - Error message if deletion fails
176 ///
177 /// # Errors
178 /// - Account not found
179 /// - Account has child accounts (cannot delete parent)
180 /// - Account is used in expenses/transactions (referential integrity)
181 ///
182 /// Inspired by Noalyss Acc_Plan_SQL::delete() validation logic
183 async fn delete(&self, id: Uuid) -> Result<(), String>;
184
185 /// Check if an account code exists within an organization
186 ///
187 /// # Arguments
188 /// * `code` - Account code
189 /// * `organization_id` - Organization ID
190 ///
191 /// # Returns
192 /// - `Ok(true)` - Account exists
193 /// - `Ok(false)` - Account does not exist
194 /// - `Err(String)` - Database error
195 async fn exists(&self, code: &str, organization_id: Uuid) -> Result<bool, String>;
196
197 /// Count accounts in an organization
198 ///
199 /// # Arguments
200 /// * `organization_id` - Organization ID
201 ///
202 /// # Returns
203 /// - `Ok(i64)` - Number of accounts
204 /// - `Err(String)` - Database error
205 async fn count_by_organization(&self, organization_id: Uuid) -> Result<i64, String>;
206}