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