mirror of
https://github.com/modrinth/code.git
synced 2026-08-28 02:24:56 +00:00
adjust payout report to show actual days
This commit is contained in:
@@ -52,9 +52,9 @@ pub enum PayoutRunStatus {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
pub struct PayoutRunReport {
|
pub struct PayoutRunReport {
|
||||||
pub revenue: PayoutRunRevenue,
|
pub days: Vec<DayRevenue>,
|
||||||
#[serde(with = "rust_decimal::serde::float")]
|
#[serde(with = "rust_decimal::serde::float")]
|
||||||
pub fees_deducted_usd: Decimal,
|
pub fees_deducted_usd: Decimal,
|
||||||
#[serde(with = "rust_decimal::serde::float")]
|
#[serde(with = "rust_decimal::serde::float")]
|
||||||
pub variance_adjustment_usd: Decimal,
|
pub variance_adjustment_usd: Decimal,
|
||||||
#[serde(with = "rust_decimal::serde::float")]
|
#[serde(with = "rust_decimal::serde::float")]
|
||||||
@@ -71,18 +71,6 @@ pub struct PayoutRunCompletion {
|
|||||||
pub revenue_usd: Decimal,
|
pub revenue_usd: Decimal,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
|
||||||
#[serde(tag = "type", rename_all = "snake_case")]
|
|
||||||
pub enum PayoutRunRevenue {
|
|
||||||
Estimated {
|
|
||||||
days: Vec<DayRevenue>,
|
|
||||||
},
|
|
||||||
Actual {
|
|
||||||
#[serde(with = "rust_decimal::serde::float")]
|
|
||||||
amount_usd: Decimal,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
pub struct DayRevenue {
|
pub struct DayRevenue {
|
||||||
pub date: NaiveDate,
|
pub date: NaiveDate,
|
||||||
|
|||||||
@@ -925,6 +925,7 @@ pub async fn get_cached_aditude_month_estimates(
|
|||||||
fetch_aditude_month_estimates,
|
fetch_aditude_month_estimates,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
.wrap_internal_err("failed to fetch cached Aditude month estimates")
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch_aditude_month_estimates(
|
async fn fetch_aditude_month_estimates(
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
use std::cmp::Reverse;
|
use std::cmp::Reverse;
|
||||||
use std::collections::HashSet;
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
use actix_web::{HttpRequest, get, web};
|
use actix_web::{HttpRequest, get, web};
|
||||||
use chrono::{Months, Utc};
|
use chrono::{Months, Utc};
|
||||||
|
use rust_decimal::Decimal;
|
||||||
|
|
||||||
use crate::auth::get_user_from_headers;
|
use crate::auth::get_user_from_headers;
|
||||||
use crate::database::models::DBUserId;
|
use crate::database::models::DBUserId;
|
||||||
use crate::database::{PgPool, ReadOnlyPgPool};
|
use crate::database::{PgPool, ReadOnlyPgPool};
|
||||||
use crate::models::pats::Scopes;
|
use crate::models::pats::Scopes;
|
||||||
use crate::models::payout_runs::{
|
use crate::models::payout_runs::{
|
||||||
Adjustment, PayoutRun, PayoutRunCompletion, PayoutRunReport,
|
Adjustment, DayRevenue, PayoutRun, PayoutRunCompletion, PayoutRunReport,
|
||||||
PayoutRunRevenue, PayoutRunStatus,
|
PayoutRunStatus,
|
||||||
};
|
};
|
||||||
use crate::queue::payouts::get_cached_aditude_month_estimates;
|
use crate::queue::payouts::get_cached_aditude_month_estimates;
|
||||||
use crate::queue::session::AuthQueue;
|
use crate::queue::session::AuthQueue;
|
||||||
@@ -19,6 +20,12 @@ use crate::util::error::Context;
|
|||||||
use crate::util::time::{YearMonth, net_60_payout_available_at};
|
use crate::util::time::{YearMonth, net_60_payout_available_at};
|
||||||
use xredis::RedisPool;
|
use xredis::RedisPool;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
enum DayRevenueEstimate {
|
||||||
|
Raw,
|
||||||
|
AdjustedToActual { actual_revenue_usd: Decimal },
|
||||||
|
}
|
||||||
|
|
||||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
cfg.service(get);
|
cfg.service(get);
|
||||||
}
|
}
|
||||||
@@ -76,48 +83,40 @@ pub async fn get(
|
|||||||
.wrap_internal_err("failed to fetch newest payout value")?;
|
.wrap_internal_err("failed to fetch newest payout value")?;
|
||||||
|
|
||||||
let mut stored_periods = HashSet::with_capacity(stored_runs.len());
|
let mut stored_periods = HashSet::with_capacity(stored_runs.len());
|
||||||
let mut estimate_periods = HashSet::new();
|
let mut revenue_estimates = HashMap::new();
|
||||||
let mut runs = Vec::with_capacity(stored_runs.len());
|
let mut runs = Vec::with_capacity(stored_runs.len());
|
||||||
for run in stored_runs {
|
for run in stored_runs {
|
||||||
let period_start = YearMonth::from_day1(run.period_start.date_naive());
|
let period_start = YearMonth::from_day1(run.period_start.date_naive());
|
||||||
let (status, report) = if run.completed_at.is_some() {
|
let (status, revenue_estimate) = if run.completed_at.is_some() {
|
||||||
let amount_usd = run
|
let amount_usd = run
|
||||||
.completed_result
|
.completed_result
|
||||||
.map(|completion| completion.revenue_usd)
|
.map(|completion| completion.revenue_usd)
|
||||||
.wrap_internal_err(
|
.wrap_internal_err(
|
||||||
"paid payout run is missing its completion result",
|
"paid payout run is missing its completion result",
|
||||||
)?;
|
)?;
|
||||||
(
|
(
|
||||||
PayoutRunStatus::Paid,
|
PayoutRunStatus::Paid,
|
||||||
PayoutRunReport {
|
DayRevenueEstimate::AdjustedToActual {
|
||||||
revenue: PayoutRunRevenue::Actual { amount_usd },
|
actual_revenue_usd: amount_usd,
|
||||||
fees_deducted_usd: todo!(),
|
},
|
||||||
variance_adjustment_usd: todo!(),
|
)
|
||||||
net_estimated_revenue_usd: todo!(),
|
} else {
|
||||||
creator_net_estimated_revenue_usd: todo!(),
|
(PayoutRunStatus::Review, DayRevenueEstimate::Raw)
|
||||||
modrinth_net_estimated_revenue_usd: todo!(),
|
};
|
||||||
},
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
estimate_periods.insert(period_start);
|
|
||||||
(
|
|
||||||
PayoutRunStatus::Review,
|
|
||||||
PayoutRunReport {
|
|
||||||
revenue: PayoutRunRevenue::Estimated { days: Vec::new() },
|
|
||||||
fees_deducted_usd: todo!(),
|
|
||||||
variance_adjustment_usd: todo!(),
|
|
||||||
net_estimated_revenue_usd: todo!(),
|
|
||||||
creator_net_estimated_revenue_usd: todo!(),
|
|
||||||
modrinth_net_estimated_revenue_usd: todo!(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
stored_periods.insert(period_start);
|
stored_periods.insert(period_start);
|
||||||
runs.push(PayoutRun {
|
revenue_estimates.insert(period_start, revenue_estimate);
|
||||||
period_start,
|
runs.push(PayoutRun {
|
||||||
status,
|
period_start,
|
||||||
report,
|
status,
|
||||||
|
report: PayoutRunReport {
|
||||||
|
days: Vec::new(),
|
||||||
|
fees_deducted_usd: todo!(),
|
||||||
|
variance_adjustment_usd: todo!(),
|
||||||
|
net_estimated_revenue_usd: todo!(),
|
||||||
|
creator_net_estimated_revenue_usd: todo!(),
|
||||||
|
modrinth_net_estimated_revenue_usd: todo!(),
|
||||||
|
},
|
||||||
started_at: is_admin.then_some(run.started_at),
|
started_at: is_admin.then_some(run.started_at),
|
||||||
started_by: is_admin
|
started_by: is_admin
|
||||||
.then_some(run.started_by.map(|id| DBUserId(id).into()))
|
.then_some(run.started_by.map(|id| DBUserId(id).into()))
|
||||||
@@ -145,15 +144,13 @@ pub async fn get(
|
|||||||
PayoutRunStatus::Pending
|
PayoutRunStatus::Pending
|
||||||
};
|
};
|
||||||
|
|
||||||
estimate_periods.insert(period);
|
revenue_estimates.insert(period, DayRevenueEstimate::Raw);
|
||||||
|
|
||||||
runs.push(PayoutRun {
|
runs.push(PayoutRun {
|
||||||
period_start: period,
|
period_start: period,
|
||||||
status,
|
status,
|
||||||
report: PayoutRunReport {
|
report: PayoutRunReport {
|
||||||
revenue: PayoutRunRevenue::Estimated {
|
days: Vec::new(),
|
||||||
days: Vec::new(),
|
|
||||||
},
|
|
||||||
fees_deducted_usd: todo!(),
|
fees_deducted_usd: todo!(),
|
||||||
variance_adjustment_usd: todo!(),
|
variance_adjustment_usd: todo!(),
|
||||||
net_estimated_revenue_usd: todo!(),
|
net_estimated_revenue_usd: todo!(),
|
||||||
@@ -181,19 +178,60 @@ pub async fn get(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let estimate_periods = estimate_periods.into_iter().collect::<Vec<_>>();
|
let estimate_periods = revenue_estimates.keys().copied().collect::<Vec<_>>();
|
||||||
let estimates =
|
let estimates =
|
||||||
get_cached_aditude_month_estimates(&estimate_periods, &redis).await?;
|
get_cached_aditude_month_estimates(&estimate_periods, &redis).await?;
|
||||||
for run in &mut runs {
|
for run in &mut runs {
|
||||||
if let Some(days) = estimates.get(&run.period_start) {
|
if let Some(days) = estimates.get(&run.period_start) {
|
||||||
run.report.net_estimated_revenue_usd =
|
let days = match revenue_estimates.get(&run.period_start) {
|
||||||
days.iter().map(|day| day.amount_usd).sum();
|
Some(DayRevenueEstimate::Raw) | None => days.clone(),
|
||||||
run.report.revenue =
|
Some(DayRevenueEstimate::AdjustedToActual {
|
||||||
PayoutRunRevenue::Estimated { days: days.clone() };
|
actual_revenue_usd,
|
||||||
}
|
}) => adjust_estimates_to_actual(days, *actual_revenue_usd)?,
|
||||||
|
};
|
||||||
|
run.report.net_estimated_revenue_usd =
|
||||||
|
days.iter().map(|day| day.amount_usd).sum();
|
||||||
|
run.report.days = days;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
runs.sort_by_key(|run| Reverse(run.period_start));
|
runs.sort_by_key(|run| Reverse(run.period_start));
|
||||||
|
|
||||||
Ok(web::Json(runs))
|
Ok(web::Json(runs))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn adjust_estimates_to_actual(
|
||||||
|
days: &[DayRevenue],
|
||||||
|
actual_revenue_usd: Decimal,
|
||||||
|
) -> Result<Vec<DayRevenue>, ApiError> {
|
||||||
|
if days.is_empty() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let estimated_revenue_usd =
|
||||||
|
days.iter().map(|day| day.amount_usd).sum::<Decimal>();
|
||||||
|
let day_count = u64::try_from(days.len())
|
||||||
|
.wrap_internal_err("failed to calculate payout period day count")?;
|
||||||
|
let mut allocated_revenue_usd = Decimal::ZERO;
|
||||||
|
let last_day = days.len() - 1;
|
||||||
|
|
||||||
|
Ok(days
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, day)| {
|
||||||
|
let amount_usd = if index == last_day {
|
||||||
|
actual_revenue_usd - allocated_revenue_usd
|
||||||
|
} else if estimated_revenue_usd.is_zero() {
|
||||||
|
actual_revenue_usd / Decimal::from(day_count)
|
||||||
|
} else {
|
||||||
|
day.amount_usd * actual_revenue_usd / estimated_revenue_usd
|
||||||
|
};
|
||||||
|
allocated_revenue_usd += amount_usd;
|
||||||
|
|
||||||
|
DayRevenue {
|
||||||
|
date: day.date,
|
||||||
|
amount_usd,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -260,11 +260,13 @@ impl CacheManager {
|
|||||||
keys,
|
keys,
|
||||||
None,
|
None,
|
||||||
|ids| async move {
|
|ids| async move {
|
||||||
Ok(closure(ids)
|
Ok::<_, E>(
|
||||||
.await?
|
closure(ids)
|
||||||
.into_iter()
|
.await?
|
||||||
.map(|(key, value)| (key, (None::<String>, value)))
|
.into_iter()
|
||||||
.collect())
|
.map(|(key, value)| (key, (None::<String>, value)))
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -277,12 +279,12 @@ impl CacheManager {
|
|||||||
keys: &[K],
|
keys: &[K],
|
||||||
expiry: i64,
|
expiry: i64,
|
||||||
closure: F,
|
closure: F,
|
||||||
) -> Result<HashMap<K, T>, E>
|
) -> Result<HashMap<K, T>>
|
||||||
where
|
where
|
||||||
P: ConnectionProvider,
|
P: ConnectionProvider,
|
||||||
F: FnOnce(Vec<K>) -> Fut,
|
F: FnOnce(Vec<K>) -> Fut,
|
||||||
Fut: Future<Output = Result<DashMap<K, T>, E>>,
|
Fut: Future<Output = Result<DashMap<K, T>, E>>,
|
||||||
E: From<Error>,
|
E: std::error::Error + Send + Sync + 'static,
|
||||||
T: Serialize + DeserializeOwned,
|
T: Serialize + DeserializeOwned,
|
||||||
K: Display
|
K: Display
|
||||||
+ Hash
|
+ Hash
|
||||||
|
|||||||
@@ -169,11 +169,11 @@ impl RedisPool {
|
|||||||
keys: &[K],
|
keys: &[K],
|
||||||
expiry: i64,
|
expiry: i64,
|
||||||
closure: F,
|
closure: F,
|
||||||
) -> Result<std::collections::HashMap<K, T>, E>
|
) -> Result<std::collections::HashMap<K, T>>
|
||||||
where
|
where
|
||||||
F: FnOnce(Vec<K>) -> Fut,
|
F: FnOnce(Vec<K>) -> Fut,
|
||||||
Fut: Future<Output = Result<DashMap<K, T>, E>>,
|
Fut: Future<Output = Result<DashMap<K, T>, E>>,
|
||||||
E: From<Error>,
|
E: std::error::Error + Send + Sync + 'static,
|
||||||
T: Serialize + DeserializeOwned,
|
T: Serialize + DeserializeOwned,
|
||||||
K: Display
|
K: Display
|
||||||
+ Hash
|
+ Hash
|
||||||
|
|||||||
Reference in New Issue
Block a user