backbone_banking/application/service/
banking_statement.rs1use backbone_orm::org_scope;
15use rust_decimal::Decimal;
16use uuid::Uuid;
17
18use crate::infrastructure::persistence::{NewBankTransactionRow, NewStatementImportRow};
19
20use super::banking_events::{BankStatementImported, BankingEvent};
21use super::banking_write_service::{
22 money, BankingError, BankingWriteService, MatchCandidate, NewStatementImport,
23};
24
25impl BankingWriteService {
26 pub async fn import_statement(&self, imp: NewStatementImport) -> Result<Uuid, BankingError> {
29 if imp.lines.is_empty() {
30 return Err(BankingError::EmptyStatement);
31 }
32 let mut net = Decimal::ZERO;
33 for l in &imp.lines {
34 if l.deposit < Decimal::ZERO || l.withdrawal < Decimal::ZERO {
35 return Err(BankingError::NegativeAmount);
36 }
37 net += l.deposit - l.withdrawal;
38 }
39 let computed = money(imp.opening_balance + net);
40 let closing = money(imp.closing_balance);
41 if computed != closing {
42 return Err(BankingError::BalanceMismatch {
43 expected: closing,
44 computed,
45 });
46 }
47 let id = Uuid::new_v4();
48 let fmt = imp.source_format.clone().unwrap_or_else(|| "manual".into());
49 let mut tx = self.rpool().begin().await?;
54 if let Some(scope) = org_scope::current_org_scope() {
55 org_scope::bind_org_scope_on(&mut tx, &scope).await?;
56 backbone_orm::audit_context::relay_ambient_audit_on(&mut tx).await?;
57 }
58 self.repos
59 .imports
60 .insert_import(
61 &mut tx,
62 &NewStatementImportRow {
63 id,
64 bank_account_id: imp.bank_account_id,
65 source_format: &fmt,
66 statement_period_start: imp.period_start,
67 statement_period_end: imp.period_end,
68 opening_balance: money(imp.opening_balance),
69 closing_balance: closing,
70 file_ref: imp.file_ref.as_deref(),
71 row_count: imp.lines.len() as i32,
72 },
73 )
74 .await?;
75 for l in &imp.lines {
76 self.repos
77 .transactions
78 .insert_transaction(
79 &mut tx,
80 &NewBankTransactionRow {
81 id: Uuid::new_v4(),
82 bank_account_id: imp.bank_account_id,
83 import_id: id,
84 txn_date: l.txn_date,
85 description: l.description.as_deref(),
86 reference_no: l.reference_no.as_deref(),
87 deposit: money(l.deposit),
88 withdrawal: money(l.withdrawal),
89 },
90 )
91 .await?;
92 }
93 tx.commit().await?;
94 self.sink
95 .publish(BankingEvent::BankStatementImported(BankStatementImported {
96 import_id: id,
97 bank_account_id: imp.bank_account_id,
98 row_count: imp.lines.len() as i32,
99 }));
100 Ok(id)
101 }
102
103 pub async fn propose_match(
106 &self,
107 bank_transaction_id: Uuid,
108 candidates: &[MatchCandidate],
109 ) -> Result<Option<MatchCandidate>, BankingError> {
110 let row = self
114 .repos
115 .transactions
116 .fetch_match_basis(&self.rpool(), bank_transaction_id)
117 .await?
118 .ok_or(BankingError::TransactionNotFound(bank_transaction_id))?;
119 let net = row.deposit + row.withdrawal;
120 let txn_ref: Option<String> = row.reference_no;
121 if let Some(c) = candidates
123 .iter()
124 .find(|c| c.amount == net && c.reference.is_some() && c.reference == txn_ref)
125 {
126 return Ok(Some(c.clone()));
127 }
128 Ok(candidates.iter().find(|c| c.amount == net).cloned())
129 }
130}