Detailed changes
@@ -416,3 +416,16 @@ CREATE TABLE dev_server_projects (
dev_server_id INTEGER NOT NULL REFERENCES dev_servers(id),
paths TEXT NOT NULL
);
+
+CREATE TABLE IF NOT EXISTS billing_subscriptions (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ user_id INTEGER NOT NULL REFERENCES users(id),
+ stripe_customer_id TEXT NOT NULL,
+ stripe_subscription_id TEXT NOT NULL,
+ stripe_subscription_status TEXT NOT NULL
+);
+
+CREATE INDEX "ix_billing_subscriptions_on_user_id" ON billing_subscriptions (user_id);
+CREATE INDEX "ix_billing_subscriptions_on_stripe_customer_id" ON billing_subscriptions (stripe_customer_id);
+CREATE UNIQUE INDEX "uix_billing_subscriptions_on_stripe_subscription_id" ON billing_subscriptions (stripe_subscription_id);
@@ -0,0 +1,12 @@
+CREATE TABLE IF NOT EXISTS billing_subscriptions (
+ id SERIAL PRIMARY KEY,
+ created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now(),
+ user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ stripe_customer_id TEXT NOT NULL,
+ stripe_subscription_id TEXT NOT NULL,
+ stripe_subscription_status TEXT NOT NULL
+);
+
+CREATE INDEX "ix_billing_subscriptions_on_user_id" ON billing_subscriptions (user_id);
+CREATE INDEX "ix_billing_subscriptions_on_stripe_customer_id" ON billing_subscriptions (stripe_customer_id);
+CREATE UNIQUE INDEX "uix_billing_subscriptions_on_stripe_subscription_id" ON billing_subscriptions (stripe_subscription_id);
@@ -45,6 +45,7 @@ use tokio::sync::{Mutex, OwnedMutexGuard};
pub use tests::TestDb;
pub use ids::*;
+pub use queries::billing_subscriptions::CreateBillingSubscriptionParams;
pub use queries::contributors::ContributorSelector;
pub use sea_orm::ConnectOptions;
pub use tables::user::Model as User;
@@ -68,6 +68,7 @@ macro_rules! id_type {
}
id_type!(AccessTokenId);
+id_type!(BillingSubscriptionId);
id_type!(BufferId);
id_type!(ChannelBufferCollaboratorId);
id_type!(ChannelChatParticipantId);
@@ -1,6 +1,7 @@
use super::*;
pub mod access_tokens;
+pub mod billing_subscriptions;
pub mod buffers;
pub mod channels;
pub mod contacts;
@@ -0,0 +1,55 @@
+use crate::db::billing_subscription::StripeSubscriptionStatus;
+
+use super::*;
+
+#[derive(Debug)]
+pub struct CreateBillingSubscriptionParams {
+ pub user_id: UserId,
+ pub stripe_customer_id: String,
+ pub stripe_subscription_id: String,
+ pub stripe_subscription_status: StripeSubscriptionStatus,
+}
+
+impl Database {
+ /// Creates a new billing subscription.
+ pub async fn create_billing_subscription(
+ &self,
+ params: &CreateBillingSubscriptionParams,
+ ) -> Result<()> {
+ self.transaction(|tx| async move {
+ billing_subscription::Entity::insert(billing_subscription::ActiveModel {
+ user_id: ActiveValue::set(params.user_id),
+ stripe_customer_id: ActiveValue::set(params.stripe_customer_id.clone()),
+ stripe_subscription_id: ActiveValue::set(params.stripe_subscription_id.clone()),
+ stripe_subscription_status: ActiveValue::set(params.stripe_subscription_status),
+ ..Default::default()
+ })
+ .exec_without_returning(&*tx)
+ .await?;
+
+ Ok(())
+ })
+ .await
+ }
+
+ /// Returns all of the active billing subscriptions for the user with the specified ID.
+ pub async fn get_active_billing_subscriptions(
+ &self,
+ user_id: UserId,
+ ) -> Result<Vec<billing_subscription::Model>> {
+ self.transaction(|tx| async move {
+ let subscriptions = billing_subscription::Entity::find()
+ .filter(
+ billing_subscription::Column::UserId.eq(user_id).and(
+ billing_subscription::Column::StripeSubscriptionStatus
+ .eq(StripeSubscriptionStatus::Active),
+ ),
+ )
+ .all(&*tx)
+ .await?;
+
+ Ok(subscriptions)
+ })
+ .await
+ }
+}
@@ -1,4 +1,5 @@
pub mod access_token;
+pub mod billing_subscription;
pub mod buffer;
pub mod buffer_operation;
pub mod buffer_snapshot;
@@ -0,0 +1,58 @@
+use crate::db::{BillingSubscriptionId, UserId};
+use sea_orm::entity::prelude::*;
+
+/// A billing subscription.
+#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)]
+#[sea_orm(table_name = "billing_subscriptions")]
+pub struct Model {
+ #[sea_orm(primary_key)]
+ pub id: BillingSubscriptionId,
+ pub user_id: UserId,
+ pub stripe_customer_id: String,
+ pub stripe_subscription_id: String,
+ pub stripe_subscription_status: StripeSubscriptionStatus,
+ pub created_at: DateTime,
+}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+pub enum Relation {
+ #[sea_orm(
+ belongs_to = "super::user::Entity",
+ from = "Column::UserId",
+ to = "super::user::Column::Id"
+ )]
+ User,
+}
+
+impl Related<super::user::Entity> for Entity {
+ fn to() -> RelationDef {
+ Relation::User.def()
+ }
+}
+
+impl ActiveModelBehavior for ActiveModel {}
+
+/// The status of a Stripe subscription.
+///
+/// [Stripe docs](https://docs.stripe.com/api/subscriptions/object#subscription_object-status)
+#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Default, Hash)]
+#[sea_orm(rs_type = "String", db_type = "String(None)")]
+pub enum StripeSubscriptionStatus {
+ #[default]
+ #[sea_orm(string_value = "incomplete")]
+ Incomplete,
+ #[sea_orm(string_value = "incomplete_expired")]
+ IncompleteExpired,
+ #[sea_orm(string_value = "trialing")]
+ Trialing,
+ #[sea_orm(string_value = "active")]
+ Active,
+ #[sea_orm(string_value = "past_due")]
+ PastDue,
+ #[sea_orm(string_value = "canceled")]
+ Canceled,
+ #[sea_orm(string_value = "unpaid")]
+ Unpaid,
+ #[sea_orm(string_value = "paused")]
+ Paused,
+}
@@ -24,6 +24,8 @@ pub struct Model {
pub enum Relation {
#[sea_orm(has_many = "super::access_token::Entity")]
AccessToken,
+ #[sea_orm(has_many = "super::billing_subscription::Entity")]
+ BillingSubscription,
#[sea_orm(has_one = "super::room_participant::Entity")]
RoomParticipant,
#[sea_orm(has_many = "super::project::Entity")]
@@ -1,3 +1,4 @@
+mod billing_subscription_tests;
mod buffer_tests;
mod channel_tests;
mod contributor_tests;
@@ -0,0 +1,70 @@
+use std::sync::Arc;
+
+use crate::db::billing_subscription::StripeSubscriptionStatus;
+use crate::db::tests::new_test_user;
+use crate::db::CreateBillingSubscriptionParams;
+use crate::test_both_dbs;
+
+use super::Database;
+
+test_both_dbs!(
+ test_get_active_billing_subscriptions,
+ test_get_active_billing_subscriptions_postgres,
+ test_get_active_billing_subscriptions_sqlite
+);
+
+async fn test_get_active_billing_subscriptions(db: &Arc<Database>) {
+ // A user with no subscription has no active billing subscriptions.
+ {
+ let user_id = new_test_user(db, "no-subscription-user@example.com").await;
+ let subscriptions = db.get_active_billing_subscriptions(user_id).await.unwrap();
+
+ assert_eq!(subscriptions.len(), 0);
+ }
+
+ // A user with an active subscription has one active billing subscription.
+ {
+ let user_id = new_test_user(db, "active-user@example.com").await;
+ db.create_billing_subscription(&CreateBillingSubscriptionParams {
+ user_id,
+ stripe_customer_id: "cus_active_user".into(),
+ stripe_subscription_id: "sub_active_user".into(),
+ stripe_subscription_status: StripeSubscriptionStatus::Active,
+ })
+ .await
+ .unwrap();
+
+ let subscriptions = db.get_active_billing_subscriptions(user_id).await.unwrap();
+ assert_eq!(subscriptions.len(), 1);
+
+ let subscription = &subscriptions[0];
+ assert_eq!(
+ subscription.stripe_customer_id,
+ "cus_active_user".to_string()
+ );
+ assert_eq!(
+ subscription.stripe_subscription_id,
+ "sub_active_user".to_string()
+ );
+ assert_eq!(
+ subscription.stripe_subscription_status,
+ StripeSubscriptionStatus::Active
+ );
+ }
+
+ // A user with a past-due subscription has no active billing subscriptions.
+ {
+ let user_id = new_test_user(db, "past-due-user@example.com").await;
+ db.create_billing_subscription(&CreateBillingSubscriptionParams {
+ user_id,
+ stripe_customer_id: "cus_past_due_user".into(),
+ stripe_subscription_id: "sub_past_due_user".into(),
+ stripe_subscription_status: StripeSubscriptionStatus::PastDue,
+ })
+ .await
+ .unwrap();
+
+ let subscriptions = db.get_active_billing_subscriptions(user_id).await.unwrap();
+ assert_eq!(subscriptions.len(), 0);
+ }
+}