Automatically cancel servers with failed payments older than 30d (#3695)

This commit is contained in:
Emma Alexia 2025-05-25 15:36:11 -04:00 committed by GitHub
parent 2ffd7476aa
commit dc0d923cee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 139 additions and 5 deletions

View File

@ -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"
}

View File

@ -254,6 +254,27 @@ impl DBCharge {
.collect::<Result<Vec<_>, serde_json::Error>>()?)
}
pub async fn get_cancellable(
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
) -> Result<Vec<DBCharge>, 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::<Result<Vec<_>, serde_json::Error>>()?)
}
pub async fn remove(
id: DBChargeId,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,

View File

@ -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