Skip to content
This repository has been archived by the owner on Oct 19, 2024. It is now read-only.

Commit

Permalink
finish most
Browse files Browse the repository at this point in the history
  • Loading branch information
Geometrically committed Nov 21, 2023
1 parent 1a47a49 commit b94df62
Show file tree
Hide file tree
Showing 13 changed files with 394 additions and 116 deletions.
2 changes: 2 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ PAYPAL_CLIENT_SECRET=none

STEAM_API_KEY=none

TREMENDOUS_API_URL=https://testflight.tremendous.com/api/v2/
TREMENDOUS_API_KEY=none
TREMENDOUS_PRIVATE_KEY=none

TURNSTILE_SECRET=none

Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions migrations/20231114175920_new-payment-methods.sql
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ ALTER TABLE payouts
ADD COLUMN method text NULL,
ADD COLUMN method_address text NULL,
ADD COLUMN platform_id text NULL,
ADD COLUMN fee numeric(40, 20) NULL,
ALTER COLUMN id TYPE bigint,
ALTER COLUMN id DROP DEFAULT;

Expand Down
2 changes: 1 addition & 1 deletion src/auth/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ where
has_totp: Some(db_user.totp_secret.is_some()),
github_id: None,
payout_data: Some(UserPayoutData {
paypal_address: db_user.paypal_id,
paypal_address: db_user.paypal_email,
paypal_country: db_user.paypal_country,
venmo_handle: db_user.venmo_handle,
balance: db_user.balance,
Expand Down
9 changes: 6 additions & 3 deletions src/database/models/payout_item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub struct Payout {
pub status: PayoutStatus,
pub amount: Decimal,

pub fee: Option<Decimal>,
pub method: Option<PayoutMethod>,
pub method_address: Option<String>,
pub platform_id: Option<String>,
Expand All @@ -26,14 +27,15 @@ impl Payout {
sqlx::query!(
"
INSERT INTO payouts (
id, amount, user_id, status, method, method_address, platform_id
id, amount, fee, user_id, status, method, method_address, platform_id
)
VALUES (
$1, $2, $3, $4, $5, $6, $7
$1, $2, $3, $4, $5, $6, $7, $8
)
",
self.id.0,
self.amount,
self.fee,
self.user_id.0,
self.status.as_str(),
self.method.map(|x| x.as_str()),
Expand Down Expand Up @@ -66,7 +68,7 @@ impl Payout {

let results = sqlx::query!(
"
SELECT id, user_id, created, amount, status, method, method_address, platform_id
SELECT id, user_id, created, amount, status, method, method_address, platform_id, fee
FROM payouts
WHERE id = ANY($1)
",
Expand All @@ -83,6 +85,7 @@ impl Payout {
method: r.method.map(|x| PayoutMethod::from_string(&x)),
method_address: r.method_address,
platform_id: r.platform_id,
fee: r.fee,
}))
})
.try_collect::<Vec<Payout>>()
Expand Down
4 changes: 4 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,10 @@ pub fn check_env_vars() -> bool {
failed |= check_var::<String>("GOOGLE_CLIENT_SECRET");
failed |= check_var::<String>("STEAM_API_KEY");

failed |= check_var::<String>("TREMENDOUS_API_URL");
failed |= check_var::<String>("TREMENDOUS_API_KEY");
failed |= check_var::<String>("TREMENDOUS_PRIVATE_KEY");

failed |= check_var::<String>("PAYPAL_API_URL");
failed |= check_var::<String>("PAYPAL_WEBHOOK_ID");
failed |= check_var::<String>("PAYPAL_CLIENT_ID");
Expand Down
17 changes: 17 additions & 0 deletions src/models/v3/payouts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,29 @@ pub struct Payout {
pub created: DateTime<Utc>,
pub amount: Decimal,

pub fee: Option<Decimal>,
pub method: Option<PayoutMethod>,
/// the address this payout was sent to: ex: email, paypal email, venmo handle
pub method_address: Option<String>,
pub platform_id: Option<String>,
}

impl Payout {
pub fn from(data: crate::database::models::payout_item::Payout) -> Self {
Self {
id: data.id.into(),
user_id: data.user_id.into(),
status: data.status,
created: data.created,
amount: data.amount,
fee: data.fee,
method: data.method,
method_address: data.method_address,
platform_id: data.platform_id,
}
}
}

#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
#[serde(rename_all = "lowercase")]
pub enum PayoutMethod {
Expand Down
164 changes: 79 additions & 85 deletions src/queue/payouts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use reqwest::Method;
use rust_decimal::Decimal;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use serde_json::Value;
use sqlx::PgPool;
use std::collections::HashMap;
use tokio::sync::RwLock;
Expand All @@ -23,23 +23,6 @@ struct PayPalCredentials {
expires: DateTime<Utc>,
}

#[derive(Serialize)]
pub struct PayoutItem {
pub amount: PayoutAmount,
pub receiver: String,
pub note: String,
pub recipient_type: String,
pub recipient_wallet: String,
pub sender_item_id: String,
}

#[derive(Serialize, Deserialize)]
pub struct PayoutAmount {
pub currency: String,
#[serde(with = "rust_decimal::serde::str")]
pub value: Decimal,
}

// Batches payouts and handles token refresh
impl PayoutsQueue {
pub fn new() -> Self {
Expand Down Expand Up @@ -154,93 +137,104 @@ impl PayoutsQueue {
ApiError::Payments("could not retrieve PayPal response body".to_string())
})?;

// TODO: remove
println!("{}", serde_json::to_string(&value)?);

if !status.is_success() {
println!("{}", serde_json::to_string(&value)?);
#[derive(Deserialize)]
struct PayPalError {
pub name: String,
pub message: String,
}

// TODO: error handling
}
#[derive(Deserialize)]
struct PayPalIdentityError {
pub error: String,
pub error_description: String,
}

println!("{}", serde_json::to_string(&value)?);
if let Ok(error) = serde_json::from_value::<PayPalError>(value.clone()) {
return Err(ApiError::Payments(format!(
"error name: {}, message: {}",
error.name, error.message
)));
}

if let Ok(error) = serde_json::from_value::<PayPalIdentityError>(value) {
return Err(ApiError::Payments(format!(
"error name: {}, message: {}",
error.error, error.error_description
)));
}

return Err(ApiError::Payments(
"could not retrieve PayPal error body".to_string(),
));
}

Ok(serde_json::from_value(value)?)
}

pub async fn send_payout(&self, mut payout: PayoutItem) -> Result<Decimal, ApiError> {
let wallet = payout.recipient_wallet.clone();

// TODO: calculate fee based on actual country data we have from paypal
let fee = if wallet == *"Venmo" {
Decimal::ONE / Decimal::from(4)
} else {
std::cmp::min(
std::cmp::max(
Decimal::ONE / Decimal::from(4),
(Decimal::from(2) / Decimal::ONE_HUNDRED) * payout.amount.value,
),
Decimal::from(20),
pub async fn make_tremendous_request<T: Serialize, X: DeserializeOwned>(
&self,
method: Method,
path: &str,
body: Option<T>,
) -> Result<X, ApiError> {
let client = reqwest::Client::new();
let mut request = client
.request(
method,
format!("{}{path}", dotenvy::var("TREMENDOUS_API_URL")?),
)
};

payout.amount.value -= fee;
payout.amount.value = payout.amount.value.round_dp(2);
.header(
"Authorization",
format!("Bearer {}", dotenvy::var("TREMENDOUS_API_KEY")?),
);

if payout.amount.value <= Decimal::ZERO {
return Err(ApiError::InvalidInput(
"You do not have enough funds to make this payout!".to_string(),
));
if let Some(body) = body {
request = request.json(&body);
}

#[derive(Deserialize)]
struct PayPalLink {
href: String,
}
let resp = request
.send()
.await
.map_err(|_| ApiError::Payments("could not communicate with Tremendous".to_string()))?;

#[derive(Deserialize)]
struct PayoutsResponse {
pub links: Vec<PayPalLink>,
}
let status = resp.status();

#[derive(Deserialize)]
struct PayoutDataItem {
payout_item_fee: PayoutAmount,
}
let value = resp.json::<Value>().await.map_err(|_| {
ApiError::Payments("could not retrieve Tremendous response body".to_string())
})?;

#[derive(Deserialize)]
struct PayoutData {
pub items: Vec<PayoutDataItem>,
}
// TODO: remove
println!("{}", serde_json::to_string(&value)?);

// TODO: get payment ID for every request and then provide it
let res: PayoutsResponse = self.make_paypal_request(
Method::POST,
"payments/payouts",
Some(
json! ({
"sender_batch_header": {
"sender_batch_id": format!("{}-payouts", Utc::now().to_rfc3339()),
"email_subject": "You have received a payment from Modrinth!",
"email_message": "Thank you for creating projects on Modrinth. Please claim this payment within 30 days.",
},
"items": vec![payout]
})
),
None
).await?;

if let Some(link) = res.links.first() {
if let Ok(res) = self
.make_paypal_request::<(), PayoutData>(Method::GET, &link.href, None, Some(true))
.await
{
if let Some(data) = res.items.first() {
if (fee - data.payout_item_fee.value) > Decimal::ZERO {
return Ok(fee - data.payout_item_fee.value);
if !status.is_success() {
if let Some(obj) = value.as_object() {
if let Some(array) = obj.get("errors") {
#[derive(Deserialize)]
struct TremendousError {
message: String,
}

let err =
serde_json::from_value::<TremendousError>(array.clone()).map_err(|_| {
ApiError::Payments(
"could not retrieve Tremendous error json body".to_string(),
)
})?;

return Err(ApiError::Payments(err.message));
}

return Err(ApiError::Payments(
"could not retrieve Tremendous error body".to_string(),
));
}
}

Ok(Decimal::ZERO)
Ok(serde_json::from_value(value)?)
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/routes/v3/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ pub fn config(cfg: &mut web::ServiceConfig) {
.configure(versions::config)
.configure(oauth::config)
.configure(oauth_clients::config)
.configure(payouts::config),
.configure(payouts::config)
.configure(users::config),
);
}

Expand Down
Loading

0 comments on commit b94df62

Please sign in to comment.