From 9187673748e95846e7a7f202123bac272e665fb1 Mon Sep 17 00:00:00 2001 From: Emma Alexia Date: Sat, 24 May 2025 11:10:27 -0400 Subject: [PATCH] Automatically cancel servers with failed payments older than 30d --- ...f4c367f03f076e5e264ec8f5e744877c6d362.json | 106 ++++++++++++++++++ .../src/database/models/charge_item.rs | 21 ++++ apps/labrinth/src/routes/internal/billing.rs | 17 ++- 3 files changed, 139 insertions(+), 5 deletions(-) create mode 100644 apps/labrinth/.sqlx/query-d4d17b6a06c2f607206373b18a1f4c367f03f076e5e264ec8f5e744877c6d362.json diff --git a/apps/labrinth/.sqlx/query-d4d17b6a06c2f607206373b18a1f4c367f03f076e5e264ec8f5e744877c6d362.json b/apps/labrinth/.sqlx/query-d4d17b6a06c2f607206373b18a1f4c367f03f076e5e264ec8f5e744877c6d362.json new file mode 100644 index 000000000..677dd8116 --- /dev/null +++ b/apps/labrinth/.sqlx/query-d4d17b6a06c2f607206373b18a1f4c367f03f076e5e264ec8f5e744877c6d362.json @@ -0,0 +1,106 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n id, user_id, price_id, amount, currency_code, status, due, last_attempt,\n charge_type, subscription_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n subscription_interval AS \"subscription_interval?\",\n payment_platform,\n payment_platform_id AS \"payment_platform_id?\",\n parent_charge_id AS \"parent_charge_id?\",\n net AS \"net?\"\n FROM charges\n \n WHERE\n charge_type = $1 AND\n status = 'failed' AND due < NOW() - INTERVAL '30 days'\n ", + "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": "subscription_interval?", + "type_info": "Text" + }, + { + "ordinal": 11, + "name": "payment_platform", + "type_info": "Text" + }, + { + "ordinal": 12, + "name": "payment_platform_id?", + "type_info": "Text" + }, + { + "ordinal": 13, + "name": "parent_charge_id?", + "type_info": "Int8" + }, + { + "ordinal": 14, + "name": "net?", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + true, + false, + true, + true, + false, + true, + true, + true + ] + }, + "hash": "d4d17b6a06c2f607206373b18a1f4c367f03f076e5e264ec8f5e744877c6d362" +} diff --git a/apps/labrinth/src/database/models/charge_item.rs b/apps/labrinth/src/database/models/charge_item.rs index 31253fa49..5acf4c69d 100644 --- a/apps/labrinth/src/database/models/charge_item.rs +++ b/apps/labrinth/src/database/models/charge_item.rs @@ -254,6 +254,27 @@ impl DBCharge { .collect::, serde_json::Error>>()?) } + pub async fn get_cancellable( + exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>, + ) -> Result, DatabaseError> { + let charge_type = ChargeType::Subscription.as_str(); + let res = select_charges_with_predicate!( + r#" + WHERE + charge_type = $1 AND + status = 'failed' AND due < NOW() - INTERVAL '30 days' + "#, + charge_type + ) + .fetch_all(exec) + .await?; + + Ok(res + .into_iter() + .map(|r| r.try_into()) + .collect::, serde_json::Error>>()?) + } + pub async fn remove( id: DBChargeId, transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, diff --git a/apps/labrinth/src/routes/internal/billing.rs b/apps/labrinth/src/routes/internal/billing.rs index 344fafa40..f2bfe5841 100644 --- a/apps/labrinth/src/routes/internal/billing.rs +++ b/apps/labrinth/src/routes/internal/billing.rs @@ -2284,12 +2284,19 @@ pub async fn index_billing( ) { info!("Indexing billing queue"); let res = async { + // If a charge has continuously failed for more than a month, it should be cancelled + let charges_to_cancel = DBCharge::get_cancellable(&pool).await?; + + for mut charge in charges_to_cancel { + charge.status = ChargeStatus::Cancelled; + + let mut transaction = pool.begin().await?; + charge.upsert(&mut transaction).await?; + transaction.commit().await?; + } + // If a charge is open and due or has been attempted more than two days ago, it should be processed - let charges_to_do = - crate::database::models::charge_item::DBCharge::get_chargeable( - &pool, - ) - .await?; + let charges_to_do = DBCharge::get_chargeable(&pool).await?; let prices = product_item::DBProductPrice::get_many( &charges_to_do