1use crate::db::{BillingCustomerId, BillingSubscriptionId};
2use crate::stripe_client;
3use chrono::{Datelike as _, NaiveDate, Utc};
4use sea_orm::entity::prelude::*;
5use serde::Serialize;
6
7/// A billing subscription.
8#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)]
9#[sea_orm(table_name = "billing_subscriptions")]
10pub struct Model {
11 #[sea_orm(primary_key)]
12 pub id: BillingSubscriptionId,
13 pub billing_customer_id: BillingCustomerId,
14 pub kind: Option<SubscriptionKind>,
15 pub stripe_subscription_id: String,
16 pub stripe_subscription_status: StripeSubscriptionStatus,
17 pub stripe_cancel_at: Option<DateTime>,
18 pub stripe_cancellation_reason: Option<StripeCancellationReason>,
19 pub stripe_current_period_start: Option<i64>,
20 pub stripe_current_period_end: Option<i64>,
21 pub created_at: DateTime,
22}
23
24impl Model {
25 pub fn current_period_start_at(&self) -> Option<DateTimeUtc> {
26 let period_start = self.stripe_current_period_start?;
27 chrono::DateTime::from_timestamp(period_start, 0)
28 }
29
30 pub fn current_period_end_at(&self) -> Option<DateTimeUtc> {
31 let period_end = self.stripe_current_period_end?;
32 chrono::DateTime::from_timestamp(period_end, 0)
33 }
34
35 pub fn current_period(
36 subscription: Option<Self>,
37 is_staff: bool,
38 ) -> Option<(DateTimeUtc, DateTimeUtc)> {
39 if is_staff {
40 let now = Utc::now();
41 let year = now.year();
42 let month = now.month();
43
44 let first_day_of_this_month =
45 NaiveDate::from_ymd_opt(year, month, 1)?.and_hms_opt(0, 0, 0)?;
46
47 let next_month = if month == 12 { 1 } else { month + 1 };
48 let next_month_year = if month == 12 { year + 1 } else { year };
49 let first_day_of_next_month =
50 NaiveDate::from_ymd_opt(next_month_year, next_month, 1)?.and_hms_opt(23, 59, 59)?;
51
52 let last_day_of_this_month = first_day_of_next_month - chrono::Days::new(1);
53
54 Some((
55 first_day_of_this_month.and_utc(),
56 last_day_of_this_month.and_utc(),
57 ))
58 } else {
59 let subscription = subscription?;
60 let period_start_at = subscription.current_period_start_at()?;
61 let period_end_at = subscription.current_period_end_at()?;
62
63 Some((period_start_at, period_end_at))
64 }
65 }
66}
67
68#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
69pub enum Relation {
70 #[sea_orm(
71 belongs_to = "super::billing_customer::Entity",
72 from = "Column::BillingCustomerId",
73 to = "super::billing_customer::Column::Id"
74 )]
75 BillingCustomer,
76}
77
78impl Related<super::billing_customer::Entity> for Entity {
79 fn to() -> RelationDef {
80 Relation::BillingCustomer.def()
81 }
82}
83
84impl ActiveModelBehavior for ActiveModel {}
85
86#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Hash, Serialize)]
87#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)")]
88#[serde(rename_all = "snake_case")]
89pub enum SubscriptionKind {
90 #[sea_orm(string_value = "zed_pro")]
91 ZedPro,
92 #[sea_orm(string_value = "zed_pro_trial")]
93 ZedProTrial,
94 #[sea_orm(string_value = "zed_free")]
95 ZedFree,
96}
97
98impl From<SubscriptionKind> for cloud_llm_client::Plan {
99 fn from(value: SubscriptionKind) -> Self {
100 match value {
101 SubscriptionKind::ZedPro => Self::ZedPro,
102 SubscriptionKind::ZedProTrial => Self::ZedProTrial,
103 SubscriptionKind::ZedFree => Self::ZedFree,
104 }
105 }
106}
107
108/// The status of a Stripe subscription.
109///
110/// [Stripe docs](https://docs.stripe.com/api/subscriptions/object#subscription_object-status)
111#[derive(
112 Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Default, Hash, Serialize,
113)]
114#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)")]
115#[serde(rename_all = "snake_case")]
116pub enum StripeSubscriptionStatus {
117 #[default]
118 #[sea_orm(string_value = "incomplete")]
119 Incomplete,
120 #[sea_orm(string_value = "incomplete_expired")]
121 IncompleteExpired,
122 #[sea_orm(string_value = "trialing")]
123 Trialing,
124 #[sea_orm(string_value = "active")]
125 Active,
126 #[sea_orm(string_value = "past_due")]
127 PastDue,
128 #[sea_orm(string_value = "canceled")]
129 Canceled,
130 #[sea_orm(string_value = "unpaid")]
131 Unpaid,
132 #[sea_orm(string_value = "paused")]
133 Paused,
134}
135
136impl StripeSubscriptionStatus {
137 pub fn is_cancelable(&self) -> bool {
138 match self {
139 Self::Trialing | Self::Active | Self::PastDue => true,
140 Self::Incomplete
141 | Self::IncompleteExpired
142 | Self::Canceled
143 | Self::Unpaid
144 | Self::Paused => false,
145 }
146 }
147}
148
149/// The cancellation reason for a Stripe subscription.
150///
151/// [Stripe docs](https://docs.stripe.com/api/subscriptions/object#subscription_object-cancellation_details-reason)
152#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Hash, Serialize)]
153#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)")]
154#[serde(rename_all = "snake_case")]
155pub enum StripeCancellationReason {
156 #[sea_orm(string_value = "cancellation_requested")]
157 CancellationRequested,
158 #[sea_orm(string_value = "payment_disputed")]
159 PaymentDisputed,
160 #[sea_orm(string_value = "payment_failed")]
161 PaymentFailed,
162}
163
164impl From<stripe_client::StripeCancellationDetailsReason> for StripeCancellationReason {
165 fn from(value: stripe_client::StripeCancellationDetailsReason) -> Self {
166 match value {
167 stripe_client::StripeCancellationDetailsReason::CancellationRequested => {
168 Self::CancellationRequested
169 }
170 stripe_client::StripeCancellationDetailsReason::PaymentDisputed => {
171 Self::PaymentDisputed
172 }
173 stripe_client::StripeCancellationDetailsReason::PaymentFailed => Self::PaymentFailed,
174 }
175 }
176}