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, Pending,
/// The ad provider should have issued payouts to us by now, and we will /// The ad provider should have issued payouts to us by now, and we will
/// soon run the payouts. /// soon run the payouts.
Review, InReview,
/// Payouts run is currently being performed. /// Payouts run is currently being performed.
Running, Running,
/// Payouts run is complete and payouts have been distributed to users. /// 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::database::PgPool;
use crate::env::ENV; use crate::env::ENV;
use chrono::{Datelike, Duration, TimeZone, Utc};
use eyre::{Context, Result, eyre}; use eyre::{Context, Result, eyre};
use rust_decimal::{Decimal, dec}; use rust_decimal::{Decimal, dec};
use tracing::warn; use tracing::warn;
use crate::database::models::{DBAffiliateCodeId, DBUserId}; 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<()> { pub async fn process_affiliate_payouts(postgres: &PgPool) -> Result<()> {
// process: // process:
@@ -91,21 +91,10 @@ pub async fn process_affiliate_payouts(postgres: &PgPool) -> Result<()> {
continue; continue;
}; };
// affiliate payouts are Net 60 from the end of the month let available = net_60_payout_available_at(YearMonth::from_day1(
// this is net 60 relative to the time of the charge's last attempt, not from now last_attempt.date_naive(),
let available = { ))
let year = last_attempt.year(); .ok_or_else(|| eyre!("failed to calculate affiliate payout date"))?;
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 revenue_split = row let revenue_split = row
.revenue_split .revenue_split
+6 -17
View File
@@ -9,12 +9,13 @@ use crate::models::payouts::{
use crate::models::projects::MonetizationStatus; use crate::models::projects::MonetizationStatus;
use crate::routes::ApiError; use crate::routes::ApiError;
use crate::util::error::Context; use crate::util::error::Context;
use crate::util::time::{YearMonth, net_60_payout_available_at};
use crate::util::webhook::{ use crate::util::webhook::{
PayoutSourceAlertType, send_slack_payout_source_alert_webhook, PayoutSourceAlertType, send_slack_payout_source_alert_webhook,
}; };
use arc_swap::ArcSwapOption; use arc_swap::ArcSwapOption;
use base64::Engine; use base64::Engine;
use chrono::{DateTime, Datelike, Duration, NaiveTime, TimeZone, Utc}; use chrono::{DateTime, Duration, NaiveTime, Utc};
use dashmap::DashMap; use dashmap::DashMap;
use eyre::Result; use eyre::Result;
use futures::TryStreamExt; use futures::TryStreamExt;
@@ -1132,22 +1133,10 @@ pub async fn process_payout(
let payout = net_revenue * (Decimal::from(1) - modrinth_cut); let payout = net_revenue * (Decimal::from(1) - modrinth_cut);
// Ad payouts are Net 60 from the end of the month let available = net_60_payout_available_at(YearMonth::from_day1(
let available = { Utc::now().date_naive(),
let now = Utc::now().date_naive(); ))
.wrap_internal_err("failed to calculate creator payout date")?;
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 ( let (
mut insert_user_ids, 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::queue::session::AuthQueue;
use crate::routes::ApiError; use crate::routes::ApiError;
use crate::util::error::Context; use crate::util::error::Context;
use crate::util::time::YearMonth; use crate::util::time::{YearMonth, net_60_payout_available_at};
use xredis::RedisPool; use xredis::RedisPool;
pub fn config(cfg: &mut web::ServiceConfig) { pub fn config(cfg: &mut web::ServiceConfig) {
@@ -100,9 +100,19 @@ pub async fn get(
while period <= newest_period { while period <= newest_period {
if !stored_periods.contains(&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 { runs.push(PayoutRun {
period_start: period, period_start: period,
status: PayoutRunStatus::Pending, status,
started_at: None, started_at: None,
started_by: None, started_by: None,
completed_at: None, completed_at: None,
+38 -1
View File
@@ -1,6 +1,6 @@
use std::{fmt, str::FromStr}; 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}; use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[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)] #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[error("a year-month must use the first day of its month")] #[error("a year-month must use the first day of its month")]
pub struct InvalidYearMonth; pub struct InvalidYearMonth;
@@ -171,4 +181,31 @@ mod tests {
assert!(serde_json::from_str::<YearMonth>(value).is_err()); 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()
)
);
}
} }