shared/domain/
billing.rs

1//! Types for billing
2
3use chrono::{DateTime, Utc};
4use macros::make_path_parts;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::fmt::{self, Debug, Display, Formatter};
8use strum_macros::{AsRefStr, Display, EnumIs, EnumIter, EnumString};
9
10use const_format::formatcp;
11use serde_json::Value;
12
13use crate::api::endpoints::PathPart;
14use crate::domain::image::ImageId;
15use crate::domain::user::{UserId, UserProfile};
16use crate::domain::{Percent, UpdateNonNullable, UpdateNullable};
17
18/// ### Shared billing constants.
19/// (please keep all constants in same place, so that we don't end up duplicates)
20
21/// Level 1 max teacher count
22pub const PLAN_SCHOOL_LEVEL_1_TEACHER_COUNT: i64 = 5;
23/// Level 2 max teacher count
24pub const PLAN_SCHOOL_LEVEL_2_TEACHER_COUNT: i64 = 10;
25/// Level 3 max teacher count
26pub const PLAN_SCHOOL_LEVEL_3_TEACHER_COUNT: i64 = 15;
27/// Level 4 max teacher count
28pub const PLAN_SCHOOL_LEVEL_4_TEACHER_COUNT: i64 = 20;
29/// Level 5 max teacher count
30pub const PLAN_SCHOOL_UNLIMITED_TEACHER_COUNT: i64 = 30;
31
32/// Individual plan trial period in days
33pub const INDIVIDUAL_TRIAL_PERIOD: i64 = 7;
34/// Schools plan trial period in days
35pub const SCHOOL_TRIAL_PERIOD: i64 = 14;
36
37/// Plan price monthly-basic
38pub const PLAN_PRICE_MONTHLY_BASIC: u32 = 17_99;
39/// Plan price annual-basic
40pub const PLAN_PRICE_ANNUAL_BASIC: u32 = 180_00;
41/// Plan price monthly-pro
42pub const PLAN_PRICE_MONTHLY_PRO: u32 = 29_99;
43/// Plan price annual-pro
44pub const PLAN_PRICE_ANNUAL_PRO: u32 = 300_00;
45/// Plan price monthly-school-1
46pub const PLAN_PRICE_MONTHLY_SCHOOL_1: u32 = 115_00;
47/// Plan price annual-school-1
48pub const PLAN_PRICE_ANNUAL_SCHOOL_1: u32 = 1_250_00;
49/// Plan price monthly-school-2
50pub const PLAN_PRICE_MONTHLY_SCHOOL_2: u32 = 150_00;
51/// Plan price annual-school-2
52pub const PLAN_PRICE_ANNUAL_SCHOOL_2: u32 = 1_500_00;
53/// Plan price monthly-school-3
54pub const PLAN_PRICE_MONTHLY_SCHOOL_3: u32 = 200_00;
55/// Plan price annual-school-3
56pub const PLAN_PRICE_ANNUAL_SCHOOL_3: u32 = 2_000_00;
57/// Plan price monthly-school-4
58pub const PLAN_PRICE_MONTHLY_SCHOOL_4: u32 = 250_00;
59/// Plan price annual-school-4
60pub const PLAN_PRICE_ANNUAL_SCHOOL_4: u32 = 2_500_00;
61/// Plan price monthly-school-unlimited
62pub const PLAN_PRICE_MONTHLY_SCHOOL_UNLIMITED: u32 = 300_00;
63/// Plan price annual-school-unlimited
64pub const PLAN_PRICE_ANNUAL_SCHOOL_UNLIMITED: u32 = 3_000_00;
65
66/// Stripe customer ID
67#[derive(Debug, Serialize, Deserialize, Clone)]
68#[cfg_attr(feature = "backend", derive(sqlx::Type), sqlx(transparent))]
69pub struct CustomerId(String);
70
71#[cfg(feature = "backend")]
72impl From<stripe::CustomerId> for CustomerId {
73    fn from(value: stripe::CustomerId) -> Self {
74        Self(value.as_str().to_owned())
75    }
76}
77
78#[cfg(feature = "backend")]
79impl From<CustomerId> for stripe::CustomerId {
80    fn from(value: CustomerId) -> Self {
81        use std::str::FromStr;
82        Self::from_str(&value.0).unwrap()
83    }
84}
85
86impl CustomerId {
87    /// Obtain a reference to the inner string
88    #[cfg(feature = "backend")]
89    pub fn as_str(&self) -> &str {
90        &self.0
91    }
92}
93
94impl fmt::Display for CustomerId {
95    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
96        write!(f, "{}", self.0)
97    }
98}
99
100/// Stripe payment method ID
101#[derive(Debug, Serialize, Deserialize, Clone)]
102pub struct StripePaymentMethodId(String);
103
104/// Last 4 digits of a card number
105#[derive(Debug, Serialize, Deserialize, Clone)]
106pub struct Last4(String);
107
108impl fmt::Display for Last4 {
109    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
110        write!(f, "{}", self.0)
111    }
112}
113
114/// Payment network associated with a [Card]
115#[derive(Debug, Serialize, Deserialize, Clone, EnumString)]
116#[serde(rename_all = "kebab-case")]
117#[strum(serialize_all = "lowercase")]
118pub enum PaymentNetwork {
119    /// Visa
120    Visa,
121    /// Mastercard
122    Mastercard,
123    /// Discover Global Network
124    Discover,
125    /// JCB Co
126    JCB,
127    /// American Express
128    #[strum(serialize = "amex")]
129    AmericanExpress,
130    /// UnionPay
131    UnionPay,
132    /// Diners
133    #[strum(serialize = "diners")]
134    DinersClub,
135    /// Unknown
136    Unknown,
137}
138
139impl Default for PaymentNetwork {
140    fn default() -> Self {
141        Self::Unknown
142    }
143}
144
145/// A display-only representation of a card
146#[derive(Debug, Serialize, Deserialize, Clone)]
147pub struct Card {
148    /// The last 4 digits of the card
149    pub last4: Last4,
150    /// The cards payment network
151    pub payment_network: PaymentNetwork,
152    /// The expiry month for this card
153    pub exp_month: u8,
154    /// The expiry year for this card
155    pub exp_year: u16,
156}
157
158#[cfg(feature = "backend")]
159impl From<stripe::CardDetails> for Card {
160    fn from(value: stripe::CardDetails) -> Self {
161        use std::str::FromStr;
162        Self {
163            last4: Last4(value.last4),
164            payment_network: PaymentNetwork::from_str(&value.brand).unwrap_or_default(),
165            exp_month: value.exp_month as u8,
166            exp_year: value.exp_year as u16,
167        }
168    }
169}
170
171/// Type of payment method
172///
173/// Note: Only the [PaymentMethodType::Card] variant has any display details.
174#[derive(Debug, Serialize, Deserialize, Clone)]
175pub enum PaymentMethodType {
176    /// Apple Pay
177    ApplePay,
178    /// Google Pay
179    GooglePay,
180    /// [Link](https://stripe.com/docs/payments/link) one-click checkout
181    Link,
182    /// Card
183    Card(Card),
184    /// Other/unknown
185    Other,
186}
187
188wrap_uuid! {
189    /// Local payment method ID
190    pub struct PaymentMethodId
191}
192
193/// Payment method
194#[derive(Debug, Serialize, Deserialize, Clone)]
195pub struct PaymentMethod {
196    /// The Stripe payment method ID
197    pub stripe_payment_method_id: StripePaymentMethodId, // Stripe payment method ID
198    /// The type of payment method
199    pub payment_method_type: PaymentMethodType,
200}
201
202#[cfg(feature = "backend")]
203impl From<stripe::PaymentMethod> for PaymentMethod {
204    fn from(value: stripe::PaymentMethod) -> Self {
205        let payment_method_type = if value.link.is_some() {
206            PaymentMethodType::Link
207        } else if let Some(card) = value.card {
208            if let Some(wallet) = card.wallet {
209                if wallet.apple_pay.is_some() {
210                    PaymentMethodType::ApplePay
211                } else if wallet.google_pay.is_some() {
212                    PaymentMethodType::GooglePay
213                } else {
214                    PaymentMethodType::Other
215                }
216            } else {
217                PaymentMethodType::Card(Card::from(card))
218            }
219        } else {
220            PaymentMethodType::Other
221        };
222
223        Self {
224            stripe_payment_method_id: StripePaymentMethodId(value.id.as_str().to_string()),
225            payment_method_type,
226        }
227    }
228}
229
230/// The tier a subscription is on. This would apply to any [`SubscriptionType`]
231#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
232#[cfg_attr(feature = "backend", derive(sqlx::Type))]
233#[repr(i16)]
234pub enum SubscriptionTier {
235    /// Basic
236    Basic = 0,
237    /// Pro
238    Pro = 1,
239}
240
241/// Stripe subscription ID
242#[derive(Debug, Serialize, Deserialize, Clone)]
243#[cfg_attr(feature = "backend", derive(sqlx::Type), sqlx(transparent))]
244pub struct StripeSubscriptionId(String);
245
246#[cfg(feature = "backend")]
247impl From<stripe::SubscriptionId> for StripeSubscriptionId {
248    fn from(value: stripe::SubscriptionId) -> Self {
249        Self(value.as_str().to_owned())
250    }
251}
252
253#[cfg(feature = "backend")]
254impl TryFrom<StripeSubscriptionId> for stripe::SubscriptionId {
255    type Error = anyhow::Error;
256
257    fn try_from(value: StripeSubscriptionId) -> Result<Self, Self::Error> {
258        <Self as std::str::FromStr>::from_str(&value.0).map_err(Into::into)
259    }
260}
261
262/// Stripe invoice ID
263#[derive(Debug, Serialize, Deserialize, Clone)]
264#[cfg_attr(feature = "backend", derive(sqlx::Type), sqlx(transparent))]
265pub struct StripeInvoiceId(String);
266
267impl StripeInvoiceId {
268    /// Returns a copy of the inner value
269    pub fn inner(&self) -> String {
270        self.0.clone()
271    }
272}
273
274#[cfg(feature = "backend")]
275impl From<&stripe::InvoiceId> for StripeInvoiceId {
276    fn from(value: &stripe::InvoiceId) -> Self {
277        Self(value.as_str().to_owned())
278    }
279}
280
281/// Stripe product ID
282#[derive(Debug, Serialize, Deserialize, Clone)]
283#[cfg_attr(feature = "backend", derive(sqlx::Type), sqlx(transparent))]
284pub struct StripeProductId(String);
285
286/// Stripe price ID
287#[derive(Debug, Serialize, Deserialize, Clone)]
288#[cfg_attr(feature = "backend", derive(sqlx::Type), sqlx(transparent))]
289pub struct StripePriceId(String);
290
291impl From<StripePriceId> for String {
292    fn from(value: StripePriceId) -> Self {
293        value.0
294    }
295}
296
297/// The subscriptions billing interval
298#[derive(Debug, Display, Serialize, Deserialize, Clone, Copy)]
299#[cfg_attr(feature = "backend", derive(sqlx::Type))]
300#[repr(i16)]
301pub enum BillingInterval {
302    /// Subscription is billed monthly
303    Monthly = 0,
304    /// Subscription is billed yearly
305    Annually = 1,
306}
307
308impl BillingInterval {
309    /// str representation
310    pub fn as_str(&self) -> &'static str {
311        self.into()
312    }
313    /// display name
314    pub fn display_name(&self) -> &'static str {
315        match self {
316            BillingInterval::Annually => "Annually",
317            BillingInterval::Monthly => "Monthly",
318        }
319    }
320}
321
322impl TryFrom<&str> for BillingInterval {
323    type Error = ();
324
325    fn try_from(value: &str) -> Result<Self, Self::Error> {
326        match value {
327            "annually" => Ok(Self::Annually),
328            "monthly" => Ok(Self::Monthly),
329            _ => Err(()),
330        }
331    }
332}
333
334impl From<&BillingInterval> for &str {
335    fn from(value: &BillingInterval) -> Self {
336        match value {
337            BillingInterval::Annually => "annually",
338            BillingInterval::Monthly => "monthly",
339        }
340    }
341}
342
343/// Status of a subscription
344#[derive(Copy, Debug, Display, Serialize, Deserialize, Clone)]
345#[cfg_attr(feature = "backend", derive(sqlx::Type))]
346#[repr(i16)]
347pub enum SubscriptionStatus {
348    /// The subscription has been created, awaiting finalization from Stripe or paused.
349    Inactive = 0,
350    /// The subscription is active, i.e. not cancelled or expired.
351    Active = 1,
352    /// The subscription is cancelled but still active, i.e. not expired.
353    Canceled = 2,
354    /// The subscription is expired.
355    Expired = 3,
356    /// The subscription has been paused.
357    Paused = 4,
358}
359
360impl SubscriptionStatus {
361    /// Whether the subscription is still valid so that a teacher is able to make use of subscription
362    /// features.
363    #[must_use]
364    pub const fn is_valid(&self) -> bool {
365        matches!(self, Self::Active | Self::Canceled)
366    }
367
368    /// Whether the subscription is active.
369    #[must_use]
370    pub const fn is_active(&self) -> bool {
371        matches!(self, Self::Active)
372    }
373
374    /// Whether the subscription is canceled.
375    #[must_use]
376    pub const fn is_canceled(&self) -> bool {
377        matches!(self, Self::Canceled)
378    }
379
380    /// Whether the subscription is paused.
381    #[must_use]
382    pub const fn is_paused(&self) -> bool {
383        matches!(self, Self::Paused)
384    }
385}
386
387#[cfg(feature = "backend")]
388impl Default for SubscriptionStatus {
389    fn default() -> Self {
390        Self::Inactive
391    }
392}
393
394#[cfg(feature = "backend")]
395impl From<stripe::SubscriptionStatus> for SubscriptionStatus {
396    fn from(value: stripe::SubscriptionStatus) -> Self {
397        match value {
398            stripe::SubscriptionStatus::Incomplete | stripe::SubscriptionStatus::Paused => {
399                Self::Inactive
400            }
401            stripe::SubscriptionStatus::Active
402            | stripe::SubscriptionStatus::PastDue
403            | stripe::SubscriptionStatus::Trialing
404            | stripe::SubscriptionStatus::Unpaid => Self::Active,
405            stripe::SubscriptionStatus::Canceled => Self::Canceled,
406            stripe::SubscriptionStatus::IncompleteExpired => Self::Expired,
407        }
408    }
409}
410
411wrap_uuid! {
412    /// Local subscription ID
413    pub struct SubscriptionId
414}
415
416/// An existing subscription for a customer
417#[derive(Debug, Serialize, Deserialize, Clone)]
418pub struct Subscription {
419    /// The local subscription ID
420    pub subscription_id: SubscriptionId,
421    /// The Stripe subscription ID
422    pub stripe_subscription_id: StripeSubscriptionId,
423    /// The subscription type
424    pub subscription_plan_type: PlanType,
425    /// Whether the subscription auto-renews
426    pub auto_renew: bool,
427    /// The subscription status
428    pub status: SubscriptionStatus,
429    /// Whether the subscription is in a trial period
430    pub is_trial: bool,
431    /// When the subscriptions current period ends/expires
432    pub current_period_end: DateTime<Utc>,
433    /// Account ID to associate this subscription with.
434    pub account_id: AccountId,
435    /// ID of the latest unpaid invoice generated for this subscription
436    pub latest_invoice_id: Option<StripeInvoiceId>,
437    /// Amount due if any
438    pub amount_due_in_cents: Option<AmountInCents>,
439    /// Price of the subscription
440    pub price: AmountInCents,
441    /// A coupon which may have been applied to the subscription
442    pub applied_coupon: Option<AppliedCoupon>,
443    /// When the subscription was originally created.
444    pub created_at: DateTime<Utc>,
445    /// When the subscription was last updated.
446    #[serde(default)]
447    #[serde(skip_serializing_if = "Option::is_none")]
448    pub updated_at: Option<DateTime<Utc>>,
449}
450
451/// Details of a coupon applied to a subscription
452#[derive(Debug, Serialize, Deserialize, Clone)]
453pub struct AppliedCoupon {
454    /// Name of the coupon applied when the subscription was created
455    pub coupon_name: String,
456    /// If a coupon was applied, this would indicate the discount percent
457    #[serde(skip_serializing_if = "Option::is_none")]
458    pub coupon_percent: Option<Percent>,
459    /// Date the coupon is valid from on this subscription
460    pub coupon_from: DateTime<Utc>,
461    /// Date this coupon is valid until on this subscription
462    #[serde(skip_serializing_if = "Option::is_none")]
463    pub coupon_to: Option<DateTime<Utc>>,
464}
465
466/// Data used to create a new subscription record
467#[derive(Debug, Serialize, Deserialize, Clone)]
468#[cfg(feature = "backend")]
469pub struct CreateSubscriptionRecord {
470    /// The Stripe subscription ID
471    pub stripe_subscription_id: StripeSubscriptionId,
472    /// The subscription plan ID
473    pub subscription_plan_id: PlanId,
474    /// The subscription status
475    pub status: SubscriptionStatus,
476    /// When the subscriptions current period ends/expires
477    pub current_period_end: DateTime<Utc>,
478    /// Account ID to associate this subscription with
479    /// User ID to associate this subscription with
480    pub account_id: AccountId,
481    /// ID of the latest unpaid invoice generated for this subscription
482    pub latest_invoice_id: Option<StripeInvoiceId>,
483    /// Amount due if any
484    pub amount_due_in_cents: Option<AmountInCents>,
485    /// Price of the subscription without any discounts applied
486    pub price: AmountInCents,
487}
488
489/// Data used to update a new subscription record
490#[derive(Debug, Serialize, Deserialize, Clone)]
491#[cfg(feature = "backend")]
492pub struct UpdateSubscriptionRecord {
493    /// The Stripe subscription ID
494    pub stripe_subscription_id: StripeSubscriptionId,
495    /// The subscription plan ID
496    pub subscription_plan_id: UpdateNonNullable<PlanId>,
497    /// The subscription status
498    #[serde(default, skip_serializing_if = "UpdateNonNullable::is_keep")]
499    pub status: UpdateNonNullable<SubscriptionStatus>,
500    /// When the subscriptions current period ends/expires
501    #[serde(default, skip_serializing_if = "UpdateNonNullable::is_keep")]
502    pub current_period_end: UpdateNonNullable<DateTime<Utc>>,
503    /// ID of the latest unpaid invoice generated for this subscription
504    #[serde(default, skip_serializing_if = "UpdateNonNullable::is_keep")]
505    pub latest_invoice_id: UpdateNonNullable<StripeInvoiceId>,
506    /// Whether the subscription is in a trial period
507    #[serde(default, skip_serializing_if = "UpdateNonNullable::is_keep")]
508    pub is_trial: UpdateNonNullable<bool>,
509    /// Price of the subscription without any discounts applied
510    #[serde(default, skip_serializing_if = "UpdateNonNullable::is_keep")]
511    pub price: UpdateNonNullable<AmountInCents>,
512    /// Name of the coupon applied when the subscription was created
513    #[serde(default, skip_serializing_if = "UpdateNullable::is_keep")]
514    pub coupon_name: UpdateNullable<String>,
515    /// If a coupon was applied, this would indicate the discount percent
516    #[serde(default, skip_serializing_if = "UpdateNullable::is_keep")]
517    pub coupon_percent: UpdateNullable<Percent>,
518    /// Date the coupon is valid from on this subscription
519    #[serde(default, skip_serializing_if = "UpdateNullable::is_keep")]
520    pub coupon_from: UpdateNullable<DateTime<Utc>>,
521    /// Date this coupon is valid until on this subscription
522    #[serde(default, skip_serializing_if = "UpdateNullable::is_keep")]
523    pub coupon_to: UpdateNullable<DateTime<Utc>>,
524}
525
526#[cfg(feature = "backend")]
527impl UpdateSubscriptionRecord {
528    /// Create a new instance with just the stripe subscription ID set
529    #[must_use]
530    pub fn new(stripe_subscription_id: StripeSubscriptionId) -> Self {
531        Self {
532            stripe_subscription_id,
533            subscription_plan_id: Default::default(),
534            status: Default::default(),
535            current_period_end: Default::default(),
536            latest_invoice_id: Default::default(),
537            is_trial: Default::default(),
538            price: Default::default(),
539            coupon_name: Default::default(),
540            coupon_percent: Default::default(),
541            coupon_from: Default::default(),
542            coupon_to: Default::default(),
543        }
544    }
545}
546
547#[cfg(feature = "backend")]
548impl TryFrom<stripe::Subscription> for UpdateSubscriptionRecord {
549    type Error = anyhow::Error;
550
551    fn try_from(value: stripe::Subscription) -> Result<Self, Self::Error> {
552        use chrono::TimeZone;
553
554        let latest_invoice_id = value
555            .latest_invoice
556            .as_ref()
557            .map(|invoice| StripeInvoiceId::from(&invoice.id()))
558            .into();
559
560        let price = AmountInCents::from(
561            value
562                .items
563                .data
564                .get(0)
565                .map(|item| item.clone())
566                .ok_or(anyhow::anyhow!("Missing plan data"))?
567                .plan
568                .ok_or(anyhow::anyhow!("Missing stripe subscription plan"))?
569                .amount
570                .ok_or(anyhow::anyhow!("Missing subscription plan amount"))?,
571        );
572
573        let (coupon_name, coupon_percent, coupon_from, coupon_to) = value.discount.map_or_else(
574            || {
575                Ok((
576                    Default::default(),
577                    Default::default(),
578                    Default::default(),
579                    Default::default(),
580                ))
581            },
582            |discount| -> Result<_, Self::Error> {
583                let start_time = Some(
584                    Utc.timestamp_opt(discount.start, 0)
585                        .latest()
586                        .ok_or(anyhow::anyhow!("Invalid timestamp"))?,
587                );
588
589                let end_time = match discount.end {
590                    Some(end) => Some(
591                        Utc.timestamp_opt(end, 0)
592                            .latest()
593                            .ok_or(anyhow::anyhow!("Invalid timestamp"))?,
594                    ),
595                    None => None,
596                };
597
598                Ok((
599                    UpdateNullable::from(discount.coupon.name.map(|name| name.to_uppercase())),
600                    UpdateNullable::from(
601                        discount
602                            .coupon
603                            .percent_off
604                            .map(|percent| Percent::from(percent / 100.0)),
605                    ),
606                    UpdateNullable::from(start_time),
607                    UpdateNullable::from(end_time),
608                ))
609            },
610        )?;
611
612        Ok(Self {
613            stripe_subscription_id: value.id.into(),
614            subscription_plan_id: UpdateNonNullable::Keep,
615            is_trial: UpdateNonNullable::Change(matches!(
616                value.status,
617                stripe::SubscriptionStatus::Trialing
618            )),
619            // This is weird.
620            status: UpdateNonNullable::Change(if value.ended_at.is_some() {
621                SubscriptionStatus::Expired
622            } else if value.canceled_at.is_some() {
623                SubscriptionStatus::Canceled
624            } else if value.pause_collection.is_some() {
625                // If we're not receiving payments or issuing invoices, then this subscription is paused
626                SubscriptionStatus::Paused
627            } else {
628                SubscriptionStatus::from(value.status)
629            }),
630            current_period_end: UpdateNonNullable::Change(
631                Utc.timestamp_opt(value.current_period_end, 0)
632                    .latest()
633                    .ok_or(anyhow::anyhow!("Invalid timestamp"))?,
634            ),
635            latest_invoice_id,
636            price: UpdateNonNullable::Change(price),
637            coupon_name,
638            coupon_percent,
639            coupon_from,
640            coupon_to,
641        })
642    }
643}
644
645/// The limit of how many accounts can be associated with the subscription. [None] means unlimited.
646#[derive(Debug, Serialize, Deserialize, Clone, Copy, Eq, PartialEq, Hash, Ord, PartialOrd)]
647#[cfg_attr(feature = "backend", derive(sqlx::Type), sqlx(transparent))]
648pub struct AccountLimit(i64);
649
650impl From<i64> for AccountLimit {
651    fn from(value: i64) -> Self {
652        Self(value)
653    }
654}
655
656/// The type of subscription
657#[derive(Debug, Display, Serialize, Deserialize, Clone, Copy)]
658#[cfg_attr(feature = "backend", derive(sqlx::Type))]
659#[repr(i16)]
660pub enum SubscriptionType {
661    /// An individual subscription
662    Individual = 0,
663    /// A school subscription
664    School = 1,
665}
666
667/// Subscription plan tier
668#[derive(
669    Debug,
670    Default,
671    Display,
672    EnumString,
673    EnumIs,
674    AsRefStr,
675    Serialize,
676    Deserialize,
677    Clone,
678    Copy,
679    PartialEq,
680    Eq,
681)]
682#[cfg_attr(feature = "backend", derive(sqlx::Type))]
683#[repr(i16)]
684pub enum PlanTier {
685    /// Free tier
686    #[default]
687    Free = 0,
688    /// Basic tier
689    Basic = 1,
690    /// Pro tier
691    Pro = 2,
692}
693
694/// Possible individual subscription plans
695#[derive(
696    Debug, Serialize, Deserialize, Clone, Copy, Eq, Ord, PartialOrd, PartialEq, Hash, EnumIter,
697)]
698#[serde(rename_all = "kebab-case")]
699#[cfg_attr(feature = "backend", derive(sqlx::Type))]
700#[repr(i16)]
701pub enum PlanType {
702    /// Basic level, monthly
703    IndividualBasicMonthly = 0,
704    /// Basic level, annually
705    IndividualBasicAnnually = 1,
706    /// Pro level, monthly
707    IndividualProMonthly = 2,
708    /// Pro level, annually
709    IndividualProAnnually = 3,
710    /// School level 1 annually
711    SchoolLevel1Monthly = 4,
712    /// School level 2 annually
713    SchoolLevel2Monthly = 5,
714    /// School level 3 annually
715    SchoolLevel3Monthly = 6,
716    /// School level 4 annually
717    SchoolLevel4Monthly = 7,
718    /// School unlimited annually
719    SchoolUnlimitedMonthly = 8,
720    /// School level 1 monthly
721    SchoolLevel1Annually = 9,
722    /// School level 2 monthly
723    SchoolLevel2Annually = 10,
724    /// School level 3 monthly
725    SchoolLevel3Annually = 11,
726    /// School level 4 monthly
727    SchoolLevel4Annually = 12,
728    /// School unlimited monthly
729    SchoolUnlimitedAnnually = 13,
730}
731
732impl Display for PlanType {
733    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
734        let s = match self {
735            Self::IndividualBasicMonthly => "Individual Basic Monthly",
736            Self::IndividualBasicAnnually => "Individual Basic Annual",
737            Self::IndividualProMonthly => "Individual Pro Monthly",
738            Self::IndividualProAnnually => "Individual Pro Annual",
739            Self::SchoolLevel1Monthly => formatcp!(
740                "School - Up to {} Monthly",
741                PLAN_SCHOOL_LEVEL_1_TEACHER_COUNT
742            ),
743            Self::SchoolLevel2Monthly => formatcp!(
744                "School - Up to {} Monthly",
745                PLAN_SCHOOL_LEVEL_2_TEACHER_COUNT
746            ),
747            Self::SchoolLevel3Monthly => formatcp!(
748                "School - Up to {} Monthly",
749                PLAN_SCHOOL_LEVEL_3_TEACHER_COUNT
750            ),
751            Self::SchoolLevel4Monthly => formatcp!(
752                "School - Up to {} Monthly",
753                PLAN_SCHOOL_LEVEL_4_TEACHER_COUNT
754            ),
755            Self::SchoolUnlimitedMonthly => {
756                formatcp!("School - {}+ Monthly", PLAN_SCHOOL_LEVEL_4_TEACHER_COUNT)
757            }
758            Self::SchoolLevel1Annually => {
759                formatcp!("School - Up to {}", PLAN_SCHOOL_LEVEL_1_TEACHER_COUNT)
760            }
761            Self::SchoolLevel2Annually => {
762                formatcp!("School - Up to {}", PLAN_SCHOOL_LEVEL_2_TEACHER_COUNT)
763            }
764            Self::SchoolLevel3Annually => {
765                formatcp!("School - Up to {}", PLAN_SCHOOL_LEVEL_3_TEACHER_COUNT)
766            }
767            Self::SchoolLevel4Annually => {
768                formatcp!("School - Up to {}", PLAN_SCHOOL_LEVEL_4_TEACHER_COUNT)
769            }
770            Self::SchoolUnlimitedAnnually => {
771                formatcp!("School - {}+", PLAN_SCHOOL_LEVEL_4_TEACHER_COUNT)
772            }
773        };
774        write!(f, "{}", s)
775    }
776}
777
778impl PlanType {
779    /// Represents the plan type as a `str`
780    #[must_use]
781    pub const fn as_str(&self) -> &'static str {
782        match self {
783            Self::IndividualBasicMonthly => "individual-basic-monthly",
784            Self::IndividualBasicAnnually => "individual-basic-annually",
785            Self::IndividualProMonthly => "individual-pro-monthly",
786            Self::IndividualProAnnually => "individual-pro-annually",
787            Self::SchoolLevel1Monthly => "school-level-1-monthly",
788            Self::SchoolLevel2Monthly => "school-level-2-monthly",
789            Self::SchoolLevel3Monthly => "school-level-3-monthly",
790            Self::SchoolLevel4Monthly => "school-level-4-monthly",
791            Self::SchoolUnlimitedMonthly => "school-unlimited-monthly",
792            Self::SchoolLevel1Annually => "school-level-1-annually",
793            Self::SchoolLevel2Annually => "school-level-2-annually",
794            Self::SchoolLevel3Annually => "school-level-3-annually",
795            Self::SchoolLevel4Annually => "school-level-4-annually",
796            Self::SchoolUnlimitedAnnually => "school-unlimited-annually",
797        }
798    }
799
800    /// Get a readable name
801    #[must_use]
802    pub const fn display_name(&self) -> &'static str {
803        match self {
804            Self::IndividualBasicMonthly => "Individual - Basic monthly",
805            Self::IndividualBasicAnnually => "Individual - Basic annual",
806            Self::IndividualProMonthly => "Individual - Pro monthly",
807            Self::IndividualProAnnually => "Individual - Pro annual",
808            Self::SchoolLevel1Monthly => formatcp!(
809                "School - Up to {} teachers - Monthly",
810                PLAN_SCHOOL_LEVEL_1_TEACHER_COUNT
811            ),
812            Self::SchoolLevel2Monthly => formatcp!(
813                "School - Up to {} teachers - Monthly",
814                PLAN_SCHOOL_LEVEL_2_TEACHER_COUNT
815            ),
816            Self::SchoolLevel3Monthly => formatcp!(
817                "School - Up to {} teachers - Monthly",
818                PLAN_SCHOOL_LEVEL_3_TEACHER_COUNT
819            ),
820            Self::SchoolLevel4Monthly => formatcp!(
821                "School - Up to {} teachers - Monthly",
822                PLAN_SCHOOL_LEVEL_4_TEACHER_COUNT
823            ),
824            Self::SchoolUnlimitedMonthly => formatcp!(
825                "School - Up to {} teachers - Monthly",
826                PLAN_SCHOOL_UNLIMITED_TEACHER_COUNT
827            ),
828            Self::SchoolLevel1Annually => formatcp!(
829                "School - Up to {} teachers",
830                PLAN_SCHOOL_LEVEL_1_TEACHER_COUNT
831            ),
832            Self::SchoolLevel2Annually => formatcp!(
833                "School - Up to {} teachers",
834                PLAN_SCHOOL_LEVEL_2_TEACHER_COUNT
835            ),
836            Self::SchoolLevel3Annually => formatcp!(
837                "School - Up to {} teachers",
838                PLAN_SCHOOL_LEVEL_3_TEACHER_COUNT
839            ),
840            Self::SchoolLevel4Annually => formatcp!(
841                "School - Up to {} teachers",
842                PLAN_SCHOOL_LEVEL_4_TEACHER_COUNT
843            ),
844            Self::SchoolUnlimitedAnnually => formatcp!(
845                "School - Up to {} teachers",
846                PLAN_SCHOOL_UNLIMITED_TEACHER_COUNT
847            ),
848        }
849    }
850
851    /// Get a user-friendly name
852    #[must_use]
853    pub const fn user_display_name(&self) -> &'static str {
854        match self {
855            Self::IndividualBasicMonthly | Self::IndividualBasicAnnually => "Basic",
856            Self::IndividualProMonthly | Self::IndividualProAnnually => "Pro",
857            Self::SchoolLevel1Monthly | Self::SchoolLevel1Annually => {
858                formatcp!("Up to {} teachers", PLAN_SCHOOL_LEVEL_1_TEACHER_COUNT)
859            }
860            Self::SchoolLevel2Monthly | Self::SchoolLevel2Annually => {
861                formatcp!("Up to {} teachers", PLAN_SCHOOL_LEVEL_2_TEACHER_COUNT)
862            }
863            Self::SchoolLevel3Monthly | Self::SchoolLevel3Annually => {
864                formatcp!("Up to {} teachers", PLAN_SCHOOL_LEVEL_3_TEACHER_COUNT)
865            }
866            Self::SchoolLevel4Monthly | Self::SchoolLevel4Annually => {
867                formatcp!("Up to {} teachers", PLAN_SCHOOL_LEVEL_4_TEACHER_COUNT)
868            }
869            Self::SchoolUnlimitedMonthly | Self::SchoolUnlimitedAnnually => {
870                formatcp!("Up to {} teachers", PLAN_SCHOOL_UNLIMITED_TEACHER_COUNT)
871            }
872        }
873    }
874
875    /// `SubscriptionTier` of the current plan
876    #[must_use]
877    pub const fn subscription_tier(&self) -> SubscriptionTier {
878        match self {
879            Self::IndividualBasicMonthly | Self::IndividualBasicAnnually => SubscriptionTier::Basic,
880            _ => SubscriptionTier::Pro,
881        }
882    }
883
884    /// Account limit of the current plan
885    #[must_use]
886    pub const fn account_limit(&self) -> Option<AccountLimit> {
887        match self {
888            Self::SchoolLevel1Monthly | Self::SchoolLevel1Annually => {
889                Some(AccountLimit(PLAN_SCHOOL_LEVEL_1_TEACHER_COUNT))
890            }
891            Self::SchoolLevel2Monthly | Self::SchoolLevel2Annually => {
892                Some(AccountLimit(PLAN_SCHOOL_LEVEL_2_TEACHER_COUNT))
893            }
894            Self::SchoolLevel3Monthly | Self::SchoolLevel3Annually => {
895                Some(AccountLimit(PLAN_SCHOOL_LEVEL_3_TEACHER_COUNT))
896            }
897            Self::SchoolLevel4Monthly | Self::SchoolLevel4Annually => {
898                Some(AccountLimit(PLAN_SCHOOL_LEVEL_4_TEACHER_COUNT))
899            }
900            Self::SchoolUnlimitedMonthly | Self::SchoolUnlimitedAnnually => None,
901            Self::IndividualBasicMonthly
902            | Self::IndividualBasicAnnually
903            | Self::IndividualProMonthly
904            | Self::IndividualProAnnually => Some(AccountLimit(1)),
905        }
906    }
907
908    /// Subscription type of the current plant
909    #[must_use]
910    pub const fn subscription_type(&self) -> SubscriptionType {
911        match self {
912            Self::IndividualBasicMonthly
913            | Self::IndividualBasicAnnually
914            | Self::IndividualProMonthly
915            | Self::IndividualProAnnually => SubscriptionType::Individual,
916            _ => SubscriptionType::School,
917        }
918    }
919
920    /// Trial period of the current plan
921    #[must_use]
922    pub const fn trial_period(&self) -> TrialPeriod {
923        match self.subscription_type() {
924            SubscriptionType::Individual => TrialPeriod(INDIVIDUAL_TRIAL_PERIOD),
925            SubscriptionType::School => TrialPeriod(SCHOOL_TRIAL_PERIOD),
926        }
927    }
928
929    /// Billing interval for the current plan
930    #[must_use]
931    pub const fn billing_interval(&self) -> BillingInterval {
932        match self {
933            Self::IndividualBasicMonthly
934            | Self::IndividualProMonthly
935            | Self::SchoolLevel1Monthly
936            | Self::SchoolLevel2Monthly
937            | Self::SchoolLevel3Monthly
938            | Self::SchoolLevel4Monthly
939            | Self::SchoolUnlimitedMonthly => BillingInterval::Monthly,
940            Self::IndividualBasicAnnually
941            | Self::IndividualProAnnually
942            | Self::SchoolLevel1Annually
943            | Self::SchoolLevel2Annually
944            | Self::SchoolLevel3Annually
945            | Self::SchoolLevel4Annually
946            | Self::SchoolUnlimitedAnnually => BillingInterval::Annually,
947        }
948    }
949
950    /// Whether it is possible to upgrade from another plan type to self
951    #[must_use]
952    pub const fn can_upgrade_from(&self, from_type: &Self) -> bool {
953        // NOTE: Cannot go from any annual plan to a monthly plan.
954        match self {
955            Self::IndividualBasicMonthly => false,
956            Self::IndividualBasicAnnually | Self::IndividualProMonthly => {
957                matches!(from_type, Self::IndividualBasicMonthly)
958            }
959            Self::IndividualProAnnually => matches!(
960                from_type,
961                Self::IndividualBasicMonthly
962                    | Self::IndividualBasicAnnually
963                    | Self::IndividualProMonthly
964            ),
965            Self::SchoolLevel1Monthly => false,
966            Self::SchoolLevel2Monthly => matches!(from_type, Self::SchoolLevel1Monthly),
967            Self::SchoolLevel3Monthly => matches!(
968                from_type,
969                Self::SchoolLevel1Monthly | Self::SchoolLevel2Monthly,
970            ),
971            Self::SchoolLevel4Monthly => matches!(
972                from_type,
973                Self::SchoolLevel1Monthly | Self::SchoolLevel2Monthly | Self::SchoolLevel3Monthly,
974            ),
975            Self::SchoolUnlimitedMonthly => matches!(
976                from_type,
977                Self::SchoolLevel1Monthly
978                    | Self::SchoolLevel2Monthly
979                    | Self::SchoolLevel3Monthly
980                    | Self::SchoolLevel4Monthly,
981            ),
982            Self::SchoolLevel1Annually => matches!(from_type, Self::SchoolLevel1Monthly),
983            Self::SchoolLevel2Annually => matches!(
984                from_type,
985                Self::SchoolLevel1Monthly | Self::SchoolLevel2Monthly | Self::SchoolLevel1Annually
986            ),
987            Self::SchoolLevel3Annually => matches!(
988                from_type,
989                Self::SchoolLevel1Monthly
990                    | Self::SchoolLevel2Monthly
991                    | Self::SchoolLevel3Monthly
992                    | Self::SchoolLevel1Annually
993                    | Self::SchoolLevel2Annually
994            ),
995            Self::SchoolLevel4Annually => matches!(
996                from_type,
997                Self::SchoolLevel1Monthly
998                    | Self::SchoolLevel2Monthly
999                    | Self::SchoolLevel3Monthly
1000                    | Self::SchoolLevel4Monthly
1001                    | Self::SchoolLevel1Annually
1002                    | Self::SchoolLevel2Annually
1003                    | Self::SchoolLevel3Annually
1004            ),
1005            Self::SchoolUnlimitedAnnually => matches!(
1006                from_type,
1007                Self::SchoolLevel1Monthly
1008                    | Self::SchoolLevel2Monthly
1009                    | Self::SchoolLevel3Monthly
1010                    | Self::SchoolLevel4Monthly
1011                    | Self::SchoolUnlimitedMonthly
1012                    | Self::SchoolLevel1Annually
1013                    | Self::SchoolLevel2Annually
1014                    | Self::SchoolLevel3Annually
1015                    | Self::SchoolLevel4Annually
1016            ),
1017        }
1018    }
1019
1020    /// Whether it is possible to upgrade from another plan type to self in the same billing interval
1021    #[must_use]
1022    pub const fn can_upgrade_from_same_interval(&self, from_type: &Self) -> bool {
1023        match self {
1024            Self::IndividualProAnnually => matches!(from_type, Self::IndividualBasicAnnually,),
1025            Self::SchoolLevel1Monthly => false,
1026            Self::SchoolLevel2Monthly => matches!(from_type, Self::SchoolLevel1Monthly),
1027            Self::SchoolLevel3Monthly => matches!(
1028                from_type,
1029                Self::SchoolLevel1Monthly | Self::SchoolLevel2Monthly,
1030            ),
1031            Self::SchoolLevel4Monthly => matches!(
1032                from_type,
1033                Self::SchoolLevel1Monthly | Self::SchoolLevel2Monthly | Self::SchoolLevel3Monthly,
1034            ),
1035            Self::SchoolUnlimitedMonthly => matches!(
1036                from_type,
1037                Self::SchoolLevel1Monthly
1038                    | Self::SchoolLevel2Monthly
1039                    | Self::SchoolLevel3Monthly
1040                    | Self::SchoolLevel4Monthly,
1041            ),
1042            Self::SchoolLevel1Annually => false,
1043            Self::SchoolLevel2Annually => matches!(from_type, Self::SchoolLevel1Annually),
1044            Self::SchoolLevel3Annually => matches!(
1045                from_type,
1046                Self::SchoolLevel1Annually | Self::SchoolLevel2Annually
1047            ),
1048            Self::SchoolLevel4Annually => matches!(
1049                from_type,
1050                Self::SchoolLevel1Annually
1051                    | Self::SchoolLevel2Annually
1052                    | Self::SchoolLevel3Annually
1053            ),
1054            Self::SchoolUnlimitedAnnually => matches!(
1055                from_type,
1056                Self::SchoolLevel1Annually
1057                    | Self::SchoolLevel2Annually
1058                    | Self::SchoolLevel3Annually
1059                    | Self::SchoolLevel4Annually
1060            ),
1061            _ => false,
1062        }
1063    }
1064
1065    /// check if is individual plan
1066    #[must_use]
1067    pub const fn is_individual_plan(&self) -> bool {
1068        matches!(
1069            self,
1070            Self::IndividualBasicMonthly
1071                | Self::IndividualBasicAnnually
1072                | Self::IndividualProMonthly
1073                | Self::IndividualProAnnually
1074        )
1075    }
1076
1077    /// check if is school plan
1078    #[must_use]
1079    pub const fn is_school_plan(&self) -> bool {
1080        match self {
1081            PlanType::IndividualBasicMonthly
1082            | PlanType::IndividualBasicAnnually
1083            | PlanType::IndividualProMonthly
1084            | PlanType::IndividualProAnnually => false,
1085            PlanType::SchoolLevel1Monthly
1086            | PlanType::SchoolLevel2Monthly
1087            | PlanType::SchoolLevel3Monthly
1088            | PlanType::SchoolLevel4Monthly
1089            | PlanType::SchoolUnlimitedMonthly
1090            | PlanType::SchoolLevel1Annually
1091            | PlanType::SchoolLevel2Annually
1092            | PlanType::SchoolLevel3Annually
1093            | PlanType::SchoolLevel4Annually
1094            | PlanType::SchoolUnlimitedAnnually => true,
1095        }
1096    }
1097
1098    /// The tier this plan type is associated with
1099    #[must_use]
1100    pub const fn plan_tier(&self) -> PlanTier {
1101        match self {
1102            PlanType::IndividualProMonthly
1103            | PlanType::IndividualProAnnually
1104            | PlanType::SchoolLevel1Monthly
1105            | PlanType::SchoolLevel2Monthly
1106            | PlanType::SchoolLevel3Monthly
1107            | PlanType::SchoolLevel4Monthly
1108            | PlanType::SchoolUnlimitedMonthly
1109            | PlanType::SchoolLevel1Annually
1110            | PlanType::SchoolLevel2Annually
1111            | PlanType::SchoolLevel3Annually
1112            | PlanType::SchoolLevel4Annually
1113            | PlanType::SchoolUnlimitedAnnually => PlanTier::Pro,
1114            PlanType::IndividualBasicMonthly | PlanType::IndividualBasicAnnually => PlanTier::Basic,
1115        }
1116    }
1117
1118    /// Price for each plan.
1119    pub const fn plan_price(&self) -> u32 {
1120        match self {
1121            Self::IndividualBasicMonthly => PLAN_PRICE_MONTHLY_BASIC,
1122            Self::IndividualBasicAnnually => PLAN_PRICE_ANNUAL_BASIC,
1123            Self::IndividualProMonthly => PLAN_PRICE_MONTHLY_PRO,
1124            Self::IndividualProAnnually => PLAN_PRICE_ANNUAL_PRO,
1125            Self::SchoolLevel1Monthly => PLAN_PRICE_MONTHLY_SCHOOL_1,
1126            Self::SchoolLevel1Annually => PLAN_PRICE_ANNUAL_SCHOOL_1,
1127            Self::SchoolLevel2Monthly => PLAN_PRICE_MONTHLY_SCHOOL_2,
1128            Self::SchoolLevel2Annually => PLAN_PRICE_ANNUAL_SCHOOL_2,
1129            Self::SchoolLevel3Monthly => PLAN_PRICE_MONTHLY_SCHOOL_3,
1130            Self::SchoolLevel3Annually => PLAN_PRICE_ANNUAL_SCHOOL_3,
1131            Self::SchoolLevel4Monthly => PLAN_PRICE_MONTHLY_SCHOOL_4,
1132            Self::SchoolLevel4Annually => PLAN_PRICE_ANNUAL_SCHOOL_4,
1133            Self::SchoolUnlimitedMonthly => PLAN_PRICE_MONTHLY_SCHOOL_UNLIMITED,
1134            Self::SchoolUnlimitedAnnually => PLAN_PRICE_ANNUAL_SCHOOL_UNLIMITED,
1135        }
1136    }
1137
1138    /// Monthly version of annual plan.
1139    pub const fn annual_to_monthly(&self) -> PlanType {
1140        match self {
1141            Self::IndividualBasicAnnually => Self::IndividualBasicMonthly,
1142            Self::IndividualProAnnually => Self::IndividualProMonthly,
1143            Self::SchoolLevel1Annually => Self::SchoolLevel1Monthly,
1144            Self::SchoolLevel2Annually => Self::SchoolLevel2Monthly,
1145            Self::SchoolLevel3Annually => Self::SchoolLevel3Monthly,
1146            Self::SchoolLevel4Annually => Self::SchoolLevel4Monthly,
1147            Self::SchoolUnlimitedAnnually => Self::SchoolUnlimitedMonthly,
1148            _ => panic!(),
1149        }
1150    }
1151
1152    /// Annual version of monthly plan.
1153    pub const fn monthly_to_annual(&self) -> PlanType {
1154        match self {
1155            Self::IndividualBasicMonthly => Self::IndividualBasicAnnually,
1156            Self::IndividualProMonthly => Self::IndividualProAnnually,
1157            Self::SchoolLevel1Monthly => Self::SchoolLevel1Annually,
1158            Self::SchoolLevel2Monthly => Self::SchoolLevel2Annually,
1159            Self::SchoolLevel3Monthly => Self::SchoolLevel3Annually,
1160            Self::SchoolLevel4Monthly => Self::SchoolLevel4Annually,
1161            Self::SchoolUnlimitedMonthly => Self::SchoolUnlimitedAnnually,
1162            _ => panic!(),
1163        }
1164    }
1165
1166    /// Pro version of basic plan.
1167    pub const fn basic_to_pro(&self) -> PlanType {
1168        match self {
1169            Self::IndividualBasicMonthly => Self::IndividualProMonthly,
1170            Self::IndividualBasicAnnually => Self::IndividualProAnnually,
1171            _ => panic!(),
1172        }
1173    }
1174}
1175
1176impl TryFrom<&str> for PlanType {
1177    type Error = ();
1178
1179    fn try_from(s: &str) -> Result<Self, Self::Error> {
1180        match s {
1181            "individual-basic-monthly" => Ok(Self::IndividualBasicMonthly),
1182            "individual-basic-annually" => Ok(Self::IndividualBasicAnnually),
1183            "individual-pro-monthly" => Ok(Self::IndividualProMonthly),
1184            "individual-pro-annually" => Ok(Self::IndividualProAnnually),
1185            "school-level-1-monthly" => Ok(Self::SchoolLevel1Monthly),
1186            "school-level-2-monthly" => Ok(Self::SchoolLevel2Monthly),
1187            "school-level-3-monthly" => Ok(Self::SchoolLevel3Monthly),
1188            "school-level-4-monthly" => Ok(Self::SchoolLevel4Monthly),
1189            "school-unlimited-monthly" => Ok(Self::SchoolUnlimitedMonthly),
1190            "school-level-1-annually" => Ok(Self::SchoolLevel1Annually),
1191            "school-level-2-annually" => Ok(Self::SchoolLevel2Annually),
1192            "school-level-3-annually" => Ok(Self::SchoolLevel3Annually),
1193            "school-level-4-annually" => Ok(Self::SchoolLevel4Annually),
1194            "school-unlimited-annually" => Ok(Self::SchoolUnlimitedAnnually),
1195            _ => Err(()),
1196        }
1197    }
1198}
1199
1200/// The type of account
1201#[derive(Debug, Display, Serialize, Deserialize, Clone, Copy)]
1202#[cfg_attr(feature = "backend", derive(sqlx::Type))]
1203#[repr(i16)]
1204pub enum AccountType {
1205    /// An individual account
1206    Individual = 0,
1207    /// A school account
1208    School = 1,
1209}
1210
1211impl AccountType {
1212    /// Whether this account type has a dedicated admin user
1213    pub fn has_admin(&self) -> bool {
1214        match self {
1215            Self::School => true,
1216            _ => false,
1217        }
1218    }
1219
1220    /// Test whether this variant matches the variant in SubscriptionType
1221    pub fn matches_subscription_type(&self, subscription_type: &SubscriptionType) -> bool {
1222        match self {
1223            Self::Individual => matches!(subscription_type, SubscriptionType::Individual),
1224            Self::School => matches!(subscription_type, SubscriptionType::School),
1225        }
1226    }
1227}
1228
1229impl From<SubscriptionType> for AccountType {
1230    fn from(value: SubscriptionType) -> Self {
1231        match value {
1232            SubscriptionType::Individual => Self::Individual,
1233            SubscriptionType::School => Self::School,
1234        }
1235    }
1236}
1237
1238/// Stripe invoice number
1239#[derive(Debug, Serialize, Deserialize, Clone)]
1240#[cfg_attr(feature = "backend", derive(sqlx::Type), sqlx(transparent))]
1241pub struct InvoiceNumber(String);
1242
1243/// Represents an amount in cents
1244#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
1245#[cfg_attr(feature = "backend", derive(sqlx::Type), sqlx(transparent))]
1246pub struct AmountInCents(i64);
1247
1248impl AmountInCents {
1249    /// Create a new instance
1250    pub fn new(amount: i64) -> Self {
1251        Self(amount)
1252    }
1253
1254    /// Returns a copy of the inner value
1255    pub fn inner(&self) -> i64 {
1256        self.0
1257    }
1258}
1259
1260impl From<i64> for AmountInCents {
1261    fn from(value: i64) -> Self {
1262        Self(value)
1263    }
1264}
1265
1266impl Display for AmountInCents {
1267    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1268        write!(f, "{:.2}", self.0 as f64 / 100.)
1269    }
1270}
1271
1272/// Represents a trial period length
1273#[derive(Debug, Serialize, Deserialize, Clone)]
1274#[cfg_attr(feature = "backend", derive(sqlx::Type), sqlx(transparent))]
1275pub struct TrialPeriod(i64);
1276
1277impl TrialPeriod {
1278    /// Create a new instance
1279    pub fn new(length: i64) -> Self {
1280        Self(length)
1281    }
1282
1283    /// Returns a copy of the inner value
1284    pub fn inner(&self) -> i64 {
1285        self.0
1286    }
1287}
1288
1289wrap_uuid! {
1290    /// Local charge ID
1291    pub struct ChargeId
1292}
1293
1294/// A charge to a customer for a subscription
1295// TODO this may need to be updated
1296pub struct Charge {
1297    /// Local ID of the charge
1298    pub charge_id: ChargeId,
1299    /// Timestamp of charge
1300    pub charged_at: DateTime<Utc>,
1301    /// Subscription tier at the time of charge
1302    pub subscription_tier: SubscriptionTier,
1303    /// Payment method used at the time of charge
1304    pub payment_method: PaymentMethod,
1305    /// Stripe invoice number
1306    pub invoice_number: InvoiceNumber,
1307    /// Amount charged in cents
1308    pub amount_in_cents: AmountInCents,
1309}
1310
1311wrap_uuid! {
1312    /// Local subscription plan ID
1313    pub struct PlanId
1314}
1315
1316/// A subscription plan
1317///
1318/// In Stripe this would correspond to a Price within a Product.
1319#[derive(Debug, Serialize, Deserialize, Clone)]
1320#[cfg_attr(feature = "backend", derive(sqlx::FromRow))]
1321pub struct SubscriptionPlan {
1322    /// Local ID of the subscription plan
1323    pub plan_id: PlanId,
1324    /// Plan type
1325    pub plan_type: PlanType,
1326    /// Stripe price ID
1327    pub price_id: StripePriceId,
1328    /// When the plan was originally created.
1329    pub created_at: DateTime<Utc>,
1330    /// When the plan was last updated.
1331    pub updated_at: Option<DateTime<Utc>>,
1332}
1333
1334make_path_parts!(SubscriptionPlanPath => "/v1/plans");
1335
1336/// Request to create or update a subscription plans
1337#[derive(Debug, Serialize, Deserialize, Clone)]
1338pub struct UpdateSubscriptionPlansRequest {
1339    /// Map of price ids
1340    #[serde(flatten)]
1341    pub plans: HashMap<PlanType, StripePriceId>,
1342}
1343
1344/// Request to create a subscription.
1345///
1346/// If no payment method information is passed with, then the system will attempt to use the
1347/// users existing payment method. Otherwise, a payment method will be saved.
1348#[derive(Debug, Serialize, Deserialize, Clone)]
1349pub struct CreateSubscriptionRequest {
1350    /// Optional setup intent ID if a payment method was created prior to subscribing. Setting this
1351    /// mark the payment method as the default payment method.
1352    pub setup_intent_id: Option<String>,
1353    /// Plan to create the subscription for
1354    pub plan_type: PlanType,
1355    /// Promotion code
1356    pub promotion_code: Option<String>,
1357}
1358
1359make_path_parts!(CreateSubscriptionPath => "/v1/billing/subscribe");
1360
1361/// Create subscription response.
1362#[derive(Debug, Serialize, Deserialize, Clone)]
1363pub struct CreateSubscriptionResponse {
1364    /// The *Stripe* subscription ID
1365    pub subscription_id: StripeSubscriptionId,
1366    /// The client secret from Stripe for reference when adding a payment method
1367    ///
1368    /// `None` indicates that the subscription was created without requiring a new payment method to
1369    /// be added.
1370    pub client_secret: String,
1371}
1372
1373/// Request to create a subscription.
1374///
1375/// If no payment method information is passed with, then the system will attempt to use the
1376/// users existing payment method. Otherwise, a payment method will be saved.
1377#[derive(Debug, Serialize, Deserialize, Clone)]
1378pub struct CreateSetupIntentRequest {
1379    /// Plan to create the subscription for
1380    pub plan_type: PlanType,
1381}
1382
1383make_path_parts!(CreateSetupIntentPath => "/v1/billing/payment-method");
1384
1385wrap_uuid! {
1386    /// Account ID
1387    pub struct AccountId
1388}
1389
1390/// A billing account
1391#[derive(Debug, Serialize, Deserialize, Clone)]
1392pub struct Account {
1393    /// Account ID
1394    pub account_id: AccountId,
1395    /// The type of account
1396    pub account_type: AccountType,
1397    /// The customer ID on stripe
1398    #[serde(default)]
1399    #[serde(skip_serializing_if = "Option::is_none")]
1400    pub stripe_customer_id: Option<CustomerId>,
1401    /// Stripe payment method, if any
1402    #[serde(default)]
1403    #[serde(skip_serializing_if = "Option::is_none")]
1404    pub payment_method: Option<PaymentMethod>,
1405    /// _Current_ subscription if any
1406    #[serde(default)]
1407    #[serde(skip_serializing_if = "Option::is_none")]
1408    pub subscription: Option<Subscription>,
1409    /// When the account was created.
1410    pub created_at: DateTime<Utc>,
1411    /// When the account was last updated.
1412    #[serde(default)]
1413    #[serde(skip_serializing_if = "Option::is_none")]
1414    pub updated_at: Option<DateTime<Utc>>,
1415}
1416
1417/// Summary of the user's account. This could be a school account that a user is a member of.
1418///
1419/// In the case that the user is a member of a school account, the subscription tier would be
1420/// `None` for a free account, or `Pro`.
1421#[derive(Debug, Serialize, Deserialize, Clone)]
1422pub struct UserAccountSummary {
1423    /// Account ID
1424    pub account_id: Option<AccountId>,
1425    /// ID of the school if this is a School account
1426    pub school_id: Option<SchoolId>,
1427    /// Name of the school if this is a School account
1428    pub school_name: Option<String>,
1429    /// The type of plan the user's account is subscribed to
1430    pub plan_type: Option<PlanType>,
1431    /// The plan tier
1432    pub plan_tier: PlanTier,
1433    /// Whether the tier has been overridden
1434    pub overridden: bool,
1435    /// Status of the accounts subscription, if any
1436    pub subscription_status: Option<SubscriptionStatus>,
1437    /// Whether this user is an admin. For non School accounts, this user will
1438    /// always be an admin
1439    pub is_admin: bool,
1440    /// Whether the account is overdue
1441    pub overdue: bool,
1442    /// Whether the user is verified for the account
1443    pub verified: bool,
1444}
1445
1446wrap_uuid! {
1447    /// Wrapper type around [`Uuid`], represents the ID of a School.
1448    pub struct SchoolId
1449}
1450
1451/// A school profile.
1452#[derive(Debug, Serialize, Deserialize, Clone)]
1453pub struct School {
1454    /// The school's id.
1455    pub id: SchoolId,
1456
1457    /// Name of the school
1458    pub school_name: String,
1459
1460    /// The school's location
1461    #[serde(default)]
1462    #[serde(skip_serializing_if = "Option::is_none")]
1463    pub location: Option<Value>,
1464
1465    /// The school's email address
1466    pub email: String,
1467
1468    /// Description for school
1469    #[serde(default)]
1470    #[serde(skip_serializing_if = "Option::is_none")]
1471    pub description: Option<String>,
1472
1473    /// ID to the school's profile image in the user image library.
1474    #[serde(default)]
1475    #[serde(skip_serializing_if = "Option::is_none")]
1476    pub profile_image: Option<ImageId>,
1477
1478    /// Website for the school
1479    #[serde(default)]
1480    #[serde(skip_serializing_if = "Option::is_none")]
1481    pub website: Option<String>,
1482
1483    /// Organization type
1484    #[serde(default)]
1485    #[serde(skip_serializing_if = "Option::is_none")]
1486    pub organization_type: Option<String>,
1487
1488    /// The school's account ID
1489    pub account_id: AccountId,
1490
1491    /// When the school was created.
1492    pub created_at: DateTime<Utc>,
1493
1494    /// When the school was last updated.
1495    #[serde(default)]
1496    #[serde(skip_serializing_if = "Option::is_none")]
1497    pub updated_at: Option<DateTime<Utc>>,
1498}
1499
1500/// Same as [`School`] but includes internal fields
1501#[derive(Debug, Serialize, Deserialize, Clone)]
1502pub struct AdminSchool {
1503    /// The school's id.
1504    pub id: SchoolId,
1505
1506    /// Name of the school
1507    pub school_name: String,
1508
1509    /// Internal name of the school
1510    pub internal_school_name: Option<SchoolName>,
1511
1512    /// Whether the school is verified
1513    pub verified: bool,
1514
1515    /// The school's location
1516    #[serde(default)]
1517    #[serde(skip_serializing_if = "Option::is_none")]
1518    pub location: Option<Value>,
1519
1520    /// The school's email address
1521    pub email: String,
1522
1523    /// Description for school
1524    #[serde(default)]
1525    #[serde(skip_serializing_if = "Option::is_none")]
1526    pub description: Option<String>,
1527
1528    /// ID to the school's profile image in the user image library.
1529    #[serde(default)]
1530    #[serde(skip_serializing_if = "Option::is_none")]
1531    pub profile_image: Option<ImageId>,
1532
1533    /// Website for the school
1534    #[serde(default)]
1535    #[serde(skip_serializing_if = "Option::is_none")]
1536    pub website: Option<String>,
1537
1538    /// Organization type
1539    #[serde(default)]
1540    #[serde(skip_serializing_if = "Option::is_none")]
1541    pub organization_type: Option<String>,
1542
1543    /// The school's account ID
1544    pub account_id: AccountId,
1545
1546    /// When the school was created.
1547    pub created_at: DateTime<Utc>,
1548
1549    /// When the school was last updated.
1550    #[serde(default)]
1551    #[serde(skip_serializing_if = "Option::is_none")]
1552    pub updated_at: Option<DateTime<Utc>>,
1553}
1554
1555/// A user associated with an account
1556#[derive(Debug, Serialize, Deserialize, Clone)]
1557pub struct AccountUser {
1558    /// The associated user
1559    pub user: UserProfile,
1560    /// Whether this user is an admin. For non School accounts, this user will
1561    /// always be an admin
1562    pub is_admin: bool,
1563    /// Whether the user is verified for the account
1564    pub verified: bool,
1565}
1566
1567wrap_uuid! {
1568    /// Wrapper type around [`Uuid`], represents the ID of a School Name.
1569    pub struct SchoolNameId
1570}
1571
1572/// A known school name
1573#[derive(Debug, Serialize, Deserialize, Clone)]
1574pub struct SchoolName {
1575    /// The id of a school name
1576    pub id: SchoolNameId,
1577    /// The school name
1578    pub name: String,
1579}
1580
1581/// Representation of a school name value
1582#[derive(Debug, Serialize, Deserialize, Clone)]
1583#[serde(transparent)]
1584pub struct SchoolNameValue(String);
1585
1586impl fmt::Display for SchoolNameValue {
1587    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1588        write!(f, "{}", self.0)
1589    }
1590}
1591
1592impl From<SchoolNameValue> for String {
1593    fn from(value: SchoolNameValue) -> Self {
1594        value.0
1595    }
1596}
1597
1598impl From<String> for SchoolNameValue {
1599    fn from(value: String) -> Self {
1600        SchoolNameValue(value)
1601    }
1602}
1603
1604impl AsRef<str> for SchoolNameValue {
1605    fn as_ref(&self) -> &str {
1606        &self.0
1607    }
1608}
1609
1610/// Whether the user is creating a new school name or chosen an existing name that we know about
1611#[derive(Debug, Serialize, Deserialize, Clone)]
1612pub enum SchoolNameRequest {
1613    /// Attempt to create a new name
1614    Value(SchoolNameValue),
1615    /// Use an existing name
1616    Id(SchoolNameId),
1617}
1618
1619make_path_parts!(CreateSchoolAccountPath => "/v1/schools");
1620
1621/// Request to create a new school account
1622#[derive(Debug, Serialize, Deserialize, Clone)]
1623pub struct CreateSchoolAccountRequest {
1624    /// School name
1625    pub name: String,
1626
1627    /// The school's email address
1628    pub email: String,
1629
1630    /// School location
1631    #[serde(default)]
1632    #[serde(skip_serializing_if = "Option::is_none")]
1633    pub location: Option<Value>,
1634
1635    /// Description for school
1636    #[serde(default)]
1637    #[serde(skip_serializing_if = "Option::is_none")]
1638    pub description: Option<String>,
1639
1640    /// ID to the school's profile image in the user image library.
1641    #[serde(default)]
1642    #[serde(skip_serializing_if = "Option::is_none")]
1643    pub profile_image: Option<ImageId>,
1644
1645    /// Website for the school
1646    #[serde(default)]
1647    #[serde(skip_serializing_if = "Option::is_none")]
1648    pub website: Option<String>,
1649
1650    /// Organization type
1651    #[serde(default)]
1652    #[serde(skip_serializing_if = "Option::is_none")]
1653    pub organization_type: Option<String>,
1654}
1655
1656make_path_parts!(SchoolAccountPath => "/v1/schools/{}" => SchoolId);
1657
1658/// Request to create a new school account
1659#[derive(Debug, Serialize, Deserialize, Clone)]
1660pub struct GetSchoolAccountResponse {
1661    /// School name
1662    pub school: School,
1663    /// Account associated with the school
1664    pub account: AccountIfAuthorized,
1665    /// School location
1666    pub users: Vec<AccountUser>,
1667}
1668
1669/// A school account only if the user requesting the account is a system admin or an account admin.
1670#[derive(Debug, Serialize, Deserialize, Clone)]
1671#[allow(clippy::large_enum_variant)]
1672#[serde(untagged)]
1673pub enum AccountIfAuthorized {
1674    /// The user is authorized
1675    Authorized(Account),
1676    /// The user is not authorized
1677    Unauthorized,
1678}
1679
1680/// Request to update a school profile.
1681#[derive(Debug, Default, Serialize, Deserialize, Clone)]
1682pub struct UpdateSchoolAccountRequest {
1683    /// The school's email address
1684    #[serde(default, skip_serializing_if = "UpdateNonNullable::is_keep")]
1685    pub email: UpdateNonNullable<String>,
1686
1687    /// The school's name
1688    #[serde(default, skip_serializing_if = "UpdateNonNullable::is_keep")]
1689    pub school_name: UpdateNonNullable<String>,
1690
1691    /// The school's location
1692    #[serde(default, skip_serializing_if = "UpdateNullable::is_keep")]
1693    pub location: UpdateNullable<Value>,
1694
1695    /// Description for school
1696    #[serde(default, skip_serializing_if = "UpdateNullable::is_keep")]
1697    pub description: UpdateNullable<String>,
1698
1699    /// ID to the school's profile image in the user image library.
1700    #[serde(default, skip_serializing_if = "UpdateNullable::is_keep")]
1701    pub profile_image: UpdateNullable<ImageId>,
1702
1703    /// Website for the school
1704    #[serde(default, skip_serializing_if = "UpdateNullable::is_keep")]
1705    pub website: UpdateNullable<String>,
1706
1707    /// Organization type
1708    #[serde(default, skip_serializing_if = "UpdateNullable::is_keep")]
1709    pub organization_type: UpdateNullable<String>,
1710}
1711
1712make_path_parts!(IndividualAccountPath => "/v1/user/me/account");
1713
1714/// Individual account response
1715#[derive(Debug, Default, Serialize, Deserialize, Clone)]
1716pub struct IndividualAccountResponse {
1717    /// The users account, if any
1718    pub account: Option<Account>,
1719}
1720
1721/// Set a subscriptions cancellation status
1722#[derive(Debug, Serialize, Deserialize, Clone)]
1723#[serde(rename_all = "kebab-case")]
1724pub enum CancellationStatus {
1725    /// Cancel a subscription at the period end
1726    #[serde(rename = "period-end")]
1727    CancelAtPeriodEnd,
1728    /// Remove a cancellation on a subscription
1729    #[serde(rename = "remove")]
1730    RemoveCancellation,
1731}
1732
1733/// Whether to cancel a subscription at period end or to remove a cancellation status.
1734#[derive(Debug, Serialize, Deserialize, Clone)]
1735pub struct SubscriptionCancellationStatusRequest {
1736    /// Set the cancellation status of a subscription
1737    pub status: CancellationStatus,
1738}
1739
1740make_path_parts!(UpdateSubscriptionCancellationPath => "/v1/billing/subscription/cancel");
1741
1742/// Whether a subscription is paused
1743#[derive(Debug, Serialize, Deserialize, Clone)]
1744pub struct SubscriptionPauseRequest {
1745    /// Set the cancellation status of a subscription
1746    pub paused: bool,
1747}
1748
1749make_path_parts!(UpdateSubscriptionPausedPath => "/v1/billing/subscription/pause");
1750
1751/// Request to upgrade a subscription plan
1752#[derive(Debug, Serialize, Deserialize, Clone)]
1753pub struct UpgradeSubscriptionPlanRequest {
1754    /// The plan type to upgrade to
1755    pub plan_type: PlanType,
1756    /// Promotion code
1757    #[serde(default)]
1758    #[serde(skip_serializing_if = "Option::is_none")]
1759    pub promotion_code: Option<String>,
1760}
1761
1762/// Request to upgrade a subscription plan
1763#[derive(Debug, Serialize, Deserialize, Clone)]
1764pub struct AdminUpgradeSubscriptionPlanRequest {
1765    /// The plan type to upgrade to
1766    pub plan_type: PlanType,
1767    /// User ID if the request is being made by an admin
1768    pub user_id: UserId,
1769}
1770
1771make_path_parts!(UpgradeSubscriptionPlanPath => "/v1/billing/subscription/upgrade");
1772
1773make_path_parts!(AdminUpgradeSubscriptionPlanPath => "/v1/admin/billing/subscription/upgrade");
1774
1775make_path_parts!(CreateCustomerPortalLinkPath => "/v1/billing/customer-portal");