add net-60 in-review payout rows

This commit is contained in:
aecsocket
2026-08-04 15:02:24 +01:00
parent 1c3fa44049
commit 8e8640bfb8
5 changed files with 62 additions and 37 deletions
+1 -1
View File
@@ -40,7 +40,7 @@ pub enum PayoutRunStatus {
Pending,
/// The ad provider should have issued payouts to us by now, and we will
/// soon run the payouts.
Review,
InReview,
/// Payouts run is currently being performed.
Running,
/// Payouts run is complete and payouts have been distributed to users.
+5 -16
View File
@@ -1,11 +1,11 @@
use crate::database::PgPool;
use crate::env::ENV;
use chrono::{Datelike, Duration, TimeZone, Utc};
use eyre::{Context, Result, eyre};
use rust_decimal::{Decimal, dec};
use tracing::warn;
use crate::database::models::{DBAffiliateCodeId, DBUserId};
use crate::util::time::{YearMonth, net_60_payout_available_at};
pub async fn process_affiliate_payouts(postgres: &PgPool) -> Result<()> {
// process:
@@ -91,21 +91,10 @@ pub async fn process_affiliate_payouts(postgres: &PgPool) -> Result<()> {
continue;
};
// affiliate payouts are Net 60 from the end of the month
// this is net 60 relative to the time of the charge's last attempt, not from now
let available = {
let year = last_attempt.year();
let month = last_attempt.month();
// get the first day of the next month
let last_day_of_month = if month == 12 {
Utc.with_ymd_and_hms(year + 1, 1, 1, 0, 0, 0).unwrap()
} else {
Utc.with_ymd_and_hms(year, month + 1, 1, 0, 0, 0).unwrap()
};
last_day_of_month + Duration::days(59)
};
let available = net_60_payout_available_at(YearMonth::from_day1(
last_attempt.date_naive(),
))
.ok_or_else(|| eyre!("failed to calculate affiliate payout date"))?;
let revenue_split = row
.revenue_split
+6 -17
View File
@@ -9,12 +9,13 @@ use crate::models::payouts::{
use crate::models::projects::MonetizationStatus;
use crate::routes::ApiError;
use crate::util::error::Context;
use crate::util::time::{YearMonth, net_60_payout_available_at};
use crate::util::webhook::{
PayoutSourceAlertType, send_slack_payout_source_alert_webhook,
};
use arc_swap::ArcSwapOption;
use base64::Engine;
use chrono::{DateTime, Datelike, Duration, NaiveTime, TimeZone, Utc};
use chrono::{DateTime, Duration, NaiveTime, Utc};
use dashmap::DashMap;
use eyre::Result;
use futures::TryStreamExt;
@@ -1132,22 +1133,10 @@ pub async fn process_payout(
let payout = net_revenue * (Decimal::from(1) - modrinth_cut);
// Ad payouts are Net 60 from the end of the month
let available = {
let now = Utc::now().date_naive();
let year = now.year();
let month = now.month();
// Get the first day of the next month
let last_day_of_month = if month == 12 {
Utc.with_ymd_and_hms(year + 1, 1, 1, 0, 0, 0).unwrap()
} else {
Utc.with_ymd_and_hms(year, month + 1, 1, 0, 0, 0).unwrap()
};
last_day_of_month + Duration::days(59)
};
let available = net_60_payout_available_at(YearMonth::from_day1(
Utc::now().date_naive(),
))
.wrap_internal_err("failed to calculate creator payout date")?;
let (
mut insert_user_ids,
+12 -2
View File
@@ -12,7 +12,7 @@ use crate::models::payout_runs::{Adjustment, PayoutRun, PayoutRunStatus};
use crate::queue::session::AuthQueue;
use crate::routes::ApiError;
use crate::util::error::Context;
use crate::util::time::YearMonth;
use crate::util::time::{YearMonth, net_60_payout_available_at};
use xredis::RedisPool;
pub fn config(cfg: &mut web::ServiceConfig) {
@@ -100,9 +100,19 @@ pub async fn get(
while period <= newest_period {
if !stored_periods.contains(&period) {
let status =
if net_60_payout_available_at(period).wrap_internal_err(
"failed to calculate payout review date",
)? <= newest_created
{
PayoutRunStatus::InReview
} else {
PayoutRunStatus::Pending
};
runs.push(PayoutRun {
period_start: period,
status: PayoutRunStatus::Pending,
status,
started_at: None,
started_by: None,
completed_at: None,
+38 -1
View File
@@ -1,6 +1,6 @@
use std::{fmt, str::FromStr};
use chrono::{Datelike, NaiveDate};
use chrono::{DateTime, Datelike, Days, Months, NaiveDate, Utc};
use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
@@ -28,6 +28,16 @@ impl YearMonth {
}
}
/// Calculate when a payout period becomes available under Net-60 terms.
pub fn net_60_payout_available_at(period: YearMonth) -> Option<DateTime<Utc>> {
period
.date()
.checked_add_months(Months::new(1))?
.and_hms_opt(0, 0, 0)?
.and_utc()
.checked_add_days(Days::new(59))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[error("a year-month must use the first day of its month")]
pub struct InvalidYearMonth;
@@ -171,4 +181,31 @@ mod tests {
assert!(serde_json::from_str::<YearMonth>(value).is_err());
}
}
#[test]
fn calculates_net_60_payout_availability() {
let august = YearMonth::from_year_month(2026, 8).unwrap();
let december = YearMonth::from_year_month(2026, 12).unwrap();
assert_eq!(
net_60_payout_available_at(august),
Some(
NaiveDate::from_ymd_opt(2026, 10, 30)
.unwrap()
.and_hms_opt(0, 0, 0)
.unwrap()
.and_utc()
)
);
assert_eq!(
net_60_payout_available_at(december),
Some(
NaiveDate::from_ymd_opt(2027, 3, 1)
.unwrap()
.and_hms_opt(0, 0, 0)
.unwrap()
.and_utc()
)
);
}
}