Supporting documents for Mural payouts (#4721)

* wip: gotenberg

* Generate and provide supporting docs for Mural payouts

* Correct docs

* shear

* update cargo lock because r-a complains otherwise

* Remove local Gotenberg queue and use Redis instead

* Store platform_id in database correctly

* Address PR comments

* Fix up CI

* fix rebase

* Add timeout to default env vars
This commit is contained in:
aecsocket
2025-11-08 15:27:31 -08:00
committed by GitHub
parent f8a5a77daa
commit 9706f1597b
15 changed files with 409 additions and 81 deletions

View File

@@ -10,11 +10,12 @@ use crate::models::payouts::{
MuralPayDetails, PayoutMethodRequest, PayoutMethodType, PayoutStatus,
TremendousDetails, TremendousForexResponse,
};
use crate::queue::payouts::PayoutsQueue;
use crate::queue::payouts::{PayoutFees, PayoutsQueue};
use crate::queue::session::AuthQueue;
use crate::routes::ApiError;
use crate::util::avalara1099;
use crate::util::error::Context;
use crate::util::gotenberg::GotenbergClient;
use actix_web::{HttpRequest, HttpResponse, delete, get, post, web};
use chrono::{DateTime, Duration, Utc};
use eyre::eyre;
@@ -476,6 +477,7 @@ pub async fn create_payout(
body: web::Json<Withdrawal>,
session_queue: web::Data<AuthQueue>,
payouts_queue: web::Data<PayoutsQueue>,
gotenberg: web::Data<GotenbergClient>,
) -> Result<(), ApiError> {
let (scopes, user) = get_user_record_from_bearer_token(
&req,
@@ -609,6 +611,8 @@ pub async fn create_payout(
body: &body,
user: &user,
payout_id,
gross_amount: body.amount,
fees,
amount_minus_fee,
total_fee: fees.total_fee(),
sent_to_method,
@@ -623,7 +627,7 @@ pub async fn create_payout(
tremendous_payout(payout_cx, method_details).await?
}
PayoutMethodRequest::MuralPay { method_details } => {
mural_pay_payout(payout_cx, method_details).await?
mural_pay_payout(payout_cx, method_details, &gotenberg).await?
}
};
@@ -648,6 +652,8 @@ struct PayoutContext<'a> {
body: &'a Withdrawal,
user: &'a DBUser,
payout_id: DBPayoutId,
gross_amount: Decimal,
fees: PayoutFees,
/// Set as the [`DBPayout::amount`] field.
amount_minus_fee: Decimal,
/// Set as the [`DBPayout::fee`] field.
@@ -674,6 +680,8 @@ async fn tremendous_payout(
body,
user,
payout_id,
gross_amount: _,
fees: _,
amount_minus_fee,
total_fee,
sent_to_method,
@@ -686,18 +694,6 @@ async fn tremendous_payout(
) -> Result<DBPayout, ApiError> {
let user_email = get_verified_email(user)?;
let mut payout_item = DBPayout {
id: payout_id,
user_id: user.id,
created: Utc::now(),
status: PayoutStatus::InTransit,
amount: amount_minus_fee,
fee: Some(total_fee),
method: Some(PayoutMethodType::Tremendous),
method_address: Some(user_email.to_string()),
platform_id: None,
};
#[derive(Deserialize)]
struct Reward {
pub id: String,
@@ -766,36 +762,47 @@ async fn tremendous_payout(
)
.await?;
if let Some(reward) = res.order.rewards.first() {
payout_item.platform_id = Some(reward.id.clone())
}
let platform_id = res.order.rewards.first().map(|reward| reward.id.clone());
Ok(payout_item)
Ok(DBPayout {
id: payout_id,
user_id: user.id,
created: Utc::now(),
status: PayoutStatus::InTransit,
amount: amount_minus_fee,
fee: Some(total_fee),
method: Some(PayoutMethodType::Tremendous),
method_address: Some(user_email.to_string()),
platform_id,
})
}
async fn mural_pay_payout(
PayoutContext {
body: _body,
body: _,
user,
payout_id,
gross_amount,
fees,
amount_minus_fee,
total_fee,
sent_to_method,
sent_to_method: _,
payouts_queue,
}: PayoutContext<'_>,
details: &MuralPayDetails,
gotenberg: &GotenbergClient,
) -> Result<DBPayout, ApiError> {
let user_email = get_verified_email(user)?;
let payout_request = payouts_queue
.create_muralpay_payout_request(
payout_id,
user.id.into(),
muralpay::TokenAmount {
token_symbol: muralpay::USDC.into(),
token_amount: sent_to_method,
},
gross_amount,
fees,
details.payout_details.clone(),
details.recipient_info.clone(),
gotenberg,
)
.await?;
@@ -817,6 +824,8 @@ async fn paypal_payout(
body,
user,
payout_id,
gross_amount: _,
fees: _,
amount_minus_fee,
total_fee,
sent_to_method,
@@ -948,18 +957,39 @@ async fn paypal_payout(
pub enum TransactionItem {
/// User withdrew some of their available payout.
Withdrawal {
/// ID of the payout.
id: PayoutId,
/// Status of this payout.
status: PayoutStatus,
/// When the payout was created.
created: DateTime<Utc>,
/// How much the user got from this payout, excluding fees.
amount: Decimal,
/// How much the user paid in fees for this payout, on top of `amount`.
fee: Option<Decimal>,
/// What payout method type was used for this.
method_type: Option<PayoutMethodType>,
/// Payout-method-specific ID for the type of payout the user got.
///
/// - Tremendous: the rewarded gift card ID.
/// - Mural: the payment rail used, i.e. crypto USDC or fiat USD.
/// - PayPal: `paypal_us`
/// - Venmo: `venmo`
///
/// For legacy transactions, this may be [`None`] as we did not always
/// store this payout info.
method_id: Option<String>,
/// Payout-method-specific address which the payout was sent to, like
/// an email address.
method_address: Option<String>,
},
/// User got a payout available for them to withdraw.
PayoutAvailable {
/// When this payout was made available for the user to withdraw.
created: DateTime<Utc>,
/// Where this payout came from.
payout_source: PayoutSource,
/// How much the payout was worth.
amount: Decimal,
},
}
@@ -1031,6 +1061,9 @@ pub async fn transaction_history(
amount: payout.amount,
fee: payout.fee,
method_type: payout.method,
// TODO: store the `method_id` in the database, and return it here
// don't use the `platform_id`, that's something else
method_id: None,
method_address: payout.method_address,
});