mirror of
https://github.com/modrinth/code.git
synced 2026-08-30 11:36:05 +00:00
adjustments on the net rev pool
This commit is contained in:
Generated
+1
-3
@@ -76,12 +76,10 @@
|
||||
"name": "delphi_severity",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"hidden",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"severe",
|
||||
"malware"
|
||||
"severe"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n\t\t\t\tINSERT INTO payout_periods (\n\t\t\t\t\tperiod,\n\t\t\t\t\traw_actual_aditude_revenue_usd,\n\t\t\t\t\trevenue_adjustments\n\t\t\t\t)\n\t\t\t\tSELECT period, 0, '[]'::jsonb\n\t\t\t\tFROM UNNEST($1::date[]) AS period\n\t\t\t\t",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"DateArray"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "093388c38a65cd30c1ad75b265cd70d17d47cf60ac42bf55855fda8053fcdf49"
|
||||
}
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n\t\t\tSELECT\n\t\t\t\tpayout_periods.period,\n\t\t\t\tpayout_periods.raw_actual_aditude_revenue_usd,\n\t\t\t\tpayout_periods.adjustments AS \"adjustments: Json<Vec<Adjustment>>\",\n\t\t\t\tactive_run.payload AS \"active_run_payload: Json<PayoutRunPayload>\",\n\t\t\t\tactive_run.execute_at AS active_run_execute_at,\n\t\t\t\tEXISTS (\n\t\t\t\t\tSELECT 1\n\t\t\t\t\tFROM payout_runs\n\t\t\t\t\tWHERE payout_runs.period = payout_periods.period\n\t\t\t\t\t\tAND payout_runs.status = 'succeeded'\n\t\t\t\t) AS \"has_succeeded_run!\"\n\t\t\tFROM payout_periods\n\t\t\tLEFT JOIN payout_runs active_run\n\t\t\t\tON active_run.period = payout_periods.period\n\t\t\t\tAND active_run.status IN ('scheduled', 'running')\n\t\t\tWHERE payout_periods.period = ANY($1)\n\t\t\t",
|
||||
"query": "\n\t\t\tSELECT\n\t\t\t\tpayout_periods.period,\n\t\t\t\tpayout_periods.raw_actual_aditude_revenue_usd,\n\t\t\t\tpayout_periods.revenue_adjustments AS \"revenue_adjustments: Json<Vec<RevenueAdjustment>>\",\n\t\t\t\tactive_run.payload AS \"active_run_payload: Json<PayoutRunPayload>\",\n\t\t\t\tactive_run.execute_at AS active_run_execute_at,\n\t\t\t\tEXISTS (\n\t\t\t\t\tSELECT 1\n\t\t\t\t\tFROM payout_runs\n\t\t\t\t\tWHERE payout_runs.period = payout_periods.period\n\t\t\t\t\t\tAND payout_runs.status = 'succeeded'\n\t\t\t\t) AS \"has_succeeded_run!\"\n\t\t\tFROM payout_periods\n\t\t\tLEFT JOIN payout_runs active_run\n\t\t\t\tON active_run.period = payout_periods.period\n\t\t\t\tAND active_run.status IN ('scheduled', 'running')\n\t\t\tWHERE payout_periods.period = ANY($1)\n\t\t\t",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -15,7 +15,7 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "adjustments: Json<Vec<Adjustment>>",
|
||||
"name": "revenue_adjustments: Json<Vec<RevenueAdjustment>>",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
@@ -48,5 +48,5 @@
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "56e3d8b2961562ef5a8be80f71814743f0cd95a7c26e4d0dd5b354d2ac3bbaf1"
|
||||
"hash": "0f8bb9a3ade9d28ee2d91e10b3d06a03bccfe57a196c62b27b5334525f32e0ca"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO payout_periods (\n period,\n raw_actual_aditude_revenue_usd,\n adjustments\n )\n VALUES ($1, 100, '[]'::jsonb)\n ",
|
||||
"query": "\n INSERT INTO payout_periods (\n period,\n raw_actual_aditude_revenue_usd,\n revenue_adjustments\n )\n VALUES ($1, 100, '[]'::jsonb)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -10,5 +10,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "291b7a70925bdd5d748048fa08dea1901ce8c2b584ec86921efe6c4d2d279b29"
|
||||
"hash": "12c7d361e554ea8d9216c420cdf6bb79c460a4dc4217a656ab4d2e11c714ea56"
|
||||
}
|
||||
Generated
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO payout_periods (\n period,\n raw_actual_aditude_revenue_usd,\n revenue_adjustments\n )\n VALUES ($1, $2, '[]'::jsonb)\n ON CONFLICT (period) DO UPDATE SET\n raw_actual_aditude_revenue_usd =\n EXCLUDED.raw_actual_aditude_revenue_usd\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Date",
|
||||
"Numeric"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "1437d67c3b497ae0ae5d3229ae4cda5dbe8c2d451fe264b7392c8d8cbec73a58"
|
||||
}
|
||||
Generated
+1
-3
@@ -76,12 +76,10 @@
|
||||
"name": "delphi_severity",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"hidden",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"severe",
|
||||
"malware"
|
||||
"severe"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Generated
-15
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO payout_periods (\n period,\n raw_actual_aditude_revenue_usd,\n adjustments\n )\n VALUES ($1, $2, '[]'::jsonb)\n ON CONFLICT (period) DO UPDATE SET\n raw_actual_aditude_revenue_usd =\n EXCLUDED.raw_actual_aditude_revenue_usd\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Date",
|
||||
"Numeric"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "4798f2780f28c4329d96af43e4902d97972573a8f4954ceab284b8d8989641a7"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO payout_runs (\n id,\n period,\n payload,\n status,\n started_at,\n started_by,\n execute_at,\n processing_started_at,\n finished_at\n )\n VALUES (\n $1,\n $2,\n '{\"raw_actual_revenue_usd\":100,\"adjustments\":[]}'::jsonb,\n 'succeeded',\n $3,\n $4,\n $5,\n $5,\n $6\n )\n ",
|
||||
"query": "\n INSERT INTO payout_runs (\n id,\n period,\n payload,\n status,\n started_at,\n started_by,\n execute_at,\n processing_started_at,\n finished_at\n )\n VALUES (\n $1,\n $2,\n '{\"raw_actual_revenue_usd\":100,\"revenue_adjustments\":[]}'::jsonb,\n 'succeeded',\n $3,\n $4,\n $5,\n $5,\n $6\n )\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "c30e4f1aad62a3a8c278340f051e91476a4fbe3d7774ef9fed712929a5816ecb"
|
||||
"hash": "49e243559bf389d86da849dcd20108c1cb8cc0633a6c90ab662543295af068c9"
|
||||
}
|
||||
Generated
+1
-3
@@ -41,12 +41,10 @@
|
||||
"name": "delphi_severity",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"hidden",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"severe",
|
||||
"malware"
|
||||
"severe"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+1
-3
@@ -26,12 +26,10 @@
|
||||
"name": "delphi_severity",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"hidden",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"severe",
|
||||
"malware"
|
||||
"severe"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Generated
-14
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n\t\t\t\tINSERT INTO payout_periods (\n\t\t\t\t\tperiod,\n\t\t\t\t\traw_actual_aditude_revenue_usd,\n\t\t\t\t\tadjustments\n\t\t\t\t)\n\t\t\t\tSELECT period, 0, '[]'::jsonb\n\t\t\t\tFROM UNNEST($1::date[]) AS period\n\t\t\t\t",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"DateArray"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "8d6cbbb4963ff2d0cc36c2c91e147fda4c6a6d69ec96bc4e531dc695e4e92f62"
|
||||
}
|
||||
Generated
+1
-3
@@ -22,12 +22,10 @@
|
||||
"name": "delphi_severity",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"hidden",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"severe",
|
||||
"malware"
|
||||
"severe"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n\t\tINSERT INTO payout_runs (\n\t\t\tid,\n\t\t\tperiod,\n\t\t\tpayload,\n\t\t\tstatus,\n\t\t\tstarted_at,\n\t\t\tstarted_by,\n\t\t\texecute_at\n\t\t)\n\t\tVALUES (\n\t\t\t$1,\n\t\t\t$2,\n\t\t\t'{\"raw_actual_revenue_usd\": 0, \"adjustments\": []}'::jsonb,\n\t\t\t$3,\n\t\t\tNOW(),\n\t\t\t$4,\n\t\t\tNOW()\n\t\t)\n\t\t",
|
||||
"query": "\n\t\tINSERT INTO payout_runs (\n\t\t\tid,\n\t\t\tperiod,\n\t\t\tpayload,\n\t\t\tstatus,\n\t\t\tstarted_at,\n\t\t\tstarted_by,\n\t\t\texecute_at\n\t\t)\n\t\tVALUES (\n\t\t\t$1,\n\t\t\t$2,\n\t\t\t'{\"raw_actual_revenue_usd\": 0, \"revenue_adjustments\": []}'::jsonb,\n\t\t\t$3,\n\t\t\tNOW(),\n\t\t\t$4,\n\t\t\tNOW()\n\t\t)\n\t\t",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -13,5 +13,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "0f4cefdd003516ad3260cd7b471fef35cf224e053933f4aec75f377fbdffbbcb"
|
||||
"hash": "b40a175bcc4465b7c2793572528de6bd1f8e4923f879149d8559b9e620054c96"
|
||||
}
|
||||
Generated
+1
-3
@@ -19,12 +19,10 @@
|
||||
"name": "delphi_severity",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"hidden",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"severe",
|
||||
"malware"
|
||||
"severe"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ CREATE TABLE payout_periods (
|
||||
CHECK (EXTRACT(DAY FROM period) = 1),
|
||||
raw_actual_aditude_revenue_usd NUMERIC(40, 20) NOT NULL
|
||||
CHECK (raw_actual_aditude_revenue_usd >= 0),
|
||||
adjustments JSONB NOT NULL
|
||||
revenue_adjustments JSONB NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE payout_period_days (
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ async fn insert_succeeded_period(
|
||||
INSERT INTO payout_periods (
|
||||
period,
|
||||
raw_actual_aditude_revenue_usd,
|
||||
adjustments
|
||||
revenue_adjustments
|
||||
)
|
||||
VALUES ($1, 100, '[]'::jsonb)
|
||||
"#,
|
||||
@@ -81,7 +81,7 @@ async fn insert_succeeded_period(
|
||||
VALUES (
|
||||
$1,
|
||||
$2,
|
||||
'{"raw_actual_revenue_usd":100,"adjustments":[]}'::jsonb,
|
||||
'{"raw_actual_revenue_usd":100,"revenue_adjustments":[]}'::jsonb,
|
||||
'succeeded',
|
||||
$3,
|
||||
$4,
|
||||
|
||||
@@ -31,7 +31,7 @@ async fn insert_run(
|
||||
VALUES (
|
||||
$1,
|
||||
$2,
|
||||
'{"raw_actual_revenue_usd": 0, "adjustments": []}'::jsonb,
|
||||
'{"raw_actual_revenue_usd": 0, "revenue_adjustments": []}'::jsonb,
|
||||
$3,
|
||||
NOW(),
|
||||
$4,
|
||||
@@ -118,7 +118,7 @@ async fn payout_run_lineage_and_active_run_constraints() {
|
||||
INSERT INTO payout_periods (
|
||||
period,
|
||||
raw_actual_aditude_revenue_usd,
|
||||
adjustments
|
||||
revenue_adjustments
|
||||
)
|
||||
SELECT period, 0, '[]'::jsonb
|
||||
FROM UNNEST($1::date[]) AS period
|
||||
|
||||
Reference in New Issue
Block a user