some validation

This commit is contained in:
aecsocket
2026-08-25 18:29:11 +01:00
parent 73afbfeb56
commit 97c6ece304
10 changed files with 118 additions and 15 deletions
@@ -76,10 +76,12 @@
"name": "delphi_severity",
"kind": {
"Enum": [
"hidden",
"low",
"medium",
"high",
"severe"
"severe",
"malware"
]
}
}
@@ -76,10 +76,12 @@
"name": "delphi_severity",
"kind": {
"Enum": [
"hidden",
"low",
"medium",
"high",
"severe"
"severe",
"malware"
]
}
}
@@ -1,12 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT MAX(created)\n FROM payouts_values\n ",
"query": "\n SELECT MAX(date)\n FROM payout_period_days\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "max",
"type_info": "Timestamptz"
"type_info": "Date"
}
],
"parameters": {
@@ -16,5 +16,5 @@
null
]
},
"hash": "154d32137068456f7c0739271790d46bbe78053eda81fb2c227d95262cb4140a"
"hash": "3962c046a8877433f2e5afd6064c0dfb11c7093d60093e8265d8cc5937c2c02c"
}
@@ -41,10 +41,12 @@
"name": "delphi_severity",
"kind": {
"Enum": [
"hidden",
"low",
"medium",
"high",
"severe"
"severe",
"malware"
]
}
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT LEAST(\n COALESCE(\n (SELECT MAX(created)::date FROM payouts_values),\n $1\n ),\n COALESCE(\n (SELECT MIN(period) FROM payout_periods),\n $1\n ),\n $1\n ) AS \"first_period!\"\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "first_period!",
"type_info": "Date"
}
],
"parameters": {
"Left": [
"Date"
]
},
"nullable": [
null
]
},
"hash": "805f9e937806491b75494627147259e3cc4cb82ba8f8645f748ad77b5abb81c8"
}
@@ -26,10 +26,12 @@
"name": "delphi_severity",
"kind": {
"Enum": [
"hidden",
"low",
"medium",
"high",
"severe"
"severe",
"malware"
]
}
}
@@ -22,10 +22,12 @@
"name": "delphi_severity",
"kind": {
"Enum": [
"hidden",
"low",
"medium",
"high",
"severe"
"severe",
"malware"
]
}
}
@@ -19,10 +19,12 @@
"name": "delphi_severity",
"kind": {
"Enum": [
"hidden",
"low",
"medium",
"high",
"severe"
"severe",
"malware"
]
}
}
+57 -1
View File
@@ -2,7 +2,7 @@
use std::collections::{HashMap, HashSet};
use chrono::{DateTime, Days, NaiveDate, Utc};
use chrono::{DateTime, Days, Months, NaiveDate, Utc};
use dashmap::DashMap;
use eyre::Result;
use rust_decimal::Decimal;
@@ -29,6 +29,39 @@ pub struct DayEstimate {
pub impressions: u128,
}
/// Validate that an estimate contains exactly one complete entry for every
/// calendar day in its payout period.
pub(crate) fn validate_complete_period_estimate(
estimate: &PeriodEstimate,
) -> Result<()> {
let period_end = estimate
.period
.date()
.checked_add_months(Months::new(1))
.wrap_err("calculating payout period end")?;
let mut expected_date = estimate.period.date();
for day in &estimate.days {
if day.date != expected_date {
return Err(eyre::eyre!(
"expected Aditude estimate for `{expected_date}`, found `{}`",
day.date,
));
}
expected_date = expected_date
.succ_opt()
.wrap_err("calculating next payout estimate date")?;
}
if expected_date != period_end {
return Err(eyre::eyre!(
"missing Aditude estimate for `{expected_date}`",
));
}
Ok(())
}
/// Get per-month and per-day estimated ad provider info for an inclusive date
/// range.
///
@@ -193,4 +226,27 @@ mod tests {
assert_eq!(estimate.days[0].raw_estimated_revenue_usd, dec!(12.34));
assert_eq!(estimate.days[0].impressions, 5678);
}
#[test]
fn validates_every_day_in_period() {
let period =
YearMonth::from_day1(NaiveDate::from_ymd_opt(2026, 2, 1).unwrap());
let mut date = period.date();
let period_end = NaiveDate::from_ymd_opt(2026, 3, 1).unwrap();
let mut days = Vec::new();
while date < period_end {
days.push(DayEstimate {
date,
raw_estimated_revenue_usd: dec!(1),
impressions: 1,
});
date = date.succ_opt().unwrap();
}
let mut estimate = PeriodEstimate { period, days };
validate_complete_period_estimate(&estimate).unwrap();
estimate.days.remove(10);
assert!(validate_complete_period_estimate(&estimate).is_err());
}
}
@@ -12,12 +12,15 @@ use crate::{
database::{
PgPool,
models::{
DBUserId, generate_payout_run_id,
DBUserId, DatabaseError, generate_payout_run_id,
payout_run_item::{DBPayoutRun, PayoutRunStatus},
},
},
models::{ids::PayoutRunId, pats::Scopes},
queue::{payout_run::estimate, session::AuthQueue},
queue::{
payout_run::{estimate, validate_complete_period_estimate},
session::AuthQueue,
},
routes::ApiError,
util::{
error::Context,
@@ -57,6 +60,7 @@ pub struct StartPayoutRunResponse {
(status = BAD_REQUEST, description = "Invalid payout run input"),
(status = UNAUTHORIZED, description = "Invalid authentication or TOTP code"),
(status = CONFLICT, description = "Payout period is unavailable or another run is active"),
(status = FAILED_DEPENDENCY, description = "Aditude returned an incomplete period"),
),
security(("bearer_auth" = ["SESSION_ACCESS"])),
)]
@@ -151,6 +155,8 @@ pub async fn start_run(
let estimate = estimates
.pop()
.wrap_internal_err("missing requested payout estimate")?;
validate_complete_period_estimate(&estimate)
.wrap_failed_dependency_err("validating payout estimate")?;
let payload = PayoutRunPayload {
raw_actual_revenue_usd: body.raw_actual_revenue_usd,
adjustments: body.adjustments.clone(),
@@ -272,9 +278,16 @@ pub async fn start_run(
cancelled_by: None,
error: None,
};
run.upsert(&mut transaction)
.await
.wrap_internal_err("creating scheduled payout run")?;
let create_run_result = run.upsert(&mut transaction).await;
if let Err(DatabaseError::Database(sqlx::Error::Database(error))) =
&create_run_result
&& error.constraint() == Some("payout_runs_single_active")
{
return Err(ApiError::Conflict(eyre::eyre!(
"another payout run is already scheduled or running",
)));
}
create_run_result.wrap_internal_err("creating scheduled payout run")?;
transaction
.commit()