mirror of
https://github.com/modrinth/code.git
synced 2026-08-30 19:46:33 +00:00
wip: estimated payout period info
This commit is contained in:
Generated
+1
@@ -5479,6 +5479,7 @@ dependencies = [
|
||||
"actix-web",
|
||||
"actix-web-prom",
|
||||
"actix-ws",
|
||||
"aditude",
|
||||
"arc-swap",
|
||||
"argon2",
|
||||
"ariadne",
|
||||
|
||||
@@ -14,7 +14,7 @@ use crate::{
|
||||
util::{error::Context, time::YearMonth},
|
||||
};
|
||||
|
||||
const REDIS_KEY: &str = "aditude_month_estimate_v1";
|
||||
const REDIS_KEY: &str = "aditude_month_estimate:v1";
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct PeriodEstimate {
|
||||
@@ -35,23 +35,24 @@ pub async fn estimate(
|
||||
redis: &RedisPool,
|
||||
periods: &[YearMonth],
|
||||
) -> Result<Vec<PeriodEstimate>> {
|
||||
redis
|
||||
let mut periods = redis
|
||||
.get_cached_keys(REDIS_KEY, periods, |periods| async move {
|
||||
fetch_estimates(aditude, redis, &periods)
|
||||
fetch_estimates(aditude, &periods)
|
||||
.await
|
||||
.map_err(ApiError::Internal)
|
||||
})
|
||||
.await
|
||||
.await?;
|
||||
periods.sort_unstable_by_key(|p| p.period);
|
||||
Ok(periods)
|
||||
}
|
||||
|
||||
async fn fetch_estimates(
|
||||
aditude: &aditude::Client,
|
||||
redis: &RedisPool,
|
||||
periods: &[YearMonth],
|
||||
) -> Result<DashMap<YearMonth, PeriodEstimate>> {
|
||||
let mut periods = periods.iter();
|
||||
let first_period = periods.next().wrap_err("no first period")?;
|
||||
let last_period = periods.last().unwrap_or(first_period);
|
||||
let mut periods_iter = periods.iter();
|
||||
let first_period = periods_iter.next().wrap_err("no first period")?;
|
||||
let last_period = periods_iter.last().unwrap_or(first_period);
|
||||
|
||||
let range_start = first_period
|
||||
.date()
|
||||
@@ -112,5 +113,14 @@ async fn fetch_estimates(
|
||||
period.days.sort_unstable_by_key(|day| day.date);
|
||||
}
|
||||
|
||||
// for any `periods` for which we don't have data yet,
|
||||
// give them an empty estimate dataset
|
||||
for &period in periods {
|
||||
map.entry(period).or_insert(PeriodEstimate {
|
||||
period,
|
||||
days: Vec::new(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(map.into_iter().collect::<DashMap<_, _>>())
|
||||
}
|
||||
|
||||
@@ -68,6 +68,8 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
mod estimate;
|
||||
|
||||
pub use estimate::*;
|
||||
|
||||
/// Fraction defining much of the net revenue goes to the platform.
|
||||
const PLATFORM_REVENUE_SPLIT: Decimal = dec!(0.25);
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
.service(web::scope("/campaign").configure(campaign::config))
|
||||
.service(web::scope("/search-management").configure(search::config))
|
||||
.service(web::scope("/globals").configure(globals::config))
|
||||
.service(web::scope("/payout-runs").configure(payout_runs::config))
|
||||
.service(web::scope("/server-ping").configure(server_ping::config))
|
||||
.service(web::scope("/attribution").configure(attribution::config))
|
||||
.configure(billing::config)
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
use actix_web::{get, web};
|
||||
use chrono::{Months, NaiveDate, Utc};
|
||||
use rust_decimal::Decimal;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use xredis::RedisPool;
|
||||
|
||||
use crate::{queue::payout_run::DayDistribution, routes::ApiError};
|
||||
use crate::{
|
||||
database::PgPool,
|
||||
queue::payout_run::{
|
||||
DayDistribution, PayoutVariances, distribution_for_day, estimate,
|
||||
},
|
||||
routes::ApiError,
|
||||
util::{
|
||||
error::Context,
|
||||
time::{YearMonth, net_60_payout_available_at},
|
||||
},
|
||||
};
|
||||
|
||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(get_runs);
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct PayoutRuns {
|
||||
@@ -10,11 +28,27 @@ pub struct PayoutRuns {
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct PayoutRunPeriod {
|
||||
pub period: YearMonth,
|
||||
pub status: PayoutRunStatus,
|
||||
pub status: PayoutPeriodStatus,
|
||||
pub days: Vec<PayoutRunDay>,
|
||||
pub adjustments: Vec<PayoutRunAdjustment>,
|
||||
}
|
||||
|
||||
/// Has revenue been distributed for a specific payout period month yet?
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum PayoutPeriodStatus {
|
||||
/// We are still waiting on the NET 60 cycle to complete for this month;
|
||||
/// revenue has not been received by the platform yet.
|
||||
Open,
|
||||
/// Revenue should have been received for the platform by now; waiting for
|
||||
/// an admin to manually execute the payout run.
|
||||
InReview,
|
||||
/// Payout run is currently executing.
|
||||
Running,
|
||||
/// Payout run has been paid out to creators.
|
||||
Paid,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct PayoutRunDay {
|
||||
pub date: NaiveDate,
|
||||
@@ -33,5 +67,79 @@ pub struct PayoutRunAdjustment {
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
pub async fn get_runs() -> Result<web::Json<PayoutRuns>, ApiError> {}
|
||||
#[get("")]
|
||||
pub async fn get_runs(
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
aditude: web::Data<aditude::Client>,
|
||||
) -> Result<web::Json<PayoutRuns>, ApiError> {
|
||||
let now = Utc::now();
|
||||
let latest_payout_value = sqlx::query_scalar!(
|
||||
r#"
|
||||
SELECT MAX(created)
|
||||
FROM payouts_values
|
||||
"#,
|
||||
)
|
||||
.fetch_one(&**pool)
|
||||
.await
|
||||
.wrap_internal_err("fetching latest payout value")?
|
||||
.unwrap_or(now)
|
||||
.max(now);
|
||||
|
||||
let current_period = YearMonth::from_day1(now.date_naive());
|
||||
let mut period = YearMonth::from_day1(latest_payout_value.date_naive());
|
||||
let mut requested_periods = Vec::new();
|
||||
|
||||
while period <= current_period {
|
||||
requested_periods.push(period);
|
||||
period = YearMonth::from_day1(
|
||||
period
|
||||
.date()
|
||||
.checked_add_months(Months::new(1))
|
||||
.wrap_internal_err("calculating next payout period")?,
|
||||
);
|
||||
}
|
||||
|
||||
let estimates =
|
||||
estimate(aditude.get_ref(), redis.get_ref(), &requested_periods)
|
||||
.await
|
||||
.wrap_internal_err("fetching payout estimates")?;
|
||||
|
||||
let periods = estimates
|
||||
.into_iter()
|
||||
.map(|estimate| {
|
||||
let status = if let Some(available_at) =
|
||||
net_60_payout_available_at(estimate.period)
|
||||
&& now >= available_at
|
||||
{
|
||||
PayoutPeriodStatus::InReview
|
||||
} else {
|
||||
PayoutPeriodStatus::Open
|
||||
};
|
||||
let days = estimate
|
||||
.days
|
||||
.into_iter()
|
||||
.map(|day| PayoutRunDay {
|
||||
date: day.date,
|
||||
estimated: distribution_for_day(
|
||||
day.date,
|
||||
day.raw_estimated_revenue_usd,
|
||||
day.impressions,
|
||||
Decimal::ZERO,
|
||||
&PayoutVariances::ZERO,
|
||||
),
|
||||
actual: None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
PayoutRunPeriod {
|
||||
period: estimate.period,
|
||||
status,
|
||||
days,
|
||||
adjustments: Vec::new(),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(web::Json(PayoutRuns { periods }))
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ pub struct YearMonth(NaiveDate);
|
||||
|
||||
impl YearMonth {
|
||||
pub fn from_day1(date: NaiveDate) -> Self {
|
||||
Self(date.with_day(1).expect("every monht has a first day"))
|
||||
Self(date.with_day(1).expect("every month has a first day"))
|
||||
}
|
||||
|
||||
pub fn from_year_month(year: i32, month: u32) -> Option<Self> {
|
||||
@@ -92,13 +92,6 @@ impl utoipa::ToSchema for YearMonth {}
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn rejects_dates_after_the_first() {
|
||||
let date = NaiveDate::from_ymd_opt(2026, 7, 2).unwrap();
|
||||
|
||||
assert_eq!(YearMonth::new(date), Err(InvalidYearMonth));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constructs_from_year_and_month() {
|
||||
let year_month = YearMonth::from_year_month(2026, 7).unwrap();
|
||||
@@ -123,7 +116,7 @@ mod tests {
|
||||
#[test]
|
||||
fn serializes_as_year_and_month() {
|
||||
let date = NaiveDate::from_ymd_opt(2026, 7, 1).unwrap();
|
||||
let year_month = YearMonth::new(date).unwrap();
|
||||
let year_month = YearMonth::from_day1(date);
|
||||
|
||||
assert_eq!(serde_json::to_string(&year_month).unwrap(), r#""2026-07""#);
|
||||
}
|
||||
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source .env
|
||||
|
||||
psql "$DATABASE_URL" < fixtures/labrinth-seed-data-202508052143.sql
|
||||
Reference in New Issue
Block a user