Skip to content

Commit a55e25c

Browse files
fix: make validation checks on payout entity (#374)
* fix: make validation checks on payout entity * test: add test for cancel_payout * test: refactor payout unit test --------- Co-authored-by: bodymindarts <justin@galoy.io>
1 parent 6c45964 commit a55e25c

File tree

6 files changed

+96
-12
lines changed

6 files changed

+96
-12
lines changed

src/api/server/convert.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -678,7 +678,10 @@ impl From<ApplicationError> for tonic::Status {
678678
ApplicationError::CouldNotParseIncomingPsbt(_) => {
679679
tonic::Status::invalid_argument(err.to_string())
680680
}
681-
ApplicationError::PayoutAlreadyCommitted => {
681+
ApplicationError::PayoutError(PayoutError::PayoutAlreadyCommitted) => {
682+
tonic::Status::failed_precondition(err.to_string())
683+
}
684+
ApplicationError::PayoutError(PayoutError::PayoutAlreadyCancelled) => {
682685
tonic::Status::failed_precondition(err.to_string())
683686
}
684687
_ => tonic::Status::internal(err.to_string()),

src/app/error.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,6 @@ pub enum ApplicationError {
7272
SigningSessionNotFoundForXPubId(crate::primitives::XPubId),
7373
#[error("Could not parse incoming psbt: {0}")]
7474
CouldNotParseIncomingPsbt(bitcoin::psbt::PsbtParseError),
75-
#[error("Payout already committed to a batch")]
76-
PayoutAlreadyCommitted,
7775
#[error("Hex decode error: {0}")]
7876
HexDecodeError(#[from] hex::FromHexError),
7977
#[error("Could not decrypt the encrypted key: {0}")]

src/app/mod.rs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -868,13 +868,7 @@ impl App {
868868
.payouts
869869
.find_by_id_for_cancellation(&mut tx, profile.account_id, id)
870870
.await?;
871-
if payout.batch_id.is_some() {
872-
return Err(ApplicationError::PayoutAlreadyCommitted);
873-
}
874-
if payout.is_cancelled() {
875-
return Ok(());
876-
}
877-
payout.cancel_payout(profile.id);
871+
payout.cancel_payout(profile.id)?;
878872
self.payouts.update(&mut tx, payout).await?;
879873
self.ledger
880874
.payout_cancelled(tx, LedgerTransactionId::new(), id)

src/entity/event.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,4 +109,9 @@ impl<T: DeserializeOwned + Serialize + 'static> EntityEvents<T> {
109109
query.execute(&mut **tx).await?;
110110
Ok(())
111111
}
112+
113+
#[cfg(test)]
114+
pub fn last(&self, n: usize) -> &[T] {
115+
&self.events[self.events.len() - n..]
116+
}
112117
}

src/payout/entity.rs

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ use serde::{Deserialize, Serialize};
33

44
use crate::{entity::*, primitives::*};
55

6+
use super::error::PayoutError;
7+
68
#[derive(Serialize, Deserialize)]
79
#[serde(tag = "type", rename_all = "snake_case")]
810
pub enum PayoutEvent {
@@ -50,10 +52,17 @@ pub struct Payout {
5052
}
5153

5254
impl Payout {
53-
pub fn cancel_payout(&mut self, profile_id: ProfileId) {
55+
pub fn cancel_payout(&mut self, profile_id: ProfileId) -> Result<(), PayoutError> {
56+
if self.is_cancelled() {
57+
return Err(PayoutError::PayoutAlreadyCancelled);
58+
}
59+
if self.is_already_committed() {
60+
return Err(PayoutError::PayoutAlreadyCommitted);
61+
}
5462
self.events.push(PayoutEvent::Cancelled {
5563
executed_by: profile_id,
56-
})
64+
});
65+
Ok(())
5766
}
5867

5968
pub fn is_cancelled(&self) -> bool {
@@ -64,6 +73,10 @@ impl Payout {
6473
}
6574
false
6675
}
76+
77+
fn is_already_committed(&self) -> bool {
78+
self.batch_id.is_some()
79+
}
6780
}
6881

6982
#[derive(Debug, Builder, Clone)]
@@ -153,3 +166,70 @@ impl TryFrom<EntityEvents<PayoutEvent>> for Payout {
153166
builder.events(events).build()
154167
}
155168
}
169+
170+
#[cfg(test)]
171+
mod tests {
172+
use rust_decimal::Decimal;
173+
174+
use super::*;
175+
176+
fn init_events() -> EntityEvents<PayoutEvent> {
177+
EntityEvents::init([
178+
PayoutEvent::Initialized {
179+
id: PayoutId::new(),
180+
wallet_id: WalletId::new(),
181+
profile_id: ProfileId::new(),
182+
payout_queue_id: PayoutQueueId::new(),
183+
destination: PayoutDestination::OnchainAddress {
184+
value: "bc1qwqdg6squsna38e46795at95yu9atm8azzmyvckulcc7kytlcckxswvvzej"
185+
.parse()
186+
.unwrap(),
187+
},
188+
satoshis: Satoshis::from(Decimal::from(21)),
189+
},
190+
PayoutEvent::ExternalIdUpdated {
191+
external_id: "external_id".to_string(),
192+
},
193+
])
194+
}
195+
196+
#[test]
197+
fn cancel_payout() {
198+
let mut payout = Payout::try_from(init_events()).unwrap();
199+
assert!(payout.cancel_payout(payout.profile_id).is_ok());
200+
assert!(matches!(
201+
payout.events.last(1)[0],
202+
PayoutEvent::Cancelled { .. }
203+
));
204+
}
205+
206+
#[test]
207+
fn can_only_cancel_payout_one_time() {
208+
let mut events = init_events();
209+
events.push(PayoutEvent::Cancelled {
210+
executed_by: ProfileId::new(),
211+
});
212+
let mut payout = Payout::try_from(events).unwrap();
213+
let result = payout.cancel_payout(payout.profile_id);
214+
assert!(matches!(result, Err(PayoutError::PayoutAlreadyCancelled)));
215+
}
216+
217+
#[test]
218+
fn errors_when_payout_already_committed() {
219+
let mut events = init_events();
220+
events.push(PayoutEvent::CommittedToBatch {
221+
batch_id: BatchId::new(),
222+
outpoint: bitcoin::OutPoint {
223+
txid: "4010e27ff7dc6d9c66a5657e6b3d94b4c4e394d968398d16fefe4637463d194d"
224+
.parse()
225+
.unwrap(),
226+
vout: 0,
227+
},
228+
});
229+
230+
let mut payout = Payout::try_from(events).unwrap();
231+
232+
let result = payout.cancel_payout(payout.profile_id);
233+
assert!(matches!(result, Err(PayoutError::PayoutAlreadyCommitted)));
234+
}
235+
}

src/payout/error.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,8 @@ pub enum PayoutError {
1010
PayoutIdNotFound(String),
1111
#[error("PayoutError - External Id does not exists")]
1212
ExternalIdNotFound,
13+
#[error("PayoutError - Payout is already committed to batch")]
14+
PayoutAlreadyCommitted,
15+
#[error("PayoutError - Payout is already cancelled")]
16+
PayoutAlreadyCancelled,
1317
}

0 commit comments

Comments
 (0)