From a0c25375052780fd85091ef88bcfabdbf14fcccf Mon Sep 17 00:00:00 2001 From: aecsocket <43144841+aecsocket@users.noreply.github.com> Date: Fri, 14 Aug 2026 12:08:38 +0000 Subject: [PATCH] adjust payout report to show actual days --- apps/labrinth/src/models/v3/payout_runs.rs | 18 +-- apps/labrinth/src/queue/payouts/mod.rs | 1 + apps/labrinth/src/routes/internal/payouts.rs | 150 ++++++++++++------- packages/xredis/src/cache.rs | 16 +- packages/xredis/src/lib.rs | 4 +- 5 files changed, 109 insertions(+), 80 deletions(-) diff --git a/apps/labrinth/src/models/v3/payout_runs.rs b/apps/labrinth/src/models/v3/payout_runs.rs index 907616a104..e195f4c251 100644 --- a/apps/labrinth/src/models/v3/payout_runs.rs +++ b/apps/labrinth/src/models/v3/payout_runs.rs @@ -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, + #[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, - }, - Actual { - #[serde(with = "rust_decimal::serde::float")] - amount_usd: Decimal, - }, -} - #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] pub struct DayRevenue { pub date: NaiveDate, diff --git a/apps/labrinth/src/queue/payouts/mod.rs b/apps/labrinth/src/queue/payouts/mod.rs index cdaf456498..f51c4d5916 100644 --- a/apps/labrinth/src/queue/payouts/mod.rs +++ b/apps/labrinth/src/queue/payouts/mod.rs @@ -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( diff --git a/apps/labrinth/src/routes/internal/payouts.rs b/apps/labrinth/src/routes/internal/payouts.rs index d28c834f88..961109938d 100644 --- a/apps/labrinth/src/routes/internal/payouts.rs +++ b/apps/labrinth/src/routes/internal/payouts.rs @@ -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::>(); - 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::>(); + 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, ApiError> { + if days.is_empty() { + return Ok(Vec::new()); + } + + let estimated_revenue_usd = + days.iter().map(|day| day.amount_usd).sum::(); + 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()) } diff --git a/packages/xredis/src/cache.rs b/packages/xredis/src/cache.rs index 310c8fb7e8..94f945636d 100644 --- a/packages/xredis/src/cache.rs +++ b/packages/xredis/src/cache.rs @@ -260,11 +260,13 @@ impl CacheManager { keys, None, |ids| async move { - Ok(closure(ids) - .await? - .into_iter() - .map(|(key, value)| (key, (None::, value))) - .collect()) + Ok::<_, E>( + closure(ids) + .await? + .into_iter() + .map(|(key, value)| (key, (None::, value))) + .collect(), + ) }, ) .await @@ -277,12 +279,12 @@ impl CacheManager { keys: &[K], expiry: i64, closure: F, - ) -> Result, E> + ) -> Result> where P: ConnectionProvider, F: FnOnce(Vec) -> Fut, Fut: Future, E>>, - E: From, + E: std::error::Error + Send + Sync + 'static, T: Serialize + DeserializeOwned, K: Display + Hash diff --git a/packages/xredis/src/lib.rs b/packages/xredis/src/lib.rs index 32240c22a8..84c60c3af5 100644 --- a/packages/xredis/src/lib.rs +++ b/packages/xredis/src/lib.rs @@ -169,11 +169,11 @@ impl RedisPool { keys: &[K], expiry: i64, closure: F, - ) -> Result, E> + ) -> Result> where F: FnOnce(Vec) -> Fut, Fut: Future, E>>, - E: From, + E: std::error::Error + Send + Sync + 'static, T: Serialize + DeserializeOwned, K: Display + Hash