use async_trait::async_trait;
use chrono::NaiveDate;
use rust_decimal::Decimal;
use sqlx::{PgPool, Row};
use uuid::Uuid;
use backbone_orm::org_scope;
use backbone_orm::org_scope::fetch_all_rows_scoped;
use crate::domain::repositories::reporting_repository::{
AccountNodeRow, AccountSumRow, AgedItemRow, GlLineRow, PartyLedgerRow, ReportingRepository,
};
fn as_of_residual_expr(pos: usize) -> String {
format!(
r#"(l.base_debit_amount + l.base_credit_amount)
- COALESCE((SELECT SUM(pr.amount) FROM accounting.partial_reconciles pr
WHERE pr.max_date <= ${pos}
AND (pr.debit_move_id = l.id OR pr.credit_move_id = l.id)), 0)"#
)
}
pub struct SqlxReportingRepository {
pool: PgPool,
}
impl SqlxReportingRepository {
pub fn new(pool: PgPool) -> Self {
Self { pool }
}
}
#[async_trait]
impl ReportingRepository for SqlxReportingRepository {
async fn account_sums(
&self,
lo: Option<NaiveDate>,
hi: NaiveDate,
) -> anyhow::Result<Vec<AccountSumRow>> {
let rows = fetch_all_rows_scoped(
&self.pool,
sqlx::query(
r#"SELECT a.id AS aid, a.account_type::text AS at, a.account_number AS num, a.name AS name,
COALESCE(SUM(l.debit_amount),0) AS dr,
COALESCE(SUM(l.credit_amount),0) AS cr
FROM accounting.accounts a
LEFT JOIN accounting.ledgers l
ON l.account_id = a.id
AND l.posting_date <= $1
AND ($2::date IS NULL OR l.posting_date >= $2)
WHERE a.is_detail = TRUE
AND (a.metadata->>'deleted_at') IS NULL
GROUP BY a.id, a.account_type, a.account_number, a.name
ORDER BY a.account_number"#,
)
.bind(hi)
.bind(lo),
)
.await?;
Ok(rows
.iter()
.map(|r| AccountSumRow {
account_id: r.get("aid"),
account_type: r.get("at"),
account_number: r.get("num"),
name: r.get("name"),
debit: r.get("dr"),
credit: r.get("cr"),
})
.collect())
}
async fn account_directory(&self) -> anyhow::Result<Vec<AccountNodeRow>> {
let rows = fetch_all_rows_scoped(
&self.pool,
sqlx::query(
r#"SELECT id, parent_id, account_number, name, account_type::text AS at,
level, is_header, is_detail
FROM accounting.accounts
WHERE (metadata->>'deleted_at') IS NULL
ORDER BY account_number"#,
),
)
.await?;
Ok(rows
.iter()
.map(|r| AccountNodeRow {
id: r.get("id"),
parent_id: r.get("parent_id"),
account_number: r.get("account_number"),
name: r.get("name"),
account_type: r.get("at"),
level: r.get("level"),
is_header: r.get("is_header"),
is_detail: r.get("is_detail"),
})
.collect())
}
async fn gl_lines(
&self,
account_id: Option<Uuid>,
lo: Option<NaiveDate>,
hi: NaiveDate,
limit: i64,
offset: i64,
) -> anyhow::Result<Vec<GlLineRow>> {
let rows = fetch_all_rows_scoped(
&self.pool,
sqlx::query(
r#"SELECT account_id, account_number, account_name, journal_number,
transaction_date, posting_date, description, reference, currency,
debit_amount, credit_amount, balance_before, balance_after,
party_type::text AS pt, party_id,
source_type, source_reference, is_reconciled
FROM (
SELECT l.account_id, l.account_number, l.account_name, l.journal_number,
l.transaction_date, l.posting_date, l.description, l.reference,
l.currency, l.debit_amount, l.credit_amount, l.sequence_number,
l.id,
COALESCE(SUM(
CASE WHEN l.normal_balance = 'debit'
THEN l.debit_amount - l.credit_amount
ELSE l.credit_amount - l.debit_amount END
) OVER (PARTITION BY l.account_id
ORDER BY l.posting_date, l.sequence_number, l.id
ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING), 0)
AS balance_before,
SUM(
CASE WHEN l.normal_balance = 'debit'
THEN l.debit_amount - l.credit_amount
ELSE l.credit_amount - l.debit_amount END
) OVER (PARTITION BY l.account_id
ORDER BY l.posting_date, l.sequence_number, l.id
ROWS UNBOUNDED PRECEDING)
AS balance_after,
l.party_type::text AS party_type, l.party_id,
l.source_type, l.source_reference, l.is_reconciled
FROM accounting.ledgers l
WHERE ($1::uuid IS NULL OR l.account_id = $1)
) hist
WHERE posting_date <= $2
AND ($3::date IS NULL OR posting_date >= $3)
ORDER BY account_number, account_id, posting_date, sequence_number, id
LIMIT $4 OFFSET $5"#,
)
.bind(account_id)
.bind(hi)
.bind(lo)
.bind(limit)
.bind(offset),
)
.await?;
Ok(rows
.iter()
.map(|r| GlLineRow {
account_id: r.get("account_id"),
account_number: r.get("account_number"),
account_name: r.get("account_name"),
journal_number: r.get("journal_number"),
transaction_date: r.get("transaction_date"),
posting_date: r.get("posting_date"),
description: r.get("description"),
reference: r.get("reference"),
currency: r.get("currency"),
debit: r.get("debit_amount"),
credit: r.get("credit_amount"),
balance_before: r.get("balance_before"),
balance_after: r.get("balance_after"),
party_type: r.get("pt"),
party_id: r.get("party_id"),
source_type: r.get("source_type"),
source_reference: r.get("source_reference"),
is_reconciled: r.get("is_reconciled"),
})
.collect())
}
async fn party_ledger_lines(
&self,
party_type: &str,
party_id: Uuid,
as_of: NaiveDate,
) -> anyhow::Result<Vec<PartyLedgerRow>> {
let sql = format!(
r#"SELECT l.id, l.account_id, l.account_number, l.account_name,
j.journal_number, j.transaction_date, l.description,
l.base_debit_amount, l.base_credit_amount, l.currency,
{residual} AS residual
FROM accounting.journal_lines l
JOIN accounting.journals j ON j.id = l.journal_id
JOIN accounting.accounts a ON a.id = l.account_id
WHERE l.party_type = $1::party_type
AND l.party_id = $2
AND j.transaction_date <= $3
AND a.account_subtype IN ('accounts_receivable'::account_subtype,
'accounts_payable'::account_subtype)
ORDER BY j.transaction_date, l.id"#,
residual = as_of_residual_expr(3)
);
let rows = fetch_all_rows_scoped(
&self.pool,
sqlx::query(&sql)
.bind(party_type)
.bind(party_id)
.bind(as_of),
)
.await?;
Ok(rows
.iter()
.map(|r| PartyLedgerRow {
line_id: r.get("id"),
account_id: r.get("account_id"),
account_number: r.get("account_number"),
account_name: r.get("account_name"),
journal_number: r.get("journal_number"),
transaction_date: r.get("transaction_date"),
description: r.get("description"),
debit: r.get("base_debit_amount"),
credit: r.get("base_credit_amount"),
currency: r.get("currency"),
residual: r.get("residual"),
})
.collect())
}
async fn aged_open_items(
&self,
account_subtype: &str,
as_of: NaiveDate,
) -> anyhow::Result<Vec<AgedItemRow>> {
let sql = format!(
r#"SELECT l.party_type::text AS pt, l.party_id, l.account_id,
l.account_number, j.transaction_date, l.source_reference, l.currency,
{residual} AS residual
FROM accounting.journal_lines l
JOIN accounting.journals j ON j.id = l.journal_id
JOIN accounting.accounts a ON a.id = l.account_id
WHERE a.account_subtype = $1::account_subtype
AND j.transaction_date <= $2
AND l.party_id IS NOT NULL
AND {residual} > 0
ORDER BY l.party_id, j.transaction_date, l.id"#,
residual = as_of_residual_expr(2)
);
let rows = fetch_all_rows_scoped(
&self.pool,
sqlx::query(&sql)
.bind(account_subtype)
.bind(as_of),
)
.await?;
Ok(rows
.iter()
.map(|r| AgedItemRow {
party_type: r.get("pt"),
party_id: r.get("party_id"),
account_id: r.get("account_id"),
account_number: r.get("account_number"),
transaction_date: r.get("transaction_date"),
source_reference: r.get("source_reference"),
currency: r.get("currency"),
residual: r.get("residual"),
})
.collect())
}
}