1use 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
18pub const PLAN_SCHOOL_LEVEL_1_TEACHER_COUNT: i64 = 5;
23pub const PLAN_SCHOOL_LEVEL_2_TEACHER_COUNT: i64 = 10;
25pub const PLAN_SCHOOL_LEVEL_3_TEACHER_COUNT: i64 = 15;
27pub const PLAN_SCHOOL_LEVEL_4_TEACHER_COUNT: i64 = 20;
29
30pub const INDIVIDUAL_TRIAL_PERIOD: i64 = 7;
32pub const SCHOOL_TRIAL_PERIOD: i64 = 14;
34
35pub const PLAN_PRICE_MONTHLY_BASIC: u32 = 17_99;
37pub const PLAN_PRICE_ANNUAL_BASIC: u32 = 180_00;
39pub const PLAN_PRICE_MONTHLY_PRO: u32 = 29_99;
41pub const PLAN_PRICE_ANNUAL_PRO: u32 = 300_00;
43pub const PLAN_PRICE_MONTHLY_SCHOOL_1: u32 = 115_00;
45pub const PLAN_PRICE_ANNUAL_SCHOOL_1: u32 = 1_250_00;
47pub const PLAN_PRICE_MONTHLY_SCHOOL_2: u32 = 150_00;
49pub const PLAN_PRICE_ANNUAL_SCHOOL_2: u32 = 1_500_00;
51pub const PLAN_PRICE_MONTHLY_SCHOOL_3: u32 = 200_00;
53pub const PLAN_PRICE_ANNUAL_SCHOOL_3: u32 = 2_000_00;
55pub const PLAN_PRICE_MONTHLY_SCHOOL_4: u32 = 250_00;
57pub const PLAN_PRICE_ANNUAL_SCHOOL_4: u32 = 2_500_00;
59pub const PLAN_PRICE_MONTHLY_SCHOOL_UNLIMITED: u32 = 300_00;
61pub const PLAN_PRICE_ANNUAL_SCHOOL_UNLIMITED: u32 = 3_000_00;
63
64#[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 #[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#[derive(Debug, Serialize, Deserialize, Clone)]
100pub struct StripePaymentMethodId(String);
101
102#[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#[derive(Debug, Serialize, Deserialize, Clone, EnumString)]
114#[serde(rename_all = "kebab-case")]
115#[strum(serialize_all = "lowercase")]
116pub enum PaymentNetwork {
117 Visa,
119 Mastercard,
121 Discover,
123 JCB,
125 #[strum(serialize = "amex")]
127 AmericanExpress,
128 UnionPay,
130 #[strum(serialize = "diners")]
132 DinersClub,
133 Unknown,
135}
136
137impl Default for PaymentNetwork {
138 fn default() -> Self {
139 Self::Unknown
140 }
141}
142
143#[derive(Debug, Serialize, Deserialize, Clone)]
145pub struct Card {
146 pub last4: Last4,
148 pub payment_network: PaymentNetwork,
150 pub exp_month: u8,
152 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#[derive(Debug, Serialize, Deserialize, Clone)]
173pub enum PaymentMethodType {
174 ApplePay,
176 GooglePay,
178 Link,
180 Card(Card),
182 Other,
184}
185
186wrap_uuid! {
187 pub struct PaymentMethodId
189}
190
191#[derive(Debug, Serialize, Deserialize, Clone)]
193pub struct PaymentMethod {
194 pub stripe_payment_method_id: StripePaymentMethodId, 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#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
230#[cfg_attr(feature = "backend", derive(sqlx::Type))]
231#[repr(i16)]
232pub enum SubscriptionTier {
233 Basic = 0,
235 Pro = 1,
237}
238
239#[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#[derive(Debug, Serialize, Deserialize, Clone)]
262#[cfg_attr(feature = "backend", derive(sqlx::Type), sqlx(transparent))]
263pub struct StripeInvoiceId(String);
264
265impl StripeInvoiceId {
266 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#[derive(Debug, Serialize, Deserialize, Clone)]
281#[cfg_attr(feature = "backend", derive(sqlx::Type), sqlx(transparent))]
282pub struct StripeProductId(String);
283
284#[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#[derive(Debug, Display, Serialize, Deserialize, Clone, Copy)]
297#[cfg_attr(feature = "backend", derive(sqlx::Type))]
298#[repr(i16)]
299pub enum BillingInterval {
300 Monthly = 0,
302 Annually = 1,
304}
305
306impl BillingInterval {
307 pub fn as_str(&self) -> &'static str {
309 self.into()
310 }
311 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#[derive(Copy, Debug, Display, Serialize, Deserialize, Clone)]
343#[cfg_attr(feature = "backend", derive(sqlx::Type))]
344#[repr(i16)]
345pub enum SubscriptionStatus {
346 Inactive = 0,
348 Active = 1,
350 Canceled = 2,
352 Expired = 3,
354 Paused = 4,
356}
357
358impl SubscriptionStatus {
359 #[must_use]
362 pub const fn is_valid(&self) -> bool {
363 matches!(self, Self::Active | Self::Canceled)
364 }
365
366 #[must_use]
368 pub const fn is_active(&self) -> bool {
369 matches!(self, Self::Active)
370 }
371
372 #[must_use]
374 pub const fn is_canceled(&self) -> bool {
375 matches!(self, Self::Canceled)
376 }
377
378 #[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 pub struct SubscriptionId
412}
413
414#[derive(Debug, Serialize, Deserialize, Clone)]
416pub struct Subscription {
417 pub subscription_id: SubscriptionId,
419 pub stripe_subscription_id: StripeSubscriptionId,
421 pub subscription_plan_type: PlanType,
423 pub auto_renew: bool,
425 pub status: SubscriptionStatus,
427 pub is_trial: bool,
429 pub current_period_end: DateTime<Utc>,
431 pub account_id: AccountId,
433 pub latest_invoice_id: Option<StripeInvoiceId>,
435 pub amount_due_in_cents: Option<AmountInCents>,
437 pub price: AmountInCents,
439 pub applied_coupon: Option<AppliedCoupon>,
441 pub created_at: DateTime<Utc>,
443 #[serde(default)]
445 #[serde(skip_serializing_if = "Option::is_none")]
446 pub updated_at: Option<DateTime<Utc>>,
447}
448
449#[derive(Debug, Serialize, Deserialize, Clone)]
451pub struct AppliedCoupon {
452 pub coupon_name: String,
454 #[serde(skip_serializing_if = "Option::is_none")]
456 pub coupon_percent: Option<Percent>,
457 pub coupon_from: DateTime<Utc>,
459 #[serde(skip_serializing_if = "Option::is_none")]
461 pub coupon_to: Option<DateTime<Utc>>,
462}
463
464#[derive(Debug, Serialize, Deserialize, Clone)]
466#[cfg(feature = "backend")]
467pub struct CreateSubscriptionRecord {
468 pub stripe_subscription_id: StripeSubscriptionId,
470 pub subscription_plan_id: PlanId,
472 pub status: SubscriptionStatus,
474 pub current_period_end: DateTime<Utc>,
476 pub account_id: AccountId,
479 pub latest_invoice_id: Option<StripeInvoiceId>,
481 pub amount_due_in_cents: Option<AmountInCents>,
483 pub price: AmountInCents,
485}
486
487#[derive(Debug, Serialize, Deserialize, Clone)]
489#[cfg(feature = "backend")]
490pub struct UpdateSubscriptionRecord {
491 pub stripe_subscription_id: StripeSubscriptionId,
493 pub subscription_plan_id: UpdateNonNullable<PlanId>,
495 #[serde(default, skip_serializing_if = "UpdateNonNullable::is_keep")]
497 pub status: UpdateNonNullable<SubscriptionStatus>,
498 #[serde(default, skip_serializing_if = "UpdateNonNullable::is_keep")]
500 pub current_period_end: UpdateNonNullable<DateTime<Utc>>,
501 #[serde(default, skip_serializing_if = "UpdateNonNullable::is_keep")]
503 pub latest_invoice_id: UpdateNonNullable<StripeInvoiceId>,
504 #[serde(default, skip_serializing_if = "UpdateNonNullable::is_keep")]
506 pub is_trial: UpdateNonNullable<bool>,
507 #[serde(default, skip_serializing_if = "UpdateNonNullable::is_keep")]
509 pub price: UpdateNonNullable<AmountInCents>,
510 #[serde(default, skip_serializing_if = "UpdateNullable::is_keep")]
512 pub coupon_name: UpdateNullable<String>,
513 #[serde(default, skip_serializing_if = "UpdateNullable::is_keep")]
515 pub coupon_percent: UpdateNullable<Percent>,
516 #[serde(default, skip_serializing_if = "UpdateNullable::is_keep")]
518 pub coupon_from: UpdateNullable<DateTime<Utc>>,
519 #[serde(default, skip_serializing_if = "UpdateNullable::is_keep")]
521 pub coupon_to: UpdateNullable<DateTime<Utc>>,
522}
523
524#[cfg(feature = "backend")]
525impl UpdateSubscriptionRecord {
526 #[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 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 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#[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#[derive(Debug, Display, Serialize, Deserialize, Clone, Copy)]
656#[cfg_attr(feature = "backend", derive(sqlx::Type))]
657#[repr(i16)]
658pub enum SubscriptionType {
659 Individual = 0,
661 School = 1,
663}
664
665#[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 #[default]
685 Free = 0,
686 Basic = 1,
688 Pro = 2,
690}
691
692#[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 IndividualBasicMonthly = 0,
702 IndividualBasicAnnually = 1,
704 IndividualProMonthly = 2,
706 IndividualProAnnually = 3,
708 SchoolLevel1Monthly = 4,
710 SchoolLevel2Monthly = 5,
712 SchoolLevel3Monthly = 6,
714 SchoolLevel4Monthly = 7,
716 SchoolUnlimitedMonthly = 8,
718 SchoolLevel1Annually = 9,
720 SchoolLevel2Annually = 10,
722 SchoolLevel3Annually = 11,
724 SchoolLevel4Annually = 12,
726 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[must_use]
950 pub const fn can_upgrade_from(&self, from_type: &Self) -> bool {
951 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 #[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 #[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 #[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 #[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 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 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 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 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#[derive(Debug, Display, Serialize, Deserialize, Clone, Copy)]
1200#[cfg_attr(feature = "backend", derive(sqlx::Type))]
1201#[repr(i16)]
1202pub enum AccountType {
1203 Individual = 0,
1205 School = 1,
1207}
1208
1209impl AccountType {
1210 pub fn has_admin(&self) -> bool {
1212 match self {
1213 Self::School => true,
1214 _ => false,
1215 }
1216 }
1217
1218 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#[derive(Debug, Serialize, Deserialize, Clone)]
1238#[cfg_attr(feature = "backend", derive(sqlx::Type), sqlx(transparent))]
1239pub struct InvoiceNumber(String);
1240
1241#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
1243#[cfg_attr(feature = "backend", derive(sqlx::Type), sqlx(transparent))]
1244pub struct AmountInCents(i64);
1245
1246impl AmountInCents {
1247 pub fn new(amount: i64) -> Self {
1249 Self(amount)
1250 }
1251
1252 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#[derive(Debug, Serialize, Deserialize, Clone)]
1272#[cfg_attr(feature = "backend", derive(sqlx::Type), sqlx(transparent))]
1273pub struct TrialPeriod(i64);
1274
1275impl TrialPeriod {
1276 pub fn new(length: i64) -> Self {
1278 Self(length)
1279 }
1280
1281 pub fn inner(&self) -> i64 {
1283 self.0
1284 }
1285}
1286
1287wrap_uuid! {
1288 pub struct ChargeId
1290}
1291
1292pub struct Charge {
1295 pub charge_id: ChargeId,
1297 pub charged_at: DateTime<Utc>,
1299 pub subscription_tier: SubscriptionTier,
1301 pub payment_method: PaymentMethod,
1303 pub invoice_number: InvoiceNumber,
1305 pub amount_in_cents: AmountInCents,
1307}
1308
1309wrap_uuid! {
1310 pub struct PlanId
1312}
1313
1314#[derive(Debug, Serialize, Deserialize, Clone)]
1318#[cfg_attr(feature = "backend", derive(sqlx::FromRow))]
1319pub struct SubscriptionPlan {
1320 pub plan_id: PlanId,
1322 pub plan_type: PlanType,
1324 pub price_id: StripePriceId,
1326 pub created_at: DateTime<Utc>,
1328 pub updated_at: Option<DateTime<Utc>>,
1330}
1331
1332make_path_parts!(SubscriptionPlanPath => "/v1/plans");
1333
1334#[derive(Debug, Serialize, Deserialize, Clone)]
1336pub struct UpdateSubscriptionPlansRequest {
1337 #[serde(flatten)]
1339 pub plans: HashMap<PlanType, StripePriceId>,
1340}
1341
1342#[derive(Debug, Serialize, Deserialize, Clone)]
1347pub struct CreateSubscriptionRequest {
1348 pub setup_intent_id: Option<String>,
1351 pub plan_type: PlanType,
1353 pub promotion_code: Option<String>,
1355}
1356
1357make_path_parts!(CreateSubscriptionPath => "/v1/billing/subscribe");
1358
1359#[derive(Debug, Serialize, Deserialize, Clone)]
1361pub struct CreateSubscriptionResponse {
1362 pub subscription_id: StripeSubscriptionId,
1364 pub client_secret: String,
1369}
1370
1371#[derive(Debug, Serialize, Deserialize, Clone)]
1376pub struct CreateSetupIntentRequest {
1377 pub plan_type: PlanType,
1379}
1380
1381make_path_parts!(CreateSetupIntentPath => "/v1/billing/payment-method");
1382
1383wrap_uuid! {
1384 pub struct AccountId
1386}
1387
1388#[derive(Debug, Serialize, Deserialize, Clone)]
1390pub struct Account {
1391 pub account_id: AccountId,
1393 pub account_type: AccountType,
1395 #[serde(default)]
1397 #[serde(skip_serializing_if = "Option::is_none")]
1398 pub stripe_customer_id: Option<CustomerId>,
1399 #[serde(default)]
1401 #[serde(skip_serializing_if = "Option::is_none")]
1402 pub payment_method: Option<PaymentMethod>,
1403 #[serde(default)]
1405 #[serde(skip_serializing_if = "Option::is_none")]
1406 pub subscription: Option<Subscription>,
1407 pub created_at: DateTime<Utc>,
1409 #[serde(default)]
1411 #[serde(skip_serializing_if = "Option::is_none")]
1412 pub updated_at: Option<DateTime<Utc>>,
1413}
1414
1415#[derive(Debug, Serialize, Deserialize, Clone)]
1420pub struct UserAccountSummary {
1421 pub account_id: Option<AccountId>,
1423 pub school_id: Option<SchoolId>,
1425 pub school_name: Option<String>,
1427 pub plan_type: Option<PlanType>,
1429 pub plan_tier: PlanTier,
1431 pub overridden: bool,
1433 pub subscription_status: Option<SubscriptionStatus>,
1435 pub is_admin: bool,
1438 pub overdue: bool,
1440 pub verified: bool,
1442}
1443
1444wrap_uuid! {
1445 pub struct SchoolId
1447}
1448
1449#[derive(Debug, Serialize, Deserialize, Clone)]
1451pub struct School {
1452 pub id: SchoolId,
1454
1455 pub school_name: String,
1457
1458 #[serde(default)]
1460 #[serde(skip_serializing_if = "Option::is_none")]
1461 pub location: Option<Value>,
1462
1463 pub email: String,
1465
1466 #[serde(default)]
1468 #[serde(skip_serializing_if = "Option::is_none")]
1469 pub description: Option<String>,
1470
1471 #[serde(default)]
1473 #[serde(skip_serializing_if = "Option::is_none")]
1474 pub profile_image: Option<ImageId>,
1475
1476 #[serde(default)]
1478 #[serde(skip_serializing_if = "Option::is_none")]
1479 pub website: Option<String>,
1480
1481 #[serde(default)]
1483 #[serde(skip_serializing_if = "Option::is_none")]
1484 pub organization_type: Option<String>,
1485
1486 pub account_id: AccountId,
1488
1489 pub created_at: DateTime<Utc>,
1491
1492 #[serde(default)]
1494 #[serde(skip_serializing_if = "Option::is_none")]
1495 pub updated_at: Option<DateTime<Utc>>,
1496}
1497
1498#[derive(Debug, Serialize, Deserialize, Clone)]
1500pub struct AdminSchool {
1501 pub id: SchoolId,
1503
1504 pub school_name: String,
1506
1507 pub internal_school_name: Option<SchoolName>,
1509
1510 pub verified: bool,
1512
1513 #[serde(default)]
1515 #[serde(skip_serializing_if = "Option::is_none")]
1516 pub location: Option<Value>,
1517
1518 pub email: String,
1520
1521 #[serde(default)]
1523 #[serde(skip_serializing_if = "Option::is_none")]
1524 pub description: Option<String>,
1525
1526 #[serde(default)]
1528 #[serde(skip_serializing_if = "Option::is_none")]
1529 pub profile_image: Option<ImageId>,
1530
1531 #[serde(default)]
1533 #[serde(skip_serializing_if = "Option::is_none")]
1534 pub website: Option<String>,
1535
1536 #[serde(default)]
1538 #[serde(skip_serializing_if = "Option::is_none")]
1539 pub organization_type: Option<String>,
1540
1541 pub account_id: AccountId,
1543
1544 pub created_at: DateTime<Utc>,
1546
1547 #[serde(default)]
1549 #[serde(skip_serializing_if = "Option::is_none")]
1550 pub updated_at: Option<DateTime<Utc>>,
1551}
1552
1553#[derive(Debug, Serialize, Deserialize, Clone)]
1555pub struct AccountUser {
1556 pub user: UserProfile,
1558 pub is_admin: bool,
1561 pub verified: bool,
1563}
1564
1565wrap_uuid! {
1566 pub struct SchoolNameId
1568}
1569
1570#[derive(Debug, Serialize, Deserialize, Clone)]
1572pub struct SchoolName {
1573 pub id: SchoolNameId,
1575 pub name: String,
1577}
1578
1579#[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#[derive(Debug, Serialize, Deserialize, Clone)]
1610pub enum SchoolNameRequest {
1611 Value(SchoolNameValue),
1613 Id(SchoolNameId),
1615}
1616
1617make_path_parts!(CreateSchoolAccountPath => "/v1/schools");
1618
1619#[derive(Debug, Serialize, Deserialize, Clone)]
1621pub struct CreateSchoolAccountRequest {
1622 pub name: String,
1624
1625 pub email: String,
1627
1628 #[serde(default)]
1630 #[serde(skip_serializing_if = "Option::is_none")]
1631 pub location: Option<Value>,
1632
1633 #[serde(default)]
1635 #[serde(skip_serializing_if = "Option::is_none")]
1636 pub description: Option<String>,
1637
1638 #[serde(default)]
1640 #[serde(skip_serializing_if = "Option::is_none")]
1641 pub profile_image: Option<ImageId>,
1642
1643 #[serde(default)]
1645 #[serde(skip_serializing_if = "Option::is_none")]
1646 pub website: Option<String>,
1647
1648 #[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#[derive(Debug, Serialize, Deserialize, Clone)]
1658pub struct GetSchoolAccountResponse {
1659 pub school: School,
1661 pub account: AccountIfAuthorized,
1663 pub users: Vec<AccountUser>,
1665}
1666
1667#[derive(Debug, Serialize, Deserialize, Clone)]
1669#[allow(clippy::large_enum_variant)]
1670#[serde(untagged)]
1671pub enum AccountIfAuthorized {
1672 Authorized(Account),
1674 Unauthorized,
1676}
1677
1678#[derive(Debug, Default, Serialize, Deserialize, Clone)]
1680pub struct UpdateSchoolAccountRequest {
1681 #[serde(default, skip_serializing_if = "UpdateNonNullable::is_keep")]
1683 pub email: UpdateNonNullable<String>,
1684
1685 #[serde(default, skip_serializing_if = "UpdateNonNullable::is_keep")]
1687 pub school_name: UpdateNonNullable<String>,
1688
1689 #[serde(default, skip_serializing_if = "UpdateNullable::is_keep")]
1691 pub location: UpdateNullable<Value>,
1692
1693 #[serde(default, skip_serializing_if = "UpdateNullable::is_keep")]
1695 pub description: UpdateNullable<String>,
1696
1697 #[serde(default, skip_serializing_if = "UpdateNullable::is_keep")]
1699 pub profile_image: UpdateNullable<ImageId>,
1700
1701 #[serde(default, skip_serializing_if = "UpdateNullable::is_keep")]
1703 pub website: UpdateNullable<String>,
1704
1705 #[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#[derive(Debug, Default, Serialize, Deserialize, Clone)]
1714pub struct IndividualAccountResponse {
1715 pub account: Option<Account>,
1717}
1718
1719#[derive(Debug, Serialize, Deserialize, Clone)]
1721#[serde(rename_all = "kebab-case")]
1722pub enum CancellationStatus {
1723 #[serde(rename = "period-end")]
1725 CancelAtPeriodEnd,
1726 #[serde(rename = "remove")]
1728 RemoveCancellation,
1729}
1730
1731#[derive(Debug, Serialize, Deserialize, Clone)]
1733pub struct SubscriptionCancellationStatusRequest {
1734 pub status: CancellationStatus,
1736}
1737
1738make_path_parts!(UpdateSubscriptionCancellationPath => "/v1/billing/subscription/cancel");
1739
1740#[derive(Debug, Serialize, Deserialize, Clone)]
1742pub struct SubscriptionPauseRequest {
1743 pub paused: bool,
1745}
1746
1747make_path_parts!(UpdateSubscriptionPausedPath => "/v1/billing/subscription/pause");
1748
1749#[derive(Debug, Serialize, Deserialize, Clone)]
1751pub struct UpgradeSubscriptionPlanRequest {
1752 pub plan_type: PlanType,
1754 #[serde(default)]
1756 #[serde(skip_serializing_if = "Option::is_none")]
1757 pub promotion_code: Option<String>,
1758}
1759
1760#[derive(Debug, Serialize, Deserialize, Clone)]
1762pub struct AdminUpgradeSubscriptionPlanRequest {
1763 pub plan_type: PlanType,
1765 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");