mirror of
https://github.com/zed-industries/zed.git
synced 2024-09-20 02:47:34 +03:00
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
@ -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;
|
||||
|
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 billing_subscription;
|
||||
pub mod buffer;
|
||||
pub mod buffer_operation;
|
||||
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 {
|
||||
#[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;
|
||||
|
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…
Reference in New Issue
Block a user