mirror of
https://github.com/modrinth/code.git
synced 2026-08-30 19:46:33 +00:00
146 lines
5.5 KiB
Rust
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),
|
|
}
|
|
}
|