You've already forked AstralRinth
forked from didirus/AstralRinth
See available funds history and withdrawls in user payout history (#4537)
* Add GET /v3/payouts/history * V3 backwards compat * Sqlx prepare * Include user ID in GET /v3/payout
This commit is contained in:
28
apps/labrinth/.sqlx/query-8f68ea8481d86687ef4ebd6311f8ec5437be07fab8c34a964f7864304681bb94.json
generated
Normal file
28
apps/labrinth/.sqlx/query-8f68ea8481d86687ef4ebd6311f8ec5437be07fab8c34a964f7864304681bb94.json
generated
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "SELECT created, amount\n FROM payouts_values\n WHERE user_id = $1\n AND NOW() >= date_available",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "created",
|
||||||
|
"type_info": "Timestamptz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 1,
|
||||||
|
"name": "amount",
|
||||||
|
"type_info": "Numeric"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Int8"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "8f68ea8481d86687ef4ebd6311f8ec5437be07fab8c34a964f7864304681bb94"
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ use crate::queue::payouts::PayoutsQueue;
|
|||||||
use crate::queue::session::AuthQueue;
|
use crate::queue::session::AuthQueue;
|
||||||
use crate::routes::ApiError;
|
use crate::routes::ApiError;
|
||||||
use crate::util::avalara1099;
|
use crate::util::avalara1099;
|
||||||
|
use crate::util::error::Context;
|
||||||
use actix_web::{HttpRequest, HttpResponse, delete, get, post, web};
|
use actix_web::{HttpRequest, HttpResponse, delete, get, post, web};
|
||||||
use chrono::{DateTime, Duration, Utc};
|
use chrono::{DateTime, Duration, Utc};
|
||||||
use hex::ToHex;
|
use hex::ToHex;
|
||||||
@@ -21,6 +22,7 @@ use serde_json::json;
|
|||||||
use sha2::Sha256;
|
use sha2::Sha256;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use tokio_stream::StreamExt;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
const COMPLIANCE_CHECK_DEBOUNCE: chrono::Duration =
|
const COMPLIANCE_CHECK_DEBOUNCE: chrono::Duration =
|
||||||
@@ -31,7 +33,18 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
|||||||
web::scope("payout")
|
web::scope("payout")
|
||||||
.service(paypal_webhook)
|
.service(paypal_webhook)
|
||||||
.service(tremendous_webhook)
|
.service(tremendous_webhook)
|
||||||
.service(user_payouts)
|
// we use `route` instead of `service` because `user_payouts` uses the logic of `transaction_history`
|
||||||
|
.route(
|
||||||
|
"",
|
||||||
|
web::get().to(
|
||||||
|
#[expect(
|
||||||
|
deprecated,
|
||||||
|
reason = "v3 backwards compatibility"
|
||||||
|
)]
|
||||||
|
user_payouts,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.route("history", web::get().to(transaction_history))
|
||||||
.service(create_payout)
|
.service(create_payout)
|
||||||
.service(cancel_payout)
|
.service(cancel_payout)
|
||||||
.service(payment_methods)
|
.service(payment_methods)
|
||||||
@@ -400,41 +413,50 @@ pub async fn tremendous_webhook(
|
|||||||
Ok(HttpResponse::NoContent().finish())
|
Ok(HttpResponse::NoContent().finish())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("")]
|
#[deprecated = "use `transaction_history` instead"]
|
||||||
pub async fn user_payouts(
|
pub async fn user_payouts(
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
redis: web::Data<RedisPool>,
|
redis: web::Data<RedisPool>,
|
||||||
session_queue: web::Data<AuthQueue>,
|
session_queue: web::Data<AuthQueue>,
|
||||||
) -> Result<HttpResponse, ApiError> {
|
) -> Result<web::Json<Vec<crate::models::payouts::Payout>>, ApiError> {
|
||||||
let user = get_user_from_headers(
|
let (_, user) = get_user_from_headers(
|
||||||
&req,
|
&req,
|
||||||
&**pool,
|
&**pool,
|
||||||
&redis,
|
&redis,
|
||||||
&session_queue,
|
&session_queue,
|
||||||
Scopes::PAYOUTS_READ,
|
Scopes::PAYOUTS_READ,
|
||||||
)
|
)
|
||||||
.await?
|
|
||||||
.1;
|
|
||||||
|
|
||||||
let payout_ids =
|
|
||||||
crate::database::models::payout_item::DBPayout::get_all_for_user(
|
|
||||||
user.id.into(),
|
|
||||||
&**pool,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
let payouts = crate::database::models::payout_item::DBPayout::get_many(
|
|
||||||
&payout_ids,
|
|
||||||
&**pool,
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().json(
|
let items = transaction_history(req, pool, redis, session_queue)
|
||||||
payouts
|
.await?
|
||||||
.into_iter()
|
.0
|
||||||
.map(crate::models::payouts::Payout::from)
|
.into_iter()
|
||||||
.collect::<Vec<_>>(),
|
.filter_map(|txn_item| match txn_item {
|
||||||
))
|
TransactionItem::Withdrawal {
|
||||||
|
id,
|
||||||
|
status,
|
||||||
|
created,
|
||||||
|
amount,
|
||||||
|
fee,
|
||||||
|
method_type,
|
||||||
|
method_address,
|
||||||
|
} => Some(crate::models::payouts::Payout {
|
||||||
|
id,
|
||||||
|
user_id: user.id,
|
||||||
|
status,
|
||||||
|
created,
|
||||||
|
amount,
|
||||||
|
fee,
|
||||||
|
method: method_type,
|
||||||
|
method_address,
|
||||||
|
platform_id: None,
|
||||||
|
}),
|
||||||
|
TransactionItem::PayoutAvailable { .. } => None,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
Ok(web::Json(items))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@@ -798,6 +820,110 @@ pub async fn create_payout(
|
|||||||
Ok(HttpResponse::NoContent().finish())
|
Ok(HttpResponse::NoContent().finish())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "type", rename_all = "snake_case")]
|
||||||
|
pub enum TransactionItem {
|
||||||
|
Withdrawal {
|
||||||
|
id: PayoutId,
|
||||||
|
status: PayoutStatus,
|
||||||
|
created: DateTime<Utc>,
|
||||||
|
amount: Decimal,
|
||||||
|
fee: Option<Decimal>,
|
||||||
|
method_type: Option<PayoutMethodType>,
|
||||||
|
method_address: Option<String>,
|
||||||
|
},
|
||||||
|
PayoutAvailable {
|
||||||
|
created: DateTime<Utc>,
|
||||||
|
payout_source: PayoutSource,
|
||||||
|
amount: Decimal,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TransactionItem {
|
||||||
|
pub fn created(&self) -> DateTime<Utc> {
|
||||||
|
match self {
|
||||||
|
Self::Withdrawal { created, .. } => *created,
|
||||||
|
Self::PayoutAvailable { created, .. } => *created,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum PayoutSource {
|
||||||
|
CreatorRewards,
|
||||||
|
Affilites,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn transaction_history(
|
||||||
|
req: HttpRequest,
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
|
redis: web::Data<RedisPool>,
|
||||||
|
session_queue: web::Data<AuthQueue>,
|
||||||
|
) -> Result<web::Json<Vec<TransactionItem>>, ApiError> {
|
||||||
|
let (_, user) = get_user_from_headers(
|
||||||
|
&req,
|
||||||
|
&**pool,
|
||||||
|
&redis,
|
||||||
|
&session_queue,
|
||||||
|
Scopes::PAYOUTS_READ,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let payout_ids =
|
||||||
|
crate::database::models::payout_item::DBPayout::get_all_for_user(
|
||||||
|
user.id.into(),
|
||||||
|
&**pool,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let payouts = crate::database::models::payout_item::DBPayout::get_many(
|
||||||
|
&payout_ids,
|
||||||
|
&**pool,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let withdrawals =
|
||||||
|
payouts
|
||||||
|
.into_iter()
|
||||||
|
.map(|payout| TransactionItem::Withdrawal {
|
||||||
|
id: payout.id.into(),
|
||||||
|
status: payout.status,
|
||||||
|
created: payout.created,
|
||||||
|
amount: payout.amount,
|
||||||
|
fee: payout.fee,
|
||||||
|
method_type: payout.method,
|
||||||
|
method_address: payout.method_address,
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut payouts_available = sqlx::query!(
|
||||||
|
"SELECT created, amount
|
||||||
|
FROM payouts_values
|
||||||
|
WHERE user_id = $1
|
||||||
|
AND NOW() >= date_available",
|
||||||
|
DBUserId::from(user.id) as DBUserId
|
||||||
|
)
|
||||||
|
.fetch(&**pool)
|
||||||
|
.map(|record| {
|
||||||
|
let record = record
|
||||||
|
.wrap_internal_err("failed to fetch available payout record")?;
|
||||||
|
Ok(TransactionItem::PayoutAvailable {
|
||||||
|
created: record.created,
|
||||||
|
payout_source: PayoutSource::CreatorRewards,
|
||||||
|
amount: record.amount,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, ApiError>>()
|
||||||
|
.await
|
||||||
|
.wrap_internal_err("failed to fetch available payouts")?;
|
||||||
|
|
||||||
|
let mut txn_items = Vec::new();
|
||||||
|
txn_items.extend(withdrawals);
|
||||||
|
txn_items.append(&mut payouts_available);
|
||||||
|
txn_items.sort_by_key(|item| item.created());
|
||||||
|
|
||||||
|
Ok(web::Json(txn_items))
|
||||||
|
}
|
||||||
|
|
||||||
#[delete("{id}")]
|
#[delete("{id}")]
|
||||||
pub async fn cancel_payout(
|
pub async fn cancel_payout(
|
||||||
info: web::Path<(PayoutId,)>,
|
info: web::Path<(PayoutId,)>,
|
||||||
|
|||||||
Reference in New Issue
Block a user