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)] #[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,
+1
View File
@@ -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(
+94 -56
View File
@@ -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())
} }
+9 -7
View File
@@ -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
+2 -2
View File
@@ -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