Files
modrinth/apps/labrinth/src/queue/payout_run/mod.rs
T
2026-08-17 17:20:12 +09:00

146 lines
5.5 KiB
Rust

//! Logic for executing payout runs, including starting a run, and performing
//! the revenue distribution.
//!
//! # Architecture
//!
//! ## Payout run
//!
//! The general flow for a payout run is as follows:
//! - For a given month (say, January) our ad provider gives us an estimate of
//! how much revenue and how many impressions we received for each individual
//! day. The month (payout period) starts in an _open_ state.
//! - After NET 60 has passed (start of March), the January payouts _should_ be
//! available; usually it takes some time for our ad provider to send the
//! money, so this is closer to NET 75. During this period, the month (payout
//! period) is in an _in review_ state.
//! - Once we receive the money from the provider, an admin enters the total
//! amount we've received into the web UI, adds any manual adjustments (for
//! campaigns outside of our ad provider's), and starts a payout run.
//! - The payout run is not immediately executed; there is a period of time in
//! which it can still be cancelled.
//! - Once the payout run is executed, we calculate the exact revenue
//! distribution to all creators, and fill `payouts_values` with those
//! amounts.
//!
//! ## Distribution
//!
//! How revenue is distributed:
//! - While a month is still open/in review:
//! - `raw_estimated_revenue_usd`: how much our ad provider estimates we'll
//! earn for a specific period
//! - We also get a value for this per day
//! - `fees_usd`: how much we pay in fees to Clean.io
//! - Based on number of impressions; we can get a per-day value for this
//! - `variance_usd`: a fixed percentage that we subtract from the raw estimated
//! revenue to account for it being an overestimate
//! - e.g. if variance is 10%, and we estimate that we'll earn $100k, then
//! our ad provider will probably give us closer to $90k - the variance
//! lets us express this difference
//! - `net_estimated_revenue_usd`: raw estimated - fees - variance
//! - `platform_net_estimated_revenue_usd`: net estimated revenue x Modrinth's cut
//! - `creator_net_estimated_revenue_usd`: net estimated revenue x (1 - Modrinth's cut)
//! - After a payout run has been executed:
//! - We save the per-day raw estimated revenue and impressions in the
//! database
//! - `raw_actual_revenue_usd`: how much we got from Aditude, input by an admin
//! - We compute this per-day by:
//! ```text
//! let factor = raw_actual_revenue_usd / raw_estimated_revenue_usd
//! raw_actual_revenue_usd_for_today = raw_estimated_revenue_usd_for_today * factor
//! ```
//! - (fees stay the same, since they're based on impressions, not revenue)
//! - (variance is ignored, since that's purely an estimation value)
//! - `adjustments_usd`: sum of all manual adjustments input by the admin
//! - `net_actual_revenue_usd`: raw actual revenue - fees + adjustments
//! - `(platform|creator)_net_estimated_revenue_usd`: same logic as estimated,
//! but using the net actual revenue
//!
//! ## Variance
//!
//! We store a table `payouts_variance` with columns:
//! - a timestamp from when this variance value applies (first entry at Unix
//! epoch)
//! - the decimal fraction of variance to apply
use chrono::NaiveDate;
use rust_decimal::{Decimal, dec};
use serde::{Deserialize, Serialize};
mod estimate;
/// Fraction defining much of the net revenue goes to the platform.
const PLATFORM_REVENUE_SPLIT: Decimal = dec!(0.25);
/// How input revenue is distributed for a specific day.
///
/// This may refer to either estimated or actual revenue.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DayDistribution {
pub raw_revenue_usd: Decimal,
pub fees_usd: Decimal,
pub variance_usd: Decimal,
pub sum_adjustments_usd: Decimal,
pub net_revenue_usd: Decimal,
pub platform_net_revenue_usd: Decimal,
pub creator_net_revenue_usd: Decimal,
}
#[derive(Debug)]
pub struct PayoutVariance {
pub starts_at: NaiveDate,
pub frac: Decimal,
}
#[derive(Debug)]
pub struct PayoutVariances {
pub fracs: Vec<PayoutVariance>,
pub default_frac: Decimal,
}
impl PayoutVariances {
pub const ZERO: Self = Self {
fracs: Vec::new(),
default_frac: Decimal::ZERO,
};
}
/// Compute the [`DayDistribution`] for an input revenue and impressions amount.
///
/// This logic may be used on both estimated and actual revenue. For actual
/// revenue, [`PayoutVariances::ZERO`] should be used, since variance only
/// applies to estimated days.
pub fn distribution_for_day(
date: NaiveDate,
raw_revenue_usd: Decimal,
impressions: u128,
sum_adjustments_usd: Decimal,
variances: &PayoutVariances,
) -> DayDistribution {
let fees_usd = {
let clean_io_cpm = Decimal::from(8) / Decimal::from(1000);
clean_io_cpm * Decimal::from(impressions) / Decimal::from(1000)
};
let variance_frac = variances
.fracs
.iter()
.find(|v| v.starts_at >= date)
.map(|v| v.frac)
.unwrap_or(variances.default_frac);
let variance_usd = raw_revenue_usd * variance_frac;
let net_estimated_revenue_usd =
raw_revenue_usd - fees_usd - variance_usd + sum_adjustments_usd;
DayDistribution {
raw_revenue_usd,
fees_usd,
variance_usd,
sum_adjustments_usd,
net_revenue_usd: net_estimated_revenue_usd,
platform_net_revenue_usd: net_estimated_revenue_usd
* PLATFORM_REVENUE_SPLIT,
creator_net_revenue_usd: net_estimated_revenue_usd
* (dec!(1) - PLATFORM_REVENUE_SPLIT),
}
}