You've already forked AstralRinth
forked from didirus/AstralRinth
Fix Mural payout status syncing (#4853)
* Fix Mural payout status syncing * Make Mural payout code more resilient * prepare sqlx * fix test
This commit is contained in:
@@ -21,7 +21,6 @@ use dashmap::DashMap;
|
||||
use eyre::{Result, eyre};
|
||||
use futures::TryStreamExt;
|
||||
use modrinth_util::decimal::Decimal2dp;
|
||||
use muralpay::MuralPay;
|
||||
use reqwest::Method;
|
||||
use rust_decimal::prelude::ToPrimitive;
|
||||
use rust_decimal::{Decimal, RoundingStrategy, dec};
|
||||
@@ -48,7 +47,7 @@ pub struct PayoutsQueue {
|
||||
}
|
||||
|
||||
pub struct MuralPayConfig {
|
||||
pub client: MuralPay,
|
||||
pub client: muralpay::Client,
|
||||
pub source_account_id: muralpay::AccountId,
|
||||
}
|
||||
|
||||
@@ -77,11 +76,11 @@ impl Default for PayoutsQueue {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_muralpay_client() -> Result<MuralPay> {
|
||||
pub fn create_muralpay_client() -> Result<muralpay::Client> {
|
||||
let api_url = env_var("MURALPAY_API_URL")?;
|
||||
let api_key = env_var("MURALPAY_API_KEY")?;
|
||||
let transfer_api_key = env_var("MURALPAY_TRANSFER_API_KEY")?;
|
||||
Ok(MuralPay::new(api_url, api_key, Some(transfer_api_key)))
|
||||
Ok(muralpay::Client::new(api_url, api_key, transfer_api_key))
|
||||
}
|
||||
|
||||
pub fn create_muralpay() -> Result<MuralPayConfig> {
|
||||
|
||||
@@ -3,7 +3,7 @@ use chrono::Utc;
|
||||
use eyre::{Result, eyre};
|
||||
use futures::{StreamExt, TryFutureExt, stream::FuturesUnordered};
|
||||
use modrinth_util::decimal::Decimal2dp;
|
||||
use muralpay::{MuralError, MuralPay, TokenFeeRequest};
|
||||
use muralpay::{MuralError, TokenFeeRequest};
|
||||
use rust_decimal::{Decimal, prelude::ToPrimitive};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::PgPool;
|
||||
@@ -69,7 +69,7 @@ impl PayoutsQueue {
|
||||
gross_amount: Decimal2dp,
|
||||
fees: PayoutFees,
|
||||
payout_details: MuralPayoutRequest,
|
||||
recipient_info: muralpay::PayoutRecipientInfo,
|
||||
recipient_info: muralpay::CreatePayoutRecipientInfo,
|
||||
gotenberg: &GotenbergClient,
|
||||
) -> Result<muralpay::PayoutRequest, ApiError> {
|
||||
let muralpay = self.muralpay.load();
|
||||
@@ -183,36 +183,27 @@ impl PayoutsQueue {
|
||||
),
|
||||
})?;
|
||||
|
||||
// try to immediately execute the payout request...
|
||||
// use a poor man's try/catch block using this `async move {}`
|
||||
// to catch any errors within this block
|
||||
let result = async move {
|
||||
muralpay
|
||||
.client
|
||||
.execute_payout_request(payout_request.id)
|
||||
.await
|
||||
.wrap_internal_err("failed to execute payout request")?;
|
||||
eyre::Ok(())
|
||||
}
|
||||
.await;
|
||||
|
||||
// and if it fails, make sure to immediately cancel it -
|
||||
// we don't want floating payout requests
|
||||
if let Err(err) = result {
|
||||
muralpay
|
||||
.client
|
||||
.cancel_payout_request(payout_request.id)
|
||||
.await
|
||||
.wrap_internal_err_with(|| {
|
||||
eyre!("failed to cancel unexecuted payout request\noriginal error: {err:#?}")
|
||||
})?;
|
||||
return Err(ApiError::Internal(err));
|
||||
}
|
||||
|
||||
Ok(payout_request)
|
||||
}
|
||||
|
||||
pub async fn cancel_muralpay_payout_request(
|
||||
pub async fn execute_mural_payout_request(
|
||||
&self,
|
||||
id: muralpay::PayoutRequestId,
|
||||
) -> Result<(), ApiError> {
|
||||
let muralpay = self.muralpay.load();
|
||||
let muralpay = muralpay
|
||||
.as_ref()
|
||||
.wrap_internal_err("Mural Pay client not available")?;
|
||||
|
||||
muralpay
|
||||
.client
|
||||
.execute_payout_request(id)
|
||||
.await
|
||||
.wrap_internal_err("failed to execute payout request")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn cancel_mural_payout_request(
|
||||
&self,
|
||||
id: muralpay::PayoutRequestId,
|
||||
) -> Result<()> {
|
||||
@@ -263,7 +254,7 @@ impl PayoutsQueue {
|
||||
/// Mural state, and updates the payout status.
|
||||
pub async fn sync_pending_payouts_from_mural(
|
||||
db: &PgPool,
|
||||
mural: &MuralPay,
|
||||
mural: &muralpay::Client,
|
||||
limit: u32,
|
||||
) -> eyre::Result<()> {
|
||||
#[derive(Debug)]
|
||||
@@ -369,9 +360,13 @@ pub async fn sync_pending_payouts_from_mural(
|
||||
|
||||
/// Queries Mural for canceled or failed payouts, and updates the corresponding
|
||||
/// Labrinth payouts' statuses.
|
||||
///
|
||||
/// This will update:
|
||||
/// - Mural payout requests which are failed or canceled
|
||||
/// - Mural payout requests where all of the payouts are failed or canceled
|
||||
pub async fn sync_failed_mural_payouts_to_labrinth(
|
||||
db: &PgPool,
|
||||
mural: &MuralPay,
|
||||
mural: &muralpay::Client,
|
||||
limit: u32,
|
||||
) -> eyre::Result<()> {
|
||||
info!("Syncing failed Mural payouts to Labrinth");
|
||||
@@ -380,12 +375,7 @@ pub async fn sync_failed_mural_payouts_to_labrinth(
|
||||
loop {
|
||||
let search_resp = mural
|
||||
.search_payout_requests(
|
||||
Some(muralpay::PayoutStatusFilter::PayoutStatus {
|
||||
statuses: vec![
|
||||
muralpay::PayoutStatus::Canceled,
|
||||
muralpay::PayoutStatus::Failed,
|
||||
],
|
||||
}),
|
||||
None,
|
||||
Some(muralpay::SearchParams {
|
||||
limit: Some(u64::from(limit)),
|
||||
next_id,
|
||||
@@ -395,48 +385,51 @@ pub async fn sync_failed_mural_payouts_to_labrinth(
|
||||
.wrap_internal_err(
|
||||
"failed to fetch failed payout requests from Mural",
|
||||
)?;
|
||||
next_id = search_resp.next_id;
|
||||
if search_resp.results.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
let num_canceled = search_resp
|
||||
.results
|
||||
.iter()
|
||||
.filter(|p| p.status == muralpay::PayoutStatus::Canceled)
|
||||
.count();
|
||||
let num_failed = search_resp
|
||||
.results
|
||||
.iter()
|
||||
.filter(|p| p.status == muralpay::PayoutStatus::Failed)
|
||||
.count();
|
||||
info!(
|
||||
"Found {num_canceled} canceled and {num_failed} failed Mural payouts"
|
||||
);
|
||||
next_id = search_resp.next_id;
|
||||
|
||||
let mut payout_platform_ids = Vec::<String>::new();
|
||||
let mut payout_new_statuses = Vec::<String>::new();
|
||||
|
||||
for payout_req in search_resp.results {
|
||||
let new_payout_status = match payout_req.status {
|
||||
muralpay::PayoutStatus::Canceled => PayoutStatus::Cancelled,
|
||||
muralpay::PayoutStatus::Failed => PayoutStatus::Failed,
|
||||
_ => {
|
||||
warn!(
|
||||
"Found payout {} with status {:?}, which should have been filtered out by our Mural request - Mural bug",
|
||||
payout_req.id, payout_req.status
|
||||
for payout_request in search_resp.results {
|
||||
let payout_platform_id = payout_request.id;
|
||||
|
||||
let new_payout_status = match payout_request.status {
|
||||
muralpay::PayoutStatus::Canceled => {
|
||||
trace!(
|
||||
"- Payout request {payout_platform_id} set to {} because it is cancelled in Mural",
|
||||
PayoutStatus::Cancelled
|
||||
);
|
||||
continue;
|
||||
Some(PayoutStatus::Cancelled)
|
||||
}
|
||||
muralpay::PayoutStatus::Failed => {
|
||||
trace!(
|
||||
"- Payout request {payout_platform_id} set to {} because it is failed in Mural",
|
||||
PayoutStatus::Failed
|
||||
);
|
||||
Some(PayoutStatus::Failed)
|
||||
}
|
||||
// this will also fail any payout request which has no payouts
|
||||
_ if payout_request
|
||||
.payouts
|
||||
.iter()
|
||||
.all(payout_should_be_failed) =>
|
||||
{
|
||||
trace!(
|
||||
"- Payout request {payout_platform_id} set to {} because all of its payouts are failed",
|
||||
PayoutStatus::Failed
|
||||
);
|
||||
Some(PayoutStatus::Failed)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let payout_platform_id = payout_req.id;
|
||||
|
||||
trace!(
|
||||
"- Payout {payout_platform_id} set to {new_payout_status:?}",
|
||||
);
|
||||
|
||||
payout_platform_ids.push(payout_platform_id.to_string());
|
||||
payout_new_statuses.push(new_payout_status.to_string());
|
||||
if let Some(new_payout_status) = new_payout_status {
|
||||
payout_platform_ids.push(payout_platform_id.to_string());
|
||||
payout_new_statuses.push(new_payout_status.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let result = sqlx::query!(
|
||||
@@ -470,6 +463,17 @@ pub async fn sync_failed_mural_payouts_to_labrinth(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn payout_should_be_failed(payout: &muralpay::Payout) -> bool {
|
||||
matches!(
|
||||
payout.details,
|
||||
muralpay::PayoutDetails::Fiat(muralpay::FiatPayoutDetails {
|
||||
fiat_payout_status: muralpay::FiatPayoutStatus::Failed { .. }
|
||||
| muralpay::FiatPayoutStatus::Refunded { .. },
|
||||
..
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -477,8 +481,8 @@ mod tests {
|
||||
api_v3::ApiV3,
|
||||
environment::{TestEnvironment, with_test_environment},
|
||||
};
|
||||
use muralpay::MuralPay;
|
||||
use muralpay::mock::MuralPayMock;
|
||||
use muralpay::MuralPayMock;
|
||||
use rust_decimal::dec;
|
||||
|
||||
fn create_mock_payout_request(
|
||||
id: &str,
|
||||
@@ -494,12 +498,51 @@ mod tests {
|
||||
transaction_hash: None,
|
||||
memo: None,
|
||||
status,
|
||||
payouts: vec![],
|
||||
payouts: vec![Payout {
|
||||
id: PayoutId(uuid::Uuid::new_v4()),
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
amount: TokenAmount {
|
||||
token_amount: dec!(10.00),
|
||||
token_symbol: "USDC".into(),
|
||||
},
|
||||
details: PayoutDetails::Fiat(FiatPayoutDetails {
|
||||
fiat_and_rail_code: FiatAndRailCode::Usd,
|
||||
fiat_payout_status: FiatPayoutStatus::Pending {
|
||||
initiated_at: chrono::Utc::now(),
|
||||
},
|
||||
fiat_amount: FiatAmount {
|
||||
fiat_amount: dec!(10.00),
|
||||
fiat_currency_code: CurrencyCode::Usd,
|
||||
},
|
||||
transaction_fee: TokenAmount {
|
||||
token_amount: dec!(1.00),
|
||||
token_symbol: "USDC".into(),
|
||||
},
|
||||
exchange_fee_percentage: dec!(0.0),
|
||||
exchange_rate: dec!(1.0),
|
||||
fee_total: TokenAmount {
|
||||
token_amount: dec!(1.00),
|
||||
token_symbol: "USDC".into(),
|
||||
},
|
||||
developer_fee: None,
|
||||
}),
|
||||
recipient_info: PayoutRecipientInfo::Inline {
|
||||
name: "John Smith".into(),
|
||||
details: InlineRecipientDetails::Fiat {
|
||||
details: InlineFiatRecipientDetails {
|
||||
fiat_currency_code: CurrencyCode::Usd,
|
||||
bank_name: "Foo Bank".into(),
|
||||
truncated_bank_account_number: "1234".into(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
fn create_mock_muralpay() -> MuralPay {
|
||||
MuralPay::from_mock(MuralPayMock {
|
||||
fn create_mock_muralpay() -> muralpay::Client {
|
||||
muralpay::Client::from_mock(MuralPayMock {
|
||||
get_payout_request: Box::new(|_id| {
|
||||
Err(muralpay::MuralError::Api(muralpay::ApiError {
|
||||
error_instance_id: uuid::Uuid::new_v4(),
|
||||
@@ -643,7 +686,7 @@ mod tests {
|
||||
})
|
||||
});
|
||||
|
||||
let mock_client = MuralPay::from_mock(mock);
|
||||
let mock_client = muralpay::Client::from_mock(mock);
|
||||
|
||||
// Run the function
|
||||
let result =
|
||||
@@ -756,7 +799,7 @@ mod tests {
|
||||
})
|
||||
});
|
||||
|
||||
let mock_client = MuralPay::from_mock(mock);
|
||||
let mock_client = muralpay::Client::from_mock(mock);
|
||||
|
||||
// Run the function
|
||||
let result =
|
||||
@@ -818,7 +861,7 @@ mod tests {
|
||||
})
|
||||
});
|
||||
|
||||
let mock_client = MuralPay::from_mock(mock);
|
||||
let mock_client = muralpay::Client::from_mock(mock);
|
||||
|
||||
// Run the function - should handle this gracefully
|
||||
sync_failed_mural_payouts_to_labrinth(
|
||||
|
||||
Reference in New Issue
Block a user