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:
@@ -28,7 +28,7 @@ use rust_decimal::{Decimal, RoundingStrategy};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use sha2::Sha256;
|
||||
use sqlx::PgPool;
|
||||
use sqlx::{PgPool, PgTransaction};
|
||||
use std::collections::HashMap;
|
||||
use tokio_stream::StreamExt;
|
||||
use tracing::error;
|
||||
@@ -623,29 +623,22 @@ pub async fn create_payout(
|
||||
total_fee: fees.total_fee(),
|
||||
sent_to_method,
|
||||
payouts_queue: &payouts_queue,
|
||||
db: PgPool::clone(&pool),
|
||||
transaction,
|
||||
};
|
||||
|
||||
let payout_item = match &body.method {
|
||||
match &body.method {
|
||||
PayoutMethodRequest::PayPal | PayoutMethodRequest::Venmo => {
|
||||
paypal_payout(payout_cx).await?
|
||||
paypal_payout(payout_cx).await?;
|
||||
}
|
||||
PayoutMethodRequest::Tremendous { method_details } => {
|
||||
tremendous_payout(payout_cx, method_details).await?
|
||||
tremendous_payout(payout_cx, method_details).await?;
|
||||
}
|
||||
PayoutMethodRequest::MuralPay { method_details } => {
|
||||
mural_pay_payout(payout_cx, method_details, &gotenberg).await?
|
||||
mural_pay_payout(payout_cx, method_details, &gotenberg).await?;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
payout_item
|
||||
.insert(&mut transaction)
|
||||
.await
|
||||
.wrap_internal_err("failed to insert payout")?;
|
||||
|
||||
transaction
|
||||
.commit()
|
||||
.await
|
||||
.wrap_internal_err("failed to commit transaction")?;
|
||||
crate::database::models::DBUser::clear_caches(&[(user.id, None)], &redis)
|
||||
.await
|
||||
.wrap_internal_err("failed to clear user caches")?;
|
||||
@@ -653,7 +646,6 @@ pub async fn create_payout(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct PayoutContext<'a> {
|
||||
body: &'a Withdrawal,
|
||||
user: &'a DBUser,
|
||||
@@ -666,6 +658,8 @@ struct PayoutContext<'a> {
|
||||
total_fee: Decimal2dp,
|
||||
sent_to_method: Decimal2dp,
|
||||
payouts_queue: &'a PayoutsQueue,
|
||||
db: PgPool,
|
||||
transaction: PgTransaction<'a>,
|
||||
}
|
||||
|
||||
fn get_verified_email(user: &DBUser) -> Result<&str, ApiError> {
|
||||
@@ -692,12 +686,14 @@ async fn tremendous_payout(
|
||||
total_fee,
|
||||
sent_to_method,
|
||||
payouts_queue,
|
||||
db: _,
|
||||
mut transaction,
|
||||
}: PayoutContext<'_>,
|
||||
TremendousDetails {
|
||||
delivery_email,
|
||||
currency,
|
||||
}: &TremendousDetails,
|
||||
) -> Result<DBPayout, ApiError> {
|
||||
) -> Result<(), ApiError> {
|
||||
let user_email = get_verified_email(user)?;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -773,7 +769,7 @@ async fn tremendous_payout(
|
||||
|
||||
let platform_id = res.order.rewards.first().map(|reward| reward.id.clone());
|
||||
|
||||
Ok(DBPayout {
|
||||
DBPayout {
|
||||
id: payout_id,
|
||||
user_id: user.id,
|
||||
created: Utc::now(),
|
||||
@@ -784,7 +780,17 @@ async fn tremendous_payout(
|
||||
method_id: Some(body.method_id.clone()),
|
||||
method_address: Some(user_email.to_string()),
|
||||
platform_id,
|
||||
})
|
||||
}
|
||||
.insert(&mut transaction)
|
||||
.await
|
||||
.wrap_internal_err("failed to insert payout")?;
|
||||
|
||||
transaction
|
||||
.commit()
|
||||
.await
|
||||
.wrap_internal_err("failed to commit transaction")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn mural_pay_payout(
|
||||
@@ -798,12 +804,35 @@ async fn mural_pay_payout(
|
||||
total_fee,
|
||||
sent_to_method: _,
|
||||
payouts_queue,
|
||||
db,
|
||||
mut transaction,
|
||||
}: PayoutContext<'_>,
|
||||
details: &MuralPayDetails,
|
||||
gotenberg: &GotenbergClient,
|
||||
) -> Result<DBPayout, ApiError> {
|
||||
) -> Result<(), ApiError> {
|
||||
let user_email = get_verified_email(user)?;
|
||||
|
||||
let method_id = match &details.payout_details {
|
||||
MuralPayoutRequest::Blockchain { .. } => {
|
||||
"blockchain-usdc-polygon".to_string()
|
||||
}
|
||||
MuralPayoutRequest::Fiat {
|
||||
fiat_and_rail_details,
|
||||
..
|
||||
} => fiat_and_rail_details.code().to_string(),
|
||||
};
|
||||
|
||||
// Once the Mural payout request has been created successfully,
|
||||
// then we *must* commit the payout into the DB,
|
||||
// to link the Mural payout request to the `payout` row.
|
||||
// Even if we can't execute the payout.
|
||||
// For this, we immediately insert and commit the txn.
|
||||
// Otherwise if we don't put it into the DB, we've got a ghost Mural
|
||||
// payout with no related database entry.
|
||||
//
|
||||
// However, this doesn't mean that the payout will definitely go through.
|
||||
// For this, we need to execute it, and handle errors.
|
||||
|
||||
let payout_request = payouts_queue
|
||||
.create_muralpay_payout_request(
|
||||
payout_id,
|
||||
@@ -816,22 +845,13 @@ async fn mural_pay_payout(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let method_id = match &details.payout_details {
|
||||
MuralPayoutRequest::Blockchain { .. } => {
|
||||
"blockchain-usdc-polygon".to_string()
|
||||
}
|
||||
MuralPayoutRequest::Fiat {
|
||||
fiat_and_rail_details,
|
||||
..
|
||||
} => fiat_and_rail_details.code().to_string(),
|
||||
};
|
||||
|
||||
Ok(DBPayout {
|
||||
let payout = DBPayout {
|
||||
id: payout_id,
|
||||
user_id: user.id,
|
||||
created: Utc::now(),
|
||||
// after the payout has been successfully executed,
|
||||
// we wait for Mural's confirmation that the funds have been delivered
|
||||
// done in `SyncPayoutStatuses` background task
|
||||
status: PayoutStatus::InTransit,
|
||||
amount: amount_minus_fee.get(),
|
||||
fee: Some(total_fee.get()),
|
||||
@@ -839,7 +859,61 @@ async fn mural_pay_payout(
|
||||
method_id: Some(method_id),
|
||||
method_address: Some(user_email.to_string()),
|
||||
platform_id: Some(payout_request.id.to_string()),
|
||||
})
|
||||
};
|
||||
payout
|
||||
.insert(&mut transaction)
|
||||
.await
|
||||
.wrap_internal_err("failed to insert payout")?;
|
||||
|
||||
transaction
|
||||
.commit()
|
||||
.await
|
||||
.wrap_internal_err("failed to commit payout insert transaction")?;
|
||||
|
||||
// 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 {
|
||||
payouts_queue
|
||||
.execute_mural_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 {
|
||||
if let Err(err) = sqlx::query!(
|
||||
"
|
||||
UPDATE payouts
|
||||
SET status = $1
|
||||
WHERE id = $2
|
||||
",
|
||||
PayoutStatus::Failed.as_str(),
|
||||
payout.id as _,
|
||||
)
|
||||
.execute(&db)
|
||||
.await
|
||||
{
|
||||
error!(
|
||||
"Created a Mural payout request, but failed to execute it, \
|
||||
and failed to mark the payout as failed: {err:#?}"
|
||||
);
|
||||
}
|
||||
|
||||
payouts_queue
|
||||
.cancel_mural_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(())
|
||||
}
|
||||
|
||||
async fn paypal_payout(
|
||||
@@ -853,8 +927,10 @@ async fn paypal_payout(
|
||||
total_fee,
|
||||
sent_to_method,
|
||||
payouts_queue,
|
||||
db: _,
|
||||
mut transaction,
|
||||
}: PayoutContext<'_>,
|
||||
) -> Result<DBPayout, ApiError> {
|
||||
) -> Result<(), ApiError> {
|
||||
let (wallet, wallet_type, address, display_address) =
|
||||
if matches!(body.method, PayoutMethodRequest::Venmo) {
|
||||
if let Some(venmo) = &user.venmo_handle {
|
||||
@@ -965,7 +1041,7 @@ async fn paypal_payout(
|
||||
|
||||
let platform_id = Some(data.payout_item_id.clone());
|
||||
|
||||
Ok(DBPayout {
|
||||
DBPayout {
|
||||
id: payout_id,
|
||||
user_id: user.id,
|
||||
created: Utc::now(),
|
||||
@@ -976,7 +1052,17 @@ async fn paypal_payout(
|
||||
method_id: Some(body.method_id.clone()),
|
||||
method_address: Some(display_address.clone()),
|
||||
platform_id,
|
||||
})
|
||||
}
|
||||
.insert(&mut transaction)
|
||||
.await
|
||||
.wrap_internal_err("failed to insert payout")?;
|
||||
|
||||
transaction
|
||||
.commit()
|
||||
.await
|
||||
.wrap_internal_err("failed to commit transaction")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// User performing a payout-related action.
|
||||
@@ -1201,7 +1287,7 @@ pub async fn cancel_payout(
|
||||
.parse::<muralpay::PayoutRequestId>()
|
||||
.wrap_request_err("invalid payout request ID")?;
|
||||
payouts
|
||||
.cancel_muralpay_payout_request(payout_request_id)
|
||||
.cancel_mural_payout_request(payout_request_id)
|
||||
.await
|
||||
.wrap_internal_err(
|
||||
"failed to cancel payout request",
|
||||
|
||||
Reference in New Issue
Block a user