collab: Add billing_subscriptions
table (#15448)
This PR adds a new `billing_subscriptions` table to the database, as well as some accompanying models/queries. In this table we store a minimal amount of data from Stripe: - The Stripe customer ID - The Stripe subscription ID - The status of the Stripe subscription This should be enough for interactions with the Stripe API (e.g., to [create a customer portal session](https://docs.stripe.com/api/customer_portal/sessions/create)), as well as determine whether a subscription is active (based on the `status`). Release Notes: - N/A
This commit is contained in:
parent
0702ed5cd6
commit
085d41b121
11 changed files with 215 additions and 0 deletions
|
@ -416,3 +416,16 @@ CREATE TABLE dev_server_projects (
|
||||||
dev_server_id INTEGER NOT NULL REFERENCES dev_servers(id),
|
dev_server_id INTEGER NOT NULL REFERENCES dev_servers(id),
|
||||||
paths TEXT NOT NULL
|
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 tests::TestDb;
|
||||||
|
|
||||||
pub use ids::*;
|
pub use ids::*;
|
||||||
|
pub use queries::billing_subscriptions::CreateBillingSubscriptionParams;
|
||||||
pub use queries::contributors::ContributorSelector;
|
pub use queries::contributors::ContributorSelector;
|
||||||
pub use sea_orm::ConnectOptions;
|
pub use sea_orm::ConnectOptions;
|
||||||
pub use tables::user::Model as User;
|
pub use tables::user::Model as User;
|
||||||
|
|
|
@ -68,6 +68,7 @@ macro_rules! id_type {
|
||||||
}
|
}
|
||||||
|
|
||||||
id_type!(AccessTokenId);
|
id_type!(AccessTokenId);
|
||||||
|
id_type!(BillingSubscriptionId);
|
||||||
id_type!(BufferId);
|
id_type!(BufferId);
|
||||||
id_type!(ChannelBufferCollaboratorId);
|
id_type!(ChannelBufferCollaboratorId);
|
||||||
id_type!(ChannelChatParticipantId);
|
id_type!(ChannelChatParticipantId);
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
pub mod access_tokens;
|
pub mod access_tokens;
|
||||||
|
pub mod billing_subscriptions;
|
||||||
pub mod buffers;
|
pub mod buffers;
|
||||||
pub mod channels;
|
pub mod channels;
|
||||||
pub mod contacts;
|
pub mod contacts;
|
||||||
|
|
55
crates/collab/src/db/queries/billing_subscriptions.rs
Normal file
55
crates/collab/src/db/queries/billing_subscriptions.rs
Normal file
|
@ -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 access_token;
|
||||||
|
pub mod billing_subscription;
|
||||||
pub mod buffer;
|
pub mod buffer;
|
||||||
pub mod buffer_operation;
|
pub mod buffer_operation;
|
||||||
pub mod buffer_snapshot;
|
pub mod buffer_snapshot;
|
||||||
|
|
58
crates/collab/src/db/tables/billing_subscription.rs
Normal file
58
crates/collab/src/db/tables/billing_subscription.rs
Normal file
|
@ -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 {
|
pub enum Relation {
|
||||||
#[sea_orm(has_many = "super::access_token::Entity")]
|
#[sea_orm(has_many = "super::access_token::Entity")]
|
||||||
AccessToken,
|
AccessToken,
|
||||||
|
#[sea_orm(has_many = "super::billing_subscription::Entity")]
|
||||||
|
BillingSubscription,
|
||||||
#[sea_orm(has_one = "super::room_participant::Entity")]
|
#[sea_orm(has_one = "super::room_participant::Entity")]
|
||||||
RoomParticipant,
|
RoomParticipant,
|
||||||
#[sea_orm(has_many = "super::project::Entity")]
|
#[sea_orm(has_many = "super::project::Entity")]
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
mod billing_subscription_tests;
|
||||||
mod buffer_tests;
|
mod buffer_tests;
|
||||||
mod channel_tests;
|
mod channel_tests;
|
||||||
mod contributor_tests;
|
mod contributor_tests;
|
||||||
|
|
70
crates/collab/src/db/tests/billing_subscription_tests.rs
Normal file
70
crates/collab/src/db/tests/billing_subscription_tests.rs
Normal file
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue