Skip to content

Commit 9706f15

Browse files
authored
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
1 parent f8a5a77 commit 9706f15

File tree

15 files changed

+409
-81
lines changed

15 files changed

+409
-81
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ redundant_clone = "warn"
249249
redundant_feature_names = "warn"
250250
redundant_type_annotations = "warn"
251251
todo = "warn"
252+
too_many_arguments = "allow"
252253
uninlined_format_args = "warn"
253254
unnested_or_patterns = "warn"
254255
wildcard_dependencies = "warn"

apps/labrinth/.env.docker-compose

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ ANROK_API_URL=none
144144

145145
GOTENBERG_URL=http://labrinth-gotenberg:13000
146146
GOTENBERG_CALLBACK_BASE=http://host.docker.internal:8000/_internal/gotenberg
147+
GOTENBERG_TIMEOUT=30000
147148

148149
ARCHON_URL=none
149150

apps/labrinth/.env.local

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ MEILISEARCH_KEY=modrinth
1919
REDIS_URL=redis://localhost
2020
REDIS_MAX_CONNECTIONS=10000
2121

22-
BIND_ADDR=127.0.0.1:8000
22+
# Must bind to broadcast, not localhost, because some
23+
# Docker services (Gotenberg) must be able to reach the backend
24+
# from a different network interface
25+
BIND_ADDR=0.0.0.0:8000
2326
SELF_ADDR=http://127.0.0.1:8000
2427

2528
MODERATION_SLACK_WEBHOOK=
@@ -145,6 +148,7 @@ ANROK_API_URL=none
145148

146149
GOTENBERG_URL=http://localhost:13000
147150
GOTENBERG_CALLBACK_BASE=http://host.docker.internal:8000/_internal/gotenberg
151+
GOTENBERG_TIMEOUT=30000
148152

149153
ARCHON_URL=none
150154

apps/labrinth/src/database/redis.rs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ use dashmap::DashMap;
55
use deadpool_redis::{Config, Runtime};
66
use futures::future::Either;
77
use prometheus::{IntGauge, Registry};
8-
use redis::{Cmd, ExistenceCheck, SetExpiry, SetOptions, cmd};
8+
use redis::{
9+
AsyncTypedCommands, Cmd, ExistenceCheck, SetExpiry, SetOptions,
10+
ToRedisArgs, cmd,
11+
};
912
use serde::de::DeserializeOwned;
1013
use serde::{Deserialize, Serialize};
1114
use std::collections::HashMap;
@@ -680,6 +683,30 @@ impl RedisConnection {
680683

681684
Ok(())
682685
}
686+
687+
pub async fn lpush(
688+
&mut self,
689+
namespace: &str,
690+
key: &str,
691+
value: impl ToRedisArgs + Send + Sync,
692+
) -> Result<(), DatabaseError> {
693+
let key = format!("{}_{namespace}:{key}", self.meta_namespace);
694+
self.connection.lpush(key, value).await?;
695+
Ok(())
696+
}
697+
698+
pub async fn brpop(
699+
&mut self,
700+
namespace: &str,
701+
key: &str,
702+
timeout: Option<f64>,
703+
) -> Result<Option<[String; 2]>, DatabaseError> {
704+
let key = format!("{}_{namespace}:{key}", self.meta_namespace);
705+
// a timeout of 0 is infinite
706+
let timeout = timeout.unwrap_or(0.0);
707+
let values = self.connection.brpop(key, timeout).await?;
708+
Ok(values)
709+
}
683710
}
684711

685712
#[derive(Serialize, Deserialize)]

