use rust_decimal::{Decimal, RoundingStrategy};
use sqlx::PgPool;
use uuid::Uuid;
use chrono::NaiveDate;
use crate::infrastructure::persistence::{FxGainLossRepository, NewFxGainLossRow};
#[derive(Debug, Clone)]
pub struct ExchangeRateSnapshot {
pub rate: Decimal,
pub rate_id: Uuid,
pub effective_at: NaiveDate,
}
#[async_trait::async_trait]
pub trait ExchangeRateProvider: Send + Sync {
async fn spot(
&self,
company_id: Uuid,
from_currency: &str,
to_currency: &str,
date: NaiveDate,
) -> Result<ExchangeRateSnapshot, String>;
}
#[derive(Debug)]
pub enum FxError {
Db(sqlx::Error),
}
impl std::fmt::Display for FxError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FxError::Db(e) => write!(f, "db: {e}"),
}
}
}
impl std::error::Error for FxError {}
impl From<sqlx::Error> for FxError {
fn from(e: sqlx::Error) -> Self {
FxError::Db(e)
}
}
#[derive(Clone)]
pub struct FxService {
db_pool: PgPool,
}
#[derive(Debug, Clone, PartialEq)]
pub struct FxResult {
pub gain_loss_id: Uuid,
pub base_amount_delta: Decimal,
pub direction: String, }
impl FxService {
fn rpool(&self) -> sqlx::PgPool {
crate::request_pool::current().unwrap_or_else(|| self.db_pool.clone())
}
pub fn new(db_pool: PgPool) -> Self {
Self { db_pool }
}
pub async fn compute_fx_gain_loss(
&self,
bank_clearance_id: Option<Uuid>,
matched_source_id: Uuid,
currency: &str,
original_rate: Decimal,
realised_rate: Decimal,
foreign_amount: Decimal,
fx_account_id: Uuid,
) -> Result<FxResult, FxError> {
let base_original = money(foreign_amount * original_rate);
let base_realised = money(foreign_amount * realised_rate);
let delta = base_realised - base_original;
let direction = if delta >= Decimal::ZERO {
"gain"
} else {
"loss"
};
let mut tx = self.rpool().begin().await?;
if let Some(scope) = backbone_orm::org_scope::current_org_scope() {
backbone_orm::org_scope::bind_org_scope_on(&mut tx, &scope).await?;
backbone_orm::audit_context::relay_ambient_audit_on(&mut tx).await?;
}
let id = Uuid::new_v4();
let gain_losses = FxGainLossRepository::new(self.rpool().clone());
gain_losses
.insert_gain_loss(
&mut *tx,
&NewFxGainLossRow {
id,
bank_clearance_id,
matched_source_id,
currency,
original_rate,
realised_rate,
base_amount_delta: delta,
direction,
fx_account_id,
},
)
.await?;
tx.commit().await?;
Ok(FxResult {
gain_loss_id: id,
base_amount_delta: delta,
direction: direction.into(),
})
}
}
fn money(v: Decimal) -> Decimal {
v.round_dp_with_strategy(2, RoundingStrategy::MidpointAwayFromZero)
}