From fe29a654a345e82e21e7061c9b436afd65e5dc47 Mon Sep 17 00:00:00 2001 From: aecsocket Date: Mon, 13 Oct 2025 10:09:26 +0100 Subject: [PATCH 1/4] wip: payout charge affiliate code --- .../20251011214648_affiliate_payout_charges.sql | 2 ++ .../labrinth/src/database/models/charge_item.rs | 17 ++++++++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) create mode 100644 apps/labrinth/migrations/20251011214648_affiliate_payout_charges.sql diff --git a/apps/labrinth/migrations/20251011214648_affiliate_payout_charges.sql b/apps/labrinth/migrations/20251011214648_affiliate_payout_charges.sql new file mode 100644 index 0000000000..c547135065 --- /dev/null +++ b/apps/labrinth/migrations/20251011214648_affiliate_payout_charges.sql @@ -0,0 +1,2 @@ +ALTER TABLE charges +ADD COLUMN affiliate_code BIGINT; diff --git a/apps/labrinth/src/database/models/charge_item.rs b/apps/labrinth/src/database/models/charge_item.rs index 1e7bdc4081..3b8dd5b8fa 100644 --- a/apps/labrinth/src/database/models/charge_item.rs +++ b/apps/labrinth/src/database/models/charge_item.rs @@ -1,5 +1,6 @@ use crate::database::models::{ - DBChargeId, DBProductPriceId, DBUserId, DBUserSubscriptionId, DatabaseError, + DBAffiliateCodeId, DBChargeId, DBProductPriceId, DBUserId, + DBUserSubscriptionId, DatabaseError, }; use crate::models::billing::{ ChargeStatus, ChargeType, PaymentPlatform, PriceDuration, @@ -34,6 +35,7 @@ pub struct DBCharge { // Net is always in USD pub net: Option, pub tax_drift_loss: Option, + pub affiliate_code: Option, } struct ChargeQueryResult { @@ -56,6 +58,7 @@ struct ChargeQueryResult { tax_last_updated: Option>, net: Option, tax_drift_loss: Option, + affiliate_code: Option, } impl TryFrom for DBCharge { @@ -84,6 +87,7 @@ impl TryFrom for DBCharge { net: r.net, tax_last_updated: r.tax_last_updated, tax_drift_loss: r.tax_drift_loss, + affiliate_code: r.affiliate_code.map(DBAffiliateCodeId), }) } } @@ -103,7 +107,8 @@ macro_rules! select_charges_with_predicate { charges.parent_charge_id AS "parent_charge_id?", charges.net AS "net?", charges.tax_last_updated AS "tax_last_updated?", - charges.tax_drift_loss AS "tax_drift_loss?" + charges.tax_drift_loss AS "tax_drift_loss?", + charges.affiliate_code AS "affiliate_code?" FROM charges "# + $predicate, @@ -119,8 +124,8 @@ impl DBCharge { ) -> Result { sqlx::query!( r#" - INSERT INTO charges (id, user_id, price_id, amount, currency_code, charge_type, status, due, last_attempt, subscription_id, subscription_interval, payment_platform, payment_platform_id, parent_charge_id, net, tax_amount, tax_platform_id, tax_last_updated, tax_drift_loss) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19) + INSERT INTO charges (id, user_id, price_id, amount, currency_code, charge_type, status, due, last_attempt, subscription_id, subscription_interval, payment_platform, payment_platform_id, parent_charge_id, net, tax_amount, tax_platform_id, tax_last_updated, tax_drift_loss, affiliate_code) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20) ON CONFLICT (id) DO UPDATE SET status = EXCLUDED.status, @@ -139,7 +144,8 @@ impl DBCharge { amount = EXCLUDED.amount, currency_code = EXCLUDED.currency_code, charge_type = EXCLUDED.charge_type, - tax_drift_loss = EXCLUDED.tax_drift_loss + tax_drift_loss = EXCLUDED.tax_drift_loss, + affiliate_code = EXCLUDED.affiliate_code "#, self.id.0, self.user_id.0, @@ -160,6 +166,7 @@ impl DBCharge { self.tax_platform_id.as_deref(), self.tax_last_updated, self.tax_drift_loss, + self.affiliate_code ) .execute(&mut **transaction) .await?; From e944a320a134a01d46d7646b3ac9be45436f7740 Mon Sep 17 00:00:00 2001 From: aecsocket Date: Mon, 13 Oct 2025 22:22:30 +0100 Subject: [PATCH 2/4] wip: background task to process affiliate code revenue --- apps/labrinth/src/background_task.rs | 10 +++- .../src/database/models/charge_item.rs | 2 +- apps/labrinth/src/queue/affiliate_codes.rs | 4 ++ apps/labrinth/src/queue/billing.rs | 2 + apps/labrinth/src/queue/mod.rs | 1 + apps/labrinth/src/routes/internal/billing.rs | 59 +++++++++++++------ 6 files changed, 58 insertions(+), 20 deletions(-) create mode 100644 apps/labrinth/src/queue/affiliate_codes.rs diff --git a/apps/labrinth/src/background_task.rs b/apps/labrinth/src/background_task.rs index b79671eaac..cad3688f86 100644 --- a/apps/labrinth/src/background_task.rs +++ b/apps/labrinth/src/background_task.rs @@ -1,4 +1,5 @@ use crate::database::redis::RedisPool; +use crate::queue::affiliate_codes::process_affiliate_code_revenue; use crate::queue::billing::{index_billing, index_subscriptions}; use crate::queue::email::EmailQueue; use crate::queue::payouts::{ @@ -179,12 +180,17 @@ pub async fn payouts( info!("Started running payouts"); let result = process_payout(&pool, &clickhouse).await; if let Err(e) = result { - warn!("Payouts run failed: {:?}", e); + warn!("Payouts run failed: {e:#?}"); } let result = index_payouts_notifications(&pool, &redis_pool).await; if let Err(e) = result { - warn!("Payouts notifications indexing failed: {:?}", e); + warn!("Payouts notifications indexing failed: {e:#?}"); + } + + let result = process_affiliate_code_revenue(&pool).await; + if let Err(e) = result { + warn!("Affiliate code revenue processing failed: {e:#?}"); } info!("Done running payouts"); diff --git a/apps/labrinth/src/database/models/charge_item.rs b/apps/labrinth/src/database/models/charge_item.rs index 3b8dd5b8fa..f654751186 100644 --- a/apps/labrinth/src/database/models/charge_item.rs +++ b/apps/labrinth/src/database/models/charge_item.rs @@ -166,7 +166,7 @@ impl DBCharge { self.tax_platform_id.as_deref(), self.tax_last_updated, self.tax_drift_loss, - self.affiliate_code + self.affiliate_code.map(|x| x.0), ) .execute(&mut **transaction) .await?; diff --git a/apps/labrinth/src/queue/affiliate_codes.rs b/apps/labrinth/src/queue/affiliate_codes.rs new file mode 100644 index 0000000000..7650b6c0e3 --- /dev/null +++ b/apps/labrinth/src/queue/affiliate_codes.rs @@ -0,0 +1,4 @@ +use eyre::Result; +use sqlx::PgPool; + +pub async fn process_affiliate_code_revenue(pool: &PgPool) -> Result<()> {} diff --git a/apps/labrinth/src/queue/billing.rs b/apps/labrinth/src/queue/billing.rs index e259662ca4..5ebfcc3494 100644 --- a/apps/labrinth/src/queue/billing.rs +++ b/apps/labrinth/src/queue/billing.rs @@ -647,6 +647,8 @@ pub async fn try_process_user_redeemal( net: None, tax_last_updated: Some(Utc::now()), tax_drift_loss: Some(0), + // Medal redeemals never have an affiliate code. + affiliate_code: None, } .upsert(&mut txn) .await?; diff --git a/apps/labrinth/src/queue/mod.rs b/apps/labrinth/src/queue/mod.rs index a0fd582758..eb2a64c9a8 100644 --- a/apps/labrinth/src/queue/mod.rs +++ b/apps/labrinth/src/queue/mod.rs @@ -1,3 +1,4 @@ +pub mod affiliate_codes; pub mod analytics; pub mod billing; pub mod email; diff --git a/apps/labrinth/src/routes/internal/billing.rs b/apps/labrinth/src/routes/internal/billing.rs index a2e230ca4a..7d3bf919f8 100644 --- a/apps/labrinth/src/routes/internal/billing.rs +++ b/apps/labrinth/src/routes/internal/billing.rs @@ -4,7 +4,8 @@ use crate::database::models::charge_item::DBCharge; use crate::database::models::notification_item::NotificationBuilder; use crate::database::models::products_tax_identifier_item::product_info_by_product_price_id; use crate::database::models::{ - charge_item, generate_charge_id, product_item, user_subscription_item, + DBAffiliateCodeId, charge_item, generate_charge_id, product_item, + user_subscription_item, }; use crate::database::redis::RedisPool; use crate::models::billing::{ @@ -12,6 +13,7 @@ use crate::models::billing::{ Product, ProductMetadata, ProductPrice, SubscriptionMetadata, SubscriptionStatus, UserSubscription, }; +use crate::models::ids::AffiliateCodeId; use crate::models::notifications::NotificationBody; use crate::models::pats::Scopes; use crate::models::users::Badges; @@ -347,6 +349,8 @@ pub async fn refund_charge( currency_code: charge.currency_code, tax_last_updated: Some(Utc::now()), tax_drift_loss: Some(0), + // Refunds have no affiliate code + affiliate_code: None, } .upsert(&mut transaction) .await?; @@ -1282,9 +1286,16 @@ pub enum ChargeRequestType { }, } +#[derive(Deserialize, Serialize)] +pub struct PaymentRequestMetadata { + #[serde(flatten)] + kind: PaymentRequestMetadataKind, + affiliate_code: Option, +} + #[derive(Deserialize, Serialize)] #[serde(tag = "type", rename_all = "snake_case")] -pub enum PaymentRequestMetadata { +pub enum PaymentRequestMetadataKind { Pyro { server_name: Option, server_region: Option, @@ -1425,9 +1436,10 @@ pub async fn stripe_webhook( break 'metadata; }; - let payment_metadata = metadata - .get(MODRINTH_PAYMENT_METADATA) - .and_then(|x| serde_json::from_str(x).ok()); + let payment_metadata = + metadata.get(MODRINTH_PAYMENT_METADATA).and_then(|x| { + serde_json::from_str::(x).ok() + }); let Some(charge_id) = metadata .get(MODRINTH_CHARGE_ID) @@ -1576,13 +1588,13 @@ pub async fn stripe_webhook( if intervals.get(&interval).is_some() { let Some(subscription_id) = metadata - .get(MODRINTH_SUBSCRIPTION_ID) - .and_then(|x| parse_base62(x).ok()) - .map(|x| { - crate::database::models::ids::DBUserSubscriptionId(x as i64) - }) else { - break 'metadata; - }; + .get(MODRINTH_SUBSCRIPTION_ID) + .and_then(|x| parse_base62(x).ok()) + .map(|x| { + crate::database::models::ids::DBUserSubscriptionId(x as i64) + }) else { + break 'metadata; + }; let subscription = if let Some(mut subscription) = user_subscription_item::DBUserSubscription::get(subscription_id, pool).await? { subscription.status = SubscriptionStatus::Unprovisioned; @@ -1615,6 +1627,11 @@ pub async fn stripe_webhook( } }; + let affiliate_code = payment_metadata + .as_ref() + .and_then(|m| m.affiliate_code) + .map(DBAffiliateCodeId::from); + let charge = DBCharge { id: charge_id, user_id, @@ -1641,6 +1658,7 @@ pub async fn stripe_webhook( net: None, tax_last_updated: Some(Utc::now()), tax_drift_loss: Some(0), + affiliate_code, }; if charge_status != ChargeStatus::Failed { @@ -1820,12 +1838,12 @@ pub async fn stripe_webhook( } else { let (server_name, server_region, source) = if let Some( - PaymentRequestMetadata::Pyro { - ref server_name, - ref server_region, - ref source, + PaymentRequestMetadataKind::Pyro { + server_name, + server_region, + source, }, - ) = metadata.payment_metadata + ) = metadata.payment_metadata.as_ref().map(|m| &m.kind) { ( server_name.clone(), @@ -1909,6 +1927,12 @@ pub async fn stripe_webhook( } } + let affiliate_code = metadata + .payment_metadata + .as_ref() + .and_then(|m| m.affiliate_code) + .map(DBAffiliateCodeId::from); + if let Some(mut subscription) = metadata.user_subscription_item { @@ -2004,6 +2028,7 @@ pub async fn stripe_webhook( tax_platform_id: None, tax_last_updated: Some(Utc::now()), tax_drift_loss: Some(0), + affiliate_code, } .upsert(&mut transaction) .await?; From d64668698c593d2455bc1a7276732eeb22a6fad6 Mon Sep 17 00:00:00 2001 From: aecsocket Date: Mon, 13 Oct 2025 23:46:47 +0100 Subject: [PATCH 3/4] wip: implement affiliate code payout queue --- Cargo.lock | 1 + apps/labrinth/Cargo.toml | 1 + ...0251011214648_affiliate_payout_charges.sql | 3 + apps/labrinth/src/queue/affiliate_codes.rs | 144 +++++++++++++++++- 4 files changed, 148 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index d6068768c4..e69fb9e43e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7254,6 +7254,7 @@ dependencies = [ "num-traits", "rand 0.8.5", "rkyv", + "rust_decimal_macros", "serde", "serde_json", ] diff --git a/apps/labrinth/Cargo.toml b/apps/labrinth/Cargo.toml index 3e57bec3ac..dd6e9eb9b2 100644 --- a/apps/labrinth/Cargo.toml +++ b/apps/labrinth/Cargo.toml @@ -86,6 +86,7 @@ reqwest = { workspace = true, features = [ "rustls-tls-webpki-roots", ] } rust_decimal = { workspace = true, features = [ + "macros", "serde-with-float", "serde-with-str", ] } diff --git a/apps/labrinth/migrations/20251011214648_affiliate_payout_charges.sql b/apps/labrinth/migrations/20251011214648_affiliate_payout_charges.sql index c547135065..c8dd128b14 100644 --- a/apps/labrinth/migrations/20251011214648_affiliate_payout_charges.sql +++ b/apps/labrinth/migrations/20251011214648_affiliate_payout_charges.sql @@ -1,2 +1,5 @@ ALTER TABLE charges ADD COLUMN affiliate_code BIGINT; + +ALTER TABLE payouts_values +ADD COLUMN affiliate_code_id BIGINT; diff --git a/apps/labrinth/src/queue/affiliate_codes.rs b/apps/labrinth/src/queue/affiliate_codes.rs index 7650b6c0e3..d6b42ddd80 100644 --- a/apps/labrinth/src/queue/affiliate_codes.rs +++ b/apps/labrinth/src/queue/affiliate_codes.rs @@ -1,4 +1,146 @@ +use std::collections::HashMap; + +use ariadne::ids::UserId; +use chrono::{Datelike, Duration, TimeZone, Utc}; use eyre::Result; +use rust_decimal::{Decimal, dec}; use sqlx::PgPool; +use tracing::{trace, warn}; + +use crate::{ + database::models::{DBAffiliateCodeId, DBUserId}, + models::ids::AffiliateCodeId, +}; + +const AFFILIATE_CUT_PERCENTAGE: Decimal = dec!(0.1); + +pub async fn process_affiliate_code_revenue(pool: &PgPool) -> Result<()> { + let end_date = Utc::now() - Duration::days(30); + let start_date = end_date - Duration::days(30); + + let affiliate_charges = sqlx::query!( + r#" + SELECT + c.id as charge_id, + c.affiliate_code, + c.net, + ac.affiliate as affiliate_user_id, + ac.revenue_split + FROM charges c + INNER JOIN affiliate_codes ac ON c.affiliate_code = ac.id + WHERE + c.status = 'succeeded' + AND c.net > 0 + AND c.due BETWEEN $1 AND $2 + "#, + start_date, + end_date + ) + .fetch_all(pool) + .await?; + + if affiliate_charges.is_empty() { + return Ok(()); + } + + let mut transaction = pool.begin().await?; + + // Group by affiliate user and affiliate code to create unique rows per affiliate code + let mut affiliate_payouts: HashMap<(DBUserId, DBAffiliateCodeId), Decimal> = + HashMap::new(); + + for charge in affiliate_charges { + let Some(net_amount) = charge.net else { + continue; + }; + let net_amount = Decimal::new(net_amount, 2); + + let affiliate_user_id = DBUserId(charge.affiliate_user_id); + let affiliate_code_id = + DBAffiliateCodeId(charge.affiliate_code.unwrap()); + + // Use the custom revenue split if specified, otherwise use the default 10% + let revenue_split = + charge + .revenue_split + .map_or(AFFILIATE_CUT_PERCENTAGE, |split| { + Decimal::from_f64_retain(split) + .unwrap_or(AFFILIATE_CUT_PERCENTAGE) + }); + + if revenue_split < dec!(0) || revenue_split > dec!(1) { + warn!( + "Charge {} resulted in invalid revenue split {revenue_split}", + charge.charge_id + ); + continue; + } + + let affiliate_cut = net_amount * revenue_split; + + if affiliate_cut > dec!(0) { + *affiliate_payouts + .entry((affiliate_user_id, affiliate_code_id)) + .or_insert(dec!(0)) += affiliate_cut; + } + } + + let mut insert_user_ids = Vec::new(); + let mut insert_payouts = Vec::new(); + let mut insert_starts = Vec::new(); + let mut insert_availables = Vec::new(); + let mut insert_affiliate_code_ids = Vec::new(); + + let created_timestamp = end_date; + + // Affiliate payouts are Net 30 from the end of the processing month + let available_timestamp = { + let processing_month = end_date.date_naive(); + let year = processing_month.year(); + let month = processing_month.month(); + + let first_of_next_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() + }; + + first_of_next_month + Duration::days(29) + }; + + for ((user_id, affiliate_code_id), total_payout) in affiliate_payouts { + if total_payout > dec!(0) { + insert_user_ids.push(user_id.0); + insert_payouts.push(total_payout); + insert_starts.push(created_timestamp); + insert_availables.push(available_timestamp); + insert_affiliate_code_ids.push(affiliate_code_id.0); + + trace!( + "User {} gets {total_payout} from affiliate code {}", + UserId::from(user_id), + AffiliateCodeId::from(affiliate_code_id), + ); + } + } + + if !insert_user_ids.is_empty() { + sqlx::query!( + r#" + INSERT INTO payouts_values (user_id, amount, created, date_available, affiliate_code_id) + SELECT * FROM UNNEST ($1::bigint[], $2::numeric[], $3::timestamptz[], $4::timestamptz[], $5::bigint[]) + "#, + &insert_user_ids[..], + &insert_payouts[..], + &insert_starts[..], + &insert_availables[..], + &insert_affiliate_code_ids[..], + ) + .execute(&mut *transaction) + .await?; + } + + transaction.commit().await?; -pub async fn process_affiliate_code_revenue(pool: &PgPool) -> Result<()> {} + Ok(()) +} From b2659eeb533768a4a1de8a6ea0058beed7cf7656 Mon Sep 17 00:00:00 2001 From: aecsocket Date: Tue, 14 Oct 2025 01:00:21 +0100 Subject: [PATCH 4/4] Add affiliate code rev split test --- ...1b455f943edb16121823b61fb737c14cb8ad3.json | 18 + ...7ed8d1aaeae0822af1a1d19a2cdc80258c41.json} | 10 +- ...20116546d9af0b14423fb7a16e73c39ba4d8.json} | 11 +- ...c87a4a56bb203ec57f12ee44adff5eec9f55.json} | 11 +- ...ce7f18c98e5e82b29aa9dc272ad7173cec51.json} | 10 +- ...c6c36259ea492a7f830afbaff499ac3072cc0.json | 33 + ...c9fd2e2c25d7aae41ba95cc36add3c3f7549.json} | 10 +- ...f178a7828a45ed3134d3336cb59572f40beab.json | 32 - ...4cd6d0ba3b1bcbc89df349cf7b5b1897603b8.json | 130 ---- ...5f374a6baa7d18a411051c33694f301d2884c.json | 136 ++++ ...4903293fc12d78db69808190f0e96a51e860e.json | 47 ++ ...f4bb03797e2dbf87f242af76e87af43ed1a9.json} | 10 +- ...c1a103b35f2e4ca224e1f3ef448a74531720.json} | 10 +- ...4d7578d353459c5046e50ecd5e4aebce6705.json} | 10 +- apps/labrinth/tests/affiliate_code_revenue.rs | 722 ++++++++++++++++++ 15 files changed, 1021 insertions(+), 179 deletions(-) create mode 100644 apps/labrinth/.sqlx/query-04ce7057512b313803252064aab1b455f943edb16121823b61fb737c14cb8ad3.json rename apps/labrinth/.sqlx/{query-9f0c73fabe99d9891faaebdd3518b362437dcdcef9cd9a68b950fba61218bb4d.json => query-08d5ffdd95f844846df8f8b2afce7ed8d1aaeae0822af1a1d19a2cdc80258c41.json} (80%) rename apps/labrinth/.sqlx/{query-7973e569e784f416c1b4f1e6f3b099dca9c0d9c84e55951a730d8c214580e0d6.json => query-17cd6a07a9315599edee66d0bc9c20116546d9af0b14423fb7a16e73c39ba4d8.json} (84%) rename apps/labrinth/.sqlx/{query-050e755134f6d1f09de805ae2cd0f7ca8f6efb96be9f070c43db7fd2049af2d2.json => query-1856a9de26a0aa0e6ea5849b7f7ec87a4a56bb203ec57f12ee44adff5eec9f55.json} (87%) rename apps/labrinth/.sqlx/{query-e2e58113bc3a3db6ffc75b5c5e10acd16403aa0679ef53330f2ce3e8a45f7b9f.json => query-94023ab7dc72dceb9a2cbf17130bce7f18c98e5e82b29aa9dc272ad7173cec51.json} (86%) create mode 100644 apps/labrinth/.sqlx/query-9b740f4572544085ff84bb95e2fc6c36259ea492a7f830afbaff499ac3072cc0.json rename apps/labrinth/.sqlx/{query-51c542076b4b3811eb12f051294f55827a27f51e65e668525b8b545f570c0bda.json => query-9fa81bfef5410d121bf4f275461ec9fd2e2c25d7aae41ba95cc36add3c3f7549.json} (89%) delete mode 100644 apps/labrinth/.sqlx/query-c0c70ebc3d59a5ab6a4c81e987df178a7828a45ed3134d3336cb59572f40beab.json delete mode 100644 apps/labrinth/.sqlx/query-cd18ae8abe81a159a134923957f4cd6d0ba3b1bcbc89df349cf7b5b1897603b8.json create mode 100644 apps/labrinth/.sqlx/query-d02552b5550bc2bd1e1eedc6bbe5f374a6baa7d18a411051c33694f301d2884c.json create mode 100644 apps/labrinth/.sqlx/query-d0fda4cc7d8562a9152afa29b3a4903293fc12d78db69808190f0e96a51e860e.json rename apps/labrinth/.sqlx/{query-9a35729acbba06eafaa205922e4987e082a000ec1b397957650e1332191613ca.json => query-d853f30019b3afb62cf81c5c8f58f4bb03797e2dbf87f242af76e87af43ed1a9.json} (90%) rename apps/labrinth/.sqlx/{query-e36e0ac1e2edb73533961a18e913f0b8e4f420a76e511571bb2eed9355771e54.json => query-fb46fe2707ebcca811a530246e50c1a103b35f2e4ca224e1f3ef448a74531720.json} (84%) rename apps/labrinth/.sqlx/{query-4e8e9f9cb42f90cc17702386fdb78385608f19dae9439cb6a860503600127b04.json => query-fffb8f9aeb39df168aaffe48c9054d7578d353459c5046e50ecd5e4aebce6705.json} (89%) create mode 100644 apps/labrinth/tests/affiliate_code_revenue.rs diff --git a/apps/labrinth/.sqlx/query-04ce7057512b313803252064aab1b455f943edb16121823b61fb737c14cb8ad3.json b/apps/labrinth/.sqlx/query-04ce7057512b313803252064aab1b455f943edb16121823b61fb737c14cb8ad3.json new file mode 100644 index 0000000000..29d7618860 --- /dev/null +++ b/apps/labrinth/.sqlx/query-04ce7057512b313803252064aab1b455f943edb16121823b61fb737c14cb8ad3.json @@ -0,0 +1,18 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO payouts_values (user_id, amount, created, date_available, affiliate_code_id)\n SELECT * FROM UNNEST ($1::bigint[], $2::numeric[], $3::timestamptz[], $4::timestamptz[], $5::bigint[])\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8Array", + "NumericArray", + "TimestamptzArray", + "TimestamptzArray", + "Int8Array" + ] + }, + "nullable": [] + }, + "hash": "04ce7057512b313803252064aab1b455f943edb16121823b61fb737c14cb8ad3" +} diff --git a/apps/labrinth/.sqlx/query-9f0c73fabe99d9891faaebdd3518b362437dcdcef9cd9a68b950fba61218bb4d.json b/apps/labrinth/.sqlx/query-08d5ffdd95f844846df8f8b2afce7ed8d1aaeae0822af1a1d19a2cdc80258c41.json similarity index 80% rename from apps/labrinth/.sqlx/query-9f0c73fabe99d9891faaebdd3518b362437dcdcef9cd9a68b950fba61218bb4d.json rename to apps/labrinth/.sqlx/query-08d5ffdd95f844846df8f8b2afce7ed8d1aaeae0822af1a1d19a2cdc80258c41.json index f5dc4759fb..d1db4a9fc7 100644 --- a/apps/labrinth/.sqlx/query-9f0c73fabe99d9891faaebdd3518b362437dcdcef9cd9a68b950fba61218bb4d.json +++ b/apps/labrinth/.sqlx/query-08d5ffdd95f844846df8f8b2afce7ed8d1aaeae0822af1a1d19a2cdc80258c41.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\"\n FROM charges\n \n INNER JOIN users_subscriptions us ON us.id = charges.subscription_id\n WHERE\n charges.charge_type = $1 AND\n (\n (charges.status = 'cancelled' AND charges.due < NOW()) OR\n (charges.status = 'expiring' AND charges.due < NOW()) OR\n (charges.status = 'failed' AND charges.last_attempt < NOW() - INTERVAL '2 days')\n )\n AND us.status = 'provisioned'\n ", + "query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\",\n\t\t\t\tcharges.affiliate_code AS \"affiliate_code?\"\n FROM charges\n \n INNER JOIN users_subscriptions us ON us.id = charges.subscription_id\n WHERE\n charges.charge_type = $1 AND\n (\n (charges.status = 'cancelled' AND charges.due < NOW()) OR\n (charges.status = 'expiring' AND charges.due < NOW()) OR\n (charges.status = 'failed' AND charges.last_attempt < NOW() - INTERVAL '2 days')\n )\n AND us.status = 'provisioned'\n ", "describe": { "columns": [ { @@ -97,6 +97,11 @@ "ordinal": 18, "name": "tax_drift_loss?", "type_info": "Int8" + }, + { + "ordinal": 19, + "name": "affiliate_code?", + "type_info": "Int8" } ], "parameters": { @@ -123,8 +128,9 @@ true, true, true, + true, true ] }, - "hash": "9f0c73fabe99d9891faaebdd3518b362437dcdcef9cd9a68b950fba61218bb4d" + "hash": "08d5ffdd95f844846df8f8b2afce7ed8d1aaeae0822af1a1d19a2cdc80258c41" } diff --git a/apps/labrinth/.sqlx/query-7973e569e784f416c1b4f1e6f3b099dca9c0d9c84e55951a730d8c214580e0d6.json b/apps/labrinth/.sqlx/query-17cd6a07a9315599edee66d0bc9c20116546d9af0b14423fb7a16e73c39ba4d8.json similarity index 84% rename from apps/labrinth/.sqlx/query-7973e569e784f416c1b4f1e6f3b099dca9c0d9c84e55951a730d8c214580e0d6.json rename to apps/labrinth/.sqlx/query-17cd6a07a9315599edee66d0bc9c20116546d9af0b14423fb7a16e73c39ba4d8.json index d8265d3399..46a94cf9a2 100644 --- a/apps/labrinth/.sqlx/query-7973e569e784f416c1b4f1e6f3b099dca9c0d9c84e55951a730d8c214580e0d6.json +++ b/apps/labrinth/.sqlx/query-17cd6a07a9315599edee66d0bc9c20116546d9af0b14423fb7a16e73c39ba4d8.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\"\n FROM charges\n WHERE user_id = $1 ORDER BY due DESC", + "query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\",\n\t\t\t\tcharges.affiliate_code AS \"affiliate_code?\"\n FROM charges\n \n\t\t\tWHERE\n\t\t\t status = 'succeeded'\n\t\t\t AND tax_platform_id IS NULL\n AND payment_platform_id IS NOT NULL\n\t\t\tORDER BY due ASC\n\t\t\tFOR NO KEY UPDATE SKIP LOCKED\n OFFSET $1\n\t\t\tLIMIT $2\n\t\t\t", "describe": { "columns": [ { @@ -97,10 +97,16 @@ "ordinal": 18, "name": "tax_drift_loss?", "type_info": "Int8" + }, + { + "ordinal": 19, + "name": "affiliate_code?", + "type_info": "Int8" } ], "parameters": { "Left": [ + "Int8", "Int8" ] }, @@ -123,8 +129,9 @@ true, true, true, + true, true ] }, - "hash": "7973e569e784f416c1b4f1e6f3b099dca9c0d9c84e55951a730d8c214580e0d6" + "hash": "17cd6a07a9315599edee66d0bc9c20116546d9af0b14423fb7a16e73c39ba4d8" } diff --git a/apps/labrinth/.sqlx/query-050e755134f6d1f09de805ae2cd0f7ca8f6efb96be9f070c43db7fd2049af2d2.json b/apps/labrinth/.sqlx/query-1856a9de26a0aa0e6ea5849b7f7ec87a4a56bb203ec57f12ee44adff5eec9f55.json similarity index 87% rename from apps/labrinth/.sqlx/query-050e755134f6d1f09de805ae2cd0f7ca8f6efb96be9f070c43db7fd2049af2d2.json rename to apps/labrinth/.sqlx/query-1856a9de26a0aa0e6ea5849b7f7ec87a4a56bb203ec57f12ee44adff5eec9f55.json index 5779cc0f39..e2db969142 100644 --- a/apps/labrinth/.sqlx/query-050e755134f6d1f09de805ae2cd0f7ca8f6efb96be9f070c43db7fd2049af2d2.json +++ b/apps/labrinth/.sqlx/query-1856a9de26a0aa0e6ea5849b7f7ec87a4a56bb203ec57f12ee44adff5eec9f55.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\"\n FROM charges\n \n\t\t\tWHERE\n\t\t\t status = 'succeeded'\n\t\t\t AND tax_platform_id IS NULL\n AND payment_platform_id IS NOT NULL\n\t\t\tORDER BY due ASC\n\t\t\tFOR NO KEY UPDATE SKIP LOCKED\n OFFSET $1\n\t\t\tLIMIT $2\n\t\t\t", + "query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\",\n\t\t\t\tcharges.affiliate_code AS \"affiliate_code?\"\n FROM charges\n WHERE subscription_id = $1 AND (status = 'open' OR status = 'expiring' OR status = 'cancelled' OR status = 'failed')", "describe": { "columns": [ { @@ -97,11 +97,15 @@ "ordinal": 18, "name": "tax_drift_loss?", "type_info": "Int8" + }, + { + "ordinal": 19, + "name": "affiliate_code?", + "type_info": "Int8" } ], "parameters": { "Left": [ - "Int8", "Int8" ] }, @@ -124,8 +128,9 @@ true, true, true, + true, true ] }, - "hash": "050e755134f6d1f09de805ae2cd0f7ca8f6efb96be9f070c43db7fd2049af2d2" + "hash": "1856a9de26a0aa0e6ea5849b7f7ec87a4a56bb203ec57f12ee44adff5eec9f55" } diff --git a/apps/labrinth/.sqlx/query-e2e58113bc3a3db6ffc75b5c5e10acd16403aa0679ef53330f2ce3e8a45f7b9f.json b/apps/labrinth/.sqlx/query-94023ab7dc72dceb9a2cbf17130bce7f18c98e5e82b29aa9dc272ad7173cec51.json similarity index 86% rename from apps/labrinth/.sqlx/query-e2e58113bc3a3db6ffc75b5c5e10acd16403aa0679ef53330f2ce3e8a45f7b9f.json rename to apps/labrinth/.sqlx/query-94023ab7dc72dceb9a2cbf17130bce7f18c98e5e82b29aa9dc272ad7173cec51.json index d17a32763a..61e146173b 100644 --- a/apps/labrinth/.sqlx/query-e2e58113bc3a3db6ffc75b5c5e10acd16403aa0679ef53330f2ce3e8a45f7b9f.json +++ b/apps/labrinth/.sqlx/query-94023ab7dc72dceb9a2cbf17130bce7f18c98e5e82b29aa9dc272ad7173cec51.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\"\n FROM charges\n \n WHERE\n charge_type = $1 AND\n (\n (status = 'open' AND due < NOW()) OR\n (status = 'failed' AND last_attempt < NOW() - INTERVAL '2 days')\n )\n ", + "query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\",\n\t\t\t\tcharges.affiliate_code AS \"affiliate_code?\"\n FROM charges\n \n WHERE\n charge_type = $1 AND\n status = 'failed' AND due < NOW() - INTERVAL '30 days'\n ", "describe": { "columns": [ { @@ -97,6 +97,11 @@ "ordinal": 18, "name": "tax_drift_loss?", "type_info": "Int8" + }, + { + "ordinal": 19, + "name": "affiliate_code?", + "type_info": "Int8" } ], "parameters": { @@ -123,8 +128,9 @@ true, true, true, + true, true ] }, - "hash": "e2e58113bc3a3db6ffc75b5c5e10acd16403aa0679ef53330f2ce3e8a45f7b9f" + "hash": "94023ab7dc72dceb9a2cbf17130bce7f18c98e5e82b29aa9dc272ad7173cec51" } diff --git a/apps/labrinth/.sqlx/query-9b740f4572544085ff84bb95e2fc6c36259ea492a7f830afbaff499ac3072cc0.json b/apps/labrinth/.sqlx/query-9b740f4572544085ff84bb95e2fc6c36259ea492a7f830afbaff499ac3072cc0.json new file mode 100644 index 0000000000..a64d46eac4 --- /dev/null +++ b/apps/labrinth/.sqlx/query-9b740f4572544085ff84bb95e2fc6c36259ea492a7f830afbaff499ac3072cc0.json @@ -0,0 +1,33 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO charges (id, user_id, price_id, amount, currency_code, charge_type, status, due, last_attempt, subscription_id, subscription_interval, payment_platform, payment_platform_id, parent_charge_id, net, tax_amount, tax_platform_id, tax_last_updated, tax_drift_loss, affiliate_code)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20)\n ON CONFLICT (id)\n DO UPDATE\n SET status = EXCLUDED.status,\n last_attempt = EXCLUDED.last_attempt,\n due = EXCLUDED.due,\n subscription_id = EXCLUDED.subscription_id,\n subscription_interval = EXCLUDED.subscription_interval,\n payment_platform = EXCLUDED.payment_platform,\n payment_platform_id = EXCLUDED.payment_platform_id,\n parent_charge_id = EXCLUDED.parent_charge_id,\n net = EXCLUDED.net,\n tax_amount = EXCLUDED.tax_amount,\n tax_platform_id = EXCLUDED.tax_platform_id,\n tax_last_updated = EXCLUDED.tax_last_updated,\n price_id = EXCLUDED.price_id,\n amount = EXCLUDED.amount,\n currency_code = EXCLUDED.currency_code,\n charge_type = EXCLUDED.charge_type,\n\t\t\t\t\ttax_drift_loss = EXCLUDED.tax_drift_loss,\n\t\t\t\t\taffiliate_code = EXCLUDED.affiliate_code\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int8", + "Int8", + "Int8", + "Text", + "Text", + "Varchar", + "Timestamptz", + "Timestamptz", + "Int8", + "Text", + "Text", + "Text", + "Int8", + "Int8", + "Int8", + "Text", + "Timestamptz", + "Int8", + "Int8" + ] + }, + "nullable": [] + }, + "hash": "9b740f4572544085ff84bb95e2fc6c36259ea492a7f830afbaff499ac3072cc0" +} diff --git a/apps/labrinth/.sqlx/query-51c542076b4b3811eb12f051294f55827a27f51e65e668525b8b545f570c0bda.json b/apps/labrinth/.sqlx/query-9fa81bfef5410d121bf4f275461ec9fd2e2c25d7aae41ba95cc36add3c3f7549.json similarity index 89% rename from apps/labrinth/.sqlx/query-51c542076b4b3811eb12f051294f55827a27f51e65e668525b8b545f570c0bda.json rename to apps/labrinth/.sqlx/query-9fa81bfef5410d121bf4f275461ec9fd2e2c25d7aae41ba95cc36add3c3f7549.json index b3efa34201..61298b5924 100644 --- a/apps/labrinth/.sqlx/query-51c542076b4b3811eb12f051294f55827a27f51e65e668525b8b545f570c0bda.json +++ b/apps/labrinth/.sqlx/query-9fa81bfef5410d121bf4f275461ec9fd2e2c25d7aae41ba95cc36add3c3f7549.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\"\n FROM charges\n WHERE parent_charge_id = $1", + "query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\",\n\t\t\t\tcharges.affiliate_code AS \"affiliate_code?\"\n FROM charges\n WHERE user_id = $1 ORDER BY due DESC", "describe": { "columns": [ { @@ -97,6 +97,11 @@ "ordinal": 18, "name": "tax_drift_loss?", "type_info": "Int8" + }, + { + "ordinal": 19, + "name": "affiliate_code?", + "type_info": "Int8" } ], "parameters": { @@ -123,8 +128,9 @@ true, true, true, + true, true ] }, - "hash": "51c542076b4b3811eb12f051294f55827a27f51e65e668525b8b545f570c0bda" + "hash": "9fa81bfef5410d121bf4f275461ec9fd2e2c25d7aae41ba95cc36add3c3f7549" } diff --git a/apps/labrinth/.sqlx/query-c0c70ebc3d59a5ab6a4c81e987df178a7828a45ed3134d3336cb59572f40beab.json b/apps/labrinth/.sqlx/query-c0c70ebc3d59a5ab6a4c81e987df178a7828a45ed3134d3336cb59572f40beab.json deleted file mode 100644 index 75a3edf56c..0000000000 --- a/apps/labrinth/.sqlx/query-c0c70ebc3d59a5ab6a4c81e987df178a7828a45ed3134d3336cb59572f40beab.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO charges (id, user_id, price_id, amount, currency_code, charge_type, status, due, last_attempt, subscription_id, subscription_interval, payment_platform, payment_platform_id, parent_charge_id, net, tax_amount, tax_platform_id, tax_last_updated, tax_drift_loss)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)\n ON CONFLICT (id)\n DO UPDATE\n SET status = EXCLUDED.status,\n last_attempt = EXCLUDED.last_attempt,\n due = EXCLUDED.due,\n subscription_id = EXCLUDED.subscription_id,\n subscription_interval = EXCLUDED.subscription_interval,\n payment_platform = EXCLUDED.payment_platform,\n payment_platform_id = EXCLUDED.payment_platform_id,\n parent_charge_id = EXCLUDED.parent_charge_id,\n net = EXCLUDED.net,\n tax_amount = EXCLUDED.tax_amount,\n tax_platform_id = EXCLUDED.tax_platform_id,\n tax_last_updated = EXCLUDED.tax_last_updated,\n price_id = EXCLUDED.price_id,\n amount = EXCLUDED.amount,\n currency_code = EXCLUDED.currency_code,\n charge_type = EXCLUDED.charge_type,\n\t\t\t\t\ttax_drift_loss = EXCLUDED.tax_drift_loss\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8", - "Int8", - "Int8", - "Int8", - "Text", - "Text", - "Varchar", - "Timestamptz", - "Timestamptz", - "Int8", - "Text", - "Text", - "Text", - "Int8", - "Int8", - "Int8", - "Text", - "Timestamptz", - "Int8" - ] - }, - "nullable": [] - }, - "hash": "c0c70ebc3d59a5ab6a4c81e987df178a7828a45ed3134d3336cb59572f40beab" -} diff --git a/apps/labrinth/.sqlx/query-cd18ae8abe81a159a134923957f4cd6d0ba3b1bcbc89df349cf7b5b1897603b8.json b/apps/labrinth/.sqlx/query-cd18ae8abe81a159a134923957f4cd6d0ba3b1bcbc89df349cf7b5b1897603b8.json deleted file mode 100644 index 458f19f968..0000000000 --- a/apps/labrinth/.sqlx/query-cd18ae8abe81a159a134923957f4cd6d0ba3b1bcbc89df349cf7b5b1897603b8.json +++ /dev/null @@ -1,130 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\"\n FROM charges\n \n\t\t\tINNER JOIN users u ON u.id = charges.user_id\n\t\t\tWHERE\n\t\t\t status = 'open'\n\t\t\t AND COALESCE(tax_last_updated, '-infinity' :: TIMESTAMPTZ) < NOW() - INTERVAL '1 day'\n\t\t\t AND u.email IS NOT NULL\n\t\t\t AND due - INTERVAL '7 days' > NOW()\n AND due - INTERVAL '14 days' < NOW() -- Due between 7 and 14 days from now\n\t\t\tORDER BY COALESCE(tax_last_updated, '-infinity' :: TIMESTAMPTZ) ASC\n\t\t\tFOR NO KEY UPDATE SKIP LOCKED\n\t\t\tLIMIT $1\n\t\t\t", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "user_id", - "type_info": "Int8" - }, - { - "ordinal": 2, - "name": "price_id", - "type_info": "Int8" - }, - { - "ordinal": 3, - "name": "amount", - "type_info": "Int8" - }, - { - "ordinal": 4, - "name": "currency_code", - "type_info": "Text" - }, - { - "ordinal": 5, - "name": "status", - "type_info": "Varchar" - }, - { - "ordinal": 6, - "name": "due", - "type_info": "Timestamptz" - }, - { - "ordinal": 7, - "name": "last_attempt", - "type_info": "Timestamptz" - }, - { - "ordinal": 8, - "name": "charge_type", - "type_info": "Text" - }, - { - "ordinal": 9, - "name": "subscription_id", - "type_info": "Int8" - }, - { - "ordinal": 10, - "name": "tax_amount", - "type_info": "Int8" - }, - { - "ordinal": 11, - "name": "tax_platform_id", - "type_info": "Text" - }, - { - "ordinal": 12, - "name": "subscription_interval?", - "type_info": "Text" - }, - { - "ordinal": 13, - "name": "payment_platform", - "type_info": "Text" - }, - { - "ordinal": 14, - "name": "payment_platform_id?", - "type_info": "Text" - }, - { - "ordinal": 15, - "name": "parent_charge_id?", - "type_info": "Int8" - }, - { - "ordinal": 16, - "name": "net?", - "type_info": "Int8" - }, - { - "ordinal": 17, - "name": "tax_last_updated?", - "type_info": "Timestamptz" - }, - { - "ordinal": 18, - "name": "tax_drift_loss?", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - false, - true, - false, - true, - false, - true, - true, - false, - true, - true, - true, - true, - true - ] - }, - "hash": "cd18ae8abe81a159a134923957f4cd6d0ba3b1bcbc89df349cf7b5b1897603b8" -} diff --git a/apps/labrinth/.sqlx/query-d02552b5550bc2bd1e1eedc6bbe5f374a6baa7d18a411051c33694f301d2884c.json b/apps/labrinth/.sqlx/query-d02552b5550bc2bd1e1eedc6bbe5f374a6baa7d18a411051c33694f301d2884c.json new file mode 100644 index 0000000000..fbf8996d58 --- /dev/null +++ b/apps/labrinth/.sqlx/query-d02552b5550bc2bd1e1eedc6bbe5f374a6baa7d18a411051c33694f301d2884c.json @@ -0,0 +1,136 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\",\n\t\t\t\tcharges.affiliate_code AS \"affiliate_code?\"\n FROM charges\n \n\t\t\tINNER JOIN users u ON u.id = charges.user_id\n\t\t\tWHERE\n\t\t\t status = 'open'\n\t\t\t AND COALESCE(tax_last_updated, '-infinity' :: TIMESTAMPTZ) < NOW() - INTERVAL '1 day'\n\t\t\t AND u.email IS NOT NULL\n\t\t\t AND due - INTERVAL '7 days' > NOW()\n AND due - INTERVAL '14 days' < NOW() -- Due between 7 and 14 days from now\n\t\t\tORDER BY COALESCE(tax_last_updated, '-infinity' :: TIMESTAMPTZ) ASC\n\t\t\tFOR NO KEY UPDATE SKIP LOCKED\n\t\t\tLIMIT $1\n\t\t\t", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "user_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "price_id", + "type_info": "Int8" + }, + { + "ordinal": 3, + "name": "amount", + "type_info": "Int8" + }, + { + "ordinal": 4, + "name": "currency_code", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "status", + "type_info": "Varchar" + }, + { + "ordinal": 6, + "name": "due", + "type_info": "Timestamptz" + }, + { + "ordinal": 7, + "name": "last_attempt", + "type_info": "Timestamptz" + }, + { + "ordinal": 8, + "name": "charge_type", + "type_info": "Text" + }, + { + "ordinal": 9, + "name": "subscription_id", + "type_info": "Int8" + }, + { + "ordinal": 10, + "name": "tax_amount", + "type_info": "Int8" + }, + { + "ordinal": 11, + "name": "tax_platform_id", + "type_info": "Text" + }, + { + "ordinal": 12, + "name": "subscription_interval?", + "type_info": "Text" + }, + { + "ordinal": 13, + "name": "payment_platform", + "type_info": "Text" + }, + { + "ordinal": 14, + "name": "payment_platform_id?", + "type_info": "Text" + }, + { + "ordinal": 15, + "name": "parent_charge_id?", + "type_info": "Int8" + }, + { + "ordinal": 16, + "name": "net?", + "type_info": "Int8" + }, + { + "ordinal": 17, + "name": "tax_last_updated?", + "type_info": "Timestamptz" + }, + { + "ordinal": 18, + "name": "tax_drift_loss?", + "type_info": "Int8" + }, + { + "ordinal": 19, + "name": "affiliate_code?", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + true, + false, + true, + false, + true, + true, + false, + true, + true, + true, + true, + true, + true + ] + }, + "hash": "d02552b5550bc2bd1e1eedc6bbe5f374a6baa7d18a411051c33694f301d2884c" +} diff --git a/apps/labrinth/.sqlx/query-d0fda4cc7d8562a9152afa29b3a4903293fc12d78db69808190f0e96a51e860e.json b/apps/labrinth/.sqlx/query-d0fda4cc7d8562a9152afa29b3a4903293fc12d78db69808190f0e96a51e860e.json new file mode 100644 index 0000000000..3bdae75210 --- /dev/null +++ b/apps/labrinth/.sqlx/query-d0fda4cc7d8562a9152afa29b3a4903293fc12d78db69808190f0e96a51e860e.json @@ -0,0 +1,47 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n c.id as charge_id,\n c.affiliate_code,\n c.net,\n ac.affiliate as affiliate_user_id,\n ac.revenue_split\n FROM charges c\n INNER JOIN affiliate_codes ac ON c.affiliate_code = ac.id\n WHERE\n c.status = 'succeeded'\n AND c.net > 0\n AND c.due BETWEEN $1 AND $2\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "charge_id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "affiliate_code", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "net", + "type_info": "Int8" + }, + { + "ordinal": 3, + "name": "affiliate_user_id", + "type_info": "Int8" + }, + { + "ordinal": 4, + "name": "revenue_split", + "type_info": "Float8" + } + ], + "parameters": { + "Left": [ + "Timestamptz", + "Timestamptz" + ] + }, + "nullable": [ + false, + true, + true, + false, + true + ] + }, + "hash": "d0fda4cc7d8562a9152afa29b3a4903293fc12d78db69808190f0e96a51e860e" +} diff --git a/apps/labrinth/.sqlx/query-9a35729acbba06eafaa205922e4987e082a000ec1b397957650e1332191613ca.json b/apps/labrinth/.sqlx/query-d853f30019b3afb62cf81c5c8f58f4bb03797e2dbf87f242af76e87af43ed1a9.json similarity index 90% rename from apps/labrinth/.sqlx/query-9a35729acbba06eafaa205922e4987e082a000ec1b397957650e1332191613ca.json rename to apps/labrinth/.sqlx/query-d853f30019b3afb62cf81c5c8f58f4bb03797e2dbf87f242af76e87af43ed1a9.json index 4659b4c794..bb8364970a 100644 --- a/apps/labrinth/.sqlx/query-9a35729acbba06eafaa205922e4987e082a000ec1b397957650e1332191613ca.json +++ b/apps/labrinth/.sqlx/query-d853f30019b3afb62cf81c5c8f58f4bb03797e2dbf87f242af76e87af43ed1a9.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\"\n FROM charges\n WHERE subscription_id = $1 AND (status = 'open' OR status = 'expiring' OR status = 'cancelled' OR status = 'failed')", + "query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\",\n\t\t\t\tcharges.affiliate_code AS \"affiliate_code?\"\n FROM charges\n WHERE id = $1", "describe": { "columns": [ { @@ -97,6 +97,11 @@ "ordinal": 18, "name": "tax_drift_loss?", "type_info": "Int8" + }, + { + "ordinal": 19, + "name": "affiliate_code?", + "type_info": "Int8" } ], "parameters": { @@ -123,8 +128,9 @@ true, true, true, + true, true ] }, - "hash": "9a35729acbba06eafaa205922e4987e082a000ec1b397957650e1332191613ca" + "hash": "d853f30019b3afb62cf81c5c8f58f4bb03797e2dbf87f242af76e87af43ed1a9" } diff --git a/apps/labrinth/.sqlx/query-e36e0ac1e2edb73533961a18e913f0b8e4f420a76e511571bb2eed9355771e54.json b/apps/labrinth/.sqlx/query-fb46fe2707ebcca811a530246e50c1a103b35f2e4ca224e1f3ef448a74531720.json similarity index 84% rename from apps/labrinth/.sqlx/query-e36e0ac1e2edb73533961a18e913f0b8e4f420a76e511571bb2eed9355771e54.json rename to apps/labrinth/.sqlx/query-fb46fe2707ebcca811a530246e50c1a103b35f2e4ca224e1f3ef448a74531720.json index 8434201121..175684fef9 100644 --- a/apps/labrinth/.sqlx/query-e36e0ac1e2edb73533961a18e913f0b8e4f420a76e511571bb2eed9355771e54.json +++ b/apps/labrinth/.sqlx/query-fb46fe2707ebcca811a530246e50c1a103b35f2e4ca224e1f3ef448a74531720.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\"\n FROM charges\n \n WHERE\n charge_type = $1 AND\n status = 'failed' AND due < NOW() - INTERVAL '30 days'\n ", + "query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\",\n\t\t\t\tcharges.affiliate_code AS \"affiliate_code?\"\n FROM charges\n \n WHERE\n charge_type = $1 AND\n (\n (status = 'open' AND due < NOW()) OR\n (status = 'failed' AND last_attempt < NOW() - INTERVAL '2 days')\n )\n ", "describe": { "columns": [ { @@ -97,6 +97,11 @@ "ordinal": 18, "name": "tax_drift_loss?", "type_info": "Int8" + }, + { + "ordinal": 19, + "name": "affiliate_code?", + "type_info": "Int8" } ], "parameters": { @@ -123,8 +128,9 @@ true, true, true, + true, true ] }, - "hash": "e36e0ac1e2edb73533961a18e913f0b8e4f420a76e511571bb2eed9355771e54" + "hash": "fb46fe2707ebcca811a530246e50c1a103b35f2e4ca224e1f3ef448a74531720" } diff --git a/apps/labrinth/.sqlx/query-4e8e9f9cb42f90cc17702386fdb78385608f19dae9439cb6a860503600127b04.json b/apps/labrinth/.sqlx/query-fffb8f9aeb39df168aaffe48c9054d7578d353459c5046e50ecd5e4aebce6705.json similarity index 89% rename from apps/labrinth/.sqlx/query-4e8e9f9cb42f90cc17702386fdb78385608f19dae9439cb6a860503600127b04.json rename to apps/labrinth/.sqlx/query-fffb8f9aeb39df168aaffe48c9054d7578d353459c5046e50ecd5e4aebce6705.json index 77bc16dade..a74eb48e75 100644 --- a/apps/labrinth/.sqlx/query-4e8e9f9cb42f90cc17702386fdb78385608f19dae9439cb6a860503600127b04.json +++ b/apps/labrinth/.sqlx/query-fffb8f9aeb39df168aaffe48c9054d7578d353459c5046e50ecd5e4aebce6705.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\"\n FROM charges\n WHERE id = $1", + "query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\",\n\t\t\t\tcharges.affiliate_code AS \"affiliate_code?\"\n FROM charges\n WHERE parent_charge_id = $1", "describe": { "columns": [ { @@ -97,6 +97,11 @@ "ordinal": 18, "name": "tax_drift_loss?", "type_info": "Int8" + }, + { + "ordinal": 19, + "name": "affiliate_code?", + "type_info": "Int8" } ], "parameters": { @@ -123,8 +128,9 @@ true, true, true, + true, true ] }, - "hash": "4e8e9f9cb42f90cc17702386fdb78385608f19dae9439cb6a860503600127b04" + "hash": "fffb8f9aeb39df168aaffe48c9054d7578d353459c5046e50ecd5e4aebce6705" } diff --git a/apps/labrinth/tests/affiliate_code_revenue.rs b/apps/labrinth/tests/affiliate_code_revenue.rs new file mode 100644 index 0000000000..b2a3a11024 --- /dev/null +++ b/apps/labrinth/tests/affiliate_code_revenue.rs @@ -0,0 +1,722 @@ +use chrono::{Datelike, Duration, TimeZone, Utc}; +use common::{ + api_v3::ApiV3, + environment::{TestEnvironment, with_test_environment}, +}; +use labrinth::database::models::{DBAffiliateCodeId, DBUserId}; +use labrinth::queue::affiliate_codes::process_affiliate_code_revenue; +use rust_decimal::dec; + +pub mod common; + +#[actix_rt::test] +pub async fn test_affiliate_code_revenue_processing_default_split() { + with_test_environment( + None, + |test_env: TestEnvironment| async move { + let pool = test_env.db.pool.clone(); + + // Create test users + let affiliate_user_id = DBUserId(1000); + let buyer_user_id = DBUserId(1001); + + // Create test users in the database + sqlx::query!( + r#" + INSERT INTO users (id, username, email, role) + VALUES + ($1, 'affiliate_user', 'affiliate@test.com', 'developer'), + ($2, 'buyer_user', 'buyer@test.com', 'developer') + "#, + affiliate_user_id.0, + buyer_user_id.0 + ) + .execute(&pool) + .await + .unwrap(); + + // Create a dummy product and price for charges to reference + sqlx::query!( + r#" + INSERT INTO products (id, metadata, unitary, name) + VALUES (9000, '{}', false, 'Test Product') + "# + ) + .execute(&pool) + .await + .unwrap(); + + sqlx::query!( + r#" + INSERT INTO products_prices (id, product_id, currency_code, prices, public) + VALUES (9000, 9000, 'USD', '[{"amount": 1000}]', true) + "# + ) + .execute(&pool) + .await + .unwrap(); + + // Create affiliate code with default split (NULL) + let affiliate_code_id = DBAffiliateCodeId(2000); + sqlx::query!( + r#" + INSERT INTO affiliate_codes (id, created_at, created_by, affiliate, revenue_split) + VALUES ($1, NOW(), $2, $2, NULL) + "#, + affiliate_code_id.0, + affiliate_user_id.0 + ) + .execute(&pool) + .await + .unwrap(); + + // Create test charges within the date range + let now = Utc::now(); + let charge_date = now - Duration::days(45); // Within the 30-day window (between 30-60 days ago) + + // Create multiple charges + let charges = [ + 1000, // $10.00 + 2000, // $20.00 + 500, // $5.00 + ]; + + for (i, amount_cents) in charges.iter().enumerate() { + sqlx::query!( + r#" + INSERT INTO charges (id, user_id, price_id, amount, currency_code, net, status, due, affiliate_code, charge_type, payment_platform, tax_amount) + VALUES ($1, $2, 9000, $3, 'USD', $3, 'succeeded', $4, $5, 'recurring', 'stripe', 0) + "#, + 3000 + i as i64, // unique ID + buyer_user_id.0, + amount_cents, + charge_date, + affiliate_code_id.0 + ) + .execute(&pool) + .await + .unwrap(); + } + + // Process affiliate code revenue + process_affiliate_code_revenue(&pool).await.unwrap(); + + + // Verify payouts were created + let payouts = sqlx::query!( + r#" + SELECT user_id, amount, created, date_available, affiliate_code_id + FROM payouts_values + WHERE user_id = $1 AND affiliate_code_id = $2 + ORDER BY created DESC + "#, + affiliate_user_id.0, + affiliate_code_id.0 + ) + .fetch_all(&pool) + .await + .unwrap(); + + // Should have one payout entry with 10% of total ($3.50) + assert_eq!(payouts.len(), 1); + assert_eq!(payouts[0].user_id, affiliate_user_id.0); + assert_eq!(payouts[0].affiliate_code_id, Some(affiliate_code_id.0)); + + // Expected payout: $35.00 * 10% = $3.50 + let payout_amount = payouts[0].amount; + assert_eq!(payout_amount, dec!(3.50)); + + // Verify availability date is Net 30 + let expected_available = { + let processing_month = (now - Duration::days(30)).date_naive(); + let year = processing_month.year(); + let month = processing_month.month(); + + let first_of_next_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() + }; + + first_of_next_month + Duration::days(29) + }; + + // Allow small time differences due to test execution + let time_diff = (payouts[0].date_available - expected_available).num_seconds(); + assert!(time_diff.abs() < 60); + }, + ) + .await; +} + +#[actix_rt::test] +pub async fn test_affiliate_code_revenue_processing_custom_split() { + with_test_environment( + None, + |test_env: TestEnvironment| async move { + let pool = test_env.db.pool.clone(); + + // Create test users + let affiliate_user_id = DBUserId(2000); + let buyer_user_id = DBUserId(2001); + + // Create test users in the database + sqlx::query!( + r#" + INSERT INTO users (id, username, email, role) + VALUES + ($1, 'affiliate_user2', 'affiliate2@test.com', 'developer'), + ($2, 'buyer_user2', 'buyer2@test.com', 'developer') + "#, + affiliate_user_id.0, + buyer_user_id.0 + ) + .execute(&pool) + .await + .unwrap(); + + // Create a dummy product and price for charges to reference + sqlx::query!( + r#" + INSERT INTO products (id, metadata, unitary, name) + VALUES (9001, '{}', false, 'Test Product') + "# + ) + .execute(&pool) + .await + .unwrap(); + + sqlx::query!( + r#" + INSERT INTO products_prices (id, product_id, currency_code, prices, public) + VALUES (9001, 9001, 'USD', '[{"amount": 1000}]', true) + "# + ) + .execute(&pool) + .await + .unwrap(); + + // Create affiliate code with custom 25% split + let affiliate_code_id = DBAffiliateCodeId(2001); + sqlx::query!( + r#" + INSERT INTO affiliate_codes (id, created_at, created_by, affiliate, revenue_split) + VALUES ($1, NOW(), $2, $2, 0.25) + "#, + affiliate_code_id.0, + affiliate_user_id.0 + ) + .execute(&pool) + .await + .unwrap(); + + // Create test charges + let now = Utc::now(); + let charge_date = now - Duration::days(45); + + let charge_amount = 1000; // $10.00 + + sqlx::query!( + r#" + INSERT INTO charges (id, user_id, price_id, amount, currency_code, net, status, due, affiliate_code, charge_type, payment_platform, tax_amount) + VALUES ($1, $2, 9001, $3, 'USD', $3, 'succeeded', $4, $5, 'recurring', 'stripe', 0) + "#, + 4000, // unique ID + buyer_user_id.0, + charge_amount, + charge_date, + affiliate_code_id.0 + ) + .execute(&pool) + .await + .unwrap(); + + // Process affiliate code revenue + process_affiliate_code_revenue(&pool).await.unwrap(); + + // Verify payout was created with custom split + let payouts = sqlx::query!( + r#" + SELECT user_id, amount, created, date_available, affiliate_code_id + FROM payouts_values + WHERE user_id = $1 AND affiliate_code_id = $2 + "#, + affiliate_user_id.0, + affiliate_code_id.0 + ) + .fetch_all(&pool) + .await + .unwrap(); + + assert_eq!(payouts.len(), 1); + + // Expected payout: $10.00 * 25% = $2.50 + let payout_amount = payouts[0].amount; + assert_eq!(payout_amount, dec!(2.50)); + }, + ) + .await; +} + +#[actix_rt::test] +pub async fn test_affiliate_code_revenue_processing_invalid_split() { + with_test_environment( + None, + |test_env: TestEnvironment| async move { + let pool = test_env.db.pool.clone(); + + // Create test users + let affiliate_user_id = DBUserId(3000); + let buyer_user_id = DBUserId(3001); + + // Create test users in the database + sqlx::query!( + r#" + INSERT INTO users (id, username, email, role) + VALUES + ($1, 'affiliate_user3', 'affiliate3@test.com', 'developer'), + ($2, 'buyer_user3', 'buyer3@test.com', 'developer') + "#, + affiliate_user_id.0, + buyer_user_id.0 + ) + .execute(&pool) + .await + .unwrap(); + + // Create a dummy product and price for charges to reference + sqlx::query!( + r#" + INSERT INTO products (id, metadata, unitary, name) + VALUES (9002, '{}', false, 'Test Product') + "# + ) + .execute(&pool) + .await + .unwrap(); + + sqlx::query!( + r#" + INSERT INTO products_prices (id, product_id, currency_code, prices, public) + VALUES (9002, 9002, 'USD', '[{"amount": 1000}]', true) + "# + ) + .execute(&pool) + .await + .unwrap(); + + // Create affiliate code with invalid split (150%) + let affiliate_code_id = DBAffiliateCodeId(2002); + sqlx::query!( + r#" + INSERT INTO affiliate_codes (id, created_at, created_by, affiliate, revenue_split) + VALUES ($1, NOW(), $2, $2, 1.5) + "#, + affiliate_code_id.0, + affiliate_user_id.0 + ) + .execute(&pool) + .await + .unwrap(); + + // Create test charge + let now = Utc::now(); + let charge_date = now - Duration::days(45); + + sqlx::query!( + r#" + INSERT INTO charges (id, user_id, price_id, amount, currency_code, net, status, due, affiliate_code, charge_type, payment_platform, tax_amount) + VALUES ($1, $2, 9002, $3, 'USD', $3, 'succeeded', $4, $5, 'recurring', 'stripe', 0) + "#, + 5000, // unique ID + buyer_user_id.0, + 1000, // $10.00 + charge_date, + affiliate_code_id.0 + ) + .execute(&pool) + .await + .unwrap(); + + // Process affiliate code revenue - should not create payout due to invalid split + process_affiliate_code_revenue(&pool).await.unwrap(); + + // Verify no payout was created + let payouts = sqlx::query!( + r#" + SELECT user_id, amount, created, date_available, affiliate_code_id + FROM payouts_values + WHERE user_id = $1 AND affiliate_code_id = $2 + "#, + affiliate_user_id.0, + affiliate_code_id.0 + ) + .fetch_all(&pool) + .await + .unwrap(); + + assert_eq!(payouts.len(), 0); + }, + ) + .await; +} + +#[actix_rt::test] +pub async fn test_affiliate_code_revenue_processing_outside_date_range() { + with_test_environment( + None, + |test_env: TestEnvironment| async move { + let pool = test_env.db.pool.clone(); + + // Create test users + let affiliate_user_id = DBUserId(4000); + let buyer_user_id = DBUserId(4001); + + // Create test users in the database + sqlx::query!( + r#" + INSERT INTO users (id, username, email, role) + VALUES + ($1, 'affiliate_user4', 'affiliate4@test.com', 'developer'), + ($2, 'buyer_user4', 'buyer4@test.com', 'developer') + "#, + affiliate_user_id.0, + buyer_user_id.0 + ) + .execute(&pool) + .await + .unwrap(); + + // Create a dummy product and price for charges to reference + sqlx::query!( + r#" + INSERT INTO products (id, metadata, unitary, name) + VALUES (9003, '{}', false, 'Test Product') + "# + ) + .execute(&pool) + .await + .unwrap(); + + sqlx::query!( + r#" + INSERT INTO products_prices (id, product_id, currency_code, prices, public) + VALUES (9003, 9003, 'USD', '[{"amount": 1000}]', true) + "# + ) + .execute(&pool) + .await + .unwrap(); + + // Create affiliate code + let affiliate_code_id = DBAffiliateCodeId(2003); + sqlx::query!( + r#" + INSERT INTO affiliate_codes (id, created_at, created_by, affiliate, revenue_split) + VALUES ($1, NOW(), $2, $2, NULL) + "#, + affiliate_code_id.0, + affiliate_user_id.0 + ) + .execute(&pool) + .await + .unwrap(); + + // Create test charge outside the date range (more than 60 days ago) + let old_charge_date = Utc::now() - Duration::days(90); + + sqlx::query!( + r#" + INSERT INTO charges (id, user_id, price_id, amount, currency_code, net, status, due, affiliate_code, charge_type, payment_platform, tax_amount) + VALUES ($1, $2, 9003, $3, 'USD', $3, 'succeeded', $4, $5, 'recurring', 'stripe', 0) + "#, + 6000, // unique ID + buyer_user_id.0, + 1000, // $10.00 + old_charge_date, + affiliate_code_id.0 + ) + .execute(&pool) + .await + .unwrap(); + + // Process affiliate code revenue + process_affiliate_code_revenue(&pool).await.unwrap(); + + // Verify no payout was created (charge is outside date range) + let payouts = sqlx::query!( + r#" + SELECT user_id, amount, created, date_available, affiliate_code_id + FROM payouts_values + WHERE user_id = $1 AND affiliate_code_id = $2 + "#, + affiliate_user_id.0, + affiliate_code_id.0 + ) + .fetch_all(&pool) + .await + .unwrap(); + + assert_eq!(payouts.len(), 0); + }, + ) + .await; +} + +#[actix_rt::test] +pub async fn test_affiliate_code_revenue_processing_failed_charges() { + with_test_environment( + None, + |test_env: TestEnvironment| async move { + let pool = test_env.db.pool.clone(); + + // Create test users + let affiliate_user_id = DBUserId(5000); + let buyer_user_id = DBUserId(5001); + + // Create test users in the database + sqlx::query!( + r#" + INSERT INTO users (id, username, email, role) + VALUES + ($1, 'affiliate_user5', 'affiliate5@test.com', 'developer'), + ($2, 'buyer_user5', 'buyer5@test.com', 'developer') + "#, + affiliate_user_id.0, + buyer_user_id.0 + ) + .execute(&pool) + .await + .unwrap(); + + // Create a dummy product and price for charges to reference + sqlx::query!( + r#" + INSERT INTO products (id, metadata, unitary, name) + VALUES (9004, '{}', false, 'Test Product') + "# + ) + .execute(&pool) + .await + .unwrap(); + + sqlx::query!( + r#" + INSERT INTO products_prices (id, product_id, currency_code, prices, public) + VALUES (9004, 9004, 'USD', '[{"amount": 1000}]', true) + "# + ) + .execute(&pool) + .await + .unwrap(); + + // Create affiliate code + let affiliate_code_id = DBAffiliateCodeId(2004); + sqlx::query!( + r#" + INSERT INTO affiliate_codes (id, created_at, created_by, affiliate, revenue_split) + VALUES ($1, NOW(), $2, $2, NULL) + "#, + affiliate_code_id.0, + affiliate_user_id.0 + ) + .execute(&pool) + .await + .unwrap(); + + // Create test charge with failed status + let now = Utc::now(); + let charge_date = now - Duration::days(45); + + sqlx::query!( + r#" + INSERT INTO charges (id, user_id, price_id, amount, currency_code, net, status, due, affiliate_code, charge_type, payment_platform, tax_amount) + VALUES ($1, $2, 9004, $3, 'USD', $3, 'failed', $4, $5, 'recurring', 'stripe', 0) + "#, + 7000, // unique ID + buyer_user_id.0, + 1000, // $10.00 + charge_date, + affiliate_code_id.0 + ) + .execute(&pool) + .await + .unwrap(); + + // Process affiliate code revenue + process_affiliate_code_revenue(&pool).await.unwrap(); + + // Verify no payout was created (charge failed) + let payouts = sqlx::query!( + r#" + SELECT user_id, amount, created, date_available, affiliate_code_id + FROM payouts_values + WHERE user_id = $1 AND affiliate_code_id = $2 + "#, + affiliate_user_id.0, + affiliate_code_id.0 + ) + .fetch_all(&pool) + .await + .unwrap(); + + assert_eq!(payouts.len(), 0); + }, + ) + .await; +} + +#[actix_rt::test] +pub async fn test_affiliate_code_revenue_processing_multiple_affiliate_codes() { + with_test_environment( + None, + |test_env: TestEnvironment| async move { + let pool = test_env.db.pool.clone(); + + // Create test users + let affiliate_user_id_1 = DBUserId(6000); + let affiliate_user_id_2 = DBUserId(6001); + let buyer_user_id = DBUserId(6002); + + // Create test users in the database + sqlx::query!( + r#" + INSERT INTO users (id, username, email, role) + VALUES + ($1, 'affiliate_user6', 'affiliate6@test.com', 'developer'), + ($2, 'affiliate_user7', 'affiliate7@test.com', 'developer'), + ($3, 'buyer_user6', 'buyer6@test.com', 'developer') + "#, + affiliate_user_id_1.0, + affiliate_user_id_2.0, + buyer_user_id.0 + ) + .execute(&pool) + .await + .unwrap(); + + // Create dummy products and prices for charges to reference + sqlx::query!( + r#" + INSERT INTO products (id, metadata, unitary, name) + VALUES + (9005, '{}', false, 'Test Product'), + (9006, '{}', false, 'Test Product') + "# + ) + .execute(&pool) + .await + .unwrap(); + + sqlx::query!( + r#" + INSERT INTO products_prices (id, product_id, currency_code, prices, public) + VALUES + (9005, 9005, 'USD', '[{"amount": 1000}]', true), + (9006, 9006, 'USD', '[{"amount": 1000}]', true) + "# + ) + .execute(&pool) + .await + .unwrap(); + + // Create two affiliate codes for different users + let affiliate_code_id_1 = DBAffiliateCodeId(2005); + let affiliate_code_id_2 = DBAffiliateCodeId(2006); + + sqlx::query!( + r#" + INSERT INTO affiliate_codes (id, created_at, created_by, affiliate, revenue_split) + VALUES + ($1, NOW(), $2, $2, NULL), + ($3, NOW(), $4, $4, 0.2) + "#, + affiliate_code_id_1.0, + affiliate_user_id_1.0, + affiliate_code_id_2.0, + affiliate_user_id_2.0 + ) + .execute(&pool) + .await + .unwrap(); + + // Create test charges for each affiliate code + let now = Utc::now(); + let charge_date = now - Duration::days(45); + + // Charge for affiliate 1: $20.00 + sqlx::query!( + r#" + INSERT INTO charges (id, user_id, price_id, amount, currency_code, net, status, due, affiliate_code, charge_type, payment_platform, tax_amount) + VALUES ($1, $2, 9005, $3, 'USD', $3, 'succeeded', $4, $5, 'recurring', 'stripe', 0) + "#, + 8000, // unique ID + buyer_user_id.0, + 2000, // $20.00 + charge_date, + affiliate_code_id_1.0 + ) + .execute(&pool) + .await + .unwrap(); + + // Charge for affiliate 2: $15.00 + sqlx::query!( + r#" + INSERT INTO charges (id, user_id, price_id, amount, currency_code, net, status, due, affiliate_code, charge_type, payment_platform, tax_amount) + VALUES ($1, $2, 9006, $3, 'USD', $3, 'succeeded', $4, $5, 'recurring', 'stripe', 0) + "#, + 9000, // unique ID + buyer_user_id.0, + 1500, // $15.00 + charge_date, + affiliate_code_id_2.0 + ) + .execute(&pool) + .await + .unwrap(); + + // Process affiliate code revenue + process_affiliate_code_revenue(&pool).await.unwrap(); + + // Verify payouts were created for both affiliates + let payouts_1 = sqlx::query!( + r#" + SELECT user_id, amount, created, date_available, affiliate_code_id + FROM payouts_values + WHERE user_id = $1 AND affiliate_code_id = $2 + "#, + affiliate_user_id_1.0, + affiliate_code_id_1.0 + ) + .fetch_all(&pool) + .await + .unwrap(); + + let payouts_2 = sqlx::query!( + r#" + SELECT user_id, amount, created, date_available, affiliate_code_id + FROM payouts_values + WHERE user_id = $1 AND affiliate_code_id = $2 + "#, + affiliate_user_id_2.0, + affiliate_code_id_2.0 + ) + .fetch_all(&pool) + .await + .unwrap(); + + // Affiliate 1: $20.00 * 10% = $2.00 + assert_eq!(payouts_1.len(), 1); + let payout_amount_1 = payouts_1[0].amount; + assert_eq!(payout_amount_1, dec!(2.00)); + + // Affiliate 2: $15.00 * 20% = $3.00 (with potential floating point precision) + assert_eq!(payouts_2.len(), 1); + let payout_amount_2 = payouts_2[0].amount; + // The actual calculation may have tiny precision differences due to f64 -> Decimal conversion + assert!(payout_amount_2 > dec!(2.99) && payout_amount_2 < dec!(3.01)); + }, + ) + .await; +}