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}