You've already forked AstralRinth
forked from didirus/AstralRinth
This allows people to cancel failed payments, currently it fails with error "There is no open charge for this subscription"
295 lines
9.3 KiB
Rust
295 lines
9.3 KiB
Rust
use crate::database::models::{
|
|
DBChargeId, DBProductPriceId, DBUserId, DBUserSubscriptionId, DatabaseError,
|
|
};
|
|
use crate::models::billing::{
|
|
ChargeStatus, ChargeType, PaymentPlatform, PriceDuration,
|
|
};
|
|
use chrono::{DateTime, Utc};
|
|
use std::convert::{TryFrom, TryInto};
|
|
|
|
pub struct DBCharge {
|
|
pub id: DBChargeId,
|
|
pub user_id: DBUserId,
|
|
pub price_id: DBProductPriceId,
|
|
pub amount: i64,
|
|
pub currency_code: String,
|
|
pub status: ChargeStatus,
|
|
pub due: DateTime<Utc>,
|
|
pub last_attempt: Option<DateTime<Utc>>,
|
|
|
|
pub type_: ChargeType,
|
|
pub subscription_id: Option<DBUserSubscriptionId>,
|
|
pub subscription_interval: Option<PriceDuration>,
|
|
|
|
pub payment_platform: PaymentPlatform,
|
|
pub payment_platform_id: Option<String>,
|
|
|
|
pub parent_charge_id: Option<DBChargeId>,
|
|
|
|
// Net is always in USD
|
|
pub net: Option<i64>,
|
|
}
|
|
|
|
struct ChargeQueryResult {
|
|
id: i64,
|
|
user_id: i64,
|
|
price_id: i64,
|
|
amount: i64,
|
|
currency_code: String,
|
|
status: String,
|
|
due: DateTime<Utc>,
|
|
last_attempt: Option<DateTime<Utc>>,
|
|
charge_type: String,
|
|
subscription_id: Option<i64>,
|
|
subscription_interval: Option<String>,
|
|
payment_platform: String,
|
|
payment_platform_id: Option<String>,
|
|
parent_charge_id: Option<i64>,
|
|
net: Option<i64>,
|
|
}
|
|
|
|
impl TryFrom<ChargeQueryResult> for DBCharge {
|
|
type Error = serde_json::Error;
|
|
|
|
fn try_from(r: ChargeQueryResult) -> Result<Self, Self::Error> {
|
|
Ok(DBCharge {
|
|
id: DBChargeId(r.id),
|
|
user_id: DBUserId(r.user_id),
|
|
price_id: DBProductPriceId(r.price_id),
|
|
amount: r.amount,
|
|
currency_code: r.currency_code,
|
|
status: ChargeStatus::from_string(&r.status),
|
|
due: r.due,
|
|
last_attempt: r.last_attempt,
|
|
type_: ChargeType::from_string(&r.charge_type),
|
|
subscription_id: r.subscription_id.map(DBUserSubscriptionId),
|
|
subscription_interval: r
|
|
.subscription_interval
|
|
.map(|x| PriceDuration::from_string(&x)),
|
|
payment_platform: PaymentPlatform::from_string(&r.payment_platform),
|
|
payment_platform_id: r.payment_platform_id,
|
|
parent_charge_id: r.parent_charge_id.map(DBChargeId),
|
|
net: r.net,
|
|
})
|
|
}
|
|
}
|
|
|
|
macro_rules! select_charges_with_predicate {
|
|
($predicate:tt, $param:ident) => {
|
|
sqlx::query_as!(
|
|
ChargeQueryResult,
|
|
r#"
|
|
SELECT
|
|
id, user_id, price_id, amount, currency_code, status, due, last_attempt,
|
|
charge_type, subscription_id,
|
|
-- Workaround for https://github.com/launchbadge/sqlx/issues/3336
|
|
subscription_interval AS "subscription_interval?",
|
|
payment_platform,
|
|
payment_platform_id AS "payment_platform_id?",
|
|
parent_charge_id AS "parent_charge_id?",
|
|
net AS "net?"
|
|
FROM charges
|
|
"#
|
|
+ $predicate,
|
|
$param
|
|
)
|
|
};
|
|
}
|
|
|
|
impl DBCharge {
|
|
pub async fn upsert(
|
|
&self,
|
|
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
|
) -> Result<DBChargeId, DatabaseError> {
|
|
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)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
|
|
ON CONFLICT (id)
|
|
DO UPDATE
|
|
SET status = EXCLUDED.status,
|
|
last_attempt = EXCLUDED.last_attempt,
|
|
due = EXCLUDED.due,
|
|
subscription_id = EXCLUDED.subscription_id,
|
|
subscription_interval = EXCLUDED.subscription_interval,
|
|
payment_platform = EXCLUDED.payment_platform,
|
|
payment_platform_id = EXCLUDED.payment_platform_id,
|
|
parent_charge_id = EXCLUDED.parent_charge_id,
|
|
net = EXCLUDED.net,
|
|
price_id = EXCLUDED.price_id,
|
|
amount = EXCLUDED.amount,
|
|
currency_code = EXCLUDED.currency_code,
|
|
charge_type = EXCLUDED.charge_type
|
|
"#,
|
|
self.id.0,
|
|
self.user_id.0,
|
|
self.price_id.0,
|
|
self.amount,
|
|
self.currency_code,
|
|
self.type_.as_str(),
|
|
self.status.as_str(),
|
|
self.due,
|
|
self.last_attempt,
|
|
self.subscription_id.map(|x| x.0),
|
|
self.subscription_interval.map(|x| x.as_str()),
|
|
self.payment_platform.as_str(),
|
|
self.payment_platform_id.as_deref(),
|
|
self.parent_charge_id.map(|x| x.0),
|
|
self.net,
|
|
)
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
|
|
Ok(self.id)
|
|
}
|
|
|
|
pub async fn get(
|
|
id: DBChargeId,
|
|
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
|
|
) -> Result<Option<DBCharge>, DatabaseError> {
|
|
let id = id.0;
|
|
let res = select_charges_with_predicate!("WHERE id = $1", id)
|
|
.fetch_optional(exec)
|
|
.await?;
|
|
|
|
Ok(res.and_then(|r| r.try_into().ok()))
|
|
}
|
|
|
|
pub async fn get_from_user(
|
|
user_id: DBUserId,
|
|
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
|
|
) -> Result<Vec<DBCharge>, DatabaseError> {
|
|
let user_id = user_id.0;
|
|
let res = select_charges_with_predicate!(
|
|
"WHERE user_id = $1 ORDER BY due DESC",
|
|
user_id
|
|
)
|
|
.fetch_all(exec)
|
|
.await?;
|
|
|
|
Ok(res
|
|
.into_iter()
|
|
.map(|r| r.try_into())
|
|
.collect::<Result<Vec<_>, serde_json::Error>>()?)
|
|
}
|
|
|
|
pub async fn get_children(
|
|
charge_id: DBChargeId,
|
|
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
|
|
) -> Result<Vec<DBCharge>, DatabaseError> {
|
|
let charge_id = charge_id.0;
|
|
let res = select_charges_with_predicate!(
|
|
"WHERE parent_charge_id = $1",
|
|
charge_id
|
|
)
|
|
.fetch_all(exec)
|
|
.await?;
|
|
|
|
Ok(res
|
|
.into_iter()
|
|
.map(|r| r.try_into())
|
|
.collect::<Result<Vec<_>, serde_json::Error>>()?)
|
|
}
|
|
|
|
pub async fn get_open_subscription(
|
|
user_subscription_id: DBUserSubscriptionId,
|
|
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
|
|
) -> 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 = 'cancelled' OR status = 'failed')",
|
|
user_subscription_id
|
|
)
|
|
.fetch_optional(exec)
|
|
.await?;
|
|
|
|
Ok(res.and_then(|r| r.try_into().ok()))
|
|
}
|
|
|
|
pub async fn get_chargeable(
|
|
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 = 'open' AND due < NOW()) OR
|
|
(status = 'failed' AND last_attempt < NOW() - INTERVAL '2 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 get_unprovision(
|
|
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 = 'cancelled' AND due < NOW()) OR
|
|
(status = 'failed' AND last_attempt < NOW() - INTERVAL '2 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 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>,
|
|
) -> Result<(), DatabaseError> {
|
|
sqlx::query!(
|
|
"
|
|
DELETE FROM charges
|
|
WHERE id = $1
|
|
",
|
|
id.0 as i64
|
|
)
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|