backbone_banking/infrastructure/persistence/
bank_transaction_repository.rs1use anyhow::Result;
13use rust_decimal::Decimal;
14use sqlx::{PgPool, Row};
15use uuid::Uuid;
16
17use backbone_orm::org_scope;
18
19use crate::domain::entity::{BankTransaction, TxnStatus};
20
21pub const TABLE_NAME: &str = "banking.bank_transactions";
23
24pub struct BankTransactionRepository(
29 backbone_orm::GenericCrudRepository<BankTransaction, backbone_orm::SoftDelete>,
30);
31
32impl std::ops::Deref for BankTransactionRepository {
33 type Target = backbone_orm::GenericCrudRepository<BankTransaction, backbone_orm::SoftDelete>;
34 fn deref(&self) -> &Self::Target {
35 &self.0
36 }
37}
38
39impl BankTransactionRepository {
40 pub fn new(pool: PgPool) -> Self {
42 Self(backbone_orm::GenericCrudRepository::new(pool, TABLE_NAME))
43 }
44}
45
46pub struct NewBankTransactionRow<'a> {
52 pub id: Uuid,
53 pub bank_account_id: Uuid,
54 pub import_id: Uuid,
55 pub txn_date: chrono::NaiveDate,
56 pub description: Option<&'a str>,
57 pub reference_no: Option<&'a str>,
58 pub deposit: Decimal,
59 pub withdrawal: Decimal,
60}
61
62pub struct MatchBasisRow {
64 pub deposit: Decimal,
65 pub withdrawal: Decimal,
66 pub reference_no: Option<String>,
67}
68
69pub struct CandidateLineBasisRow {
71 pub bank_account_id: Uuid,
72 pub deposit: Decimal,
73 pub withdrawal: Decimal,
74 pub txn_date: chrono::NaiveDate,
75 pub reference_no: Option<String>,
76}
77
78pub struct ClearingLineRow {
81 pub deposit: Decimal,
82 pub withdrawal: Decimal,
83 pub allocated_amount: Decimal,
85 pub currency: String,
86 pub gl_account_id: Uuid,
88 pub clearing_account_id: Uuid,
90}
91
92pub struct ChargeLineRow {
95 pub deposit: Decimal,
96 pub withdrawal: Decimal,
97 pub allocated_amount: Decimal,
98 pub currency: String,
99 pub gl_account_id: Uuid,
100}
101
102impl BankTransactionRepository {
105 pub async fn insert_transaction(
111 &self,
112 conn: &mut sqlx::PgConnection,
113 t: &NewBankTransactionRow<'_>,
114 ) -> Result<(), sqlx::Error> {
115 sqlx::query(
116 r#"INSERT INTO banking.bank_transactions
117 (id, bank_account_id, import_id, txn_date, description, reference_no,
118 deposit, withdrawal, currency, status, allocated_amount)
119 VALUES ($1,$2,$3,$4,$5,$6,$7,$8,'IDR','unreconciled'::txn_status,0)"#,
120 )
121 .bind(t.id)
122 .bind(t.bank_account_id)
123 .bind(t.import_id)
124 .bind(t.txn_date)
125 .bind(t.description)
126 .bind(t.reference_no)
127 .bind(t.deposit)
128 .bind(t.withdrawal)
129 .execute(conn)
130 .await?;
131 Ok(())
132 }
133
134 pub async fn fetch_match_basis(
141 &self,
142 pool: &PgPool,
143 bank_transaction_id: Uuid,
144 ) -> Result<Option<MatchBasisRow>, sqlx::Error> {
145 let row = org_scope::fetch_optional_row_scoped(
146 pool,
147 sqlx::query(
148 "SELECT deposit, withdrawal, reference_no FROM banking.bank_transactions WHERE id=$1 AND (metadata->>'deleted_at') IS NULL",
149 )
150 .bind(bank_transaction_id),
151 )
152 .await?;
153 Ok(row.map(|r| MatchBasisRow {
154 deposit: r.get("deposit"),
155 withdrawal: r.get("withdrawal"),
156 reference_no: r.get("reference_no"),
157 }))
158 }
159
160 pub async fn fetch_candidate_basis(
164 &self,
165 pool: &PgPool,
166 bank_transaction_id: Uuid,
167 ) -> Result<Option<crate::infrastructure::persistence::CandidateLineBasisRow>, sqlx::Error>
168 {
169 let row = org_scope::fetch_optional_row_scoped(
170 pool,
171 sqlx::query(
172 "SELECT bank_account_id, deposit, withdrawal, txn_date, reference_no FROM banking.bank_transactions WHERE id=$1 AND (metadata->>'deleted_at') IS NULL",
173 )
174 .bind(bank_transaction_id),
175 )
176 .await?;
177 Ok(row.map(
178 |r| crate::infrastructure::persistence::CandidateLineBasisRow {
179 bank_account_id: r.get("bank_account_id"),
180 deposit: r.get("deposit"),
181 withdrawal: r.get("withdrawal"),
182 txn_date: r.get("txn_date"),
183 reference_no: r.get("reference_no"),
184 },
185 ))
186 }
187
188 pub async fn fetch_clearing_line(
192 &self,
193 pool: &PgPool,
194 bank_transaction_id: Uuid,
195 ) -> Result<Option<ClearingLineRow>, sqlx::Error> {
196 let row = org_scope::fetch_optional_row_scoped(
197 pool,
198 sqlx::query(
199 r#"SELECT t.bank_account_id, t.deposit, t.withdrawal, t.allocated_amount, t.currency,
200 a.gl_account_id, a.clearing_account_id
201 FROM banking.bank_transactions t
202 JOIN banking.bank_accounts a ON a.id = t.bank_account_id
203 WHERE t.id=$1 AND (t.metadata->>'deleted_at') IS NULL"#,
204 )
205 .bind(bank_transaction_id),
206 )
207 .await?;
208 Ok(row.map(|r| ClearingLineRow {
209 deposit: r.get("deposit"),
210 withdrawal: r.get("withdrawal"),
211 allocated_amount: r.get("allocated_amount"),
212 currency: r.get("currency"),
213 gl_account_id: r.get("gl_account_id"),
214 clearing_account_id: r.get("clearing_account_id"),
215 }))
216 }
217
218 pub async fn fetch_charge_line(
221 &self,
222 pool: &PgPool,
223 bank_transaction_id: Uuid,
224 ) -> Result<Option<ChargeLineRow>, sqlx::Error> {
225 let row = org_scope::fetch_optional_row_scoped(
226 pool,
227 sqlx::query(
228 r#"SELECT t.deposit, t.withdrawal, t.allocated_amount, t.currency, a.gl_account_id
229 FROM banking.bank_transactions t JOIN banking.bank_accounts a ON a.id=t.bank_account_id
230 WHERE t.id=$1 AND (t.metadata->>'deleted_at') IS NULL"#,
231 )
232 .bind(bank_transaction_id),
233 )
234 .await?;
235 Ok(row.map(|r| ChargeLineRow {
236 deposit: r.get("deposit"),
237 withdrawal: r.get("withdrawal"),
238 allocated_amount: r.get("allocated_amount"),
239 currency: r.get("currency"),
240 gl_account_id: r.get("gl_account_id"),
241 }))
242 }
243
244 pub async fn set_allocation(
257 &self,
258 conn: &mut sqlx::PgConnection,
259 bank_transaction_id: Uuid,
260 allocated_amount: Decimal,
261 status: TxnStatus,
262 ) -> Result<(), sqlx::Error> {
263 sqlx::query(
264 "UPDATE banking.bank_transactions SET allocated_amount=$2, status=$3 WHERE id=$1",
265 )
266 .bind(bank_transaction_id)
267 .bind(allocated_amount)
268 .bind(status)
269 .execute(conn)
270 .await?;
271 Ok(())
272 }
273
274 pub async fn count_open_in_period(
284 &self,
285 pool: &PgPool,
286 bank_account_id: Uuid,
287 from_date: chrono::NaiveDate,
288 to_date: chrono::NaiveDate,
289 ) -> Result<i64, sqlx::Error> {
290 let row = org_scope::fetch_optional_row_scoped(
291 pool,
292 sqlx::query(
293 r#"SELECT count(*) AS open_lines FROM banking.bank_transactions
294 WHERE bank_account_id=$1 AND txn_date BETWEEN $2 AND $3
295 AND status IN ('unreconciled'::txn_status,'partly_reconciled'::txn_status)
296 AND (metadata->>'deleted_at') IS NULL"#,
297 )
298 .bind(bank_account_id)
299 .bind(from_date)
300 .bind(to_date),
301 )
302 .await?;
303 row.map(|r| r.get::<i64, _>("open_lines"))
304 .ok_or(sqlx::Error::RowNotFound)
305 }
306}
307
308backbone_core::impl_crud_repository!(BankTransactionRepository, BankTransaction, soft_delete);