You've already forked AstralRinth
forked from didirus/AstralRinth
Move charges to DB + fix subscription recurring payments (#971)
* Move charges to DB + fix subscription recurring payments * Finish most + pyro integration * Finish billing * Run prepare * Fix intervals * Fix clippy * Remove unused test
This commit is contained in:
181
src/database/models/charge_item.rs
Normal file
181
src/database/models/charge_item.rs
Normal file
@@ -0,0 +1,181 @@
|
||||
use crate::database::models::{
|
||||
ChargeId, DatabaseError, ProductPriceId, UserId, UserSubscriptionId,
|
||||
};
|
||||
use crate::models::billing::{ChargeStatus, ChargeType, PriceDuration};
|
||||
use chrono::{DateTime, Utc};
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
|
||||
pub struct ChargeItem {
|
||||
pub id: ChargeId,
|
||||
pub user_id: UserId,
|
||||
pub price_id: ProductPriceId,
|
||||
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<UserSubscriptionId>,
|
||||
pub subscription_interval: Option<PriceDuration>,
|
||||
}
|
||||
|
||||
struct ChargeResult {
|
||||
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>,
|
||||
}
|
||||
|
||||
impl TryFrom<ChargeResult> for ChargeItem {
|
||||
type Error = serde_json::Error;
|
||||
|
||||
fn try_from(r: ChargeResult) -> Result<Self, Self::Error> {
|
||||
Ok(ChargeItem {
|
||||
id: ChargeId(r.id),
|
||||
user_id: UserId(r.user_id),
|
||||
price_id: ProductPriceId(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(UserSubscriptionId),
|
||||
subscription_interval: r
|
||||
.subscription_interval
|
||||
.map(|x| PriceDuration::from_string(&x)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! select_charges_with_predicate {
|
||||
($predicate:tt, $param:ident) => {
|
||||
sqlx::query_as!(
|
||||
ChargeResult,
|
||||
r#"
|
||||
SELECT id, user_id, price_id, amount, currency_code, status, due, last_attempt, charge_type, subscription_id, subscription_interval
|
||||
FROM charges
|
||||
"#
|
||||
+ $predicate,
|
||||
$param
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
impl ChargeItem {
|
||||
pub async fn upsert(
|
||||
&self,
|
||||
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||
) -> Result<ChargeId, 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)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
||||
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
|
||||
"#,
|
||||
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()),
|
||||
)
|
||||
.execute(&mut **transaction)
|
||||
.await?;
|
||||
|
||||
Ok(self.id)
|
||||
}
|
||||
|
||||
pub async fn get(
|
||||
id: ChargeId,
|
||||
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
|
||||
) -> Result<Option<ChargeItem>, 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: UserId,
|
||||
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
|
||||
) -> Result<Vec<ChargeItem>, 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_open_subscription(
|
||||
user_subscription_id: UserSubscriptionId,
|
||||
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
|
||||
) -> Result<Option<ChargeItem>, 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')",
|
||||
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<ChargeItem>, DatabaseError> {
|
||||
let now = Utc::now();
|
||||
|
||||
let res = select_charges_with_predicate!("WHERE (status = 'open' AND due < $1) OR (status = 'failed' AND last_attempt < $1 - INTERVAL '2 days')", now)
|
||||
.fetch_all(exec)
|
||||
.await?;
|
||||
|
||||
Ok(res
|
||||
.into_iter()
|
||||
.map(|r| r.try_into())
|
||||
.collect::<Result<Vec<_>, serde_json::Error>>()?)
|
||||
}
|
||||
|
||||
pub async fn remove(
|
||||
id: ChargeId,
|
||||
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(())
|
||||
}
|
||||
}
|
||||
@@ -256,6 +256,14 @@ generate_ids!(
|
||||
UserSubscriptionId
|
||||
);
|
||||
|
||||
generate_ids!(
|
||||
pub generate_charge_id,
|
||||
ChargeId,
|
||||
8,
|
||||
"SELECT EXISTS(SELECT 1 FROM charges WHERE id=$1)",
|
||||
ChargeId
|
||||
);
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Type, Hash, Serialize, Deserialize)]
|
||||
#[sqlx(transparent)]
|
||||
pub struct UserId(pub i64);
|
||||
@@ -386,6 +394,10 @@ pub struct ProductPriceId(pub i64);
|
||||
#[sqlx(transparent)]
|
||||
pub struct UserSubscriptionId(pub i64);
|
||||
|
||||
#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize, Eq, PartialEq, Hash)]
|
||||
#[sqlx(transparent)]
|
||||
pub struct ChargeId(pub i64);
|
||||
|
||||
use crate::models::ids;
|
||||
|
||||
impl From<ids::ProjectId> for ProjectId {
|
||||
@@ -571,3 +583,14 @@ impl From<UserSubscriptionId> for ids::UserSubscriptionId {
|
||||
ids::UserSubscriptionId(id.0 as u64)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ids::ChargeId> for ChargeId {
|
||||
fn from(id: ids::ChargeId) -> Self {
|
||||
ChargeId(id.0 as i64)
|
||||
}
|
||||
}
|
||||
impl From<ChargeId> for ids::ChargeId {
|
||||
fn from(id: ChargeId) -> Self {
|
||||
ids::ChargeId(id.0 as u64)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use thiserror::Error;
|
||||
|
||||
pub mod categories;
|
||||
pub mod charge_item;
|
||||
pub mod collection_item;
|
||||
pub mod flow_item;
|
||||
pub mod ids;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use crate::database::models::{DatabaseError, ProductPriceId, UserId, UserSubscriptionId};
|
||||
use crate::models::billing::{PriceDuration, SubscriptionStatus};
|
||||
use crate::models::billing::{PriceDuration, SubscriptionMetadata, SubscriptionStatus};
|
||||
use chrono::{DateTime, Utc};
|
||||
use itertools::Itertools;
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
|
||||
pub struct UserSubscriptionItem {
|
||||
pub id: UserSubscriptionId,
|
||||
@@ -9,9 +10,8 @@ pub struct UserSubscriptionItem {
|
||||
pub price_id: ProductPriceId,
|
||||
pub interval: PriceDuration,
|
||||
pub created: DateTime<Utc>,
|
||||
pub expires: DateTime<Utc>,
|
||||
pub last_charge: Option<DateTime<Utc>>,
|
||||
pub status: SubscriptionStatus,
|
||||
pub metadata: Option<SubscriptionMetadata>,
|
||||
}
|
||||
|
||||
struct UserSubscriptionResult {
|
||||
@@ -20,9 +20,8 @@ struct UserSubscriptionResult {
|
||||
price_id: i64,
|
||||
interval: String,
|
||||
pub created: DateTime<Utc>,
|
||||
pub expires: DateTime<Utc>,
|
||||
pub last_charge: Option<DateTime<Utc>>,
|
||||
pub status: String,
|
||||
pub metadata: serde_json::Value,
|
||||
}
|
||||
|
||||
macro_rules! select_user_subscriptions_with_predicate {
|
||||
@@ -31,8 +30,8 @@ macro_rules! select_user_subscriptions_with_predicate {
|
||||
UserSubscriptionResult,
|
||||
r#"
|
||||
SELECT
|
||||
id, user_id, price_id, interval, created, expires, last_charge, status
|
||||
FROM users_subscriptions
|
||||
us.id, us.user_id, us.price_id, us.interval, us.created, us.status, us.metadata
|
||||
FROM users_subscriptions us
|
||||
"#
|
||||
+ $predicate,
|
||||
$param
|
||||
@@ -40,18 +39,19 @@ macro_rules! select_user_subscriptions_with_predicate {
|
||||
};
|
||||
}
|
||||
|
||||
impl From<UserSubscriptionResult> for UserSubscriptionItem {
|
||||
fn from(r: UserSubscriptionResult) -> Self {
|
||||
UserSubscriptionItem {
|
||||
impl TryFrom<UserSubscriptionResult> for UserSubscriptionItem {
|
||||
type Error = serde_json::Error;
|
||||
|
||||
fn try_from(r: UserSubscriptionResult) -> Result<Self, Self::Error> {
|
||||
Ok(UserSubscriptionItem {
|
||||
id: UserSubscriptionId(r.id),
|
||||
user_id: UserId(r.user_id),
|
||||
price_id: ProductPriceId(r.price_id),
|
||||
interval: PriceDuration::from_string(&r.interval),
|
||||
created: r.created,
|
||||
expires: r.expires,
|
||||
last_charge: r.last_charge,
|
||||
status: SubscriptionStatus::from_string(&r.status),
|
||||
}
|
||||
metadata: serde_json::from_value(r.metadata)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,11 +70,14 @@ impl UserSubscriptionItem {
|
||||
let ids = ids.iter().map(|id| id.0).collect_vec();
|
||||
let ids_ref: &[i64] = &ids;
|
||||
let results =
|
||||
select_user_subscriptions_with_predicate!("WHERE id = ANY($1::bigint[])", ids_ref)
|
||||
select_user_subscriptions_with_predicate!("WHERE us.id = ANY($1::bigint[])", ids_ref)
|
||||
.fetch_all(exec)
|
||||
.await?;
|
||||
|
||||
Ok(results.into_iter().map(|r| r.into()).collect())
|
||||
Ok(results
|
||||
.into_iter()
|
||||
.map(|r| r.try_into())
|
||||
.collect::<Result<Vec<_>, serde_json::Error>>()?)
|
||||
}
|
||||
|
||||
pub async fn get_all_user(
|
||||
@@ -82,22 +85,38 @@ impl UserSubscriptionItem {
|
||||
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
|
||||
) -> Result<Vec<UserSubscriptionItem>, DatabaseError> {
|
||||
let user_id = user_id.0;
|
||||
let results = select_user_subscriptions_with_predicate!("WHERE user_id = $1", user_id)
|
||||
let results = select_user_subscriptions_with_predicate!("WHERE us.user_id = $1", user_id)
|
||||
.fetch_all(exec)
|
||||
.await?;
|
||||
|
||||
Ok(results.into_iter().map(|r| r.into()).collect())
|
||||
Ok(results
|
||||
.into_iter()
|
||||
.map(|r| r.try_into())
|
||||
.collect::<Result<Vec<_>, serde_json::Error>>()?)
|
||||
}
|
||||
|
||||
pub async fn get_all_expired(
|
||||
pub async fn get_all_unprovision(
|
||||
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
|
||||
) -> Result<Vec<UserSubscriptionItem>, DatabaseError> {
|
||||
let now = Utc::now();
|
||||
let results = select_user_subscriptions_with_predicate!("WHERE expires < $1", now)
|
||||
.fetch_all(exec)
|
||||
.await?;
|
||||
let results = select_user_subscriptions_with_predicate!(
|
||||
"
|
||||
INNER JOIN charges c
|
||||
ON c.subscription_id = us.id
|
||||
AND (
|
||||
(c.status = 'cancelled' AND c.due < $1) OR
|
||||
(c.status = 'failed' AND c.last_attempt < $1 - INTERVAL '2 days')
|
||||
)
|
||||
",
|
||||
now
|
||||
)
|
||||
.fetch_all(exec)
|
||||
.await?;
|
||||
|
||||
Ok(results.into_iter().map(|r| r.into()).collect())
|
||||
Ok(results
|
||||
.into_iter()
|
||||
.map(|r| r.try_into())
|
||||
.collect::<Result<Vec<_>, serde_json::Error>>()?)
|
||||
}
|
||||
|
||||
pub async fn upsert(
|
||||
@@ -107,44 +126,25 @@ impl UserSubscriptionItem {
|
||||
sqlx::query!(
|
||||
"
|
||||
INSERT INTO users_subscriptions (
|
||||
id, user_id, price_id, interval, created, expires, last_charge, status
|
||||
id, user_id, price_id, interval, created, status, metadata
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8
|
||||
$1, $2, $3, $4, $5, $6, $7
|
||||
)
|
||||
ON CONFLICT (id)
|
||||
DO UPDATE
|
||||
SET interval = EXCLUDED.interval,
|
||||
expires = EXCLUDED.expires,
|
||||
last_charge = EXCLUDED.last_charge,
|
||||
status = EXCLUDED.status,
|
||||
price_id = EXCLUDED.price_id
|
||||
price_id = EXCLUDED.price_id,
|
||||
metadata = EXCLUDED.metadata
|
||||
",
|
||||
self.id.0,
|
||||
self.user_id.0,
|
||||
self.price_id.0,
|
||||
self.interval.as_str(),
|
||||
self.created,
|
||||
self.expires,
|
||||
self.last_charge,
|
||||
self.status.as_str(),
|
||||
)
|
||||
.execute(&mut **transaction)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn remove(
|
||||
id: UserSubscriptionId,
|
||||
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||
) -> Result<(), DatabaseError> {
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM users_subscriptions
|
||||
WHERE id = $1
|
||||
",
|
||||
id.0 as i64
|
||||
serde_json::to_value(&self.metadata)?,
|
||||
)
|
||||
.execute(&mut **transaction)
|
||||
.await?;
|
||||
|
||||
@@ -15,7 +15,7 @@ pub async fn delete_file_version(
|
||||
file_name: &str,
|
||||
) -> Result<DeleteFileData, FileHostingError> {
|
||||
let response = reqwest::Client::new()
|
||||
.post(&format!(
|
||||
.post(format!(
|
||||
"{}/b2api/v2/b2_delete_file_version",
|
||||
authorization_data.api_url
|
||||
))
|
||||
|
||||
29
src/lib.rs
29
src/lib.rs
@@ -259,15 +259,24 @@ pub fn app_setup(
|
||||
}
|
||||
|
||||
let stripe_client = stripe::Client::new(dotenvy::var("STRIPE_API_KEY").unwrap());
|
||||
// {
|
||||
// let pool_ref = pool.clone();
|
||||
// let redis_ref = redis_pool.clone();
|
||||
// let stripe_client_ref = stripe_client.clone();
|
||||
//
|
||||
// actix_rt::spawn(async move {
|
||||
// routes::internal::billing::task(stripe_client_ref, pool_ref, redis_ref).await;
|
||||
// });
|
||||
// }
|
||||
{
|
||||
let pool_ref = pool.clone();
|
||||
let redis_ref = redis_pool.clone();
|
||||
let stripe_client_ref = stripe_client.clone();
|
||||
|
||||
actix_rt::spawn(async move {
|
||||
routes::internal::billing::task(stripe_client_ref, pool_ref, redis_ref).await;
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let pool_ref = pool.clone();
|
||||
let redis_ref = redis_pool.clone();
|
||||
|
||||
actix_rt::spawn(async move {
|
||||
routes::internal::billing::subscription_task(pool_ref, redis_ref).await;
|
||||
});
|
||||
}
|
||||
|
||||
let ip_salt = Pepper {
|
||||
pepper: models::ids::Base62Id(models::ids::random_base62(11)).to_string(),
|
||||
@@ -456,5 +465,7 @@ pub fn check_env_vars() -> bool {
|
||||
|
||||
failed |= check_var::<u64>("ADITUDE_API_KEY");
|
||||
|
||||
failed |= check_var::<String>("PYRO_API_KEY");
|
||||
|
||||
failed
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ pub struct Product {
|
||||
#[serde(tag = "type", rename_all = "kebab-case")]
|
||||
pub enum ProductMetadata {
|
||||
Midas,
|
||||
Pyro { ram: u32 },
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
|
||||
@@ -55,6 +56,13 @@ pub enum PriceDuration {
|
||||
}
|
||||
|
||||
impl PriceDuration {
|
||||
pub fn duration(&self) -> chrono::Duration {
|
||||
match self {
|
||||
PriceDuration::Monthly => chrono::Duration::days(30),
|
||||
PriceDuration::Yearly => chrono::Duration::days(365),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_string(string: &str) -> PriceDuration {
|
||||
match string {
|
||||
"monthly" => PriceDuration::Monthly,
|
||||
@@ -84,8 +92,7 @@ pub struct UserSubscription {
|
||||
pub interval: PriceDuration,
|
||||
pub status: SubscriptionStatus,
|
||||
pub created: DateTime<Utc>,
|
||||
pub expires: DateTime<Utc>,
|
||||
pub last_charge: Option<DateTime<Utc>>,
|
||||
pub metadata: Option<SubscriptionMetadata>,
|
||||
}
|
||||
|
||||
impl From<crate::database::models::user_subscription_item::UserSubscriptionItem>
|
||||
@@ -99,38 +106,119 @@ impl From<crate::database::models::user_subscription_item::UserSubscriptionItem>
|
||||
interval: x.interval,
|
||||
status: x.status,
|
||||
created: x.created,
|
||||
expires: x.expires,
|
||||
last_charge: x.last_charge,
|
||||
metadata: x.metadata,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Eq, PartialEq)]
|
||||
#[derive(Serialize, Deserialize, Eq, PartialEq, Copy, Clone)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum SubscriptionStatus {
|
||||
Active,
|
||||
PaymentProcessing,
|
||||
PaymentFailed,
|
||||
Cancelled,
|
||||
Provisioned,
|
||||
Unprovisioned,
|
||||
}
|
||||
|
||||
impl SubscriptionStatus {
|
||||
pub fn from_string(string: &str) -> SubscriptionStatus {
|
||||
match string {
|
||||
"active" => SubscriptionStatus::Active,
|
||||
"payment-processing" => SubscriptionStatus::PaymentProcessing,
|
||||
"payment-failed" => SubscriptionStatus::PaymentFailed,
|
||||
"cancelled" => SubscriptionStatus::Cancelled,
|
||||
_ => SubscriptionStatus::Cancelled,
|
||||
"provisioned" => SubscriptionStatus::Provisioned,
|
||||
"unprovisioned" => SubscriptionStatus::Unprovisioned,
|
||||
_ => SubscriptionStatus::Provisioned,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
SubscriptionStatus::Active => "active",
|
||||
SubscriptionStatus::PaymentProcessing => "payment-processing",
|
||||
SubscriptionStatus::PaymentFailed => "payment-failed",
|
||||
SubscriptionStatus::Cancelled => "cancelled",
|
||||
SubscriptionStatus::Provisioned => "provisioned",
|
||||
SubscriptionStatus::Unprovisioned => "unprovisioned",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "kebab-case")]
|
||||
pub enum SubscriptionMetadata {
|
||||
Pyro { id: String },
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
|
||||
#[serde(from = "Base62Id")]
|
||||
#[serde(into = "Base62Id")]
|
||||
pub struct ChargeId(pub u64);
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Charge {
|
||||
pub id: ChargeId,
|
||||
pub user_id: UserId,
|
||||
pub price_id: ProductPriceId,
|
||||
pub amount: i64,
|
||||
pub currency_code: String,
|
||||
pub status: ChargeStatus,
|
||||
pub due: DateTime<Utc>,
|
||||
pub last_attempt: Option<DateTime<Utc>>,
|
||||
#[serde(flatten)]
|
||||
pub type_: ChargeType,
|
||||
pub subscription_id: Option<UserSubscriptionId>,
|
||||
pub subscription_interval: Option<PriceDuration>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "kebab-case")]
|
||||
pub enum ChargeType {
|
||||
OneTime,
|
||||
Subscription,
|
||||
Proration,
|
||||
}
|
||||
|
||||
impl ChargeType {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
ChargeType::OneTime => "one-time",
|
||||
ChargeType::Subscription { .. } => "subscription",
|
||||
ChargeType::Proration { .. } => "proration",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_string(string: &str) -> ChargeType {
|
||||
match string {
|
||||
"one-time" => ChargeType::OneTime,
|
||||
"subscription" => ChargeType::Subscription,
|
||||
"proration" => ChargeType::Proration,
|
||||
_ => ChargeType::OneTime,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Eq, PartialEq, Copy, Clone)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum ChargeStatus {
|
||||
// Open charges are for the next billing interval
|
||||
Open,
|
||||
Processing,
|
||||
Succeeded,
|
||||
Failed,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
impl ChargeStatus {
|
||||
pub fn from_string(string: &str) -> ChargeStatus {
|
||||
match string {
|
||||
"processing" => ChargeStatus::Processing,
|
||||
"succeeded" => ChargeStatus::Succeeded,
|
||||
"failed" => ChargeStatus::Failed,
|
||||
"open" => ChargeStatus::Open,
|
||||
"cancelled" => ChargeStatus::Cancelled,
|
||||
_ => ChargeStatus::Failed,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
ChargeStatus::Processing => "processing",
|
||||
ChargeStatus::Succeeded => "succeeded",
|
||||
ChargeStatus::Failed => "failed",
|
||||
ChargeStatus::Open => "open",
|
||||
ChargeStatus::Cancelled => "cancelled",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,7 @@ pub use super::teams::TeamId;
|
||||
pub use super::threads::ThreadId;
|
||||
pub use super::threads::ThreadMessageId;
|
||||
pub use super::users::UserId;
|
||||
pub use crate::models::billing::UserSubscriptionId;
|
||||
pub use crate::models::v3::billing::{ProductId, ProductPriceId};
|
||||
pub use crate::models::billing::{ChargeId, ProductId, ProductPriceId, UserSubscriptionId};
|
||||
use thiserror::Error;
|
||||
|
||||
/// Generates a random 64 bit integer that is exactly `n` characters
|
||||
@@ -137,6 +136,7 @@ base62_id_impl!(PayoutId, PayoutId);
|
||||
base62_id_impl!(ProductId, ProductId);
|
||||
base62_id_impl!(ProductPriceId, ProductPriceId);
|
||||
base62_id_impl!(UserSubscriptionId, UserSubscriptionId);
|
||||
base62_id_impl!(ChargeId, ChargeId);
|
||||
|
||||
pub mod base62_impl {
|
||||
use serde::de::{self, Deserializer, Visitor};
|
||||
|
||||
@@ -74,7 +74,7 @@ impl PayoutsQueue {
|
||||
}
|
||||
|
||||
let credential: PaypalCredential = client
|
||||
.post(&format!("{}oauth2/token", dotenvy::var("PAYPAL_API_URL")?))
|
||||
.post(format!("{}oauth2/token", dotenvy::var("PAYPAL_API_URL")?))
|
||||
.header("Accept", "application/json")
|
||||
.header("Accept-Language", "en_US")
|
||||
.header("Authorization", formatted_key)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -510,7 +510,7 @@ impl AuthProvider {
|
||||
map.insert("grant_type", "authorization_code");
|
||||
|
||||
let token: AccessToken = reqwest::Client::new()
|
||||
.post(&format!("{api_url}oauth2/token"))
|
||||
.post(format!("{api_url}oauth2/token"))
|
||||
.header(reqwest::header::ACCEPT, "application/json")
|
||||
.header(
|
||||
AUTHORIZATION,
|
||||
@@ -766,7 +766,7 @@ impl AuthProvider {
|
||||
let api_url = dotenvy::var("PAYPAL_API_URL")?;
|
||||
|
||||
let paypal_user: PayPalUser = reqwest::Client::new()
|
||||
.get(&format!(
|
||||
.get(format!(
|
||||
"{api_url}identity/openidconnect/userinfo?schema=openid"
|
||||
))
|
||||
.header(reqwest::header::USER_AGENT, "Modrinth")
|
||||
@@ -1393,7 +1393,7 @@ pub async fn sign_up_beehiiv(email: &str) -> Result<(), AuthenticationError> {
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
client
|
||||
.post(&format!(
|
||||
.post(format!(
|
||||
"https://api.beehiiv.com/v2/publications/{id}/subscriptions"
|
||||
))
|
||||
.header(AUTHORIZATION, format!("Bearer {}", api_key))
|
||||
|
||||
@@ -21,7 +21,7 @@ pub async fn export(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&*session_queue,
|
||||
&session_queue,
|
||||
Some(&[Scopes::SESSION_ACCESS]),
|
||||
)
|
||||
.await?
|
||||
@@ -34,19 +34,19 @@ pub async fn export(
|
||||
crate::database::models::Collection::get_many(&collection_ids, &**pool, &redis)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|x| crate::models::collections::Collection::from(x))
|
||||
.map(crate::models::collections::Collection::from)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let follows = crate::database::models::User::get_follows(user_id, &**pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|x| crate::models::ids::ProjectId::from(x))
|
||||
.map(crate::models::ids::ProjectId::from)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let projects = crate::database::models::User::get_projects(user_id, &**pool, &redis)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|x| crate::models::ids::ProjectId::from(x))
|
||||
.map(crate::models::ids::ProjectId::from)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let org_ids = crate::database::models::User::get_organizations(user_id, &**pool).await?;
|
||||
@@ -64,7 +64,7 @@ pub async fn export(
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|x| crate::models::notifications::Notification::from(x))
|
||||
.map(crate::models::notifications::Notification::from)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let oauth_clients =
|
||||
@@ -73,7 +73,7 @@ pub async fn export(
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|x| crate::models::oauth_clients::OAuthClient::from(x))
|
||||
.map(crate::models::oauth_clients::OAuthClient::from)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let oauth_authorizations = crate::database::models::oauth_client_authorization_item::OAuthClientAuthorization::get_all_for_user(
|
||||
@@ -81,7 +81,7 @@ pub async fn export(
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|x| crate::models::oauth_clients::OAuthClientAuthorization::from(x))
|
||||
.map(crate::models::oauth_clients::OAuthClientAuthorization::from)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let pat_ids = crate::database::models::pat_item::PersonalAccessToken::get_user_pats(
|
||||
@@ -102,7 +102,7 @@ pub async fn export(
|
||||
let payouts = crate::database::models::payout_item::Payout::get_many(&payout_ids, &**pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|x| crate::models::payouts::Payout::from(x))
|
||||
.map(crate::models::payouts::Payout::from)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let report_ids =
|
||||
@@ -110,7 +110,7 @@ pub async fn export(
|
||||
let reports = crate::database::models::report_item::Report::get_many(&report_ids, &**pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|x| crate::models::reports::Report::from(x))
|
||||
.map(crate::models::reports::Report::from)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let message_ids = sqlx::query!(
|
||||
@@ -146,7 +146,7 @@ pub async fn export(
|
||||
crate::database::models::image_item::Image::get_many(&uploaded_images_ids, &**pool, &redis)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|x| crate::models::images::Image::from(x))
|
||||
.map(crate::models::images::Image::from)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let subscriptions =
|
||||
@@ -155,7 +155,7 @@ pub async fn export(
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|x| crate::models::billing::UserSubscription::from(x))
|
||||
.map(crate::models::billing::UserSubscription::from)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(HttpResponse::Ok().json(serde_json::json!({
|
||||
|
||||
@@ -106,14 +106,11 @@ pub async fn version_create(
|
||||
|
||||
// Get all possible side-types for loaders given- we will use these to check if we need to convert/apply singleplayer, etc.
|
||||
let loaders = match v3::tags::loader_list(client.clone(), redis.clone()).await {
|
||||
Ok(loader_response) => match v2_reroute::extract_ok_json::<
|
||||
Vec<v3::tags::LoaderData>,
|
||||
>(loader_response)
|
||||
.await
|
||||
{
|
||||
Ok(loaders) => loaders,
|
||||
Err(_) => vec![],
|
||||
},
|
||||
Ok(loader_response) => {
|
||||
(v2_reroute::extract_ok_json::<Vec<v3::tags::LoaderData>>(loader_response)
|
||||
.await)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
Err(_) => vec![],
|
||||
};
|
||||
|
||||
|
||||
@@ -352,7 +352,7 @@ pub async fn create_payout(
|
||||
.fetch_optional(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
let balance = get_user_balance(user.id.into(), &**pool).await?;
|
||||
let balance = get_user_balance(user.id, &pool).await?;
|
||||
if balance.available < body.amount || body.amount < Decimal::ZERO {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"You do not have enough funds to make this payout!".to_string(),
|
||||
@@ -734,7 +734,7 @@ pub async fn get_balance(
|
||||
.await?
|
||||
.1;
|
||||
|
||||
let balance = get_user_balance(user.id.into(), &**pool).await?;
|
||||
let balance = get_user_balance(user.id.into(), &pool).await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(balance))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user