From 87aa698148a22d5a31050c7390e720dfe271b307 Mon Sep 17 00:00:00 2001 From: aecsocket <43144841+aecsocket@users.noreply.github.com> Date: Tue, 25 Aug 2026 16:17:45 +0100 Subject: [PATCH] merge migrations --- ...fef35cf224e053933f4aec75f377fbdffbbcb.json | 17 ++ ...d6c4b3559cd68cee070570dc7706805eeedca.json | 23 ++ ...711e2caa2691460c6bb1c138ef89aa9c74561.json | 20 ++ ...47fda4c6a6d69ec96bc4e531dc695e4e92f62.json | 14 + ...bd52473573907006b78066dbd4a7b0dab8b73.json | 15 ++ ...a5ae391d7be351a6c4328c2a9435f53f22772.json | 16 ++ .../20260821120000_payout-estimates.sql | 17 ++ apps/labrinth/tests/payout_run_lineage.rs | 245 ++++++++++++++++++ 8 files changed, 367 insertions(+) create mode 100644 apps/labrinth/.sqlx/query-0f4cefdd003516ad3260cd7b471fef35cf224e053933f4aec75f377fbdffbbcb.json create mode 100644 apps/labrinth/.sqlx/query-2296d6f4147eaf7e2f2e101aafcd6c4b3559cd68cee070570dc7706805eeedca.json create mode 100644 apps/labrinth/.sqlx/query-3e27668db2331820d032e02b61f711e2caa2691460c6bb1c138ef89aa9c74561.json create mode 100644 apps/labrinth/.sqlx/query-8d6cbbb4963ff2d0cc36c2c91e147fda4c6a6d69ec96bc4e531dc695e4e92f62.json create mode 100644 apps/labrinth/.sqlx/query-96a4279eff106656a6b7b7b7e17bd52473573907006b78066dbd4a7b0dab8b73.json create mode 100644 apps/labrinth/.sqlx/query-cdb198ab95f7fb5a4ae685125a7a5ae391d7be351a6c4328c2a9435f53f22772.json create mode 100644 apps/labrinth/tests/payout_run_lineage.rs diff --git a/apps/labrinth/.sqlx/query-0f4cefdd003516ad3260cd7b471fef35cf224e053933f4aec75f377fbdffbbcb.json b/apps/labrinth/.sqlx/query-0f4cefdd003516ad3260cd7b471fef35cf224e053933f4aec75f377fbdffbbcb.json new file mode 100644 index 0000000000..572820c95c --- /dev/null +++ b/apps/labrinth/.sqlx/query-0f4cefdd003516ad3260cd7b471fef35cf224e053933f4aec75f377fbdffbbcb.json @@ -0,0 +1,17 @@ +{ + "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", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Date", + "Text", + "Int8" + ] + }, + "nullable": [] + }, + "hash": "0f4cefdd003516ad3260cd7b471fef35cf224e053933f4aec75f377fbdffbbcb" +} diff --git a/apps/labrinth/.sqlx/query-2296d6f4147eaf7e2f2e101aafcd6c4b3559cd68cee070570dc7706805eeedca.json b/apps/labrinth/.sqlx/query-2296d6f4147eaf7e2f2e101aafcd6c4b3559cd68cee070570dc7706805eeedca.json new file mode 100644 index 0000000000..689d5f8c8a --- /dev/null +++ b/apps/labrinth/.sqlx/query-2296d6f4147eaf7e2f2e101aafcd6c4b3559cd68cee070570dc7706805eeedca.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "\n\t\t\t\tSELECT payout_run_id\n\t\t\t\tFROM payouts_values\n\t\t\t\tWHERE\n\t\t\t\t\tuser_id = $1\n\t\t\t\t\tAND (mod_id IS NULL OR mod_id = $2)\n\t\t\t\tORDER BY payout_run_id NULLS FIRST\n\t\t\t\t", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "payout_run_id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8", + "Int8" + ] + }, + "nullable": [ + true + ] + }, + "hash": "2296d6f4147eaf7e2f2e101aafcd6c4b3559cd68cee070570dc7706805eeedca" +} diff --git a/apps/labrinth/.sqlx/query-3e27668db2331820d032e02b61f711e2caa2691460c6bb1c138ef89aa9c74561.json b/apps/labrinth/.sqlx/query-3e27668db2331820d032e02b61f711e2caa2691460c6bb1c138ef89aa9c74561.json new file mode 100644 index 0000000000..3bee7ef4b6 --- /dev/null +++ b/apps/labrinth/.sqlx/query-3e27668db2331820d032e02b61f711e2caa2691460c6bb1c138ef89aa9c74561.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "\n\t\t\t\tSELECT period\n\t\t\t\tFROM payout_runs\n\t\t\t\tWHERE status = 'succeeded'\n\t\t\t\tORDER BY period\n\t\t\t\t", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "period", + "type_info": "Date" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false + ] + }, + "hash": "3e27668db2331820d032e02b61f711e2caa2691460c6bb1c138ef89aa9c74561" +} diff --git a/apps/labrinth/.sqlx/query-8d6cbbb4963ff2d0cc36c2c91e147fda4c6a6d69ec96bc4e531dc695e4e92f62.json b/apps/labrinth/.sqlx/query-8d6cbbb4963ff2d0cc36c2c91e147fda4c6a6d69ec96bc4e531dc695e4e92f62.json new file mode 100644 index 0000000000..da8a369d8b --- /dev/null +++ b/apps/labrinth/.sqlx/query-8d6cbbb4963ff2d0cc36c2c91e147fda4c6a6d69ec96bc4e531dc695e4e92f62.json @@ -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\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" +} diff --git a/apps/labrinth/.sqlx/query-96a4279eff106656a6b7b7b7e17bd52473573907006b78066dbd4a7b0dab8b73.json b/apps/labrinth/.sqlx/query-96a4279eff106656a6b7b7b7e17bd52473573907006b78066dbd4a7b0dab8b73.json new file mode 100644 index 0000000000..8669d5be93 --- /dev/null +++ b/apps/labrinth/.sqlx/query-96a4279eff106656a6b7b7b7e17bd52473573907006b78066dbd4a7b0dab8b73.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE payout_runs SET status = $1 WHERE id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Int8" + ] + }, + "nullable": [] + }, + "hash": "96a4279eff106656a6b7b7b7e17bd52473573907006b78066dbd4a7b0dab8b73" +} diff --git a/apps/labrinth/.sqlx/query-cdb198ab95f7fb5a4ae685125a7a5ae391d7be351a6c4328c2a9435f53f22772.json b/apps/labrinth/.sqlx/query-cdb198ab95f7fb5a4ae685125a7a5ae391d7be351a6c4328c2a9435f53f22772.json new file mode 100644 index 0000000000..708d851c20 --- /dev/null +++ b/apps/labrinth/.sqlx/query-cdb198ab95f7fb5a4ae685125a7a5ae391d7be351a6c4328c2a9435f53f22772.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "\n\t\tINSERT INTO payouts_values (\n\t\t\tuser_id,\n\t\t\tmod_id,\n\t\t\tamount,\n\t\t\tcreated,\n\t\t\tdate_available,\n\t\t\tpayout_run_id\n\t\t)\n\t\tVALUES ($1, $2, 1, NOW(), NOW(), $3)\n\t\t", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int8", + "Int8" + ] + }, + "nullable": [] + }, + "hash": "cdb198ab95f7fb5a4ae685125a7a5ae391d7be351a6c4328c2a9435f53f22772" +} diff --git a/apps/labrinth/migrations/20260821120000_payout-estimates.sql b/apps/labrinth/migrations/20260821120000_payout-estimates.sql index 8fb7289479..bc1b1a99fc 100644 --- a/apps/labrinth/migrations/20260821120000_payout-estimates.sql +++ b/apps/labrinth/migrations/20260821120000_payout-estimates.sql @@ -19,3 +19,20 @@ CREATE INDEX payout_estimates_mod_id CREATE INDEX payout_estimates_created ON payout_estimates (created); + +ALTER TABLE payouts_values + ADD COLUMN payout_run_id BIGINT REFERENCES payout_runs(id); + +ALTER TABLE payouts_values + ADD CONSTRAINT payouts_values_payout_run_creator_only + CHECK (payout_run_id IS NULL OR mod_id IS NOT NULL); + +CREATE UNIQUE INDEX payouts_values_payout_run_distribution + ON payouts_values (payout_run_id, user_id, mod_id) + WHERE payout_run_id IS NOT NULL; + +DROP INDEX payout_runs_active_period; + +CREATE UNIQUE INDEX payout_runs_single_active + ON payout_runs ((TRUE)) + WHERE status IN ('scheduled', 'running'); diff --git a/apps/labrinth/tests/payout_run_lineage.rs b/apps/labrinth/tests/payout_run_lineage.rs new file mode 100644 index 0000000000..1b3efc1cfe --- /dev/null +++ b/apps/labrinth/tests/payout_run_lineage.rs @@ -0,0 +1,245 @@ +use ariadne::ids::base62_impl::parse_base62; +use chrono::NaiveDate; +use sqlx::postgres::PgQueryResult; + +use common::{ + api_v3::ApiV3, + database::{FRIEND_USER_ID_PARSED, USER_USER_ID_PARSED}, + environment::{TestEnvironment, with_test_environment}, +}; +use labrinth::database::PgPool; + +pub mod common; + +async fn insert_run( + pool: &PgPool, + id: i64, + period: NaiveDate, + status: &str, +) -> Result { + sqlx::query!( + r#" + INSERT INTO payout_runs ( + id, + period, + payload, + status, + started_at, + started_by, + execute_at + ) + VALUES ( + $1, + $2, + '{"raw_actual_revenue_usd": 0, "adjustments": []}'::jsonb, + $3, + NOW(), + $4, + NOW() + ) + "#, + id, + period, + status, + USER_USER_ID_PARSED, + ) + .execute(pool) + .await +} + +async fn set_run_status(pool: &PgPool, id: i64, status: &str) { + sqlx::query!( + "UPDATE payout_runs SET status = $1 WHERE id = $2", + status, + id, + ) + .execute(pool) + .await + .unwrap(); +} + +async fn insert_payout_value( + pool: &PgPool, + payout_run_id: Option, + user_id: i64, + mod_id: Option, +) -> Result { + sqlx::query!( + r#" + INSERT INTO payouts_values ( + user_id, + mod_id, + amount, + created, + date_available, + payout_run_id + ) + VALUES ($1, $2, 1, NOW(), NOW(), $3) + "#, + user_id, + mod_id, + payout_run_id, + ) + .execute(pool) + .await +} + +fn assert_constraint(error: sqlx::Error, expected: &str) { + assert_eq!( + error + .as_database_error() + .and_then(|error| error.constraint()), + Some(expected), + ); +} + +#[actix_rt::test] +async fn payout_run_lineage_and_active_run_constraints() { + with_test_environment( + None, + |test_env: TestEnvironment| async move { + let pool = &test_env.db.pool; + let alpha_project_id = + parse_base62(&test_env.dummy.project_alpha.project_id).unwrap() + as i64; + let beta_project_id = + parse_base62(&test_env.dummy.project_beta.project_id).unwrap() + as i64; + let periods = [ + NaiveDate::from_ymd_opt(2000, 1, 1).unwrap(), + NaiveDate::from_ymd_opt(2000, 2, 1).unwrap(), + NaiveDate::from_ymd_opt(2000, 3, 1).unwrap(), + NaiveDate::from_ymd_opt(2000, 4, 1).unwrap(), + NaiveDate::from_ymd_opt(2000, 5, 1).unwrap(), + ]; + + sqlx::query!( + r#" + INSERT INTO payout_periods ( + period, + raw_actual_aditude_revenue_usd, + adjustments + ) + SELECT period, 0, '[]'::jsonb + FROM UNNEST($1::date[]) AS period + "#, + &periods[..], + ) + .execute(pool) + .await + .unwrap(); + + insert_run(pool, 1001, periods[0], "succeeded") + .await + .unwrap(); + + insert_payout_value(pool, None, USER_USER_ID_PARSED, None) + .await + .unwrap(); + insert_payout_value( + pool, + Some(1001), + USER_USER_ID_PARSED, + Some(alpha_project_id), + ) + .await + .unwrap(); + insert_payout_value( + pool, + Some(1001), + FRIEND_USER_ID_PARSED, + Some(alpha_project_id), + ) + .await + .unwrap(); + insert_payout_value( + pool, + Some(1001), + USER_USER_ID_PARSED, + Some(beta_project_id), + ) + .await + .unwrap(); + + let payout_run_ids = sqlx::query_scalar!( + r#" + SELECT payout_run_id + FROM payouts_values + WHERE + user_id = $1 + AND (mod_id IS NULL OR mod_id = $2) + ORDER BY payout_run_id NULLS FIRST + "#, + USER_USER_ID_PARSED, + alpha_project_id, + ) + .fetch_all(pool) + .await + .unwrap(); + assert_eq!(payout_run_ids, vec![None, Some(1001)]); + + let duplicate = insert_payout_value( + pool, + Some(1001), + USER_USER_ID_PARSED, + Some(alpha_project_id), + ) + .await + .unwrap_err(); + assert_constraint( + duplicate, + "payouts_values_payout_run_distribution", + ); + + let projectless = insert_payout_value( + pool, + Some(1001), + USER_USER_ID_PARSED, + None, + ) + .await + .unwrap_err(); + assert_constraint( + projectless, + "payouts_values_payout_run_creator_only", + ); + + insert_run(pool, 1002, periods[1], "scheduled") + .await + .unwrap(); + let second_active = insert_run(pool, 1003, periods[2], "running") + .await + .unwrap_err(); + assert_constraint(second_active, "payout_runs_single_active"); + + set_run_status(pool, 1002, "cancelled").await; + insert_run(pool, 1003, periods[2], "scheduled") + .await + .unwrap(); + + set_run_status(pool, 1003, "failed").await; + insert_run(pool, 1004, periods[3], "scheduled") + .await + .unwrap(); + + set_run_status(pool, 1004, "succeeded").await; + insert_run(pool, 1005, periods[4], "scheduled") + .await + .unwrap(); + + let succeeded_periods = sqlx::query_scalar!( + r#" + SELECT period + FROM payout_runs + WHERE status = 'succeeded' + ORDER BY period + "#, + ) + .fetch_all(pool) + .await + .unwrap(); + assert_eq!(succeeded_periods, vec![periods[0], periods[3]]); + }, + ) + .await; +}