Skip to content

Commit

Permalink
feat: overdraft prevention (#1253)
Browse files Browse the repository at this point in the history
* feat: overdraft prevention

* test: overdraft prevention

* fix: entry type names in initiate withdrawal

* fix: create idempotent deposit velocity control

* chore: idempotent limit creation
  • Loading branch information
thevaibhav-dixit authored Jan 15, 2025
1 parent 474dcc5 commit 917aa66
Show file tree
Hide file tree
Showing 10 changed files with 137 additions and 18 deletions.
27 changes: 14 additions & 13 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions core/deposit/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
uuid = { workspace = true }
derive_builder = { workspace = true }
rust_decimal = { workspace = true }
async-trait = { workspace = true }
Expand Down
2 changes: 2 additions & 0 deletions core/deposit/src/ledger/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ pub enum DepositLedgerError {
CalaBalance(#[from] cala_ledger::balance::error::BalanceError),
#[error("DepositLedgerError - CalaTransactionError: {0}")]
CalaTransaction(#[from] cala_ledger::transaction::error::TransactionError),
#[error("DepositLedgerError - CalaVelocityError: {0}")]
CalaVelocity(#[from] cala_ledger::velocity::error::VelocityError),
#[error("DepositLedgerError - ConversionError: {0}")]
ConversionError(#[from] core_money::ConversionError),
#[error("DepositLedgerError - MissingTxMetadata")]
Expand Down
59 changes: 59 additions & 0 deletions core/deposit/src/ledger/mod.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
pub mod error;
mod templates;
mod velocity;

use cala_ledger::{
account::{error::AccountError, *},
tx_template::Params,
velocity::{NewVelocityControl, VelocityControlId},
CalaLedger, Currency, DebitOrCredit, JournalId, TransactionId,
};

use crate::{primitives::UsdCents, DepositAccountBalance};

use error::*;

pub const DEPOSITS_VELOCITY_CONTROL_ID: uuid::Uuid =
uuid::uuid!("00000000-0000-0000-0000-000000000001");

#[derive(Clone)]
pub struct DepositLedger {
cala: CalaLedger,
journal_id: JournalId,
deposit_omnibus_account_id: AccountId,
usd: Currency,
deposit_control_id: VelocityControlId,
}

impl DepositLedger {
Expand All @@ -32,10 +39,25 @@ impl DepositLedger {
templates::CancelWithdraw::init(cala).await?;
templates::ConfirmWithdraw::init(cala).await?;

let overdraft_prevention_id = velocity::OverdraftPrevention::init(cala).await?;

let deposit_control_id = Self::create_deposit_control(cala).await?;

match cala
.velocities()
.add_limit_to_control(deposit_control_id, overdraft_prevention_id)
.await
{
Ok(_)
| Err(cala_ledger::velocity::error::VelocityError::LimitAlreadyAddedToControl) => {}
Err(e) => return Err(e.into()),
}

Ok(Self {
cala: cala.clone(),
journal_id,
deposit_omnibus_account_id,
deposit_control_id,
usd: "USD".parse().expect("Could not parse 'USD'"),
})
}
Expand Down Expand Up @@ -187,4 +209,41 @@ impl DepositLedger {
Err(e) => Err(e.into()),
}
}

pub async fn create_deposit_control(
cala: &CalaLedger,
) -> Result<VelocityControlId, DepositLedgerError> {
let control = NewVelocityControl::builder()
.id(DEPOSITS_VELOCITY_CONTROL_ID)
.name("Deposit Control")
.description("Velocity Control for Deposits")
.build()
.expect("build control");

match cala.velocities().create_control(control).await {
Err(cala_ledger::velocity::error::VelocityError::ControlIdAlreadyExists) => {
Ok(DEPOSITS_VELOCITY_CONTROL_ID.into())
}
Err(e) => Err(e.into()),
Ok(control) => Ok(control.id()),
}
}

pub async fn add_deposit_control_to_account(
&self,
op: &mut cala_ledger::LedgerOperation<'_>,
account_id: impl Into<AccountId>,
) -> Result<(), DepositLedgerError> {
self.cala
.velocities()
.attach_control_to_account_in_op(
op,
self.deposit_control_id,
account_id.into(),
Params::default(),
)
.await?;

Ok(())
}
}
1 change: 0 additions & 1 deletion core/deposit/src/ledger/templates/cancel_withdraw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,6 @@ impl CancelWithdraw {
.build()
.expect("Couldn't build TxInput");
let entries = vec![
// check in graphql/cancel-withdraw the entry type
NewTxTemplateEntry::builder()
.entry_type("'CANCEL_WITHDRAW_PENDING_CR'")
.currency("params.currency")
Expand Down
4 changes: 2 additions & 2 deletions core/deposit/src/ledger/templates/initiate_withdraw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ impl InitiateWithdraw {
.expect("Couldn't build TxInput");
let entries = vec![
NewTxTemplateEntry::builder()
.entry_type("'INITIATE_WITHDRAW_SETTLED_DR'")
.entry_type("'INITIATE_WITHDRAW_SETTLED_CR'")
.currency("params.currency")
.account_id("params.deposit_omnibus_account_id")
.direction("CREDIT")
Expand All @@ -101,7 +101,7 @@ impl InitiateWithdraw {
.build()
.expect("Couldn't build entry"),
NewTxTemplateEntry::builder()
.entry_type("'INITIATE_WITHDRAW_SETTLED_CR'")
.entry_type("'INITIATE_WITHDRAW_SETTLED_DR'")
.currency("params.currency")
.account_id("params.credit_account_id")
.direction("DEBIT")
Expand Down
3 changes: 3 additions & 0 deletions core/deposit/src/ledger/velocity/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mod overdraft_prevention;

pub use overdraft_prevention::*;
41 changes: 41 additions & 0 deletions core/deposit/src/ledger/velocity/overdraft_prevention.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
use tracing::instrument;

use cala_ledger::{velocity::*, *};

use crate::ledger::error::*;

pub struct OverdraftPrevention;

const OVERDRAFT_PREVENTION_ID: uuid::Uuid = uuid::uuid!("00000000-0000-0000-0000-000000000001");

impl OverdraftPrevention {
#[instrument(name = "ledger.overdraft_prevention.init", skip_all)]
pub async fn init(ledger: &CalaLedger) -> Result<VelocityLimitId, DepositLedgerError> {
let limit = NewVelocityLimit::builder()
.id(OVERDRAFT_PREVENTION_ID)
.name("Overdraft Prevention")
.description("Prevent overdraft on withdrawals")
.window(vec![])
.limit(
NewLimit::builder()
.balance(vec![NewBalanceLimit::builder()
.layer("SETTLED")
.amount("decimal('0.0')")
.enforcement_direction("DEBIT")
.build()
.expect("balance limit")])
.build()
.expect("limit"),
)
.build()
.expect("velocity limit");

match ledger.velocities().create_limit(limit).await {
Err(cala_ledger::velocity::error::VelocityError::LimitIdAlreadyExists) => {
Ok(OVERDRAFT_PREVENTION_ID.into())
}
Err(e) => Err(e.into()),
Ok(limit) => Ok(limit.id()),
}
}
}
5 changes: 4 additions & 1 deletion core/deposit/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,10 @@ where
)
.await?;

self.ledger
.add_deposit_control_to_account(&mut op, account_id)
.await?;

op.commit().await?;

Ok(account)
Expand Down Expand Up @@ -255,7 +259,6 @@ where
.create_in_op(&mut op, new_withdrawal)
.await?;

// TODO: add approval process and check for balance
self.ledger
.initiate_withdrawal(op, withdrawal_id, amount, deposit_account_id)
.await?;
Expand Down
12 changes: 11 additions & 1 deletion core/deposit/tests/withdraw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use deposit::*;
use helpers::{action, event, object};

#[tokio::test]
async fn cancel_withdrawal() -> anyhow::Result<()> {
async fn overdraw_and_cancel_withdrawal() -> anyhow::Result<()> {
use rand::Rng;

let pool = helpers::init_pool().await?;
Expand Down Expand Up @@ -86,6 +86,16 @@ async fn cancel_withdrawal() -> anyhow::Result<()> {
.record_deposit(&DummySubject, account.id, deposit_amount, None)
.await?;

// overdraw
let withdrawal_amount = UsdCents::try_from_usd(dec!(5000000)).unwrap();
let withdrawal = deposit
.initiate_withdrawal(&DummySubject, account.id, withdrawal_amount, None)
.await;
assert!(matches!(
withdrawal,
Err(deposit::error::CoreDepositError::DepositLedgerError(_))
));

let withdrawal_amount = UsdCents::try_from_usd(dec!(500000)).unwrap();

let withdrawal = deposit
Expand Down

0 comments on commit 917aa66

Please sign in to comment.