apps/labrinth/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,7 @@ pub fn check_env_vars() -> bool {
503503

504504
failed |= check_var::<String>("GOTENBERG_URL");
505505
failed |= check_var::<String>("GOTENBERG_CALLBACK_BASE");
506+
failed |= check_var::<String>("GOTENBERG_TIMEOUT");
506507

507508
failed |= check_var::<String>("STRIPE_API_KEY");
508509
failed |= check_var::<String>("STRIPE_WEBHOOK_SECRET");

apps/labrinth/src/main.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,8 @@ async fn main() -> std::io::Result<()> {
153153
let email_queue =
154154
EmailQueue::init(pool.clone(), redis_pool.clone()).unwrap();
155155

156-
let gotenberg_client =
157-
GotenbergClient::from_env().expect("Failed to create Gotenberg client");
156+
let gotenberg_client = GotenbergClient::from_env(redis_pool.clone())
157+
.expect("Failed to create Gotenberg client");
158158

159159
if let Some(task) = args.run_background_task {
160160
info!("Running task {task:?} and exiting");

apps/labrinth/src/queue/payouts/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -726,7 +726,7 @@ impl PayoutsQueue {
726726
}
727727
}
728728

729-
#[derive(Debug, Clone)]
729+
#[derive(Debug, Clone, Copy)]
730730
pub struct PayoutFees {
731731
/// Fee which is taken by the underlying method we're using.
732732
///

apps/labrinth/src/queue/payouts/mural.rs

Lines changed: 77 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
use ariadne::ids::UserId;
2+
use chrono::Utc;
23
use eyre::{Result, eyre};
34
use muralpay::{MuralError, TokenFeeRequest};
4-
use rust_decimal::Decimal;
5+
use rust_decimal::{Decimal, prelude::ToPrimitive};
56
use serde::{Deserialize, Serialize};
67

78
use crate::{
8-
queue::payouts::{AccountBalance, PayoutsQueue},
9+
database::models::DBPayoutId,
10+
queue::payouts::{AccountBalance, PayoutFees, PayoutsQueue},
911
routes::ApiError,
10-
util::error::Context,
12+
util::{
13+
error::Context,
14+
gotenberg::{GotenbergClient, PaymentStatement},
15+
},
1116
};
1217

1318
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
@@ -54,10 +59,13 @@ impl PayoutsQueue {
5459

5560
pub async fn create_muralpay_payout_request(
5661
&self,
62+
payout_id: DBPayoutId,
5763
user_id: UserId,
58-
amount: muralpay::TokenAmount,
64+
gross_amount: Decimal,
65+
fees: PayoutFees,
5966
payout_details: MuralPayoutRequest,
6067
recipient_info: muralpay::PayoutRecipientInfo,
68+
gotenberg: &GotenbergClient,
6169
) -> Result<muralpay::PayoutRequest, ApiError> {
6270
let muralpay = self.muralpay.load();
6371
let muralpay = muralpay
@@ -86,11 +94,71 @@ impl PayoutsQueue {
8694
}
8795
};
8896

97+
// Mural takes `fees.method_fee` off the top of the amount we tell them to send
98+
let sent_to_method = gross_amount - fees.platform_fee;
99+
// ..so the net is `gross - platform_fee - method_fee`
100+
let net_amount = gross_amount - fees.total_fee();
101+
102+
let recipient_address = recipient_info.physical_address();
103+
let recipient_email = recipient_info.email().to_string();
104+
let gross_amount_cents = gross_amount * Decimal::from(100);
105+
let net_amount_cents = net_amount * Decimal::from(100);
106+
let fees_cents = fees.total_fee() * Decimal::from(100);
107+
let address_line_3 = format!(
108+
"{}, {}, {}",
109+
recipient_address.city,
110+
recipient_address.state,
111+
recipient_address.zip
112+
);
113+
114+
let payment_statement = PaymentStatement {
115+
payment_id: payout_id.into(),
116+
recipient_address_line_1: Some(recipient_address.address1.clone()),
117+
recipient_address_line_2: recipient_address.address2.clone(),
118+
recipient_address_line_3: Some(address_line_3),
119+
recipient_email,
120+
payment_date: Utc::now(),
121+
gross_amount_cents: gross_amount_cents
122+
.to_i64()
123+
.wrap_internal_err_with(|| eyre!("gross amount of cents `{gross_amount_cents}` cannot be expressed as an `i64`"))?,
124+
net_amount_cents: net_amount_cents
125+
.to_i64()
126+
.wrap_internal_err_with(|| eyre!("net amount of cents `{net_amount_cents}` cannot be expressed as an `i64`"))?,
127+
fees_cents: fees_cents
128+
.to_i64()
129+
.wrap_internal_err_with(|| eyre!("fees amount of cents `{fees_cents}` cannot be expressed as an `i64`"))?,
130+
currency_code: "USD".into(),
131+
};
132+
let payment_statement_doc = gotenberg
133+
.wait_for_payment_statement(&payment_statement)
134+
.await
135+
.wrap_internal_err("failed to generate payment statement")?;
136+
137+
// TODO
138+
// std::fs::write(
139+
// "/tmp/modrinth-payout-statement.pdf",
140+
// base64::Engine::decode(
141+
// &base64::engine::general_purpose::STANDARD,
142+
// &payment_statement_doc.body,
143+
// )
144+
// .unwrap(),
145+
// )
146+
// .unwrap();
147+
89148
let payout = muralpay::CreatePayout {
90-
amount,
149+
amount: muralpay::TokenAmount {
150+
token_amount: sent_to_method,
151+
token_symbol: muralpay::USDC.into(),
152+
},
91153
payout_details,
92154
recipient_info,
93-
supporting_details: None,
155+
supporting_details: Some(muralpay::SupportingDetails {
156+
supporting_document: Some(format!(
157+
"data:application/pdf;base64,{}",
158+
payment_statement_doc.body
159+
)),
160+
payout_purpose: Some(muralpay::PayoutPurpose::VendorPayment),
161+
}),
94162
};
95163

96164
let payout_request = muralpay
@@ -103,7 +171,9 @@ impl PayoutsQueue {
103171
.await
104172
.map_err(|err| match err {
105173
MuralError::Api(err) => ApiError::Request(err.into()),
106-
err => ApiError::Internal(err.into()),
174+
err => ApiError::Internal(
175+
eyre!(err).wrap_err("failed to create payout request"),
176+
),
107177
})?;
108178

109179
// try to immediately execute the payout request...

0 commit comments

Comments
 (0)