diff --git a/apps/labrinth/.env.docker-compose b/apps/labrinth/.env.docker-compose index bff0a03574..2d2333a571 100644 --- a/apps/labrinth/.env.docker-compose +++ b/apps/labrinth/.env.docker-compose @@ -160,6 +160,7 @@ FLAME_ANVIL_URL=none STRIPE_API_KEY=none STRIPE_WEBHOOK_SECRET=none +ADITUDE_BASE_URL=https://cloud.aditude.io/api ADITUDE_API_KEY=none PYRO_API_KEY=none diff --git a/apps/labrinth/.env.local b/apps/labrinth/.env.local index 79220889d5..46fcf4a60a 100644 --- a/apps/labrinth/.env.local +++ b/apps/labrinth/.env.local @@ -186,6 +186,7 @@ FLAME_ANVIL_URL=none STRIPE_API_KEY=none STRIPE_WEBHOOK_SECRET=none +ADITUDE_BASE_URL=https://cloud.aditude.io/api ADITUDE_API_KEY=none PYRO_API_KEY=none diff --git a/apps/labrinth/src/env.rs b/apps/labrinth/src/env.rs index ce0bba6fed..286a11fbae 100644 --- a/apps/labrinth/src/env.rs +++ b/apps/labrinth/src/env.rs @@ -335,6 +335,7 @@ vars! { STRIPE_API_KEY: String = "none"; STRIPE_WEBHOOK_SECRET: String = "none"; + ADITUDE_BASE_URL: String = "https://cloud.aditude.io/api"; ADITUDE_API_KEY: String = "none"; PYRO_API_KEY: String = "none"; diff --git a/apps/labrinth/src/lib.rs b/apps/labrinth/src/lib.rs index 382dd9fc66..d7b7157d82 100644 --- a/apps/labrinth/src/lib.rs +++ b/apps/labrinth/src/lib.rs @@ -70,6 +70,7 @@ pub struct LabrinthConfig { pub rate_limiter: web::Data, pub stripe_client: stripe::Client, pub anrok_client: anrok::Client, + pub aditude_client: web::Data, pub email_queue: web::Data, pub archon_client: web::Data, pub gotenberg_client: GotenbergClient, @@ -99,6 +100,10 @@ pub fn app_setup( let scheduler = scheduler::Scheduler::new(); let http_client = web::Data::new(HttpClient::new()); + let aditude_client = web::Data::new(aditude::Client::new( + ENV.ADITUDE_BASE_URL.clone(), + ENV.ADITUDE_API_KEY.clone(), + )); let tiltify_client = web::Data::new(TiltifyClient::new(http_client.get_ref().clone())); let search_state = web::Data::new(search::SearchState { @@ -302,6 +307,7 @@ pub fn app_setup( rate_limiter: limiter, stripe_client, anrok_client, + aditude_client, gotenberg_client, http_client, tiltify_client, @@ -359,6 +365,7 @@ pub fn app_data_config( .app_data(labrinth_config.archon_client.clone()) .app_data(web::Data::new(labrinth_config.stripe_client.clone())) .app_data(web::Data::new(labrinth_config.anrok_client.clone())) + .app_data(labrinth_config.aditude_client.clone()) .app_data(labrinth_config.rate_limiter.clone()) .app_data(labrinth_config.kafka_client.clone()) .app_data(labrinth_config.search_state.clone()) diff --git a/apps/labrinth/src/queue/payout_run/estimate.rs b/apps/labrinth/src/queue/payout_run/estimate.rs index ca9a2e18e2..adda4c3ea4 100644 --- a/apps/labrinth/src/queue/payout_run/estimate.rs +++ b/apps/labrinth/src/queue/payout_run/estimate.rs @@ -36,7 +36,7 @@ pub async fn estimate( periods: &[YearMonth], ) -> Result> { redis - .get_cached_keys(REDIS_KEY, periods, |periods| async { + .get_cached_keys(REDIS_KEY, periods, |periods| async move { fetch_estimates(aditude, redis, &periods) .await .map_err(ApiError::Internal) diff --git a/apps/labrinth/src/queue/payout_run/mod.rs b/apps/labrinth/src/queue/payout_run/mod.rs index 4a4fafa2b2..9e6c5b5b83 100644 --- a/apps/labrinth/src/queue/payout_run/mod.rs +++ b/apps/labrinth/src/queue/payout_run/mod.rs @@ -26,7 +26,7 @@ //! //! How revenue is distributed: //! - While a month is still open/in review: -//! - `raw_estimated_revenue_usd`: how much our ad provider estimates we'll +//! - `estimated.raw_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 @@ -36,23 +36,23 @@ //! - 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) +//! - `net_revenue_usd`: raw estimated - fees - variance +//! - `platform_net_revenue_usd`: net estimated revenue x Modrinth's cut +//! - `creator_net_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 +//! - `actual.raw_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 +//! let factor = actual.raw_revenue_usd / raw_estimated_revenue_usd +//! raw_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, +//! - `actual.net_revenue_usd`: raw actual revenue - fees + adjustments +//! - `actual.(platform|creator)_net_revenue_usd`: same logic as estimated, //! but using the net actual revenue //! //! ## Variance @@ -76,12 +76,22 @@ const PLATFORM_REVENUE_SPLIT: Decimal = dec!(0.25); /// This may refer to either estimated or actual revenue. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DayDistribution { + /// Amount of revenue input into the algorithm. pub raw_revenue_usd: Decimal, + /// Operational fees to subtract. pub fees_usd: Decimal, + /// Estimation variance to subtract. + /// + /// For non-estimates (actual revenue values), this is zero. pub variance_usd: Decimal, + /// Manually-input adjustments on top of raw revenue. pub sum_adjustments_usd: Decimal, + /// Total net revenue that we earned; + /// `raw_revenue - fees - variance + sum_adjustments`. pub net_revenue_usd: Decimal, + /// How much of the net revenue goes to the platform. pub platform_net_revenue_usd: Decimal, + /// How much of the net revenue goes to creators. pub creator_net_revenue_usd: Decimal, } diff --git a/apps/labrinth/src/routes/internal/mod.rs b/apps/labrinth/src/routes/internal/mod.rs index 1ad10523b1..2018d9ba1f 100644 --- a/apps/labrinth/src/routes/internal/mod.rs +++ b/apps/labrinth/src/routes/internal/mod.rs @@ -15,6 +15,7 @@ pub mod medal; pub mod moderation; pub mod mural; pub mod pats; +pub mod payout_runs; pub mod search; pub mod server_ping; pub mod session; diff --git a/apps/labrinth/src/routes/internal/payout_runs.rs b/apps/labrinth/src/routes/internal/payout_runs.rs new file mode 100644 index 0000000000..826d93279d --- /dev/null +++ b/apps/labrinth/src/routes/internal/payout_runs.rs @@ -0,0 +1,37 @@ +use actix_web::{get, web}; + +use crate::{queue::payout_run::DayDistribution, routes::ApiError}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct PayoutRuns { + pub periods: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PayoutRunPeriod { + pub period: YearMonth, + pub status: PayoutRunStatus, + pub days: Vec, + pub adjustments: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PayoutRunDay { + pub date: NaiveDate, + pub estimated: DayDistribution, + pub actual: Option, +} + +/// Manual admin-input adjustment to a [`PayoutRunPeriod`]. +#[derive(Debug, Serialize, Deserialize)] +pub struct PayoutRunAdjustment { + /// Total value of the adjustment. + pub amount_usd: Decimal, + /// Why this adjustment was applied. + /// + /// Only visible to admins. + pub description: Option, +} + +#[get("/")] +pub async fn get_runs() -> Result, ApiError> {}