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:
Geometrically
2024-10-09 21:11:30 -07:00
committed by GitHub
parent 28b6bf8603
commit c88bfbb5f0
33 changed files with 1692 additions and 816 deletions

View File

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

View File

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