From d513c6c756155ef26aa99e6c38acf70948cd3768 Mon Sep 17 00:00:00 2001 From: aecsocket <43144841+aecsocket@users.noreply.github.com> Date: Wed, 26 Aug 2026 15:08:56 +0100 Subject: [PATCH] adjustments on the net rev pool --- ...a04f3f791205e6c2df15c5bf9d5afcf71a76a.json | 4 +- ...d70d17d47cf60ac42bf55855fda8053fcdf49.json | 14 ++ ...6a03bccfe57a196c62b27b5334525f32e0ca.json} | 6 +- ...bb79c460a4dc4217a656ab4d2e11c714ea56.json} | 4 +- ...cda5dbe8c2d451fe264b7392c8d8cbec73a58.json | 15 ++ ...584d26550d01a1917d1af64fe236d63dd5005.json | 4 +- ...02d97972573a8f4954ceab284b8d8989641a7.json | 15 -- ...08c1cb8cc0633a6c90ab662543295af068c9.json} | 4 +- ...41e2a83b50d89f5954ab8d951a090e702de73.json | 4 +- ...08ff40af3b6b56f43540ec3798ef30e5c0b9d.json | 4 +- ...47fda4c6a6d69ec96bc4e531dc695e4e92f62.json | 14 -- ...ba49f4963315fd7667c6db96e6153e54a2fd2.json | 4 +- ...e6bd1f8e4923f879149d8559b9e620054c96.json} | 4 +- ...de1e7cddd68ac956143bef994104280a8dc07.json | 4 +- .../20260817120000_payout-period-revenue.sql | 2 +- .../src/database/models/payout_period_item.rs | 8 +- apps/labrinth/src/queue/payout_run/mod.rs | 128 +++++++++++++++--- .../src/routes/internal/payout_runs/admin.rs | 32 +++-- .../src/routes/internal/payout_runs/fetch.rs | 34 +++-- .../src/routes/internal/payout_runs/mod.rs | 2 +- apps/labrinth/tests/payout_estimates.rs | 4 +- apps/labrinth/tests/payout_run_lineage.rs | 4 +- 22 files changed, 203 insertions(+), 111 deletions(-) create mode 100644 apps/labrinth/.sqlx/query-093388c38a65cd30c1ad75b265cd70d17d47cf60ac42bf55855fda8053fcdf49.json rename apps/labrinth/.sqlx/{query-56e3d8b2961562ef5a8be80f71814743f0cd95a7c26e4d0dd5b354d2ac3bbaf1.json => query-0f8bb9a3ade9d28ee2d91e10b3d06a03bccfe57a196c62b27b5334525f32e0ca.json} (55%) rename apps/labrinth/.sqlx/{query-291b7a70925bdd5d748048fa08dea1901ce8c2b584ec86921efe6c4d2d279b29.json => query-12c7d361e554ea8d9216c420cdf6bb79c460a4dc4217a656ab4d2e11c714ea56.json} (63%) create mode 100644 apps/labrinth/.sqlx/query-1437d67c3b497ae0ae5d3229ae4cda5dbe8c2d451fe264b7392c8d8cbec73a58.json delete mode 100644 apps/labrinth/.sqlx/query-4798f2780f28c4329d96af43e4902d97972573a8f4954ceab284b8d8989641a7.json rename apps/labrinth/.sqlx/{query-c30e4f1aad62a3a8c278340f051e91476a4fbe3d7774ef9fed712929a5816ecb.json => query-49e243559bf389d86da849dcd20108c1cb8cc0633a6c90ab662543295af068c9.json} (65%) delete mode 100644 apps/labrinth/.sqlx/query-8d6cbbb4963ff2d0cc36c2c91e147fda4c6a6d69ec96bc4e531dc695e4e92f62.json rename apps/labrinth/.sqlx/{query-0f4cefdd003516ad3260cd7b471fef35cf224e053933f4aec75f377fbdffbbcb.json => query-b40a175bcc4465b7c2793572528de6bd1f8e4923f879149d8559b9e620054c96.json} (63%) diff --git a/apps/labrinth/.sqlx/query-055f71ec6d193c5f5259cafc989a04f3f791205e6c2df15c5bf9d5afcf71a76a.json b/apps/labrinth/.sqlx/query-055f71ec6d193c5f5259cafc989a04f3f791205e6c2df15c5bf9d5afcf71a76a.json index 086322f4a6..cf686ee65a 100644 --- a/apps/labrinth/.sqlx/query-055f71ec6d193c5f5259cafc989a04f3f791205e6c2df15c5bf9d5afcf71a76a.json +++ b/apps/labrinth/.sqlx/query-055f71ec6d193c5f5259cafc989a04f3f791205e6c2df15c5bf9d5afcf71a76a.json @@ -76,12 +76,10 @@ "name": "delphi_severity", "kind": { "Enum": [ - "hidden", "low", "medium", "high", - "severe", - "malware" + "severe" ] } } diff --git a/apps/labrinth/.sqlx/query-093388c38a65cd30c1ad75b265cd70d17d47cf60ac42bf55855fda8053fcdf49.json b/apps/labrinth/.sqlx/query-093388c38a65cd30c1ad75b265cd70d17d47cf60ac42bf55855fda8053fcdf49.json new file mode 100644 index 0000000000..9d66f6f3d7 --- /dev/null +++ b/apps/labrinth/.sqlx/query-093388c38a65cd30c1ad75b265cd70d17d47cf60ac42bf55855fda8053fcdf49.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\trevenue_adjustments\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": "093388c38a65cd30c1ad75b265cd70d17d47cf60ac42bf55855fda8053fcdf49" +} diff --git a/apps/labrinth/.sqlx/query-56e3d8b2961562ef5a8be80f71814743f0cd95a7c26e4d0dd5b354d2ac3bbaf1.json b/apps/labrinth/.sqlx/query-0f8bb9a3ade9d28ee2d91e10b3d06a03bccfe57a196c62b27b5334525f32e0ca.json similarity index 55% rename from apps/labrinth/.sqlx/query-56e3d8b2961562ef5a8be80f71814743f0cd95a7c26e4d0dd5b354d2ac3bbaf1.json rename to apps/labrinth/.sqlx/query-0f8bb9a3ade9d28ee2d91e10b3d06a03bccfe57a196c62b27b5334525f32e0ca.json index fa1bbf6566..befdf60dba 100644 --- a/apps/labrinth/.sqlx/query-56e3d8b2961562ef5a8be80f71814743f0cd95a7c26e4d0dd5b354d2ac3bbaf1.json +++ b/apps/labrinth/.sqlx/query-0f8bb9a3ade9d28ee2d91e10b3d06a03bccfe57a196c62b27b5334525f32e0ca.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n\t\t\tSELECT\n\t\t\t\tpayout_periods.period,\n\t\t\t\tpayout_periods.raw_actual_aditude_revenue_usd,\n\t\t\t\tpayout_periods.adjustments AS \"adjustments: Json>\",\n\t\t\t\tactive_run.payload AS \"active_run_payload: Json\",\n\t\t\t\tactive_run.execute_at AS active_run_execute_at,\n\t\t\t\tEXISTS (\n\t\t\t\t\tSELECT 1\n\t\t\t\t\tFROM payout_runs\n\t\t\t\t\tWHERE payout_runs.period = payout_periods.period\n\t\t\t\t\t\tAND payout_runs.status = 'succeeded'\n\t\t\t\t) AS \"has_succeeded_run!\"\n\t\t\tFROM payout_periods\n\t\t\tLEFT JOIN payout_runs active_run\n\t\t\t\tON active_run.period = payout_periods.period\n\t\t\t\tAND active_run.status IN ('scheduled', 'running')\n\t\t\tWHERE payout_periods.period = ANY($1)\n\t\t\t", + "query": "\n\t\t\tSELECT\n\t\t\t\tpayout_periods.period,\n\t\t\t\tpayout_periods.raw_actual_aditude_revenue_usd,\n\t\t\t\tpayout_periods.revenue_adjustments AS \"revenue_adjustments: Json>\",\n\t\t\t\tactive_run.payload AS \"active_run_payload: Json\",\n\t\t\t\tactive_run.execute_at AS active_run_execute_at,\n\t\t\t\tEXISTS (\n\t\t\t\t\tSELECT 1\n\t\t\t\t\tFROM payout_runs\n\t\t\t\t\tWHERE payout_runs.period = payout_periods.period\n\t\t\t\t\t\tAND payout_runs.status = 'succeeded'\n\t\t\t\t) AS \"has_succeeded_run!\"\n\t\t\tFROM payout_periods\n\t\t\tLEFT JOIN payout_runs active_run\n\t\t\t\tON active_run.period = payout_periods.period\n\t\t\t\tAND active_run.status IN ('scheduled', 'running')\n\t\t\tWHERE payout_periods.period = ANY($1)\n\t\t\t", "describe": { "columns": [ { @@ -15,7 +15,7 @@ }, { "ordinal": 2, - "name": "adjustments: Json>", + "name": "revenue_adjustments: Json>", "type_info": "Jsonb" }, { @@ -48,5 +48,5 @@ null ] }, - "hash": "56e3d8b2961562ef5a8be80f71814743f0cd95a7c26e4d0dd5b354d2ac3bbaf1" + "hash": "0f8bb9a3ade9d28ee2d91e10b3d06a03bccfe57a196c62b27b5334525f32e0ca" } diff --git a/apps/labrinth/.sqlx/query-291b7a70925bdd5d748048fa08dea1901ce8c2b584ec86921efe6c4d2d279b29.json b/apps/labrinth/.sqlx/query-12c7d361e554ea8d9216c420cdf6bb79c460a4dc4217a656ab4d2e11c714ea56.json similarity index 63% rename from apps/labrinth/.sqlx/query-291b7a70925bdd5d748048fa08dea1901ce8c2b584ec86921efe6c4d2d279b29.json rename to apps/labrinth/.sqlx/query-12c7d361e554ea8d9216c420cdf6bb79c460a4dc4217a656ab4d2e11c714ea56.json index eadd7ae8f1..6a49df1f55 100644 --- a/apps/labrinth/.sqlx/query-291b7a70925bdd5d748048fa08dea1901ce8c2b584ec86921efe6c4d2d279b29.json +++ b/apps/labrinth/.sqlx/query-12c7d361e554ea8d9216c420cdf6bb79c460a4dc4217a656ab4d2e11c714ea56.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n INSERT INTO payout_periods (\n period,\n raw_actual_aditude_revenue_usd,\n adjustments\n )\n VALUES ($1, 100, '[]'::jsonb)\n ", + "query": "\n INSERT INTO payout_periods (\n period,\n raw_actual_aditude_revenue_usd,\n revenue_adjustments\n )\n VALUES ($1, 100, '[]'::jsonb)\n ", "describe": { "columns": [], "parameters": { @@ -10,5 +10,5 @@ }, "nullable": [] }, - "hash": "291b7a70925bdd5d748048fa08dea1901ce8c2b584ec86921efe6c4d2d279b29" + "hash": "12c7d361e554ea8d9216c420cdf6bb79c460a4dc4217a656ab4d2e11c714ea56" } diff --git a/apps/labrinth/.sqlx/query-1437d67c3b497ae0ae5d3229ae4cda5dbe8c2d451fe264b7392c8d8cbec73a58.json b/apps/labrinth/.sqlx/query-1437d67c3b497ae0ae5d3229ae4cda5dbe8c2d451fe264b7392c8d8cbec73a58.json new file mode 100644 index 0000000000..3c5efea587 --- /dev/null +++ b/apps/labrinth/.sqlx/query-1437d67c3b497ae0ae5d3229ae4cda5dbe8c2d451fe264b7392c8d8cbec73a58.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO payout_periods (\n period,\n raw_actual_aditude_revenue_usd,\n revenue_adjustments\n )\n VALUES ($1, $2, '[]'::jsonb)\n ON CONFLICT (period) DO UPDATE SET\n raw_actual_aditude_revenue_usd =\n EXCLUDED.raw_actual_aditude_revenue_usd\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Date", + "Numeric" + ] + }, + "nullable": [] + }, + "hash": "1437d67c3b497ae0ae5d3229ae4cda5dbe8c2d451fe264b7392c8d8cbec73a58" +} diff --git a/apps/labrinth/.sqlx/query-1551b022217df05490a01e715b3584d26550d01a1917d1af64fe236d63dd5005.json b/apps/labrinth/.sqlx/query-1551b022217df05490a01e715b3584d26550d01a1917d1af64fe236d63dd5005.json index 434d224cc7..db45e607d0 100644 --- a/apps/labrinth/.sqlx/query-1551b022217df05490a01e715b3584d26550d01a1917d1af64fe236d63dd5005.json +++ b/apps/labrinth/.sqlx/query-1551b022217df05490a01e715b3584d26550d01a1917d1af64fe236d63dd5005.json @@ -76,12 +76,10 @@ "name": "delphi_severity", "kind": { "Enum": [ - "hidden", "low", "medium", "high", - "severe", - "malware" + "severe" ] } } diff --git a/apps/labrinth/.sqlx/query-4798f2780f28c4329d96af43e4902d97972573a8f4954ceab284b8d8989641a7.json b/apps/labrinth/.sqlx/query-4798f2780f28c4329d96af43e4902d97972573a8f4954ceab284b8d8989641a7.json deleted file mode 100644 index 32408bbe65..0000000000 --- a/apps/labrinth/.sqlx/query-4798f2780f28c4329d96af43e4902d97972573a8f4954ceab284b8d8989641a7.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO payout_periods (\n period,\n raw_actual_aditude_revenue_usd,\n adjustments\n )\n VALUES ($1, $2, '[]'::jsonb)\n ON CONFLICT (period) DO UPDATE SET\n raw_actual_aditude_revenue_usd =\n EXCLUDED.raw_actual_aditude_revenue_usd\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Date", - "Numeric" - ] - }, - "nullable": [] - }, - "hash": "4798f2780f28c4329d96af43e4902d97972573a8f4954ceab284b8d8989641a7" -} diff --git a/apps/labrinth/.sqlx/query-c30e4f1aad62a3a8c278340f051e91476a4fbe3d7774ef9fed712929a5816ecb.json b/apps/labrinth/.sqlx/query-49e243559bf389d86da849dcd20108c1cb8cc0633a6c90ab662543295af068c9.json similarity index 65% rename from apps/labrinth/.sqlx/query-c30e4f1aad62a3a8c278340f051e91476a4fbe3d7774ef9fed712929a5816ecb.json rename to apps/labrinth/.sqlx/query-49e243559bf389d86da849dcd20108c1cb8cc0633a6c90ab662543295af068c9.json index 3bcaaf94d6..cfa8bdceda 100644 --- a/apps/labrinth/.sqlx/query-c30e4f1aad62a3a8c278340f051e91476a4fbe3d7774ef9fed712929a5816ecb.json +++ b/apps/labrinth/.sqlx/query-49e243559bf389d86da849dcd20108c1cb8cc0633a6c90ab662543295af068c9.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n INSERT INTO payout_runs (\n id,\n period,\n payload,\n status,\n started_at,\n started_by,\n execute_at,\n processing_started_at,\n finished_at\n )\n VALUES (\n $1,\n $2,\n '{\"raw_actual_revenue_usd\":100,\"adjustments\":[]}'::jsonb,\n 'succeeded',\n $3,\n $4,\n $5,\n $5,\n $6\n )\n ", + "query": "\n INSERT INTO payout_runs (\n id,\n period,\n payload,\n status,\n started_at,\n started_by,\n execute_at,\n processing_started_at,\n finished_at\n )\n VALUES (\n $1,\n $2,\n '{\"raw_actual_revenue_usd\":100,\"revenue_adjustments\":[]}'::jsonb,\n 'succeeded',\n $3,\n $4,\n $5,\n $5,\n $6\n )\n ", "describe": { "columns": [], "parameters": { @@ -15,5 +15,5 @@ }, "nullable": [] }, - "hash": "c30e4f1aad62a3a8c278340f051e91476a4fbe3d7774ef9fed712929a5816ecb" + "hash": "49e243559bf389d86da849dcd20108c1cb8cc0633a6c90ab662543295af068c9" } diff --git a/apps/labrinth/.sqlx/query-63a9a89443566baa880ddbe05f741e2a83b50d89f5954ab8d951a090e702de73.json b/apps/labrinth/.sqlx/query-63a9a89443566baa880ddbe05f741e2a83b50d89f5954ab8d951a090e702de73.json index 86b2c5a1d5..b9c6d3543c 100644 --- a/apps/labrinth/.sqlx/query-63a9a89443566baa880ddbe05f741e2a83b50d89f5954ab8d951a090e702de73.json +++ b/apps/labrinth/.sqlx/query-63a9a89443566baa880ddbe05f741e2a83b50d89f5954ab8d951a090e702de73.json @@ -41,12 +41,10 @@ "name": "delphi_severity", "kind": { "Enum": [ - "hidden", "low", "medium", "high", - "severe", - "malware" + "severe" ] } } diff --git a/apps/labrinth/.sqlx/query-8d37c8c2633b7d601c7ba74d63608ff40af3b6b56f43540ec3798ef30e5c0b9d.json b/apps/labrinth/.sqlx/query-8d37c8c2633b7d601c7ba74d63608ff40af3b6b56f43540ec3798ef30e5c0b9d.json index cf1d9b23c6..475dba1b98 100644 --- a/apps/labrinth/.sqlx/query-8d37c8c2633b7d601c7ba74d63608ff40af3b6b56f43540ec3798ef30e5c0b9d.json +++ b/apps/labrinth/.sqlx/query-8d37c8c2633b7d601c7ba74d63608ff40af3b6b56f43540ec3798ef30e5c0b9d.json @@ -26,12 +26,10 @@ "name": "delphi_severity", "kind": { "Enum": [ - "hidden", "low", "medium", "high", - "severe", - "malware" + "severe" ] } } diff --git a/apps/labrinth/.sqlx/query-8d6cbbb4963ff2d0cc36c2c91e147fda4c6a6d69ec96bc4e531dc695e4e92f62.json b/apps/labrinth/.sqlx/query-8d6cbbb4963ff2d0cc36c2c91e147fda4c6a6d69ec96bc4e531dc695e4e92f62.json deleted file mode 100644 index da8a369d8b..0000000000 --- a/apps/labrinth/.sqlx/query-8d6cbbb4963ff2d0cc36c2c91e147fda4c6a6d69ec96bc4e531dc695e4e92f62.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "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-9369f0659c5fbd08463923a9b2bba49f4963315fd7667c6db96e6153e54a2fd2.json b/apps/labrinth/.sqlx/query-9369f0659c5fbd08463923a9b2bba49f4963315fd7667c6db96e6153e54a2fd2.json index f3609a305c..8ca4b69491 100644 --- a/apps/labrinth/.sqlx/query-9369f0659c5fbd08463923a9b2bba49f4963315fd7667c6db96e6153e54a2fd2.json +++ b/apps/labrinth/.sqlx/query-9369f0659c5fbd08463923a9b2bba49f4963315fd7667c6db96e6153e54a2fd2.json @@ -22,12 +22,10 @@ "name": "delphi_severity", "kind": { "Enum": [ - "hidden", "low", "medium", "high", - "severe", - "malware" + "severe" ] } } diff --git a/apps/labrinth/.sqlx/query-0f4cefdd003516ad3260cd7b471fef35cf224e053933f4aec75f377fbdffbbcb.json b/apps/labrinth/.sqlx/query-b40a175bcc4465b7c2793572528de6bd1f8e4923f879149d8559b9e620054c96.json similarity index 63% rename from apps/labrinth/.sqlx/query-0f4cefdd003516ad3260cd7b471fef35cf224e053933f4aec75f377fbdffbbcb.json rename to apps/labrinth/.sqlx/query-b40a175bcc4465b7c2793572528de6bd1f8e4923f879149d8559b9e620054c96.json index 572820c95c..2b99756600 100644 --- a/apps/labrinth/.sqlx/query-0f4cefdd003516ad3260cd7b471fef35cf224e053933f4aec75f377fbdffbbcb.json +++ b/apps/labrinth/.sqlx/query-b40a175bcc4465b7c2793572528de6bd1f8e4923f879149d8559b9e620054c96.json @@ -1,6 +1,6 @@ { "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", + "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, \"revenue_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": { @@ -13,5 +13,5 @@ }, "nullable": [] }, - "hash": "0f4cefdd003516ad3260cd7b471fef35cf224e053933f4aec75f377fbdffbbcb" + "hash": "b40a175bcc4465b7c2793572528de6bd1f8e4923f879149d8559b9e620054c96" } diff --git a/apps/labrinth/.sqlx/query-f2054ae7dcc89b21ed6b2f04526de1e7cddd68ac956143bef994104280a8dc07.json b/apps/labrinth/.sqlx/query-f2054ae7dcc89b21ed6b2f04526de1e7cddd68ac956143bef994104280a8dc07.json index 1e2ab737e9..8cbe94abd5 100644 --- a/apps/labrinth/.sqlx/query-f2054ae7dcc89b21ed6b2f04526de1e7cddd68ac956143bef994104280a8dc07.json +++ b/apps/labrinth/.sqlx/query-f2054ae7dcc89b21ed6b2f04526de1e7cddd68ac956143bef994104280a8dc07.json @@ -19,12 +19,10 @@ "name": "delphi_severity", "kind": { "Enum": [ - "hidden", "low", "medium", "high", - "severe", - "malware" + "severe" ] } } diff --git a/apps/labrinth/migrations/20260817120000_payout-period-revenue.sql b/apps/labrinth/migrations/20260817120000_payout-period-revenue.sql index c4e43e5746..143ff3f7aa 100644 --- a/apps/labrinth/migrations/20260817120000_payout-period-revenue.sql +++ b/apps/labrinth/migrations/20260817120000_payout-period-revenue.sql @@ -3,7 +3,7 @@ CREATE TABLE payout_periods ( CHECK (EXTRACT(DAY FROM period) = 1), raw_actual_aditude_revenue_usd NUMERIC(40, 20) NOT NULL CHECK (raw_actual_aditude_revenue_usd >= 0), - adjustments JSONB NOT NULL + revenue_adjustments JSONB NOT NULL ); CREATE TABLE payout_period_days ( diff --git a/apps/labrinth/src/database/models/payout_period_item.rs b/apps/labrinth/src/database/models/payout_period_item.rs index 2dca554d90..b0ef6cfca3 100644 --- a/apps/labrinth/src/database/models/payout_period_item.rs +++ b/apps/labrinth/src/database/models/payout_period_item.rs @@ -5,13 +5,13 @@ use rust_decimal::Decimal; use sqlx::types::Json; use super::DatabaseError; -use crate::queue::payout_run::{Adjustment, PayoutRunPayload}; +use crate::queue::payout_run::{PayoutRunPayload, RevenueAdjustment}; #[derive(Debug, Clone)] pub struct DBPayoutPeriod { pub period: NaiveDate, pub raw_actual_aditude_revenue_usd: Decimal, - pub adjustments: Vec, + pub revenue_adjustments: Vec, pub active_run_payload: Option, pub active_run_execute_at: Option>, pub days: Vec, @@ -39,7 +39,7 @@ impl DBPayoutPeriod { SELECT payout_periods.period, payout_periods.raw_actual_aditude_revenue_usd, - payout_periods.adjustments AS "adjustments: Json>", + payout_periods.revenue_adjustments AS "revenue_adjustments: Json>", active_run.payload AS "active_run_payload: Json", active_run.execute_at AS active_run_execute_at, EXISTS ( @@ -68,7 +68,7 @@ impl DBPayoutPeriod { period: row.period, raw_actual_aditude_revenue_usd: row .raw_actual_aditude_revenue_usd, - adjustments: row.adjustments.0, + revenue_adjustments: row.revenue_adjustments.0, active_run_payload: row .active_run_payload .map(|payload| payload.0), diff --git a/apps/labrinth/src/queue/payout_run/mod.rs b/apps/labrinth/src/queue/payout_run/mod.rs index 931583fbdd..9bf52febcb 100644 --- a/apps/labrinth/src/queue/payout_run/mod.rs +++ b/apps/labrinth/src/queue/payout_run/mod.rs @@ -14,8 +14,9 @@ //! money, so this is closer to NET 75. During this period, the month (payout //! period) is in an _in review_ state. //! - Once we receive the money from the provider, an admin enters the total -//! amount we've received into the web UI, adds any manual adjustments (for -//! campaigns outside of our ad provider's), and starts a payout run. +//! amount we've received into the web UI, adds any manual revenue +//! adjustments (for campaigns outside of our ad provider's), and starts a +//! payout run. //! - The payout run is not immediately executed; there is a period of time in //! which it can still be cancelled. //! - Once the payout run is executed, we calculate the exact revenue @@ -50,11 +51,12 @@ //! ``` //! - (fees stay the same, since they're based on impressions, not revenue) //! - (variance is ignored, since that's purely an estimation value) -//! - `actual.net_revenue_usd`: raw actual revenue - fees +//! - Revenue adjustments are split evenly across the days in the period. +//! - `actual.net_revenue_usd`: raw actual revenue - fees + revenue adjustment //! - `actual.(platform|creator)_net_revenue_usd`: same logic as estimated, //! but using the net actual revenue -//! - Manual adjustments are stored separately on the payout period and applied -//! on top of its actual distribution. +//! - Manual revenue adjustments are stored on the payout period and applied to +//! its actual distribution before the platform/creator split. //! //! ## Variance //! @@ -63,10 +65,12 @@ //! epoch date) //! - the decimal fraction of variance to apply -use chrono::NaiveDate; +use chrono::{Months, NaiveDate}; use rust_decimal::{Decimal, dec}; use serde::{Deserialize, Serialize}; +use crate::util::time::YearMonth; + mod estimate; pub use estimate::*; @@ -77,13 +81,13 @@ pub struct PayoutRunPayload { /// Actual raw revenue received from the ad provider for the period. #[serde(with = "rust_decimal::serde::float")] pub raw_actual_revenue_usd: Decimal, - /// Manual adjustments to apply on top of actual revenue. - pub adjustments: Vec, + /// Manual revenue adjustments to apply on top of actual revenue. + pub revenue_adjustments: Vec, } -/// Manual admin-input adjustment to a payout period. +/// Manual admin-input revenue adjustment to a payout period. #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] -pub struct Adjustment { +pub struct RevenueAdjustment { /// Total value of the adjustment. #[serde(with = "rust_decimal::serde::float")] pub amount_usd: Decimal, @@ -113,8 +117,14 @@ pub struct DayDistribution { /// For non-estimates (actual revenue values), this is zero. #[serde(with = "rust_decimal::serde::float")] pub variance_usd: Decimal, + /// Revenue added after fees and variance, before the platform/creator + /// split. + /// + /// This is zero for estimated revenue. + #[serde(with = "rust_decimal::serde::float")] + pub revenue_adjustment_usd: Decimal, /// Total net revenue that we earned; - /// `raw_revenue - fees - variance`. + /// `raw_revenue - fees - variance + revenue_adjustment`. #[serde(with = "rust_decimal::serde::float")] pub net_revenue_usd: Decimal, /// How much of the net revenue goes to the platform. @@ -153,6 +163,7 @@ pub fn distribution_for_day( date: NaiveDate, raw_revenue_usd: Decimal, impressions: u128, + revenue_adjustment_usd: Decimal, variances: &PayoutVariances, ) -> DayDistribution { let fees_usd = { @@ -168,16 +179,17 @@ pub fn distribution_for_day( .unwrap_or(variances.default_frac); let variance_usd = raw_revenue_usd * variance_frac; - let net_estimated_revenue_usd = raw_revenue_usd - fees_usd - variance_usd; + let net_revenue_usd = + raw_revenue_usd - fees_usd - variance_usd + revenue_adjustment_usd; DayDistribution { raw_revenue_usd, fees_usd, variance_usd, - net_revenue_usd: net_estimated_revenue_usd, - platform_net_revenue_usd: net_estimated_revenue_usd - * PLATFORM_REVENUE_SPLIT, - creator_net_revenue_usd: net_estimated_revenue_usd + revenue_adjustment_usd, + net_revenue_usd, + platform_net_revenue_usd: net_revenue_usd * PLATFORM_REVENUE_SPLIT, + creator_net_revenue_usd: net_revenue_usd * (dec!(1) - PLATFORM_REVENUE_SPLIT), } } @@ -186,6 +198,9 @@ pub fn distribution_for_day( #[derive(Debug, Clone, Copy)] pub struct ActualDistributionFlow { share: Decimal, + period: YearMonth, + daily_revenue_adjustment_usd: Decimal, + final_day_revenue_adjustment_usd: Decimal, } /// Start a flow for computing the actual revenue distribution of a payout @@ -202,12 +217,17 @@ pub struct ActualDistributionFlow { /// ``` /// /// If the period's estimated revenue is zero, the share is `1`. +/// Revenue adjustments are split evenly across every calendar day. Any +/// decimal rounding remainder is placed on the final day so the daily values +/// sum exactly to the period adjustment. /// /// We use a type-state-ish pattern here to ensure that the same flow is used /// for each day in a period. pub fn compute_actual_distribution_flow( + period: YearMonth, raw_estimated_revenue_usd: Decimal, raw_actual_revenue_usd: Decimal, + revenue_adjustment_usd: Decimal, ) -> ActualDistributionFlow { let share = if raw_estimated_revenue_usd.is_zero() { Decimal::ONE @@ -215,7 +235,21 @@ pub fn compute_actual_distribution_flow( raw_actual_revenue_usd / raw_estimated_revenue_usd }; - ActualDistributionFlow { share } + let next_period = period + .date() + .checked_add_months(Months::new(1)) + .expect("a payout period must have a following month"); + let day_count = Decimal::from((next_period - period.date()).num_days()); + let daily_revenue_adjustment_usd = revenue_adjustment_usd / day_count; + let final_day_revenue_adjustment_usd = revenue_adjustment_usd + - daily_revenue_adjustment_usd * (day_count - Decimal::ONE); + + ActualDistributionFlow { + share, + period, + daily_revenue_adjustment_usd, + final_day_revenue_adjustment_usd, + } } impl ActualDistributionFlow { @@ -226,12 +260,72 @@ impl ActualDistributionFlow { raw_estimated_revenue_usd: Decimal, impressions: u128, ) -> DayDistribution { + debug_assert_eq!(YearMonth::from_day1(date), self.period); + let next_period = self + .period + .date() + .checked_add_months(Months::new(1)) + .expect("a payout period must have a following month"); + let revenue_adjustment_usd = if date.succ_opt() == Some(next_period) { + self.final_day_revenue_adjustment_usd + } else { + self.daily_revenue_adjustment_usd + }; + distribution_for_day( date, raw_estimated_revenue_usd * self.share, impressions, + revenue_adjustment_usd, // actual rev distribution always has no variance &PayoutVariances::ZERO, ) } } + +#[cfg(test)] +mod tests { + use chrono::Days; + + use super::*; + + #[test] + fn splits_revenue_adjustment_across_period_exactly() { + let period = YearMonth::from_year_month(2026, 4).unwrap(); + let adjustment = dec!(500); + let flow = compute_actual_distribution_flow( + period, + dec!(30), + dec!(30), + adjustment, + ); + let mut date = period.date(); + let mut distributions = Vec::new(); + + for _ in 0..30 { + distributions.push(flow.distribution_for_day(date, dec!(1), 0)); + date = date.checked_add_days(Days::new(1)).unwrap(); + } + + assert_eq!( + distributions + .iter() + .map(|day| day.revenue_adjustment_usd) + .sum::(), + adjustment, + ); + assert_eq!( + distributions + .iter() + .map(|day| day.net_revenue_usd) + .sum::(), + dec!(530), + ); + assert!( + distributions[..29] + .iter() + .all(|day| day.revenue_adjustment_usd + == distributions[0].revenue_adjustment_usd), + ); + } +} diff --git a/apps/labrinth/src/routes/internal/payout_runs/admin.rs b/apps/labrinth/src/routes/internal/payout_runs/admin.rs index 8aae8cdc75..ce0397738a 100644 --- a/apps/labrinth/src/routes/internal/payout_runs/admin.rs +++ b/apps/labrinth/src/routes/internal/payout_runs/admin.rs @@ -4,7 +4,7 @@ use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use xredis::RedisPool; -use super::{Adjustment, PayoutRunDay, PayoutRunPayload}; +use super::{PayoutRunDay, PayoutRunPayload, RevenueAdjustment}; use crate::{ auth::{ AuthenticationError, get_user_from_headers, two_factor::verify_2fa_code, @@ -45,7 +45,7 @@ pub struct StartPayoutRun { pub ignore_totp: bool, #[serde(with = "rust_decimal::serde::float")] pub raw_actual_revenue_usd: Decimal, - pub adjustments: Vec, + pub revenue_adjustments: Vec, } #[derive(Debug, Serialize, utoipa::ToSchema)] @@ -58,10 +58,11 @@ pub struct StartPayoutRunResponse { pub struct CalculatePayoutRunResponse { pub period: YearMonth, pub days: Vec, - /// Sum of all adjustments that would be applied on top of actual revenue. + /// Sum of all revenue adjustments that would be applied on top of actual + /// revenue. #[serde(with = "rust_decimal::serde::float")] - pub total_adjustments: Decimal, - pub adjustments: Vec, + pub total_revenue_adjustment_usd: Decimal, + pub revenue_adjustments: Vec, } /// Calculate a payout run without scheduling it. @@ -148,9 +149,16 @@ pub async fn calculate_run( .iter() .map(|day| day.raw_estimated_revenue_usd) .sum(); + let total_revenue_adjustment_usd = body + .revenue_adjustments + .iter() + .map(|adjustment| adjustment.amount_usd) + .sum(); let actual_flow = compute_actual_distribution_flow( + body.period, total_estimated_revenue_usd, body.raw_actual_revenue_usd, + total_revenue_adjustment_usd, ); let days = estimate .days @@ -161,6 +169,7 @@ pub async fn calculate_run( day.date, day.raw_estimated_revenue_usd, day.impressions, + Decimal::ZERO, &variances, ), actual: Some(actual_flow.distribution_for_day( @@ -170,17 +179,12 @@ pub async fn calculate_run( )), }) .collect(); - let total_adjustments = body - .adjustments - .iter() - .map(|adjustment| adjustment.amount_usd) - .sum(); Ok(web::Json(CalculatePayoutRunResponse { period: body.period, days, - total_adjustments, - adjustments: body.adjustments, + total_revenue_adjustment_usd, + revenue_adjustments: body.revenue_adjustments, })) } @@ -290,7 +294,7 @@ pub async fn start_run( .wrap_failed_dependency_err("validating payout estimate")?; let payload = PayoutRunPayload { raw_actual_revenue_usd: body.raw_actual_revenue_usd, - adjustments: body.adjustments.clone(), + revenue_adjustments: body.revenue_adjustments.clone(), }; let mut transaction = pool @@ -303,7 +307,7 @@ pub async fn start_run( INSERT INTO payout_periods ( period, raw_actual_aditude_revenue_usd, - adjustments + revenue_adjustments ) VALUES ($1, $2, '[]'::jsonb) ON CONFLICT (period) DO UPDATE SET diff --git a/apps/labrinth/src/routes/internal/payout_runs/fetch.rs b/apps/labrinth/src/routes/internal/payout_runs/fetch.rs index d8b8110c5b..a72677fa4c 100644 --- a/apps/labrinth/src/routes/internal/payout_runs/fetch.rs +++ b/apps/labrinth/src/routes/internal/payout_runs/fetch.rs @@ -6,7 +6,7 @@ use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use xredis::RedisPool; -use super::Adjustment; +use super::RevenueAdjustment; use crate::{ auth::get_user_from_headers, database::{ @@ -43,14 +43,15 @@ pub struct PayoutRunPeriod { /// When the active payout run is scheduled to begin executing. pub runs_at: Option>, pub days: Vec, - /// Sum of all adjustments applied on top of actual revenue. + /// Sum of all revenue adjustments applied on top of actual revenue. #[serde(with = "rust_decimal::serde::float")] - pub total_adjustments: Decimal, - /// Individual adjustments, including their admin-provided descriptions. + pub total_revenue_adjustment_usd: Decimal, + /// Individual revenue adjustments, including their admin-provided + /// descriptions. /// /// Only visible to admins. #[serde(skip_serializing_if = "Option::is_none")] - pub adjustments: Option>, + pub revenue_adjustments: Option>, } /// Has revenue been distributed for a specific payout period month yet? @@ -200,25 +201,28 @@ pub async fn get_runs( .get(&requested_period.date()) .filter(|period| !period.days.is_empty()) { - let period_adjustments = + let period_revenue_adjustments = if let Some(payload) = &period.active_run_payload { - &payload.adjustments + &payload.revenue_adjustments } else { - &period.adjustments + &period.revenue_adjustments }; - let total_adjustments = period_adjustments + let total_revenue_adjustment_usd = period_revenue_adjustments .iter() .map(|adjustment| adjustment.amount_usd) .sum(); - let adjustments = is_admin.then(|| period_adjustments.clone()); + let revenue_adjustments = + is_admin.then(|| period_revenue_adjustments.clone()); let total_estimated_revenue_usd = period .days .iter() .map(|day| day.raw_estimated_aditude_revenue_usd) .sum(); let actual_flow = compute_actual_distribution_flow( + requested_period, total_estimated_revenue_usd, period.raw_actual_aditude_revenue_usd, + total_revenue_adjustment_usd, ); let days = period .days @@ -235,6 +239,7 @@ pub async fn get_runs( day.date, day.raw_estimated_aditude_revenue_usd, impressions, + Decimal::ZERO, &variances, ), actual: Some(actual_flow.distribution_for_day( @@ -258,8 +263,8 @@ pub async fn get_runs( status, runs_at: period.active_run_execute_at, days, - total_adjustments, - adjustments, + total_revenue_adjustment_usd, + revenue_adjustments, }) } else { let estimate = live_estimates @@ -282,6 +287,7 @@ pub async fn get_runs( day.date, day.raw_estimated_revenue_usd, day.impressions, + Decimal::ZERO, &variances, ), actual: None, @@ -293,8 +299,8 @@ pub async fn get_runs( status, runs_at: None, days, - total_adjustments: Decimal::ZERO, - adjustments: is_admin.then(Vec::new), + total_revenue_adjustment_usd: Decimal::ZERO, + revenue_adjustments: is_admin.then(Vec::new), }) } }) diff --git a/apps/labrinth/src/routes/internal/payout_runs/mod.rs b/apps/labrinth/src/routes/internal/payout_runs/mod.rs index 53ea9a38a1..fc310f4043 100644 --- a/apps/labrinth/src/routes/internal/payout_runs/mod.rs +++ b/apps/labrinth/src/routes/internal/payout_runs/mod.rs @@ -1,7 +1,7 @@ mod admin; mod fetch; -pub use crate::queue::payout_run::{Adjustment, PayoutRunPayload}; +pub use crate::queue::payout_run::{PayoutRunPayload, RevenueAdjustment}; pub use admin::*; pub use fetch::*; diff --git a/apps/labrinth/tests/payout_estimates.rs b/apps/labrinth/tests/payout_estimates.rs index a033d68782..785c53c633 100644 --- a/apps/labrinth/tests/payout_estimates.rs +++ b/apps/labrinth/tests/payout_estimates.rs @@ -54,7 +54,7 @@ async fn insert_succeeded_period( INSERT INTO payout_periods ( period, raw_actual_aditude_revenue_usd, - adjustments + revenue_adjustments ) VALUES ($1, 100, '[]'::jsonb) "#, @@ -81,7 +81,7 @@ async fn insert_succeeded_period( VALUES ( $1, $2, - '{"raw_actual_revenue_usd":100,"adjustments":[]}'::jsonb, + '{"raw_actual_revenue_usd":100,"revenue_adjustments":[]}'::jsonb, 'succeeded', $3, $4, diff --git a/apps/labrinth/tests/payout_run_lineage.rs b/apps/labrinth/tests/payout_run_lineage.rs index 1b3efc1cfe..b2df12bc22 100644 --- a/apps/labrinth/tests/payout_run_lineage.rs +++ b/apps/labrinth/tests/payout_run_lineage.rs @@ -31,7 +31,7 @@ async fn insert_run( VALUES ( $1, $2, - '{"raw_actual_revenue_usd": 0, "adjustments": []}'::jsonb, + '{"raw_actual_revenue_usd": 0, "revenue_adjustments": []}'::jsonb, $3, NOW(), $4, @@ -118,7 +118,7 @@ async fn payout_run_lineage_and_active_run_constraints() { INSERT INTO payout_periods ( period, raw_actual_aditude_revenue_usd, - adjustments + revenue_adjustments ) SELECT period, 0, '[]'::jsonb FROM UNNEST($1::date[]) AS period