use std::sync::Arc;
use chrono::{Datelike, Utc};
use rust_decimal::{Decimal, RoundingStrategy};
use sqlx::PgPool;
use uuid::Uuid;
use backbone_orm::org_scope;
use crate::domain::gl_posting::PostingLine;
use crate::domain::reconcile_graph::{
EdgeOutcome, LineLocator, LocatorResolution, MatchingGroup, NewPartial, PairRequest,
PartyResidual, ReconcileError, ReconcileLineSnapshot, ORIGIN_MANUAL,
};
use crate::domain::repositories::deferred_tax::DeferredTaxLookup;
use crate::domain::repositories::posting_repository::{PostingRepository, PostingWrite};
use crate::domain::repositories::reconcile_graph_repository::ReconcileGraphRepository;
use crate::domain::services::reconcile_rules;
pub use crate::domain::reconcile_graph::PartialRow;
fn internal(e: anyhow::Error) -> ReconcileError {
ReconcileError::Internal(e.to_string())
}
fn round2(d: Decimal) -> Decimal {
d.round_dp_with_strategy(2, RoundingStrategy::MidpointAwayFromZero)
}
fn res_of(residuals: &[(Uuid, Decimal)], line_id: Uuid) -> Decimal {
residuals
.iter()
.find(|(id, _)| *id == line_id)
.map(|(_, r)| *r)
.unwrap_or(Decimal::ZERO)
}
fn fx_pair(
fx_account: Uuid,
account_id: Uuid,
party_type: Option<String>,
party_id: Option<Uuid>,
diff: Decimal,
) -> (PostingLine, PostingLine) {
let abs = diff.abs();
let desc = Some("exchange difference".to_string());
let fx_dr = diff > Decimal::ZERO;
let (fx_d, fx_c) = if fx_dr {
(abs, Decimal::ZERO)
} else {
(Decimal::ZERO, abs)
};
let (ar_d, ar_c) = if fx_dr {
(Decimal::ZERO, abs)
} else {
(abs, Decimal::ZERO)
};
(
PostingLine {
account_id: fx_account,
debit: fx_d,
credit: fx_c,
party_type: None,
party_id: None,
cost_center_id: None,
project_id: None,
department_id: None,
description: desc.clone(),
},
PostingLine {
account_id,
debit: ar_d,
credit: ar_c,
party_type,
party_id,
cost_center_id: None,
project_id: None,
department_id: None,
description: desc,
},
)
}
#[derive(Clone)]
pub struct ReconcileWriteService {
repo: Arc<dyn ReconcileGraphRepository>,
posting: Arc<dyn PostingRepository>,
pool: PgPool,
exchange_account_id: Option<Uuid>,
deferred_tax: Option<Arc<dyn DeferredTaxLookup>>,
}
fn ambient_company() -> Uuid {
backbone_orm::org_scope::current_org_scope()
.and_then(|s| s.legacy_company_id())
.unwrap_or_default()
}
impl ReconcileWriteService {
pub fn new(
repo: Arc<dyn ReconcileGraphRepository>,
posting: Arc<dyn PostingRepository>,
pool: PgPool,
exchange_account_id: Option<Uuid>,
) -> Self {
Self {
repo,
posting,
pool,
exchange_account_id,
deferred_tax: None,
}
}
pub fn with_deferred_tax(mut self, port: Arc<dyn DeferredTaxLookup>) -> Self {
self.deferred_tax = Some(port);
self
}
pub fn with_deferred_tax_if_set(mut self, port: Option<Arc<dyn DeferredTaxLookup>>) -> Self {
self.deferred_tax = port;
self
}
pub async fn reconcile_pair_on(
&self,
conn: &mut sqlx::PgConnection,
req: &PairRequest,
) -> Result<EdgeOutcome, ReconcileError> {
let mut lines = Vec::with_capacity(2);
for locator in [&req.debit, &req.credit] {
match self
.repo
.lock_line_by_locator(conn, locator)
.await
.map_err(internal)?
{
LocatorResolution::One(l) => lines.push(l),
LocatorResolution::NotFound => return Err(ReconcileError::LineNotFound),
LocatorResolution::Ambiguous(n) => return Err(ReconcileError::AmbiguousLocator(n)),
}
}
let (debit, credit) = (lines[0].clone(), lines[1].clone());
let flags = self
.repo
.account_flags(conn, debit.account_id)
.await
.map_err(internal)?
.ok_or_else(|| ReconcileError::Conflict("account not found".into()))?;
let residuals = self
.repo
.residuals_of(conn, &[debit.id, credit.id])
.await
.map_err(internal)?;
let applied = reconcile_rules::validate_pair(
ambient_company(),
&debit,
&credit,
&flags,
req.amount,
res_of(&residuals, debit.id),
res_of(&residuals, credit.id),
)?;
if applied == Decimal::ZERO {
return Ok(EdgeOutcome {
partial_id: None,
applied: Decimal::ZERO,
full_reconcile_id: None,
});
}
let now = Utc::now();
let max_date = debit.transaction_date.max(credit.transaction_date);
let partial_id = self
.repo
.insert_partial(
conn,
&NewPartial {
company_id: ambient_company(),
debit_move_id: debit.id,
credit_move_id: credit.id,
amount: applied,
currency: debit.currency.clone(),
max_date,
origin: req.origin.clone(),
source_type: Some(req.origin.clone()),
source_id: credit.source_id,
metadata: serde_json::json!({
"actor": req.actor,
"debit_source": debit.source_type,
"credit_source": credit.source_type,
}),
},
)
.await
.map_err(internal)?;
let mut exchange_total = Decimal::ZERO;
if debit.exchange_rate != credit.exchange_rate {
let after = self
.repo
.residuals_of(conn, &[debit.id, credit.id])
.await
.map_err(internal)?;
let res_d = res_of(&after, debit.id);
let res_c = res_of(&after, credit.id);
let (hi, lo) = if debit.exchange_rate > credit.exchange_rate {
(debit.exchange_rate, credit.exchange_rate)
} else {
(credit.exchange_rate, debit.exchange_rate)
};
let predicted = round2(applied * (hi - lo) / lo);
let tolerance = Decimal::new(5, 3); let diff = if res_c == Decimal::ZERO
&& res_d > Decimal::ZERO
&& (res_d - predicted).abs() <= tolerance
{
Some(res_d)
} else if res_d == Decimal::ZERO
&& res_c > Decimal::ZERO
&& (res_c - predicted).abs() <= tolerance
{
Some(-res_c)
} else {
None
};
if let Some(diff) = diff {
let journal_id = self
.post_exchange_move(conn, req, &debit, partial_id, diff, max_date)
.await?;
self.repo
.set_exchange_move(conn, partial_id, journal_id)
.await
.map_err(internal)?;
let exch_line = self
.repo
.journal_lines_with_ids(conn, journal_id)
.await
.map_err(internal)?
.into_iter()
.find(|(_, l)| l.account_id == debit.account_id)
.map(|(id, _)| id)
.ok_or_else(|| {
ReconcileError::Internal("exchange journal missing account line".into())
})?;
let (d_id, c_id) = if diff > Decimal::ZERO {
(debit.id, exch_line)
} else {
(exch_line, credit.id)
};
self.repo
.insert_partial(
conn,
&NewPartial {
company_id: ambient_company(),
debit_move_id: d_id,
credit_move_id: c_id,
amount: diff.abs(),
currency: debit.currency.clone(),
max_date,
origin: req.origin.clone(),
source_type: Some(req.origin.clone()),
source_id: Some(partial_id),
metadata: serde_json::json!({
"rule": "exchange_pair",
"parent_partial": partial_id,
}),
},
)
.await
.map_err(internal)?;
exchange_total = diff;
}
}
self.post_cash_basis_move(
conn,
req,
&debit,
partial_id,
applied,
res_of(&residuals, debit.id),
max_date,
)
.await?;
self.post_cash_basis_move(
conn,
req,
&credit,
partial_id,
applied,
res_of(&residuals, credit.id),
max_date,
)
.await?;
let full = self
.complete_group_for(
conn,
&[debit.id, credit.id],
exchange_total,
now,
)
.await
.map_err(internal)?;
self.pair_reversal_counterparts(conn, &[debit.id, credit.id])
.await
.map_err(internal)?;
Ok(EdgeOutcome {
partial_id: Some(partial_id),
applied,
full_reconcile_id: full,
})
}
async fn post_exchange_move(
&self,
conn: &mut sqlx::PgConnection,
req: &PairRequest,
debit: &ReconcileLineSnapshot,
partial_id: Uuid,
diff: Decimal,
posting_date: chrono::NaiveDate,
) -> Result<Uuid, ReconcileError> {
let fx = self
.exchange_account_id
.ok_or(ReconcileError::ExchangeAccountUnconfigured)?;
if self
.repo
.period_closed(conn, posting_date)
.await
.map_err(internal)?
{
return Err(ReconcileError::PeriodClosed);
}
let (fx_line, ar_line) = fx_pair(
fx,
debit.account_id,
debit.party_type.clone(),
debit.party_id,
diff,
);
let write = PostingWrite {
company_id: ambient_company(),
branch_id: None,
source_type: "reconciliation".to_string(),
source_id: partial_id,
source_reference: Some(format!("exch:{partial_id}")),
posting_date,
fiscal_period_id: None,
fiscal_year: posting_date.year(),
fiscal_month: posting_date.month() as i32,
currency: debit.currency.clone(),
posting_type: "adjustment".to_string(),
reverses_post_id: None,
reverses_journal_id: None,
description: Some("exchange difference on reconciliation".into()),
idempotency_key: Some(format!("exch:{partial_id}")),
posted_by: req.actor,
now: Utc::now(),
lines: vec![fx_line, ar_line],
};
let commit = self
.posting
.commit_posting_on(conn, write)
.await
.map_err(internal)?;
Ok(commit.journal_id)
}
async fn post_cash_basis_move(
&self,
conn: &mut sqlx::PgConnection,
req: &PairRequest,
side: &ReconcileLineSnapshot,
partial_id: Uuid,
applied: Decimal,
res_before: Decimal,
max_date: chrono::NaiveDate,
) -> Result<(), ReconcileError> {
if !matches!(
side.account_subtype.as_str(),
"accounts_receivable" | "accounts_payable"
) {
return Ok(());
}
let Some(port) = self.deferred_tax.as_ref() else {
return Ok(()); };
let base = side.base_amount;
if base == Decimal::ZERO {
return Ok(()); }
let deferred = port
.deferred_lines_on(
conn,
side.journal_id,
side.source_type.as_deref(),
side.source_id,
)
.await
.map_err(internal)?;
if deferred.is_empty() {
return Ok(()); }
let cum_applied = base - res_before + applied;
let anchors: std::collections::HashMap<Uuid, Decimal> = self
.repo
.residuals_of(
conn,
&deferred
.iter()
.map(|d| d.source_line_id)
.collect::<Vec<_>>(),
)
.await
.map_err(internal)?
.into_iter()
.collect();
let mut flips = Vec::with_capacity(deferred.len());
for d in deferred {
let target = round2(d.amount * cum_applied / base);
let already = d.amount - anchors.get(&d.source_line_id).copied().unwrap_or(d.amount);
let amt = target - already;
if amt > Decimal::ZERO {
flips.push((d, amt));
}
}
if flips.is_empty() {
return Ok(());
}
if self
.repo
.period_closed(conn, max_date)
.await
.map_err(internal)?
{
return Err(ReconcileError::PeriodClosed);
}
let desc = "tax exigibility flip on payment".to_string();
let mut lines = Vec::with_capacity(flips.len() * 2);
for (d, amt) in &flips {
let (real_d, real_c) = if d.is_debit {
(*amt, Decimal::ZERO)
} else {
(Decimal::ZERO, *amt)
};
let (tr_d, tr_c) = if d.is_debit {
(Decimal::ZERO, *amt)
} else {
(*amt, Decimal::ZERO)
};
lines.push(PostingLine {
account_id: d.real_account_id,
debit: real_d,
credit: real_c,
party_type: None,
party_id: None,
cost_center_id: None,
project_id: None,
department_id: None,
description: Some(desc.clone()),
});
lines.push(PostingLine {
account_id: d.transition_account_id,
debit: tr_d,
credit: tr_c,
party_type: None,
party_id: None,
cost_center_id: None,
project_id: None,
department_id: None,
description: Some(desc.clone()),
});
}
let write = PostingWrite {
company_id: ambient_company(),
branch_id: None,
source_type: "reconciliation".to_string(),
source_id: partial_id,
source_reference: Some(format!("caba:{partial_id}:{}", side.id)),
posting_date: max_date,
fiscal_period_id: None,
fiscal_year: max_date.year(),
fiscal_month: max_date.month() as i32,
currency: side.currency.clone(),
posting_type: "adjustment".to_string(),
reverses_post_id: None,
reverses_journal_id: None,
description: Some("cash-basis tax exigibility flip".into()),
idempotency_key: Some(format!("caba:{partial_id}:{}", side.id)),
posted_by: req.actor,
now: Utc::now(),
lines,
};
let commit = self
.posting
.commit_posting_on(conn, write)
.await
.map_err(internal)?;
let journal_id = commit.journal_id;
let legs = self
.repo
.journal_lines_with_ids(conn, journal_id)
.await
.map_err(internal)?;
let mut used = std::collections::HashSet::new();
for (d, amt) in &flips {
let mirrored_credit = d.is_debit;
let leg = legs
.iter()
.find(|(id, l)| {
!used.contains(id)
&& l.account_id == d.transition_account_id
&& if mirrored_credit {
l.credit > Decimal::ZERO
} else {
l.debit > Decimal::ZERO
}
})
.map(|(id, _)| *id)
.ok_or_else(|| {
ReconcileError::Internal("flip journal missing transition leg".into())
})?;
used.insert(leg);
let (debit_move_id, credit_move_id) = if d.is_debit {
(d.source_line_id, leg)
} else {
(leg, d.source_line_id)
};
self.repo
.insert_partial(
conn,
&NewPartial {
company_id: ambient_company(),
debit_move_id,
credit_move_id,
amount: *amt,
currency: side.currency.clone(),
max_date,
origin: req.origin.clone(),
source_type: Some(req.origin.clone()),
source_id: Some(partial_id),
metadata: serde_json::json!({
"rule": "caba_pair",
"parent_partial": partial_id,
}),
},
)
.await
.map_err(internal)?;
}
Ok(())
}
async fn complete_group_for(
&self,
conn: &mut sqlx::PgConnection,
seeds: &[Uuid],
exchange_total: Decimal,
now: chrono::DateTime<Utc>,
) -> anyhow::Result<Option<Uuid>> {
let comp_lines = self
.repo
.component_line_ids(conn, seeds)
.await?;
if comp_lines.is_empty() {
return Ok(None);
}
self.repo.lock_lines(conn, &comp_lines).await?;
let residuals = self
.repo
.residuals_of(conn, &comp_lines)
.await?;
if residuals.iter().any(|(_, r)| *r != Decimal::ZERO) {
return Ok(None);
}
let stamps = self
.repo
.distinct_group_stamps(conn, &comp_lines)
.await?;
let group = match stamps.as_slice() {
[] => {
self.repo
.create_full_group(conn, exchange_total, now)
.await?
}
[existing] => *existing,
_ => return Ok(None),
};
let comp_partials = self
.repo
.component_partial_ids(conn, &comp_lines)
.await?;
self.repo
.attach_group(conn, group, &comp_lines, &comp_partials, now)
.await?;
Ok(Some(group))
}
async fn pair_reversal_counterparts(
&self,
conn: &mut sqlx::PgConnection,
line_ids: &[Uuid],
) -> anyhow::Result<()> {
for id in line_ids {
let line = self.repo.lock_line(conn, *id).await?;
let Some(line) = line else { continue };
if line.source_type.as_deref() == Some("reconciliation") {
continue;
}
let residual = res_of(
&self.repo.residuals_of(conn, &[line.id]).await?,
line.id,
);
if !reconcile_rules::fully_unapplied(&line, residual) {
continue;
}
if let Some(counterpart) = self
.repo
.reversal_counterpart(conn, &line)
.await?
{
let c_residual = res_of(
&self
.repo
.residuals_of(conn, &[counterpart.id])
.await?,
counterpart.id,
);
if !reconcile_rules::fully_unapplied(&counterpart, c_residual) {
continue;
}
let (d, c) = if line.debit_amount > Decimal::ZERO {
(line.clone(), counterpart)
} else {
(counterpart, line.clone())
};
self.repo
.insert_partial(
conn,
&NewPartial {
company_id: ambient_company(),
debit_move_id: d.id,
credit_move_id: c.id,
amount: d.base_amount.min(c.base_amount),
currency: d.currency.clone(),
max_date: d.transaction_date.max(c.transaction_date),
origin: ORIGIN_MANUAL.to_string(),
source_type: Some(ORIGIN_MANUAL.to_string()),
source_id: d.source_id,
metadata: serde_json::json!({"rule": "reverse_then_reconcile"}),
},
)
.await?;
self.complete_group_for(conn, &[d.id, c.id], Decimal::ZERO, Utc::now())
.await?;
}
}
Ok(())
}
pub async fn unreconcile_pair_on(
&self,
conn: &mut sqlx::PgConnection,
debit: &LineLocator,
credit: &LineLocator,
) -> Result<(), ReconcileError> {
let mut lines = Vec::with_capacity(2);
for locator in [debit, credit] {
match self
.repo
.lock_line_by_locator(conn, locator)
.await
.map_err(internal)?
{
LocatorResolution::One(l) => lines.push(l),
LocatorResolution::NotFound => return Err(ReconcileError::LineNotFound),
LocatorResolution::Ambiguous(n) => return Err(ReconcileError::AmbiguousLocator(n)),
}
}
let mut closure = self
.repo
.partials_between(conn, lines[0].id, lines[1].id)
.await
.map_err(internal)?;
if closure.is_empty() {
return Ok(());
}
let mut seen: Vec<Uuid> = closure.iter().map(|p| p.id).collect();
for p in closure.clone() {
for d in self
.repo
.derived_partials(conn, p.id)
.await
.map_err(internal)?
{
if !seen.contains(&d.id) {
seen.push(d.id);
closure.push(d);
}
}
}
self.unlink_partials(conn, &closure).await
}
pub async fn unreconcile_on(
&self,
conn: &mut sqlx::PgConnection,
partial_id: Uuid,
actor: Option<Uuid>,
) -> Result<(), ReconcileError> {
let partial = self
.repo
.load_partial(conn, partial_id)
.await
.map_err(internal)?
.ok_or(ReconcileError::LineNotFound)?;
self.repo
.lock_lines(
conn,
&[partial.debit_move_id, partial.credit_move_id],
)
.await
.map_err(internal)?;
let mut closure = vec![partial.clone()];
for d in self
.repo
.derived_partials(conn, partial.id)
.await
.map_err(internal)?
{
if d.id != partial.id {
closure.push(d);
}
}
self.unlink_partials_with_actor(conn, &closure, actor)
.await
}
async fn unlink_partials(
&self,
conn: &mut sqlx::PgConnection,
closure: &[PartialRow],
) -> Result<(), ReconcileError> {
self.unlink_partials_with_actor(conn, closure, None)
.await
}
async fn unlink_partials_with_actor(
&self,
conn: &mut sqlx::PgConnection,
closure: &[PartialRow],
actor: Option<Uuid>,
) -> Result<(), ReconcileError> {
let ids: Vec<Uuid> = closure.iter().map(|p| p.id).collect();
let journal_ids = self
.repo
.generated_journal_ids(conn, &ids)
.await
.map_err(internal)?;
for jid in journal_ids {
self.reverse_generated_journal(conn, jid, actor)
.await?;
}
let mut affected_groups: Vec<Uuid> =
closure.iter().filter_map(|p| p.full_reconcile_id).collect();
affected_groups.sort_unstable();
affected_groups.dedup();
let mut affected_lines: Vec<Uuid> = closure
.iter()
.flat_map(|p| [p.debit_move_id, p.credit_move_id])
.collect();
affected_lines.sort_unstable();
affected_lines.dedup();
self.repo
.delete_partials(conn, &ids)
.await
.map_err(internal)?;
self.repo
.clear_line_flags(conn, &affected_lines)
.await
.map_err(internal)?;
for group in &affected_groups {
let survivors = self
.repo
.group_partial_ids(conn, *group)
.await
.map_err(internal)?;
if survivors.is_empty() {
self.repo
.dissolve_group(conn, *group)
.await
.map_err(internal)?;
}
}
self.complete_group_for(conn, &affected_lines, Decimal::ZERO, Utc::now())
.await
.map_err(internal)?;
self.pair_reversal_counterparts(conn, &affected_lines)
.await
.map_err(internal)?;
Ok(())
}
async fn reverse_generated_journal(
&self,
conn: &mut sqlx::PgConnection,
journal_id: Uuid,
actor: Option<Uuid>,
) -> Result<(), ReconcileError> {
let Some(meta) = self
.repo
.journal_reversal_meta(conn, journal_id)
.await
.map_err(internal)?
else {
return Ok(());
};
let lines = self
.repo
.journal_lines_with_ids(conn, journal_id)
.await
.map_err(internal)?;
if lines.is_empty() {
return Ok(());
}
let swapped: Vec<PostingLine> = lines
.iter()
.map(|(_, l)| PostingLine {
account_id: l.account_id,
debit: l.credit,
credit: l.debit,
party_type: l.party_type.clone(),
party_id: l.party_id,
cost_center_id: l.cost_center_id,
project_id: l.project_id,
department_id: l.department_id,
description: l.description.clone(),
})
.collect();
let today = Utc::now().date_naive();
let write = PostingWrite {
company_id: ambient_company(),
branch_id: meta.branch_id,
source_type: "reconciliation".to_string(),
source_id: meta.journal_id,
source_reference: Some(format!("unlink:{journal_id}")),
posting_date: today,
fiscal_period_id: None,
fiscal_year: today.year(),
fiscal_month: today.month() as i32,
currency: meta.currency,
posting_type: "reversal".to_string(),
reverses_post_id: meta.reverses_post_id,
reverses_journal_id: Some(meta.journal_id),
description: Some("reconciliation unlink reversal".into()),
idempotency_key: Some(format!("unlink:{journal_id}")),
posted_by: actor,
now: Utc::now(),
lines: swapped,
};
self.posting
.commit_posting_on(conn, write)
.await
.map_err(internal)?;
Ok(())
}
pub async fn matching_group_on(
&self,
conn: &mut sqlx::PgConnection,
line_id: Uuid,
) -> Result<MatchingGroup, ReconcileError> {
self.repo
.matching_group(conn, line_id)
.await
.map_err(internal)
}
pub async fn residuals_for_party_on(
&self,
conn: &mut sqlx::PgConnection,
account_id: Uuid,
party_type: &str,
party_id: Uuid,
) -> Result<Vec<PartyResidual>, ReconcileError> {
self.repo
.residuals_for_party(conn, account_id, party_type, party_id)
.await
.map_err(internal)
}
pub async fn reconcile_pair(&self, req: &PairRequest) -> Result<EdgeOutcome, ReconcileError> {
let mut tx = self.pool.begin().await.map_err(|e| internal(e.into()))?;
if let Some(scope) = org_scope::current_org_scope() {
org_scope::bind_org_scope_on(&mut tx, &scope)
.await
.map_err(|e| internal(e.into()))?;
backbone_orm::audit_context::relay_ambient_audit_on(&mut tx)
.await
.map_err(|e| internal(e.into()))?;
}
let out = self.reconcile_pair_on(&mut tx, req).await?;
tx.commit().await.map_err(|e| internal(e.into()))?;
Ok(out)
}
pub async fn unreconcile(
&self,
partial_id: Uuid,
actor: Option<Uuid>,
) -> Result<(), ReconcileError> {
let mut tx = self.pool.begin().await.map_err(|e| internal(e.into()))?;
if let Some(scope) = org_scope::current_org_scope() {
org_scope::bind_org_scope_on(&mut tx, &scope)
.await
.map_err(|e| internal(e.into()))?;
backbone_orm::audit_context::relay_ambient_audit_on(&mut tx)
.await
.map_err(|e| internal(e.into()))?;
}
self.unreconcile_on(&mut tx, partial_id, actor)
.await?;
tx.commit().await.map_err(|e| internal(e.into()))?;
Ok(())
}
pub async fn matching_group(
&self,
line_id: Uuid,
) -> Result<MatchingGroup, ReconcileError> {
let mut tx = self.pool.begin().await.map_err(|e| internal(e.into()))?;
if let Some(scope) = org_scope::current_org_scope() {
org_scope::bind_org_scope_on(&mut tx, &scope)
.await
.map_err(|e| internal(e.into()))?;
backbone_orm::audit_context::relay_ambient_audit_on(&mut tx)
.await
.map_err(|e| internal(e.into()))?;
}
let group = self.matching_group_on(&mut tx, line_id).await?;
tx.commit().await.map_err(|e| internal(e.into()))?;
Ok(group)
}
pub async fn residuals_for_party(
&self,
account_id: Uuid,
party_type: &str,
party_id: Uuid,
) -> Result<Vec<PartyResidual>, ReconcileError> {
let mut tx = self.pool.begin().await.map_err(|e| internal(e.into()))?;
if let Some(scope) = org_scope::current_org_scope() {
org_scope::bind_org_scope_on(&mut tx, &scope)
.await
.map_err(|e| internal(e.into()))?;
backbone_orm::audit_context::relay_ambient_audit_on(&mut tx)
.await
.map_err(|e| internal(e.into()))?;
}
let rows = self
.residuals_for_party_on(&mut tx, account_id, party_type, party_id)
.await?;
tx.commit().await.map_err(|e| internal(e.into()))?;
Ok(rows)
}
}