backbone_accounting/domain/
reconcile_graph.rs1use chrono::{DateTime, NaiveDate, Utc};
22use rust_decimal::Decimal;
23use uuid::Uuid;
24
25#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct LineLocator {
29 pub source_type: String,
32 pub source_id: Uuid,
34 pub account_id: Uuid,
36 pub reversing: bool,
38}
39
40impl LineLocator {
41 pub fn new(source_type: &str, source_id: Uuid, account_id: Uuid) -> Self {
42 Self {
43 source_type: source_type.to_string(),
44 source_id,
45 account_id,
46 reversing: false,
47 }
48 }
49 pub fn reversing(mut self) -> Self {
50 self.reversing = true;
51 self
52 }
53}
54
55pub const ORIGIN_SETTLEMENT: &str = "settlement";
57pub const ORIGIN_CLEARING: &str = "clearing";
58pub const ORIGIN_MANUAL: &str = "manual";
59
60#[derive(Debug, Clone)]
63pub struct ReconcileLineSnapshot {
64 pub id: Uuid,
65 pub journal_id: Uuid,
66 pub company_id: Uuid,
69 pub account_id: Uuid,
70 pub account_subtype: String,
72 pub party_type: Option<String>,
73 pub party_id: Option<Uuid>,
74 pub debit_amount: Decimal,
76 pub credit_amount: Decimal,
77 pub currency: String,
78 pub exchange_rate: Decimal,
80 pub transaction_date: NaiveDate,
81 pub is_posted: bool,
82 pub journal_status: String,
83 pub journal_is_reversing: bool,
84 pub source_type: Option<String>,
85 pub source_id: Option<Uuid>,
86 pub is_reconciled: bool,
87 pub full_reconcile_id: Option<Uuid>,
88 pub base_amount: Decimal,
91}
92
93#[derive(Debug, Clone)]
95pub struct NewPartial {
96 pub company_id: Uuid,
99 pub debit_move_id: Uuid,
100 pub credit_move_id: Uuid,
101 pub amount: Decimal,
103 pub currency: String,
104 pub max_date: NaiveDate,
105 pub origin: String,
106 pub source_type: Option<String>,
107 pub source_id: Option<Uuid>,
108 pub metadata: serde_json::Value,
109}
110
111#[derive(Debug, Clone)]
113pub struct PartialRow {
114 pub id: Uuid,
115 pub company_id: Uuid,
117 pub debit_move_id: Uuid,
118 pub credit_move_id: Uuid,
119 pub amount: Decimal,
120 pub max_date: NaiveDate,
121 pub origin: String,
122 pub full_reconcile_id: Option<Uuid>,
123 pub exchange_move_id: Option<Uuid>,
124 pub source_type: Option<String>,
125 pub source_id: Option<Uuid>,
126}
127
128#[derive(Debug, Clone)]
130pub struct AccountReconcileFlags {
131 pub is_reconcilable: bool,
132 pub subtype: String,
133}
134
135#[derive(Debug)]
142#[allow(clippy::large_enum_variant)]
143pub enum LocatorResolution {
144 One(ReconcileLineSnapshot),
145 NotFound,
146 Ambiguous(usize),
147}
148
149#[derive(Debug, Clone)]
151pub struct MatchingGroup {
152 pub label: String,
155 pub full_reconcile_id: Option<Uuid>,
156 pub line_ids: Vec<Uuid>,
157 pub partial_ids: Vec<Uuid>,
158 pub residuals: Vec<(Uuid, Decimal)>,
160}
161
162impl MatchingGroup {
163 pub fn residual_of(&self, line_id: Uuid) -> Option<Decimal> {
164 self.residuals
165 .iter()
166 .find(|(id, _)| *id == line_id)
167 .map(|(_, r)| *r)
168 }
169}
170
171#[derive(Debug, Clone)]
173pub struct PartyResidual {
174 pub line_id: Uuid,
175 pub journal_id: Uuid,
176 pub journal_number: String,
177 pub transaction_date: NaiveDate,
178 pub source_reference: Option<String>,
179 pub residual: Decimal,
180 pub currency: String,
181 pub is_reconciled: bool,
182}
183
184#[derive(Debug)]
187pub enum ReconcileError {
188 LineNotFound,
189 AmbiguousLocator(usize),
190 SameCompanyRequired,
191 SameAccountRequired,
192 CurrencyMismatch(String, String),
193 AccountNotReconcilable,
194 DirectionMismatch,
195 LineNotPosted,
196 PartyMismatch,
197 ExchangeAccountUnconfigured,
198 PeriodClosed,
199 Conflict(String),
200 Internal(String),
201}
202
203impl ReconcileError {
204 pub fn code(&self) -> &'static str {
205 match self {
206 ReconcileError::LineNotFound => "line_not_found",
207 ReconcileError::AmbiguousLocator(_) => "ambiguous_locator",
208 ReconcileError::SameCompanyRequired => "same_company_required",
209 ReconcileError::SameAccountRequired => "same_account_required",
210 ReconcileError::CurrencyMismatch(..) => "currency_mismatch",
211 ReconcileError::AccountNotReconcilable => "account_not_reconcilable",
212 ReconcileError::DirectionMismatch => "direction_mismatch",
213 ReconcileError::LineNotPosted => "line_not_posted",
214 ReconcileError::PartyMismatch => "party_mismatch",
215 ReconcileError::ExchangeAccountUnconfigured => "exchange_account_unconfigured",
216 ReconcileError::PeriodClosed => "period_closed",
217 ReconcileError::Conflict(_) => "conflict",
218 ReconcileError::Internal(_) => "internal_error",
219 }
220 }
221
222 pub fn http_status(&self) -> u16 {
224 match self {
225 ReconcileError::LineNotFound => 404,
226 ReconcileError::AmbiguousLocator(_) | ReconcileError::Conflict(_) => 409,
227 ReconcileError::Internal(_) => 500,
228 _ => 422,
229 }
230 }
231}
232
233impl std::fmt::Display for ReconcileError {
234 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
235 write!(f, "{}", self.code())
236 }
237}
238impl std::error::Error for ReconcileError {}
239
240#[derive(Debug, Clone)]
244pub struct EdgeOutcome {
245 pub partial_id: Option<Uuid>,
246 pub applied: Decimal,
247 pub full_reconcile_id: Option<Uuid>,
248}
249
250#[derive(Debug, Clone)]
252pub struct PairRequest {
253 pub debit: LineLocator,
254 pub credit: LineLocator,
255 pub amount: Decimal,
256 pub origin: String,
257 pub actor: Option<Uuid>,
258}
259
260pub type ReconciledAt = DateTime<Utc>;