mirror of
https://github.com/modrinth/code.git
synced 2026-09-02 13:05:50 +00:00
fetch from db first instead of aditude
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
//! Logic for fetching and caching revenue estimations from our ad provider.
|
//! Logic for fetching and caching revenue estimations from our ad provider.
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
use chrono::{Months, NaiveDate};
|
use chrono::{DateTime, Days, NaiveDate, Utc};
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
use eyre::Result;
|
use eyre::Result;
|
||||||
use rust_decimal::Decimal;
|
use rust_decimal::Decimal;
|
||||||
@@ -29,15 +29,25 @@ pub struct DayEstimate {
|
|||||||
pub impressions: u128,
|
pub impressions: u128,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get per-month and per-day estimated ad provider info.
|
/// Get per-month and per-day estimated ad provider info for an inclusive date
|
||||||
|
/// range.
|
||||||
|
///
|
||||||
|
/// Dates are interpreted as Phoenix calendar dates. They are converted to UTC
|
||||||
|
/// instants only when constructing the Aditude API request.
|
||||||
pub async fn estimate(
|
pub async fn estimate(
|
||||||
aditude: &aditude::Client,
|
aditude: &aditude::Client,
|
||||||
redis: &RedisPool,
|
redis: &RedisPool,
|
||||||
periods: &[YearMonth],
|
periods: &[YearMonth],
|
||||||
|
start_date: NaiveDate,
|
||||||
|
end_date: NaiveDate,
|
||||||
) -> Result<Vec<PeriodEstimate>> {
|
) -> Result<Vec<PeriodEstimate>> {
|
||||||
|
if start_date > end_date {
|
||||||
|
return Err(eyre::eyre!("estimate start date is after end date"));
|
||||||
|
}
|
||||||
|
|
||||||
let mut periods = redis
|
let mut periods = redis
|
||||||
.get_cached_keys(REDIS_KEY, periods, |periods| async move {
|
.get_cached_keys(REDIS_KEY, periods, |periods| async move {
|
||||||
fetch_estimates(aditude, &periods)
|
fetch_estimates(aditude, &periods, start_date, end_date)
|
||||||
.await
|
.await
|
||||||
.map_err(ApiError::Internal)
|
.map_err(ApiError::Internal)
|
||||||
})
|
})
|
||||||
@@ -49,16 +59,13 @@ pub async fn estimate(
|
|||||||
async fn fetch_estimates(
|
async fn fetch_estimates(
|
||||||
aditude: &aditude::Client,
|
aditude: &aditude::Client,
|
||||||
periods: &[YearMonth],
|
periods: &[YearMonth],
|
||||||
|
start_date: NaiveDate,
|
||||||
|
end_date: NaiveDate,
|
||||||
) -> Result<DashMap<YearMonth, PeriodEstimate>> {
|
) -> Result<DashMap<YearMonth, PeriodEstimate>> {
|
||||||
let mut periods_iter = periods.iter();
|
let range_start = aditude::phoenix_midnight(start_date);
|
||||||
let first_period = periods_iter.next().wrap_err("no first period")?;
|
let range_end_date = end_date
|
||||||
let last_period = periods_iter.last().unwrap_or(first_period);
|
.checked_add_days(Days::new(1))
|
||||||
|
.wrap_err("calculating day after estimate range end")?;
|
||||||
let range_start = aditude::phoenix_midnight(first_period.date());
|
|
||||||
let range_end_date = last_period
|
|
||||||
.date()
|
|
||||||
.checked_add_months(Months::new(1))
|
|
||||||
.wrap_err("calculating month after payout period end")?;
|
|
||||||
let range_end = aditude::phoenix_midnight(range_end_date);
|
let range_end = aditude::phoenix_midnight(range_end_date);
|
||||||
|
|
||||||
let metrics = aditude
|
let metrics = aditude
|
||||||
@@ -76,33 +83,59 @@ async fn fetch_estimates(
|
|||||||
.await
|
.await
|
||||||
.wrap_err("fetching metrics from Aditude")?;
|
.wrap_err("fetching metrics from Aditude")?;
|
||||||
|
|
||||||
let mut map = HashMap::<YearMonth, PeriodEstimate>::new();
|
Ok(period_estimates(metrics, periods))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct PartialDayEstimate {
|
||||||
|
raw_estimated_revenue_usd: Option<Decimal>,
|
||||||
|
impressions: Option<u128>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn period_estimates(
|
||||||
|
metrics: aditude::v2::Metrics,
|
||||||
|
periods: &[YearMonth],
|
||||||
|
) -> DashMap<YearMonth, PeriodEstimate> {
|
||||||
|
let requested_periods = periods.iter().copied().collect::<HashSet<_>>();
|
||||||
|
let mut partial_days = HashMap::<DateTime<Utc>, PartialDayEstimate>::new();
|
||||||
for response in metrics.responses {
|
for response in metrics.responses {
|
||||||
for row in response.rows {
|
for row in response.rows {
|
||||||
let Some(raw_estimated_revenue_usd) = row.revenue else {
|
let day = partial_days.entry(row.time).or_default();
|
||||||
continue;
|
if let Some(raw_estimated_revenue_usd) = row.revenue {
|
||||||
};
|
day.raw_estimated_revenue_usd = Some(raw_estimated_revenue_usd);
|
||||||
let Some(impressions) = row.impressions else {
|
}
|
||||||
continue;
|
if let Some(impressions) = row.impressions {
|
||||||
};
|
day.impressions = Some(impressions);
|
||||||
|
}
|
||||||
let date = aditude::phoenix_date(row.time);
|
|
||||||
let period = YearMonth::from_day1(date);
|
|
||||||
|
|
||||||
let period_estimate = map.entry(period).or_insert(PeriodEstimate {
|
|
||||||
period,
|
|
||||||
days: Vec::new(),
|
|
||||||
});
|
|
||||||
let days = &mut period_estimate.days;
|
|
||||||
|
|
||||||
days.push(DayEstimate {
|
|
||||||
date,
|
|
||||||
raw_estimated_revenue_usd,
|
|
||||||
impressions,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut map = HashMap::<YearMonth, PeriodEstimate>::new();
|
||||||
|
for (time, day) in partial_days {
|
||||||
|
let Some(raw_estimated_revenue_usd) = day.raw_estimated_revenue_usd
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(impressions) = day.impressions else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let date = aditude::phoenix_date(time);
|
||||||
|
let period = YearMonth::from_day1(date);
|
||||||
|
if !requested_periods.contains(&period) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let period_estimate = map.entry(period).or_insert(PeriodEstimate {
|
||||||
|
period,
|
||||||
|
days: Vec::new(),
|
||||||
|
});
|
||||||
|
period_estimate.days.push(DayEstimate {
|
||||||
|
date,
|
||||||
|
raw_estimated_revenue_usd,
|
||||||
|
impressions,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// we have no clue if the Aditude row return order is stable,
|
// we have no clue if the Aditude row return order is stable,
|
||||||
// so for safety sort the days here
|
// so for safety sort the days here
|
||||||
for period in map.values_mut() {
|
for period in map.values_mut() {
|
||||||
@@ -118,5 +151,46 @@ async fn fetch_estimates(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(map.into_iter().collect::<DashMap<_, _>>())
|
map.into_iter().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use chrono::TimeZone;
|
||||||
|
use rust_decimal::dec;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn combines_metrics_from_separate_responses() {
|
||||||
|
let time = Utc.with_ymd_and_hms(2026, 8, 1, 7, 0, 0).unwrap();
|
||||||
|
let metrics = aditude::v2::Metrics {
|
||||||
|
responses: vec![
|
||||||
|
aditude::v2::Response {
|
||||||
|
rows: vec![aditude::v2::Row {
|
||||||
|
impressions: None,
|
||||||
|
revenue: Some(dec!(12.34)),
|
||||||
|
time,
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
aditude::v2::Response {
|
||||||
|
rows: vec![aditude::v2::Row {
|
||||||
|
impressions: Some(5678),
|
||||||
|
revenue: None,
|
||||||
|
time,
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
let period =
|
||||||
|
YearMonth::from_day1(NaiveDate::from_ymd_opt(2026, 8, 1).unwrap());
|
||||||
|
|
||||||
|
let estimates = period_estimates(metrics, &[period]);
|
||||||
|
let estimate = estimates.get(&period).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(estimate.days.len(), 1);
|
||||||
|
assert_eq!(estimate.days[0].date, period.date());
|
||||||
|
assert_eq!(estimate.days[0].raw_estimated_revenue_usd, dec!(12.34));
|
||||||
|
assert_eq!(estimate.days[0].impressions, 5678);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use actix_web::{HttpRequest, post, web};
|
use actix_web::{HttpRequest, post, web};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Months, Utc};
|
||||||
use rust_decimal::Decimal;
|
use rust_decimal::Decimal;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use xredis::RedisPool;
|
use xredis::RedisPool;
|
||||||
@@ -133,10 +133,21 @@ pub async fn start_run(
|
|||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut estimates =
|
let estimate_end_date = body
|
||||||
estimate(aditude.get_ref(), redis.get_ref(), &[body.period])
|
.period
|
||||||
.await
|
.date()
|
||||||
.wrap_internal_err("fetching payout estimate")?;
|
.checked_add_months(Months::new(1))
|
||||||
|
.and_then(|date| date.pred_opt())
|
||||||
|
.wrap_internal_err("calculating payout estimate end date")?;
|
||||||
|
let mut estimates = estimate(
|
||||||
|
aditude.get_ref(),
|
||||||
|
redis.get_ref(),
|
||||||
|
&[body.period],
|
||||||
|
body.period.date(),
|
||||||
|
estimate_end_date,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.wrap_internal_err("fetching payout estimate")?;
|
||||||
let estimate = estimates
|
let estimate = estimates
|
||||||
.pop()
|
.pop()
|
||||||
.wrap_internal_err("missing requested payout estimate")?;
|
.wrap_internal_err("missing requested payout estimate")?;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use std::collections::HashMap;
|
|||||||
|
|
||||||
use actix_web::{HttpRequest, get, web};
|
use actix_web::{HttpRequest, get, web};
|
||||||
use chrono::{Months, NaiveDate, Utc};
|
use chrono::{Months, NaiveDate, Utc};
|
||||||
|
use rust_decimal::Decimal;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use xredis::RedisPool;
|
use xredis::RedisPool;
|
||||||
|
|
||||||
@@ -40,7 +41,14 @@ pub struct PayoutRunPeriod {
|
|||||||
pub period: YearMonth,
|
pub period: YearMonth,
|
||||||
pub status: PayoutPeriodStatus,
|
pub status: PayoutPeriodStatus,
|
||||||
pub days: Vec<PayoutRunDay>,
|
pub days: Vec<PayoutRunDay>,
|
||||||
pub adjustments: Vec<Adjustment>,
|
/// Sum of all adjustments applied on top of actual revenue.
|
||||||
|
#[serde(with = "rust_decimal::serde::float")]
|
||||||
|
pub total_adjustments: Decimal,
|
||||||
|
/// Individual adjustments, including their admin-provided descriptions.
|
||||||
|
///
|
||||||
|
/// Only visible to admins.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub adjustments: Option<Vec<Adjustment>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Has revenue been distributed for a specific payout period month yet?
|
/// Has revenue been distributed for a specific payout period month yet?
|
||||||
@@ -83,7 +91,7 @@ pub async fn get_runs(
|
|||||||
aditude: web::Data<aditude::Client>,
|
aditude: web::Data<aditude::Client>,
|
||||||
session_queue: web::Data<AuthQueue>,
|
session_queue: web::Data<AuthQueue>,
|
||||||
) -> Result<web::Json<PayoutRuns>, ApiError> {
|
) -> Result<web::Json<PayoutRuns>, ApiError> {
|
||||||
let show_adjustment_descriptions = get_user_from_headers(
|
let is_admin = get_user_from_headers(
|
||||||
&req,
|
&req,
|
||||||
&**pool,
|
&**pool,
|
||||||
&redis,
|
&redis,
|
||||||
@@ -93,20 +101,29 @@ pub async fn get_runs(
|
|||||||
.await
|
.await
|
||||||
.is_ok_and(|(_, user)| user.role.is_admin());
|
.is_ok_and(|(_, user)| user.role.is_admin());
|
||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
let latest_payout_value = sqlx::query_scalar!(
|
let current_date = aditude::phoenix_date(now);
|
||||||
|
let first_period_date = sqlx::query_scalar!(
|
||||||
r#"
|
r#"
|
||||||
SELECT MAX(created)
|
SELECT LEAST(
|
||||||
FROM payouts_values
|
COALESCE(
|
||||||
|
(SELECT MAX(created)::date FROM payouts_values),
|
||||||
|
$1
|
||||||
|
),
|
||||||
|
COALESCE(
|
||||||
|
(SELECT MIN(period) FROM payout_periods),
|
||||||
|
$1
|
||||||
|
),
|
||||||
|
$1
|
||||||
|
) AS "first_period!"
|
||||||
"#,
|
"#,
|
||||||
|
current_date,
|
||||||
)
|
)
|
||||||
.fetch_one(&**pool)
|
.fetch_one(&**pool)
|
||||||
.await
|
.await
|
||||||
.wrap_internal_err("fetching latest payout value")?
|
.wrap_internal_err("fetching first payout period")?;
|
||||||
.unwrap_or(now)
|
|
||||||
.min(now);
|
|
||||||
|
|
||||||
let current_period = YearMonth::from_day1(now.date_naive());
|
let current_period = YearMonth::from_day1(current_date);
|
||||||
let mut period = YearMonth::from_day1(latest_payout_value.date_naive());
|
let mut period = YearMonth::from_day1(first_period_date);
|
||||||
let mut requested_periods = Vec::new();
|
let mut requested_periods = Vec::new();
|
||||||
|
|
||||||
while period <= current_period {
|
while period <= current_period {
|
||||||
@@ -130,6 +147,15 @@ pub async fn get_runs(
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|period| (period.period, period))
|
.map(|period| (period.period, period))
|
||||||
.collect::<HashMap<_, _>>();
|
.collect::<HashMap<_, _>>();
|
||||||
|
let newest_stored_day = sqlx::query_scalar!(
|
||||||
|
r#"
|
||||||
|
SELECT MAX(date)
|
||||||
|
FROM payout_period_days
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.fetch_one(&**pool)
|
||||||
|
.await
|
||||||
|
.wrap_internal_err("fetching newest stored payout period day")?;
|
||||||
let stored_variances = DBPayoutVariance::get_all(&**pool)
|
let stored_variances = DBPayoutVariance::get_all(&**pool)
|
||||||
.await
|
.await
|
||||||
.wrap_internal_err("fetching payout variances")?;
|
.wrap_internal_err("fetching payout variances")?;
|
||||||
@@ -147,26 +173,54 @@ pub async fn get_runs(
|
|||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
};
|
};
|
||||||
let estimates =
|
let live_periods = requested_periods
|
||||||
estimate(aditude.get_ref(), redis.get_ref(), &requested_periods)
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter(|period| {
|
||||||
|
stored_periods
|
||||||
|
.get(&period.date())
|
||||||
|
.is_none_or(|stored| stored.days.is_empty())
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let mut live_estimates =
|
||||||
|
if let Some(first_live_period) = live_periods.first() {
|
||||||
|
let start_date = newest_stored_day
|
||||||
|
.unwrap_or_else(|| first_live_period.date())
|
||||||
|
.min(current_date);
|
||||||
|
estimate(
|
||||||
|
aditude.get_ref(),
|
||||||
|
redis.get_ref(),
|
||||||
|
&live_periods,
|
||||||
|
start_date,
|
||||||
|
current_date,
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.wrap_internal_err("fetching payout estimates")?;
|
.wrap_internal_err("fetching payout estimates")?
|
||||||
|
.into_iter()
|
||||||
|
.map(|estimate| (estimate.period, estimate))
|
||||||
|
.collect::<HashMap<_, _>>()
|
||||||
|
} else {
|
||||||
|
HashMap::new()
|
||||||
|
};
|
||||||
|
|
||||||
let periods = estimates
|
let periods = requested_periods
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|estimate| -> Result<_, ApiError> {
|
.map(|requested_period| -> Result<_, ApiError> {
|
||||||
if let Some(period) = stored_periods.get(&estimate.period.date()) {
|
if let Some(period) = stored_periods
|
||||||
let mut adjustments =
|
.get(&requested_period.date())
|
||||||
|
.filter(|period| !period.days.is_empty())
|
||||||
|
{
|
||||||
|
let period_adjustments =
|
||||||
if let Some(payload) = &period.active_run_payload {
|
if let Some(payload) = &period.active_run_payload {
|
||||||
payload.adjustments.clone()
|
&payload.adjustments
|
||||||
} else {
|
} else {
|
||||||
period.adjustments.clone()
|
&period.adjustments
|
||||||
};
|
};
|
||||||
if !show_adjustment_descriptions {
|
let total_adjustments = period_adjustments
|
||||||
for adjustment in &mut adjustments {
|
.iter()
|
||||||
adjustment.description = None;
|
.map(|adjustment| adjustment.amount_usd)
|
||||||
}
|
.sum();
|
||||||
}
|
let adjustments = is_admin.then(|| period_adjustments.clone());
|
||||||
let total_estimated_revenue_usd = period
|
let total_estimated_revenue_usd = period
|
||||||
.days
|
.days
|
||||||
.iter()
|
.iter()
|
||||||
@@ -212,14 +266,18 @@ pub async fn get_runs(
|
|||||||
};
|
};
|
||||||
|
|
||||||
Ok(PayoutRunPeriod {
|
Ok(PayoutRunPeriod {
|
||||||
period: estimate.period,
|
period: requested_period,
|
||||||
status,
|
status,
|
||||||
days,
|
days,
|
||||||
|
total_adjustments,
|
||||||
adjustments,
|
adjustments,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
let estimate = live_estimates
|
||||||
|
.remove(&requested_period)
|
||||||
|
.wrap_internal_err("missing live payout estimate")?;
|
||||||
let status = if let Some(available_at) =
|
let status = if let Some(available_at) =
|
||||||
net_60_payout_available_at(estimate.period)
|
net_60_payout_available_at(requested_period)
|
||||||
&& now >= available_at
|
&& now >= available_at
|
||||||
{
|
{
|
||||||
PayoutPeriodStatus::InReview
|
PayoutPeriodStatus::InReview
|
||||||
@@ -242,10 +300,11 @@ pub async fn get_runs(
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
Ok(PayoutRunPeriod {
|
Ok(PayoutRunPeriod {
|
||||||
period: estimate.period,
|
period: requested_period,
|
||||||
status,
|
status,
|
||||||
days,
|
days,
|
||||||
adjustments: Vec::new(),
|
total_adjustments: Decimal::ZERO,
|
||||||
|
adjustments: is_admin.then(Vec::new),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user