Credit subscriptions (#4575)

* Implement subscription crediting

* chore: query cache, clippy, fmt

* Improve code, improve query for next open charge

* chore: query cache, clippy, fmt

* Move server ID copy button up

* Node + region crediting

* Make it less ugly

* chore: query cache, clippy, fmt

* Bugfixes

* Fix lint

* Adjust migration

* Adjust migration

* Remove billing change

* Move DEFAULT_CREDIT_EMAIL_MESSAGE to utils.ts

* Lint

* Merge

* bump clickhouse, disable validation

* tombi fmt

* Update cargo lock
This commit is contained in:
François-Xavier Talbot
2025-10-20 18:35:44 +01:00
committed by GitHub
parent 79502a19d6
commit eeed4e572d
22 changed files with 1052 additions and 8 deletions

View File

@@ -233,7 +233,10 @@ impl DBCharge {
) -> Result<Option<DBCharge>, DatabaseError> {
let user_subscription_id = user_subscription_id.0;
let res = select_charges_with_predicate!(
"WHERE subscription_id = $1 AND (status = 'open' OR status = 'expiring' OR status = 'cancelled' OR status = 'failed')",
"WHERE
subscription_id = $1
AND (status = 'open' OR status = 'expiring' OR status = 'cancelled' OR status = 'failed')
ORDER BY due ASC LIMIT 1",
user_subscription_id
)
.fetch_optional(exec)

View File

@@ -35,6 +35,7 @@ pub mod user_subscription_item;
pub mod users_compliance;
pub mod users_notifications_preferences_item;
pub mod users_redeemals;
pub mod users_subscriptions_credits;
pub mod version_item;
pub use affiliate_code_item::DBAffiliateCode;

View File

@@ -160,6 +160,32 @@ impl DBUserSubscription {
Ok(())
}
pub async fn get_many_by_server_ids(
server_ids: &[String],
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
) -> Result<Vec<DBUserSubscription>, DatabaseError> {
if server_ids.is_empty() {
return Ok(vec![]);
}
let results = sqlx::query_as!(
UserSubscriptionQueryResult,
r#"
SELECT us.id, us.user_id, us.price_id, us.interval, us.created, us.status, us.metadata
FROM users_subscriptions us
WHERE us.metadata->>'type' = 'pyro' AND us.metadata->>'id' = ANY($1::text[])
"#,
server_ids
)
.fetch_all(exec)
.await?;
Ok(results
.into_iter()
.map(|r| r.try_into())
.collect::<Result<Vec<_>, serde_json::Error>>()?)
}
}
pub struct SubscriptionWithCharge {

View File

@@ -0,0 +1,82 @@
use crate::database::models::{DBUserId, DBUserSubscriptionId};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::query_scalar;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DBUserSubscriptionCredit {
pub id: i32,
pub subscription_id: DBUserSubscriptionId,
pub user_id: DBUserId,
pub creditor_id: DBUserId,
pub days: i32,
pub previous_due: DateTime<Utc>,
pub next_due: DateTime<Utc>,
pub created: DateTime<Utc>,
}
impl DBUserSubscriptionCredit {
/// Inserts this audit entry and sets its id.
pub async fn insert<'a, E>(&mut self, exec: E) -> sqlx::Result<()>
where
E: sqlx::PgExecutor<'a>,
{
let id = query_scalar!(
r#"
INSERT INTO users_subscriptions_credits
(subscription_id, user_id, creditor_id, days, previous_due, next_due)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id
"#,
self.subscription_id.0,
self.user_id.0,
self.creditor_id.0,
self.days,
self.previous_due,
self.next_due,
)
.fetch_one(exec)
.await?;
self.id = id;
Ok(())
}
pub async fn insert_many(
exec: &mut sqlx::Transaction<'_, sqlx::Postgres>,
subscription_ids: &[DBUserSubscriptionId],
user_ids: &[DBUserId],
creditor_ids: &[DBUserId],
days: &[i32],
previous_dues: &[DateTime<Utc>],
next_dues: &[DateTime<Utc>],
) -> sqlx::Result<()> {
debug_assert_eq!(subscription_ids.len(), user_ids.len());
debug_assert_eq!(user_ids.len(), creditor_ids.len());
debug_assert_eq!(creditor_ids.len(), days.len());
debug_assert_eq!(days.len(), previous_dues.len());
debug_assert_eq!(previous_dues.len(), next_dues.len());
let subs: Vec<i64> = subscription_ids.iter().map(|x| x.0).collect();
let users: Vec<i64> = user_ids.iter().map(|x| x.0).collect();
let creditors: Vec<i64> = creditor_ids.iter().map(|x| x.0).collect();
sqlx::query!(
r#"
INSERT INTO users_subscriptions_credits
(subscription_id, user_id, creditor_id, days, previous_due, next_due)
SELECT * FROM UNNEST($1::bigint[], $2::bigint[], $3::bigint[], $4::int[], $5::timestamptz[], $6::timestamptz[])
"#,
&subs[..],
&users[..],
&creditors[..],
&days[..],
&previous_dues[..],
&next_dues[..],
)
.execute(&mut **exec)
.await?;
Ok(())
}
}