use anyhow::Result;
use rust_decimal::Decimal;
use sqlx::{PgPool, Row};
use uuid::Uuid;
use backbone_orm::org_scope;
use crate::domain::entity::{BankTransaction, TxnStatus};
pub const TABLE_NAME: &str = "banking.bank_transactions";
pub struct BankTransactionRepository(
backbone_orm::GenericCrudRepository<BankTransaction, backbone_orm::SoftDelete>,
);
impl std::ops::Deref for BankTransactionRepository {
type Target = backbone_orm::GenericCrudRepository<BankTransaction, backbone_orm::SoftDelete>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl BankTransactionRepository {
pub fn new(pool: PgPool) -> Self {
Self(backbone_orm::GenericCrudRepository::new(pool, TABLE_NAME))
}
}
pub struct NewBankTransactionRow<'a> {
pub id: Uuid,
pub bank_account_id: Uuid,
pub import_id: Uuid,
pub txn_date: chrono::NaiveDate,
pub description: Option<&'a str>,
pub reference_no: Option<&'a str>,
pub deposit: Decimal,
pub withdrawal: Decimal,
}
pub struct MatchBasisRow {
pub deposit: Decimal,
pub withdrawal: Decimal,
pub reference_no: Option<String>,
}
pub struct CandidateLineBasisRow {
pub bank_account_id: Uuid,
pub deposit: Decimal,
pub withdrawal: Decimal,
pub txn_date: chrono::NaiveDate,
pub reference_no: Option<String>,
}
pub struct ClearingLineRow {
pub deposit: Decimal,
pub withdrawal: Decimal,
pub allocated_amount: Decimal,
pub currency: String,
pub gl_account_id: Uuid,
pub clearing_account_id: Uuid,
}
pub struct ChargeLineRow {
pub deposit: Decimal,
pub withdrawal: Decimal,
pub allocated_amount: Decimal,
pub currency: String,
pub gl_account_id: Uuid,
}
impl BankTransactionRepository {
pub async fn insert_transaction(
&self,
conn: &mut sqlx::PgConnection,
t: &NewBankTransactionRow<'_>,
) -> Result<(), sqlx::Error> {
sqlx::query(
r#"INSERT INTO banking.bank_transactions
(id, bank_account_id, import_id, txn_date, description, reference_no,
deposit, withdrawal, currency, status, allocated_amount)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,'IDR','unreconciled'::txn_status,0)"#,
)
.bind(t.id)
.bind(t.bank_account_id)
.bind(t.import_id)
.bind(t.txn_date)
.bind(t.description)
.bind(t.reference_no)
.bind(t.deposit)
.bind(t.withdrawal)
.execute(conn)
.await?;
Ok(())
}
pub async fn fetch_match_basis(
&self,
pool: &PgPool,
bank_transaction_id: Uuid,
) -> Result<Option<MatchBasisRow>, sqlx::Error> {
let row = org_scope::fetch_optional_row_scoped(
pool,
sqlx::query(
"SELECT deposit, withdrawal, reference_no FROM banking.bank_transactions WHERE id=$1 AND (metadata->>'deleted_at') IS NULL",
)
.bind(bank_transaction_id),
)
.await?;
Ok(row.map(|r| MatchBasisRow {
deposit: r.get("deposit"),
withdrawal: r.get("withdrawal"),
reference_no: r.get("reference_no"),
}))
}
pub async fn fetch_candidate_basis(
&self,
pool: &PgPool,
bank_transaction_id: Uuid,
) -> Result<Option<crate::infrastructure::persistence::CandidateLineBasisRow>, sqlx::Error>
{
let row = org_scope::fetch_optional_row_scoped(
pool,
sqlx::query(
"SELECT bank_account_id, deposit, withdrawal, txn_date, reference_no FROM banking.bank_transactions WHERE id=$1 AND (metadata->>'deleted_at') IS NULL",
)
.bind(bank_transaction_id),
)
.await?;
Ok(row.map(
|r| crate::infrastructure::persistence::CandidateLineBasisRow {
bank_account_id: r.get("bank_account_id"),
deposit: r.get("deposit"),
withdrawal: r.get("withdrawal"),
txn_date: r.get("txn_date"),
reference_no: r.get("reference_no"),
},
))
}
pub async fn fetch_clearing_line(
&self,
pool: &PgPool,
bank_transaction_id: Uuid,
) -> Result<Option<ClearingLineRow>, sqlx::Error> {
let row = org_scope::fetch_optional_row_scoped(
pool,
sqlx::query(
r#"SELECT t.bank_account_id, t.deposit, t.withdrawal, t.allocated_amount, t.currency,
a.gl_account_id, a.clearing_account_id
FROM banking.bank_transactions t
JOIN banking.bank_accounts a ON a.id = t.bank_account_id
WHERE t.id=$1 AND (t.metadata->>'deleted_at') IS NULL"#,
)
.bind(bank_transaction_id),
)
.await?;
Ok(row.map(|r| ClearingLineRow {
deposit: r.get("deposit"),
withdrawal: r.get("withdrawal"),
allocated_amount: r.get("allocated_amount"),
currency: r.get("currency"),
gl_account_id: r.get("gl_account_id"),
clearing_account_id: r.get("clearing_account_id"),
}))
}
pub async fn fetch_charge_line(
&self,
pool: &PgPool,
bank_transaction_id: Uuid,
) -> Result<Option<ChargeLineRow>, sqlx::Error> {
let row = org_scope::fetch_optional_row_scoped(
pool,
sqlx::query(
r#"SELECT t.deposit, t.withdrawal, t.allocated_amount, t.currency, a.gl_account_id
FROM banking.bank_transactions t JOIN banking.bank_accounts a ON a.id=t.bank_account_id
WHERE t.id=$1 AND (t.metadata->>'deleted_at') IS NULL"#,
)
.bind(bank_transaction_id),
)
.await?;
Ok(row.map(|r| ChargeLineRow {
deposit: r.get("deposit"),
withdrawal: r.get("withdrawal"),
allocated_amount: r.get("allocated_amount"),
currency: r.get("currency"),
gl_account_id: r.get("gl_account_id"),
}))
}
pub async fn set_allocation(
&self,
conn: &mut sqlx::PgConnection,
bank_transaction_id: Uuid,
allocated_amount: Decimal,
status: TxnStatus,
) -> Result<(), sqlx::Error> {
sqlx::query(
"UPDATE banking.bank_transactions SET allocated_amount=$2, status=$3 WHERE id=$1",
)
.bind(bank_transaction_id)
.bind(allocated_amount)
.bind(status)
.execute(conn)
.await?;
Ok(())
}
pub async fn count_open_in_period(
&self,
pool: &PgPool,
bank_account_id: Uuid,
from_date: chrono::NaiveDate,
to_date: chrono::NaiveDate,
) -> Result<i64, sqlx::Error> {
let row = org_scope::fetch_optional_row_scoped(
pool,
sqlx::query(
r#"SELECT count(*) AS open_lines FROM banking.bank_transactions
WHERE bank_account_id=$1 AND txn_date BETWEEN $2 AND $3
AND status IN ('unreconciled'::txn_status,'partly_reconciled'::txn_status)
AND (metadata->>'deleted_at') IS NULL"#,
)
.bind(bank_account_id)
.bind(from_date)
.bind(to_date),
)
.await?;
row.map(|r| r.get::<i64, _>("open_lines"))
.ok_or(sqlx::Error::RowNotFound)
}
}
backbone_core::impl_crud_repository!(BankTransactionRepository, BankTransaction, soft_delete);