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

@@ -726,7 +726,7 @@ impl PayoutsQueue {
}
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Copy)]
pub struct PayoutFees {
/// Fee which is taken by the underlying method we're using.
///

View File

@@ -1,13 +1,18 @@
use ariadne::ids::UserId;
use chrono::Utc;
use eyre::{Result, eyre};
use muralpay::{MuralError, TokenFeeRequest};
use rust_decimal::Decimal;
use rust_decimal::{Decimal, prelude::ToPrimitive};
use serde::{Deserialize, Serialize};
use crate::{
queue::payouts::{AccountBalance, PayoutsQueue},
database::models::DBPayoutId,
queue::payouts::{AccountBalance, PayoutFees, PayoutsQueue},
routes::ApiError,
util::error::Context,
util::{
error::Context,
gotenberg::{GotenbergClient, PaymentStatement},
},
};
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
@@ -54,10 +59,13 @@ impl PayoutsQueue {
pub async fn create_muralpay_payout_request(
&self,
payout_id: DBPayoutId,
user_id: UserId,
amount: muralpay::TokenAmount,
gross_amount: Decimal,
fees: PayoutFees,
payout_details: MuralPayoutRequest,
recipient_info: muralpay::PayoutRecipientInfo,
gotenberg: &GotenbergClient,
) -> Result<muralpay::PayoutRequest, ApiError> {
let muralpay = self.muralpay.load();
let muralpay = muralpay
@@ -86,11 +94,71 @@ impl PayoutsQueue {
}
};
// Mural takes `fees.method_fee` off the top of the amount we tell them to send
let sent_to_method = gross_amount - fees.platform_fee;
// ..so the net is `gross - platform_fee - method_fee`
let net_amount = gross_amount - fees.total_fee();
let recipient_address = recipient_info.physical_address();
let recipient_email = recipient_info.email().to_string();
let gross_amount_cents = gross_amount * Decimal::from(100);
let net_amount_cents = net_amount * Decimal::from(100);
let fees_cents = fees.total_fee() * Decimal::from(100);
let address_line_3 = format!(
"{}, {}, {}",
recipient_address.city,
recipient_address.state,
recipient_address.zip
);
let payment_statement = PaymentStatement {
payment_id: payout_id.into(),
recipient_address_line_1: Some(recipient_address.address1.clone()),
recipient_address_line_2: recipient_address.address2.clone(),
recipient_address_line_3: Some(address_line_3),
recipient_email,
payment_date: Utc::now(),
gross_amount_cents: gross_amount_cents
.to_i64()
.wrap_internal_err_with(|| eyre!("gross amount of cents `{gross_amount_cents}` cannot be expressed as an `i64`"))?,
net_amount_cents: net_amount_cents
.to_i64()
.wrap_internal_err_with(|| eyre!("net amount of cents `{net_amount_cents}` cannot be expressed as an `i64`"))?,
fees_cents: fees_cents
.to_i64()
.wrap_internal_err_with(|| eyre!("fees amount of cents `{fees_cents}` cannot be expressed as an `i64`"))?,
currency_code: "USD".into(),
};
let payment_statement_doc = gotenberg
.wait_for_payment_statement(&payment_statement)
.await
.wrap_internal_err("failed to generate payment statement")?;
// TODO
// std::fs::write(
// "/tmp/modrinth-payout-statement.pdf",
// base64::Engine::decode(
// &base64::engine::general_purpose::STANDARD,
// &payment_statement_doc.body,
// )
// .unwrap(),
// )
// .unwrap();
let payout = muralpay::CreatePayout {
amount,
amount: muralpay::TokenAmount {
token_amount: sent_to_method,
token_symbol: muralpay::USDC.into(),
},
payout_details,
recipient_info,
supporting_details: None,
supporting_details: Some(muralpay::SupportingDetails {
supporting_document: Some(format!(
"data:application/pdf;base64,{}",
payment_statement_doc.body
)),
payout_purpose: Some(muralpay::PayoutPurpose::VendorPayment),
}),
};
let payout_request = muralpay
@@ -103,7 +171,9 @@ impl PayoutsQueue {
.await
.map_err(|err| match err {
MuralError::Api(err) => ApiError::Request(err.into()),
err => ApiError::Internal(err.into()),
err => ApiError::Internal(
eyre!(err).wrap_err("failed to create payout request"),
),
})?;
// try to immediately execute the payout request...