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