mirror of
https://github.com/modrinth/code.git
synced 2026-08-31 12:05:53 +00:00
adjustments on the net rev pool
This commit is contained in:
@@ -5,13 +5,13 @@ use rust_decimal::Decimal;
|
||||
use sqlx::types::Json;
|
||||
|
||||
use super::DatabaseError;
|
||||
use crate::queue::payout_run::{Adjustment, PayoutRunPayload};
|
||||
use crate::queue::payout_run::{PayoutRunPayload, RevenueAdjustment};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DBPayoutPeriod {
|
||||
pub period: NaiveDate,
|
||||
pub raw_actual_aditude_revenue_usd: Decimal,
|
||||
pub adjustments: Vec<Adjustment>,
|
||||
pub revenue_adjustments: Vec<RevenueAdjustment>,
|
||||
pub active_run_payload: Option<PayoutRunPayload>,
|
||||
pub active_run_execute_at: Option<DateTime<Utc>>,
|
||||
pub days: Vec<DBPayoutPeriodDay>,
|
||||
@@ -39,7 +39,7 @@ impl DBPayoutPeriod {
|
||||
SELECT
|
||||
payout_periods.period,
|
||||
payout_periods.raw_actual_aditude_revenue_usd,
|
||||
payout_periods.adjustments AS "adjustments: Json<Vec<Adjustment>>",
|
||||
payout_periods.revenue_adjustments AS "revenue_adjustments: Json<Vec<RevenueAdjustment>>",
|
||||
active_run.payload AS "active_run_payload: Json<PayoutRunPayload>",
|
||||
active_run.execute_at AS active_run_execute_at,
|
||||
EXISTS (
|
||||
@@ -68,7 +68,7 @@ impl DBPayoutPeriod {
|
||||
period: row.period,
|
||||
raw_actual_aditude_revenue_usd: row
|
||||
.raw_actual_aditude_revenue_usd,
|
||||
adjustments: row.adjustments.0,
|
||||
revenue_adjustments: row.revenue_adjustments.0,
|
||||
active_run_payload: row
|
||||
.active_run_payload
|
||||
.map(|payload| payload.0),
|
||||
|
||||
@@ -14,8 +14,9 @@
|
||||
//! 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.
|
||||
//! amount we've received into the web UI, adds any manual revenue
|
||||
//! 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
|
||||
@@ -50,11 +51,12 @@
|
||||
//! ```
|
||||
//! - (fees stay the same, since they're based on impressions, not revenue)
|
||||
//! - (variance is ignored, since that's purely an estimation value)
|
||||
//! - `actual.net_revenue_usd`: raw actual revenue - fees
|
||||
//! - Revenue adjustments are split evenly across the days in the period.
|
||||
//! - `actual.net_revenue_usd`: raw actual revenue - fees + revenue adjustment
|
||||
//! - `actual.(platform|creator)_net_revenue_usd`: same logic as estimated,
|
||||
//! but using the net actual revenue
|
||||
//! - Manual adjustments are stored separately on the payout period and applied
|
||||
//! on top of its actual distribution.
|
||||
//! - Manual revenue adjustments are stored on the payout period and applied to
|
||||
//! its actual distribution before the platform/creator split.
|
||||
//!
|
||||
//! ## Variance
|
||||
//!
|
||||
@@ -63,10 +65,12 @@
|
||||
//! epoch date)
|
||||
//! - the decimal fraction of variance to apply
|
||||
|
||||
use chrono::NaiveDate;
|
||||
use chrono::{Months, NaiveDate};
|
||||
use rust_decimal::{Decimal, dec};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::util::time::YearMonth;
|
||||
|
||||
mod estimate;
|
||||
|
||||
pub use estimate::*;
|
||||
@@ -77,13 +81,13 @@ pub struct PayoutRunPayload {
|
||||
/// Actual raw revenue received from the ad provider for the period.
|
||||
#[serde(with = "rust_decimal::serde::float")]
|
||||
pub raw_actual_revenue_usd: Decimal,
|
||||
/// Manual adjustments to apply on top of actual revenue.
|
||||
pub adjustments: Vec<Adjustment>,
|
||||
/// Manual revenue adjustments to apply on top of actual revenue.
|
||||
pub revenue_adjustments: Vec<RevenueAdjustment>,
|
||||
}
|
||||
|
||||
/// Manual admin-input adjustment to a payout period.
|
||||
/// Manual admin-input revenue adjustment to a payout period.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct Adjustment {
|
||||
pub struct RevenueAdjustment {
|
||||
/// Total value of the adjustment.
|
||||
#[serde(with = "rust_decimal::serde::float")]
|
||||
pub amount_usd: Decimal,
|
||||
@@ -113,8 +117,14 @@ pub struct DayDistribution {
|
||||
/// For non-estimates (actual revenue values), this is zero.
|
||||
#[serde(with = "rust_decimal::serde::float")]
|
||||
pub variance_usd: Decimal,
|
||||
/// Revenue added after fees and variance, before the platform/creator
|
||||
/// split.
|
||||
///
|
||||
/// This is zero for estimated revenue.
|
||||
#[serde(with = "rust_decimal::serde::float")]
|
||||
pub revenue_adjustment_usd: Decimal,
|
||||
/// Total net revenue that we earned;
|
||||
/// `raw_revenue - fees - variance`.
|
||||
/// `raw_revenue - fees - variance + revenue_adjustment`.
|
||||
#[serde(with = "rust_decimal::serde::float")]
|
||||
pub net_revenue_usd: Decimal,
|
||||
/// How much of the net revenue goes to the platform.
|
||||
@@ -153,6 +163,7 @@ pub fn distribution_for_day(
|
||||
date: NaiveDate,
|
||||
raw_revenue_usd: Decimal,
|
||||
impressions: u128,
|
||||
revenue_adjustment_usd: Decimal,
|
||||
variances: &PayoutVariances,
|
||||
) -> DayDistribution {
|
||||
let fees_usd = {
|
||||
@@ -168,16 +179,17 @@ pub fn distribution_for_day(
|
||||
.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;
|
||||
let net_revenue_usd =
|
||||
raw_revenue_usd - fees_usd - variance_usd + revenue_adjustment_usd;
|
||||
|
||||
DayDistribution {
|
||||
raw_revenue_usd,
|
||||
fees_usd,
|
||||
variance_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
|
||||
revenue_adjustment_usd,
|
||||
net_revenue_usd,
|
||||
platform_net_revenue_usd: net_revenue_usd * PLATFORM_REVENUE_SPLIT,
|
||||
creator_net_revenue_usd: net_revenue_usd
|
||||
* (dec!(1) - PLATFORM_REVENUE_SPLIT),
|
||||
}
|
||||
}
|
||||
@@ -186,6 +198,9 @@ pub fn distribution_for_day(
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct ActualDistributionFlow {
|
||||
share: Decimal,
|
||||
period: YearMonth,
|
||||
daily_revenue_adjustment_usd: Decimal,
|
||||
final_day_revenue_adjustment_usd: Decimal,
|
||||
}
|
||||
|
||||
/// Start a flow for computing the actual revenue distribution of a payout
|
||||
@@ -202,12 +217,17 @@ pub struct ActualDistributionFlow {
|
||||
/// ```
|
||||
///
|
||||
/// If the period's estimated revenue is zero, the share is `1`.
|
||||
/// Revenue adjustments are split evenly across every calendar day. Any
|
||||
/// decimal rounding remainder is placed on the final day so the daily values
|
||||
/// sum exactly to the period adjustment.
|
||||
///
|
||||
/// We use a type-state-ish pattern here to ensure that the same flow is used
|
||||
/// for each day in a period.
|
||||
pub fn compute_actual_distribution_flow(
|
||||
period: YearMonth,
|
||||
raw_estimated_revenue_usd: Decimal,
|
||||
raw_actual_revenue_usd: Decimal,
|
||||
revenue_adjustment_usd: Decimal,
|
||||
) -> ActualDistributionFlow {
|
||||
let share = if raw_estimated_revenue_usd.is_zero() {
|
||||
Decimal::ONE
|
||||
@@ -215,7 +235,21 @@ pub fn compute_actual_distribution_flow(
|
||||
raw_actual_revenue_usd / raw_estimated_revenue_usd
|
||||
};
|
||||
|
||||
ActualDistributionFlow { share }
|
||||
let next_period = period
|
||||
.date()
|
||||
.checked_add_months(Months::new(1))
|
||||
.expect("a payout period must have a following month");
|
||||
let day_count = Decimal::from((next_period - period.date()).num_days());
|
||||
let daily_revenue_adjustment_usd = revenue_adjustment_usd / day_count;
|
||||
let final_day_revenue_adjustment_usd = revenue_adjustment_usd
|
||||
- daily_revenue_adjustment_usd * (day_count - Decimal::ONE);
|
||||
|
||||
ActualDistributionFlow {
|
||||
share,
|
||||
period,
|
||||
daily_revenue_adjustment_usd,
|
||||
final_day_revenue_adjustment_usd,
|
||||
}
|
||||
}
|
||||
|
||||
impl ActualDistributionFlow {
|
||||
@@ -226,12 +260,72 @@ impl ActualDistributionFlow {
|
||||
raw_estimated_revenue_usd: Decimal,
|
||||
impressions: u128,
|
||||
) -> DayDistribution {
|
||||
debug_assert_eq!(YearMonth::from_day1(date), self.period);
|
||||
let next_period = self
|
||||
.period
|
||||
.date()
|
||||
.checked_add_months(Months::new(1))
|
||||
.expect("a payout period must have a following month");
|
||||
let revenue_adjustment_usd = if date.succ_opt() == Some(next_period) {
|
||||
self.final_day_revenue_adjustment_usd
|
||||
} else {
|
||||
self.daily_revenue_adjustment_usd
|
||||
};
|
||||
|
||||
distribution_for_day(
|
||||
date,
|
||||
raw_estimated_revenue_usd * self.share,
|
||||
impressions,
|
||||
revenue_adjustment_usd,
|
||||
// actual rev distribution always has no variance
|
||||
&PayoutVariances::ZERO,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use chrono::Days;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn splits_revenue_adjustment_across_period_exactly() {
|
||||
let period = YearMonth::from_year_month(2026, 4).unwrap();
|
||||
let adjustment = dec!(500);
|
||||
let flow = compute_actual_distribution_flow(
|
||||
period,
|
||||
dec!(30),
|
||||
dec!(30),
|
||||
adjustment,
|
||||
);
|
||||
let mut date = period.date();
|
||||
let mut distributions = Vec::new();
|
||||
|
||||
for _ in 0..30 {
|
||||
distributions.push(flow.distribution_for_day(date, dec!(1), 0));
|
||||
date = date.checked_add_days(Days::new(1)).unwrap();
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
distributions
|
||||
.iter()
|
||||
.map(|day| day.revenue_adjustment_usd)
|
||||
.sum::<Decimal>(),
|
||||
adjustment,
|
||||
);
|
||||
assert_eq!(
|
||||
distributions
|
||||
.iter()
|
||||
.map(|day| day.net_revenue_usd)
|
||||
.sum::<Decimal>(),
|
||||
dec!(530),
|
||||
);
|
||||
assert!(
|
||||
distributions[..29]
|
||||
.iter()
|
||||
.all(|day| day.revenue_adjustment_usd
|
||||
== distributions[0].revenue_adjustment_usd),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use rust_decimal::Decimal;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use xredis::RedisPool;
|
||||
|
||||
use super::{Adjustment, PayoutRunDay, PayoutRunPayload};
|
||||
use super::{PayoutRunDay, PayoutRunPayload, RevenueAdjustment};
|
||||
use crate::{
|
||||
auth::{
|
||||
AuthenticationError, get_user_from_headers, two_factor::verify_2fa_code,
|
||||
@@ -45,7 +45,7 @@ pub struct StartPayoutRun {
|
||||
pub ignore_totp: bool,
|
||||
#[serde(with = "rust_decimal::serde::float")]
|
||||
pub raw_actual_revenue_usd: Decimal,
|
||||
pub adjustments: Vec<Adjustment>,
|
||||
pub revenue_adjustments: Vec<RevenueAdjustment>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, utoipa::ToSchema)]
|
||||
@@ -58,10 +58,11 @@ pub struct StartPayoutRunResponse {
|
||||
pub struct CalculatePayoutRunResponse {
|
||||
pub period: YearMonth,
|
||||
pub days: Vec<PayoutRunDay>,
|
||||
/// Sum of all adjustments that would be applied on top of actual revenue.
|
||||
/// Sum of all revenue adjustments that would be applied on top of actual
|
||||
/// revenue.
|
||||
#[serde(with = "rust_decimal::serde::float")]
|
||||
pub total_adjustments: Decimal,
|
||||
pub adjustments: Vec<Adjustment>,
|
||||
pub total_revenue_adjustment_usd: Decimal,
|
||||
pub revenue_adjustments: Vec<RevenueAdjustment>,
|
||||
}
|
||||
|
||||
/// Calculate a payout run without scheduling it.
|
||||
@@ -148,9 +149,16 @@ pub async fn calculate_run(
|
||||
.iter()
|
||||
.map(|day| day.raw_estimated_revenue_usd)
|
||||
.sum();
|
||||
let total_revenue_adjustment_usd = body
|
||||
.revenue_adjustments
|
||||
.iter()
|
||||
.map(|adjustment| adjustment.amount_usd)
|
||||
.sum();
|
||||
let actual_flow = compute_actual_distribution_flow(
|
||||
body.period,
|
||||
total_estimated_revenue_usd,
|
||||
body.raw_actual_revenue_usd,
|
||||
total_revenue_adjustment_usd,
|
||||
);
|
||||
let days = estimate
|
||||
.days
|
||||
@@ -161,6 +169,7 @@ pub async fn calculate_run(
|
||||
day.date,
|
||||
day.raw_estimated_revenue_usd,
|
||||
day.impressions,
|
||||
Decimal::ZERO,
|
||||
&variances,
|
||||
),
|
||||
actual: Some(actual_flow.distribution_for_day(
|
||||
@@ -170,17 +179,12 @@ pub async fn calculate_run(
|
||||
)),
|
||||
})
|
||||
.collect();
|
||||
let total_adjustments = body
|
||||
.adjustments
|
||||
.iter()
|
||||
.map(|adjustment| adjustment.amount_usd)
|
||||
.sum();
|
||||
|
||||
Ok(web::Json(CalculatePayoutRunResponse {
|
||||
period: body.period,
|
||||
days,
|
||||
total_adjustments,
|
||||
adjustments: body.adjustments,
|
||||
total_revenue_adjustment_usd,
|
||||
revenue_adjustments: body.revenue_adjustments,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -290,7 +294,7 @@ pub async fn start_run(
|
||||
.wrap_failed_dependency_err("validating payout estimate")?;
|
||||
let payload = PayoutRunPayload {
|
||||
raw_actual_revenue_usd: body.raw_actual_revenue_usd,
|
||||
adjustments: body.adjustments.clone(),
|
||||
revenue_adjustments: body.revenue_adjustments.clone(),
|
||||
};
|
||||
|
||||
let mut transaction = pool
|
||||
@@ -303,7 +307,7 @@ pub async fn start_run(
|
||||
INSERT INTO payout_periods (
|
||||
period,
|
||||
raw_actual_aditude_revenue_usd,
|
||||
adjustments
|
||||
revenue_adjustments
|
||||
)
|
||||
VALUES ($1, $2, '[]'::jsonb)
|
||||
ON CONFLICT (period) DO UPDATE SET
|
||||
|
||||
@@ -6,7 +6,7 @@ use rust_decimal::Decimal;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use xredis::RedisPool;
|
||||
|
||||
use super::Adjustment;
|
||||
use super::RevenueAdjustment;
|
||||
use crate::{
|
||||
auth::get_user_from_headers,
|
||||
database::{
|
||||
@@ -43,14 +43,15 @@ pub struct PayoutRunPeriod {
|
||||
/// When the active payout run is scheduled to begin executing.
|
||||
pub runs_at: Option<DateTime<Utc>>,
|
||||
pub days: Vec<PayoutRunDay>,
|
||||
/// Sum of all adjustments applied on top of actual revenue.
|
||||
/// Sum of all revenue adjustments applied on top of actual revenue.
|
||||
#[serde(with = "rust_decimal::serde::float")]
|
||||
pub total_adjustments: Decimal,
|
||||
/// Individual adjustments, including their admin-provided descriptions.
|
||||
pub total_revenue_adjustment_usd: Decimal,
|
||||
/// Individual revenue adjustments, including their admin-provided
|
||||
/// descriptions.
|
||||
///
|
||||
/// Only visible to admins.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub adjustments: Option<Vec<Adjustment>>,
|
||||
pub revenue_adjustments: Option<Vec<RevenueAdjustment>>,
|
||||
}
|
||||
|
||||
/// Has revenue been distributed for a specific payout period month yet?
|
||||
@@ -200,25 +201,28 @@ pub async fn get_runs(
|
||||
.get(&requested_period.date())
|
||||
.filter(|period| !period.days.is_empty())
|
||||
{
|
||||
let period_adjustments =
|
||||
let period_revenue_adjustments =
|
||||
if let Some(payload) = &period.active_run_payload {
|
||||
&payload.adjustments
|
||||
&payload.revenue_adjustments
|
||||
} else {
|
||||
&period.adjustments
|
||||
&period.revenue_adjustments
|
||||
};
|
||||
let total_adjustments = period_adjustments
|
||||
let total_revenue_adjustment_usd = period_revenue_adjustments
|
||||
.iter()
|
||||
.map(|adjustment| adjustment.amount_usd)
|
||||
.sum();
|
||||
let adjustments = is_admin.then(|| period_adjustments.clone());
|
||||
let revenue_adjustments =
|
||||
is_admin.then(|| period_revenue_adjustments.clone());
|
||||
let total_estimated_revenue_usd = period
|
||||
.days
|
||||
.iter()
|
||||
.map(|day| day.raw_estimated_aditude_revenue_usd)
|
||||
.sum();
|
||||
let actual_flow = compute_actual_distribution_flow(
|
||||
requested_period,
|
||||
total_estimated_revenue_usd,
|
||||
period.raw_actual_aditude_revenue_usd,
|
||||
total_revenue_adjustment_usd,
|
||||
);
|
||||
let days = period
|
||||
.days
|
||||
@@ -235,6 +239,7 @@ pub async fn get_runs(
|
||||
day.date,
|
||||
day.raw_estimated_aditude_revenue_usd,
|
||||
impressions,
|
||||
Decimal::ZERO,
|
||||
&variances,
|
||||
),
|
||||
actual: Some(actual_flow.distribution_for_day(
|
||||
@@ -258,8 +263,8 @@ pub async fn get_runs(
|
||||
status,
|
||||
runs_at: period.active_run_execute_at,
|
||||
days,
|
||||
total_adjustments,
|
||||
adjustments,
|
||||
total_revenue_adjustment_usd,
|
||||
revenue_adjustments,
|
||||
})
|
||||
} else {
|
||||
let estimate = live_estimates
|
||||
@@ -282,6 +287,7 @@ pub async fn get_runs(
|
||||
day.date,
|
||||
day.raw_estimated_revenue_usd,
|
||||
day.impressions,
|
||||
Decimal::ZERO,
|
||||
&variances,
|
||||
),
|
||||
actual: None,
|
||||
@@ -293,8 +299,8 @@ pub async fn get_runs(
|
||||
status,
|
||||
runs_at: None,
|
||||
days,
|
||||
total_adjustments: Decimal::ZERO,
|
||||
adjustments: is_admin.then(Vec::new),
|
||||
total_revenue_adjustment_usd: Decimal::ZERO,
|
||||
revenue_adjustments: is_admin.then(Vec::new),
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
mod admin;
|
||||
mod fetch;
|
||||
|
||||
pub use crate::queue::payout_run::{Adjustment, PayoutRunPayload};
|
||||
pub use crate::queue::payout_run::{PayoutRunPayload, RevenueAdjustment};
|
||||
pub use admin::*;
|
||||
pub use fetch::*;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user