adjust payout report to show actual days

This commit is contained in:
aecsocket
2026-08-14 12:08:38 +00:00
parent ba1b7ce3c1
commit a0c2537505
5 changed files with 109 additions and 80 deletions
+3 -15
View File
@@ -52,9 +52,9 @@ pub enum PayoutRunStatus {
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct PayoutRunReport {
pub revenue: PayoutRunRevenue,
#[serde(with = "rust_decimal::serde::float")]
pub fees_deducted_usd: Decimal,
pub days: Vec<DayRevenue>,
#[serde(with = "rust_decimal::serde::float")]
pub fees_deducted_usd: Decimal,
#[serde(with = "rust_decimal::serde::float")]
pub variance_adjustment_usd: Decimal,
#[serde(with = "rust_decimal::serde::float")]
@@ -71,18 +71,6 @@ pub struct PayoutRunCompletion {
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)]
pub struct DayRevenue {
pub date: NaiveDate,
+1
View File
@@ -925,6 +925,7 @@ pub async fn get_cached_aditude_month_estimates(
fetch_aditude_month_estimates,
)
.await
.wrap_internal_err("failed to fetch cached Aditude month estimates")
}
async fn fetch_aditude_month_estimates(
+94 -56
View File
@@ -1,16 +1,17 @@
use std::cmp::Reverse;
use std::collections::HashSet;
use std::collections::{HashMap, HashSet};
use actix_web::{HttpRequest, get, web};
use chrono::{Months, Utc};
use rust_decimal::Decimal;
use crate::auth::get_user_from_headers;
use crate::database::models::DBUserId;
use crate::database::{PgPool, ReadOnlyPgPool};
use crate::models::pats::Scopes;
use crate::models::payout_runs::{
Adjustment, PayoutRun, PayoutRunCompletion, PayoutRunReport,
PayoutRunRevenue, PayoutRunStatus,
Adjustment, DayRevenue, PayoutRun, PayoutRunCompletion, PayoutRunReport,
PayoutRunStatus,
};
use crate::queue::payouts::get_cached_aditude_month_estimates;
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 xredis::RedisPool;
#[derive(Debug, Clone, Copy)]
enum DayRevenueEstimate {
Raw,
AdjustedToActual { actual_revenue_usd: Decimal },
}
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(get);
}
@@ -76,48 +83,40 @@ pub async fn get(
.wrap_internal_err("failed to fetch newest payout value")?;
let mut stored_periods = HashSet::with_capacity(stored_runs.len());
let mut estimate_periods = HashSet::new();
let mut runs = Vec::with_capacity(stored_runs.len());
for run in stored_runs {
let period_start = YearMonth::from_day1(run.period_start.date_naive());
let (status, report) = if run.completed_at.is_some() {
let amount_usd = run
.completed_result
let mut revenue_estimates = HashMap::new();
let mut runs = Vec::with_capacity(stored_runs.len());
for run in stored_runs {
let period_start = YearMonth::from_day1(run.period_start.date_naive());
let (status, revenue_estimate) = if run.completed_at.is_some() {
let amount_usd = run
.completed_result
.map(|completion| completion.revenue_usd)
.wrap_internal_err(
"paid payout run is missing its completion result",
)?;
(
PayoutRunStatus::Paid,
PayoutRunReport {
revenue: PayoutRunRevenue::Actual { amount_usd },
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!(),
},
)
} 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!(),
},
)
};
(
PayoutRunStatus::Paid,
DayRevenueEstimate::AdjustedToActual {
actual_revenue_usd: amount_usd,
},
)
} else {
(PayoutRunStatus::Review, DayRevenueEstimate::Raw)
};
stored_periods.insert(period_start);
runs.push(PayoutRun {
period_start,
status,
report,
stored_periods.insert(period_start);
revenue_estimates.insert(period_start, revenue_estimate);
runs.push(PayoutRun {
period_start,
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_by: is_admin
.then_some(run.started_by.map(|id| DBUserId(id).into()))
@@ -145,15 +144,13 @@ pub async fn get(
PayoutRunStatus::Pending
};
estimate_periods.insert(period);
revenue_estimates.insert(period, DayRevenueEstimate::Raw);
runs.push(PayoutRun {
period_start: period,
status,
report: PayoutRunReport {
revenue: PayoutRunRevenue::Estimated {
days: Vec::new(),
},
report: PayoutRunReport {
days: Vec::new(),
fees_deducted_usd: todo!(),
variance_adjustment_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 estimates =
get_cached_aditude_month_estimates(&estimate_periods, &redis).await?;
for run in &mut runs {
if let Some(days) = estimates.get(&run.period_start) {
run.report.net_estimated_revenue_usd =
days.iter().map(|day| day.amount_usd).sum();
run.report.revenue =
PayoutRunRevenue::Estimated { days: days.clone() };
}
let estimate_periods = revenue_estimates.keys().copied().collect::<Vec<_>>();
let estimates =
get_cached_aditude_month_estimates(&estimate_periods, &redis).await?;
for run in &mut runs {
if let Some(days) = estimates.get(&run.period_start) {
let days = match revenue_estimates.get(&run.period_start) {
Some(DayRevenueEstimate::Raw) | None => days.clone(),
Some(DayRevenueEstimate::AdjustedToActual {
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));
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())
}
+9 -7
View File
@@ -260,11 +260,13 @@ impl CacheManager {
keys,
None,
|ids| async move {
Ok(closure(ids)
.await?
.into_iter()
.map(|(key, value)| (key, (None::<String>, value)))
.collect())
Ok::<_, E>(
closure(ids)
.await?
.into_iter()
.map(|(key, value)| (key, (None::<String>, value)))
.collect(),
)
},
)
.await
@@ -277,12 +279,12 @@ impl CacheManager {
keys: &[K],
expiry: i64,
closure: F,
) -> Result<HashMap<K, T>, E>
) -> Result<HashMap<K, T>>
where
P: ConnectionProvider,
F: FnOnce(Vec<K>) -> Fut,
Fut: Future<Output = Result<DashMap<K, T>, E>>,
E: From<Error>,
E: std::error::Error + Send + Sync + 'static,
T: Serialize + DeserializeOwned,
K: Display
+ Hash
+2 -2
View File
@@ -169,11 +169,11 @@ impl RedisPool {
keys: &[K],
expiry: i64,
closure: F,
) -> Result<std::collections::HashMap<K, T>, E>
) -> Result<std::collections::HashMap<K, T>>
where
F: FnOnce(Vec<K>) -> Fut,
Fut: Future<Output = Result<DashMap<K, T>, E>>,
E: From<Error>,
E: std::error::Error + Send + Sync + 'static,
T: Serialize + DeserializeOwned,
K: Display
+ Hash