You've already forked AstralRinth
forked from didirus/AstralRinth
Payouts code (#765)
* push to rebase * finish most * finish most * Finish impl * Finish paypal * run prep * Fix comp err
This commit is contained in:
@@ -3,11 +3,8 @@ use std::{collections::HashMap, sync::Arc};
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use rust_decimal::Decimal;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use sqlx::PgPool;
|
||||
use tokio::sync::Mutex;
|
||||
use validator::Validate;
|
||||
|
||||
use crate::{
|
||||
@@ -20,9 +17,9 @@ use crate::{
|
||||
notifications::Notification,
|
||||
pats::Scopes,
|
||||
projects::Project,
|
||||
users::{Badges, Payout, PayoutStatus, RecipientStatus, Role, UserPayoutData},
|
||||
users::{Badges, Role},
|
||||
},
|
||||
queue::{payouts::PayoutsQueue, session::AuthQueue},
|
||||
queue::session::AuthQueue,
|
||||
util::{routes::read_from_payload, validate::validation_errors_to_string},
|
||||
};
|
||||
|
||||
@@ -43,9 +40,6 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
.route("{id}", web::delete().to(user_delete))
|
||||
.route("{id}/follows", web::get().to(user_follows))
|
||||
.route("{id}/notifications", web::get().to(user_notifications))
|
||||
.route("{id}/payouts", web::get().to(user_payouts))
|
||||
.route("{id}/payouts_fees", web::get().to(user_payouts_fees))
|
||||
.route("{id}/payouts", web::post().to(user_payouts_request))
|
||||
.route("{id}/oauth_apps", web::get().to(get_user_clients)),
|
||||
);
|
||||
}
|
||||
@@ -302,6 +296,8 @@ pub struct EditUser {
|
||||
pub bio: Option<Option<String>>,
|
||||
pub role: Option<Role>,
|
||||
pub badges: Option<Badges>,
|
||||
#[validate(length(max = 160))]
|
||||
pub venmo_handle: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn user_edit(
|
||||
@@ -312,7 +308,7 @@ pub async fn user_edit(
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let (_scopes, user) = get_user_from_headers(
|
||||
let (scopes, user) = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
@@ -432,6 +428,27 @@ pub async fn user_edit(
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(venmo_handle) = &new_user.venmo_handle {
|
||||
if !scopes.contains(Scopes::PAYOUTS_WRITE) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have the permissions to edit the venmo handle of this user!"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE users
|
||||
SET venmo_handle = $1
|
||||
WHERE (id = $2)
|
||||
",
|
||||
venmo_handle,
|
||||
id as crate::database::models::ids::UserId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
|
||||
User::clear_caches(&[(id, Some(actual_user.username))], &redis).await?;
|
||||
transaction.commit().await?;
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
@@ -682,233 +699,3 @@ pub async fn user_notifications(
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn user_payouts(
|
||||
req: HttpRequest,
|
||||
info: web::Path<(String,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PAYOUTS_READ]),
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
let id_option = User::get(&info.into_inner().0, &**pool, &redis).await?;
|
||||
|
||||
if let Some(id) = id_option.map(|x| x.id) {
|
||||
if !user.role.is_admin() && user.id != id.into() {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have permission to see the payouts of this user!".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let (all_time, last_month, payouts) = futures::future::try_join3(
|
||||
sqlx::query!(
|
||||
"
|
||||
SELECT SUM(pv.amount) amount
|
||||
FROM payouts_values pv
|
||||
WHERE pv.user_id = $1
|
||||
",
|
||||
id as crate::database::models::UserId
|
||||
)
|
||||
.fetch_one(&**pool),
|
||||
sqlx::query!(
|
||||
"
|
||||
SELECT SUM(pv.amount) amount
|
||||
FROM payouts_values pv
|
||||
WHERE pv.user_id = $1 AND created > NOW() - '1 month'::interval
|
||||
",
|
||||
id as crate::database::models::UserId
|
||||
)
|
||||
.fetch_one(&**pool),
|
||||
sqlx::query!(
|
||||
"
|
||||
SELECT hp.created, hp.amount, hp.status
|
||||
FROM historical_payouts hp
|
||||
WHERE hp.user_id = $1
|
||||
ORDER BY hp.created DESC
|
||||
",
|
||||
id as crate::database::models::UserId
|
||||
)
|
||||
.fetch_many(&**pool)
|
||||
.try_filter_map(|e| async {
|
||||
Ok(e.right().map(|row| Payout {
|
||||
created: row.created,
|
||||
amount: row.amount,
|
||||
status: PayoutStatus::from_string(&row.status),
|
||||
}))
|
||||
})
|
||||
.try_collect::<Vec<Payout>>(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
use futures::TryStreamExt;
|
||||
|
||||
Ok(HttpResponse::Ok().json(json!({
|
||||
"all_time": all_time.amount,
|
||||
"last_month": last_month.amount,
|
||||
"payouts": payouts,
|
||||
})))
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct FeeEstimateAmount {
|
||||
pub amount: Decimal,
|
||||
}
|
||||
|
||||
pub async fn user_payouts_fees(
|
||||
req: HttpRequest,
|
||||
info: web::Path<(String,)>,
|
||||
web::Query(amount): web::Query<FeeEstimateAmount>,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
payouts_queue: web::Data<Mutex<PayoutsQueue>>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PAYOUTS_READ]),
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
let actual_user = User::get(&info.into_inner().0, &**pool, &redis).await?;
|
||||
|
||||
if let Some(actual_user) = actual_user {
|
||||
if !user.role.is_admin() && user.id != actual_user.id.into() {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have permission to request payouts of this user!".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(UserPayoutData {
|
||||
trolley_id: Some(trolley_id),
|
||||
..
|
||||
}) = user.payout_data
|
||||
{
|
||||
let payouts = payouts_queue
|
||||
.lock()
|
||||
.await
|
||||
.get_estimated_fees(&trolley_id, amount.amount)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(payouts))
|
||||
} else {
|
||||
Err(ApiError::InvalidInput(
|
||||
"You must set up your trolley account first!".to_string(),
|
||||
))
|
||||
}
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct PayoutData {
|
||||
pub amount: Decimal,
|
||||
}
|
||||
|
||||
pub async fn user_payouts_request(
|
||||
req: HttpRequest,
|
||||
info: web::Path<(String,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
data: web::Json<PayoutData>,
|
||||
payouts_queue: web::Data<Mutex<PayoutsQueue>>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let mut payouts_queue = payouts_queue.lock().await;
|
||||
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PAYOUTS_WRITE]),
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
let id_option = User::get(&info.into_inner().0, &**pool, &redis).await?;
|
||||
|
||||
if let Some(id) = id_option.map(|x| x.id) {
|
||||
if !user.role.is_admin() && user.id != id.into() {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have permission to request payouts of this user!".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(UserPayoutData {
|
||||
trolley_id: Some(trolley_id),
|
||||
trolley_status: Some(trolley_status),
|
||||
balance,
|
||||
..
|
||||
}) = user.payout_data
|
||||
{
|
||||
if trolley_status == RecipientStatus::Active {
|
||||
return if data.amount < balance {
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
let (batch_id, payment_id) =
|
||||
payouts_queue.send_payout(&trolley_id, data.amount).await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
INSERT INTO historical_payouts (user_id, amount, status, batch_id, payment_id)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
",
|
||||
id as crate::database::models::ids::UserId,
|
||||
data.amount,
|
||||
"processing",
|
||||
batch_id,
|
||||
payment_id,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE users
|
||||
SET balance = balance - $1
|
||||
WHERE id = $2
|
||||
",
|
||||
data.amount,
|
||||
id as crate::database::models::ids::UserId
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
User::clear_caches(&[(id, None)], &redis).await?;
|
||||
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
} else {
|
||||
Err(ApiError::InvalidInput(
|
||||
"You do not have enough funds to make this payout!".to_string(),
|
||||
))
|
||||
};
|
||||
} else {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"Please complete payout information via the trolley dashboard!".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Err(ApiError::InvalidInput(
|
||||
"You are not enrolled in the payouts program yet!".to_string(),
|
||||
))
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user