diff --git a/apps/labrinth/.env.docker-compose b/apps/labrinth/.env.docker-compose index c5a151996..8a583480b 100644 --- a/apps/labrinth/.env.docker-compose +++ b/apps/labrinth/.env.docker-compose @@ -140,8 +140,6 @@ AVALARA_1099_API_KEY=none AVALARA_1099_API_TEAM_ID=none AVALARA_1099_COMPANY_ID=207337084 -COMPLIANCE_PAYOUT_THRESHOLD=disabled - ANROK_API_KEY=none ANROK_API_URL=none diff --git a/apps/labrinth/.env.local b/apps/labrinth/.env.local index abab2d9f9..7a284e2f0 100644 --- a/apps/labrinth/.env.local +++ b/apps/labrinth/.env.local @@ -150,8 +150,6 @@ AVALARA_1099_API_KEY=none AVALARA_1099_API_TEAM_ID=none AVALARA_1099_COMPANY_ID=207337084 -COMPLIANCE_PAYOUT_THRESHOLD=disabled - ANROK_API_KEY=none ANROK_API_URL=none diff --git a/apps/labrinth/src/env.rs b/apps/labrinth/src/env.rs index da834a3e3..e5dcf2978 100644 --- a/apps/labrinth/src/env.rs +++ b/apps/labrinth/src/env.rs @@ -242,8 +242,6 @@ vars! { ANROK_API_URL: String; ANROK_API_KEY: String; - COMPLIANCE_PAYOUT_THRESHOLD: String; - PAYOUT_ALERT_SLACK_WEBHOOK: String; CLOUDFLARE_INTEGRATION: bool = false; diff --git a/apps/labrinth/src/routes/internal/globals.rs b/apps/labrinth/src/routes/internal/globals.rs index 0317c5a94..6dbb0c931 100644 --- a/apps/labrinth/src/routes/internal/globals.rs +++ b/apps/labrinth/src/routes/internal/globals.rs @@ -1,7 +1,13 @@ -use std::{collections::HashMap, sync::LazyLock}; +use std::{ + collections::HashMap, + sync::{Arc, LazyLock}, +}; use crate::env::ENV; use actix_web::{get, web}; +use arc_swap::ArcSwapOption; +use chrono::{Datelike, Utc}; +use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) { @@ -33,9 +39,82 @@ static GLOBALS: LazyLock = LazyLock::new(|| Globals { && ENV.HCAPTCHA_SECRET != "none", }); +struct TaxComplianceCache { + year: i32, + value: Option, +} + +static TAX_COMPLIANCE_CACHE: ArcSwapOption = + ArcSwapOption::const_empty(); + +pub fn tax_compliance_payout_threshold() -> Option { + tax_compliance_payout_threshold_for_year(Utc::now().year()) +} + +pub fn tax_compliance_payout_threshold_for_year( + current_year: i32, +) -> Option { + let cache = TAX_COMPLIANCE_CACHE.load(); + + if let Some(cache) = &*cache + && cache.year == current_year + { + return cache.value; + } + + let value = (|| { + if let Some(value_this_year) = GLOBALS + .tax_compliance_thresholds + .get(&(current_year as u16)) + .copied() + { + return Some(Decimal::from(value_this_year)); + } + + let mut years_to_values = GLOBALS + .tax_compliance_thresholds + .iter() + .map(|(k, v)| (*k, *v)) + .collect::>(); + years_to_values.sort_by_key(|(year, _)| *year); + + let &(_, last_value) = years_to_values.last()?; + Some(Decimal::from(last_value)) + })(); + + TAX_COMPLIANCE_CACHE.store(Some(Arc::new(TaxComplianceCache { + year: current_year, + value, + }))); + value +} + /// Gets configured global non-secret variables for this backend instance. #[utoipa::path] #[get("")] pub async fn get_globals() -> web::Json { web::Json(GLOBALS.clone()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn cache_rolls_over_by_year() { + TAX_COMPLIANCE_CACHE.store(None); + + let first = tax_compliance_payout_threshold_for_year(2025); + assert_eq!(first, Some(Decimal::from(600_u64))); + + let second = tax_compliance_payout_threshold_for_year(2026); + assert_eq!(second, Some(Decimal::from(2000_u64))); + + let second = tax_compliance_payout_threshold_for_year(2027); + assert_eq!(second, Some(Decimal::from(2000_u64))); + + TAX_COMPLIANCE_CACHE.store(None); + let second = tax_compliance_payout_threshold_for_year(2027); + assert_eq!(second, Some(Decimal::from(2000_u64))); + } +} diff --git a/apps/labrinth/src/routes/v3/payouts.rs b/apps/labrinth/src/routes/v3/payouts.rs index 69a598cb3..77df4167b 100644 --- a/apps/labrinth/src/routes/v3/payouts.rs +++ b/apps/labrinth/src/routes/v3/payouts.rs @@ -11,6 +11,7 @@ use crate::models::payouts::{PayoutMethodType, PayoutStatus, Withdrawal}; use crate::queue::payouts::PayoutsQueue; use crate::queue::session::AuthQueue; use crate::routes::ApiError; +use crate::routes::internal::globals::tax_compliance_payout_threshold; use crate::util::avalara1099; use crate::util::error::Context; use crate::util::gotenberg::GotenbergClient; @@ -1116,10 +1117,6 @@ async fn update_compliance_status( } } -fn tax_compliance_payout_threshold() -> Option { - ENV.COMPLIANCE_PAYOUT_THRESHOLD.parse().ok() -} - #[derive(Deserialize)] pub struct RevenueQuery { pub start: Option>,