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;
29pub const PLAN_SCHOOL_UNLIMITED_TEACHER_COUNT: i64 = 30;
31
32pub const INDIVIDUAL_TRIAL_PERIOD: i64 = 7;
34pub const SCHOOL_TRIAL_PERIOD: i64 = 14;
36
37pub const PLAN_PRICE_MONTHLY_BASIC: u32 = 17_99;
39pub const PLAN_PRICE_ANNUAL_BASIC: u32 = 180_00;
41pub const PLAN_PRICE_MONTHLY_PRO: u32 = 29_99;
43pub const PLAN_PRICE_ANNUAL_PRO: u32 = 300_00;
45pub const PLAN_PRICE_MONTHLY_SCHOOL_1: u32 = 115_00;
47pub const PLAN_PRICE_ANNUAL_SCHOOL_1: u32 = 1_250_00;
49pub const PLAN_PRICE_MONTHLY_SCHOOL_2: u32 = 150_00;
51pub const PLAN_PRICE_ANNUAL_SCHOOL_2: u32 = 1_500_00;
53pub const PLAN_PRICE_MONTHLY_SCHOOL_3: u32 = 200_00;
55pub const PLAN_PRICE_ANNUAL_SCHOOL_3: u32 = 2_000_00;
57pub const PLAN_PRICE_MONTHLY_SCHOOL_4: u32 = 250_00;
59pub const PLAN_PRICE_ANNUAL_SCHOOL_4: u32 = 2_500_00;
61pub const PLAN_PRICE_MONTHLY_SCHOOL_UNLIMITED: u32 = 300_00;
63pub const PLAN_PRICE_ANNUAL_SCHOOL_UNLIMITED: u32 = 3_000_00;
65
66#[derive(Debug, Serialize, Deserialize, Clone)]
68#[cfg_attr(feature = "backend", derive(sqlx::Type), sqlx(transparent))]
69pub struct CustomerId(String);
70
71#[cfg(feature = "backend")]
72impl From<stripe::CustomerId> for CustomerId {
73 fn from(value: stripe::CustomerId) -> Self {
74 Self(value.as_str().to_owned())
75 }
76}
77
78#[cfg(feature = "backend")]
79impl From<CustomerId> for stripe::CustomerId {
80 fn from(value: CustomerId) -> Self {
81 use std::str::FromStr;
82 Self::from_str(&value.0).unwrap()
83 }
84}
85
86impl CustomerId {
87 #[cfg(feature = "backend")]
89 pub fn as_str(&self) -> &str {
90 &self.0
91 }
92}
93
94impl fmt::Display for CustomerId {
95 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
96 write!(f, "{}", self.0)
97 }
98}
99
100#[derive(Debug, Serialize, Deserialize, Clone)]
102pub struct StripePaymentMethodId(String);
103
104#[derive(Debug, Serialize, Deserialize, Clone)]
106pub struct Last4(String);
107
108impl fmt::Display for Last4 {
109 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
110 write!(f, "{}", self.0)
111 }
112}
113
114#[derive(Debug, Serialize, Deserialize, Clone, EnumString)]
116#[serde(rename_all = "kebab-case")]
117#[strum(serialize_all = "lowercase")]
118pub enum PaymentNetwork {
119 Visa,
121 Mastercard,
123 Discover,
125 JCB,
127 #[strum(serialize = "amex")]
129 AmericanExpress,
130 UnionPay,
132 #[strum(serialize = "diners")]
134 DinersClub,
135 Unknown,
137}
138
139impl Default for PaymentNetwork {
140 fn default() -> Self {
141 Self::Unknown
142 }
143}
144
145#[derive(Debug, Serialize, Deserialize, Clone)]
147pub struct Card {
148 pub last4: Last4,
150 pub payment_network: PaymentNetwork,
152 pub exp_month: u8,
154 pub exp_year: u16,
156}
157
158#[cfg(feature = "backend")]
159impl From<stripe::CardDetails> for Card {
160 fn from(value: stripe::CardDetails) -> Self {
161 use std::str::FromStr;
162 Self {
163 last4: Last4(value.last4),
164 payment_network: PaymentNetwork::from_str(&value.brand).unwrap_or_default(),
165 exp_month: value.exp_month as u8,
166 exp_year: value.exp_year as u16,
167 }
168 }
169}
170
171#[derive(Debug, Serialize, Deserialize, Clone)]
175pub enum PaymentMethodType {
176 ApplePay,
178 GooglePay,
180 Link,
182 Card(Card),
184 Other,
186}
187
188wrap_uuid! {
189 pub struct PaymentMethodId
191}
192
193#[derive(Debug, Serialize, Deserialize, Clone)]
195pub struct PaymentMethod {
196 pub stripe_payment_method_id: StripePaymentMethodId, pub payment_method_type: PaymentMethodType,
200}
201
202#[cfg(feature = "backend")]
203impl From<stripe::PaymentMethod> for PaymentMethod {
204 fn from(value: stripe::PaymentMethod) -> Self {
205 let payment_method_type = if value.link.is_some() {
206 PaymentMethodType::Link
207 } else if let Some(card) = value.card {
208 if let Some(wallet) = card.wallet {
209 if wallet.apple_pay.is_some() {
210 PaymentMethodType::ApplePay
211 } else if wallet.google_pay.is_some() {
212 PaymentMethodType::GooglePay
213 } else {
214 PaymentMethodType::Other
215 }
216 } else {
217 PaymentMethodType::Card(Card::from(card))
218 }
219 } else {
220 PaymentMethodType::Other
221 };
222
223 Self {
224 stripe_payment_method_id: StripePaymentMethodId(value.id.as_str().to_string()),
225 payment_method_type,
226 }
227 }
228}
229
230#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
232#[cfg_attr(feature = "backend", derive(sqlx::Type))]
233#[repr(i16)]
234pub enum SubscriptionTier {
235 Basic = 0,
237 Pro = 1,
239}
240
241#[derive(Debug, Serialize, Deserialize, Clone)]
243#[cfg_attr(feature = "backend", derive(sqlx::Type), sqlx(transparent))]
244pub struct StripeSubscriptionId(String);
245
246#[cfg(feature = "backend")]
247impl From<stripe::SubscriptionId> for StripeSubscriptionId {
248 fn from(value: stripe::SubscriptionId) -> Self {
249 Self(value.as_str().to_owned())
250 }
251}
252
253#[cfg(feature = "backend")]
254impl TryFrom<StripeSubscriptionId> for stripe::SubscriptionId {
255 type Error = anyhow::Error;
256
257 fn try_from(value: StripeSubscriptionId) -> Result<Self, Self::Error> {
258 <Self as std::str::FromStr>::from_str(&value.0).map_err(Into::into)
259 }
260}
261
262#[derive(Debug, Serialize, Deserialize, Clone)]
264#[cfg_attr(feature = "backend", derive(sqlx::Type), sqlx(transparent))]
265pub struct StripeInvoiceId(String);
266
267impl StripeInvoiceId {
268 pub fn inner(&self) -> String {
270 self.0.clone()
271 }
272}
273
274#[cfg(feature = "backend")]
275impl From<&stripe::InvoiceId> for StripeInvoiceId {
276 fn from(value: &stripe::InvoiceId) -> Self {
277 Self(value.as_str().to_owned())
278 }
279}
280
281#[derive(Debug, Serialize, Deserialize, Clone)]
283#[cfg_attr(feature = "backend", derive(sqlx::Type), sqlx(transparent))]
284pub struct StripeProductId(String);
285
286#[derive(Debug, Serialize, Deserialize, Clone)]
288#[cfg_attr(feature = "backend", derive(sqlx::Type), sqlx(transparent))]
289pub struct StripePriceId(String);
290
291impl From<StripePriceId> for String {
292 fn from(value: StripePriceId) -> Self {
293 value.0
294 }
295}
296
297#[derive(Debug, Display, Serialize, Deserialize, Clone, Copy)]
299#[cfg_attr(feature = "backend", derive(sqlx::Type))]
300#[repr(i16)]
301pub enum BillingInterval {
302 Monthly = 0,
304 Annually = 1,
306}
307
308impl BillingInterval {
309 pub fn as_str(&self) -> &'static str {
311 self.into()
312 }
313 pub fn display_name(&self) -> &'static str {
315 match self {
316 BillingInterval::Annually => "Annually",
317 BillingInterval::Monthly => "Monthly",
318 }
319 }
320}
321
322impl TryFrom<&str> for BillingInterval {
323 type Error = ();
324
325 fn try_from(value: &str) -> Result<Self, Self::Error> {
326 match value {
327 "annually" => Ok(Self::Annually),
328 "monthly" => Ok(Self::Monthly),
329 _ => Err(()),
330 }
331 }
332}
333
334impl From<&BillingInterval> for &str {
335 fn from(value: &BillingInterval) -> Self {
336 match value {
337 BillingInterval::Annually => "annually",
338 BillingInterval::Monthly => "monthly",
339 }
340 }
341}
342
343#[derive(Copy, Debug, Display, Serialize, Deserialize, Clone)]
345#[cfg_attr(feature = "backend", derive(sqlx::Type))]
346#[repr(i16)]
347pub enum SubscriptionStatus {
348 Inactive = 0,
350 Active = 1,
352 Canceled = 2,
354 Expired = 3,
356 Paused = 4,
358}
359
360impl SubscriptionStatus {
361 #[must_use]
364 pub const fn is_valid(&self) -> bool {
365 matches!(self, Self::Active | Self::Canceled)
366 }
367
368 #[must_use]
370 pub const fn is_active(&self) -> bool {
371 matches!(self, Self::Active)
372 }
373
374 #[must_use]
376 pub const fn is_canceled(&self) -> bool {
377 matches!(self, Self::Canceled)
378 }
379
380 #[must_use]
382 pub const fn is_paused(&self) -> bool {
383 matches!(self, Self::Paused)
384 }
385}
386
387#[cfg(feature = "backend")]
388impl Default for SubscriptionStatus {
389 fn default() -> Self {
390 Self::Inactive
391 }
392}
393
394#[cfg(feature = "backend")]
395impl From<stripe::SubscriptionStatus> for SubscriptionStatus {
396 fn from(value: stripe::SubscriptionStatus) -> Self {
397 match value {
398 stripe::SubscriptionStatus::Incomplete | stripe::SubscriptionStatus::Paused => {
399 Self::Inactive
400 }
401 stripe::SubscriptionStatus::Active
402 | stripe::SubscriptionStatus::PastDue
403 | stripe::SubscriptionStatus::Trialing
404 | stripe::SubscriptionStatus::Unpaid => Self::Active,
405 stripe::SubscriptionStatus::Canceled => Self::Canceled,
406 stripe::SubscriptionStatus::IncompleteExpired => Self::Expired,
407 }
408 }
409}
410
411wrap_uuid! {
412 pub struct SubscriptionId
414}
415
416#[derive(Debug, Serialize, Deserialize, Clone)]
418pub struct Subscription {
419 pub subscription_id: SubscriptionId,
421 pub stripe_subscription_id: StripeSubscriptionId,
423 pub subscription_plan_type: PlanType,
425 pub auto_renew: bool,
427 pub status: SubscriptionStatus,
429 pub is_trial: bool,
431 pub current_period_end: DateTime<Utc>,
433 pub account_id: AccountId,
435 pub latest_invoice_id: Option<StripeInvoiceId>,
437 pub amount_due_in_cents: Option<AmountInCents>,
439 pub price: AmountInCents,
441 pub applied_coupon: Option<AppliedCoupon>,
443 pub created_at: DateTime<Utc>,
445 #[serde(default)]
447 #[serde(skip_serializing_if = "Option::is_none")]
448 pub updated_at: Option<DateTime<Utc>>,
449}
450
451#[derive(Debug, Serialize, Deserialize, Clone)]
453pub struct AppliedCoupon {
454 pub coupon_name: String,
456 #[serde(skip_serializing_if = "Option::is_none")]
458 pub coupon_percent: Option<Percent>,
459 pub coupon_from: DateTime<Utc>,
461 #[serde(skip_serializing_if = "Option::is_none")]
463 pub coupon_to: Option<DateTime<Utc>>,
464}
465
466#[derive(Debug, Serialize, Deserialize, Clone)]
468#[cfg(feature = "backend")]
469pub struct CreateSubscriptionRecord {
470 pub stripe_subscription_id: StripeSubscriptionId,
472 pub subscription_plan_id: PlanId,
474 pub status: SubscriptionStatus,
476 pub current_period_end: DateTime<Utc>,
478 pub account_id: AccountId,
481 pub latest_invoice_id: Option<StripeInvoiceId>,
483 pub amount_due_in_cents: Option<AmountInCents>,
485 pub price: AmountInCents,
487}
488
489#[derive(Debug, Serialize, Deserialize, Clone)]
491#[cfg(feature = "backend")]
492pub struct UpdateSubscriptionRecord {
493 pub stripe_subscription_id: StripeSubscriptionId,
495 pub subscription_plan_id: UpdateNonNullable<PlanId>,
497 #[serde(default, skip_serializing_if = "UpdateNonNullable::is_keep")]
499 pub status: UpdateNonNullable<SubscriptionStatus>,
500 #[serde(default, skip_serializing_if = "UpdateNonNullable::is_keep")]
502 pub current_period_end: UpdateNonNullable<DateTime<Utc>>,
503 #[serde(default, skip_serializing_if = "UpdateNonNullable::is_keep")]
505 pub latest_invoice_id: UpdateNonNullable<StripeInvoiceId>,
506 #[serde(default, skip_serializing_if = "UpdateNonNullable::is_keep")]
508 pub is_trial: UpdateNonNullable<bool>,
509 #[serde(default, skip_serializing_if = "UpdateNonNullable::is_keep")]
511 pub price: UpdateNonNullable<AmountInCents>,
512 #[serde(default, skip_serializing_if = "UpdateNullable::is_keep")]
514 pub coupon_name: UpdateNullable<String>,
515 #[serde(default, skip_serializing_if = "UpdateNullable::is_keep")]
517 pub coupon_percent: UpdateNullable<Percent>,
518 #[serde(default, skip_serializing_if = "UpdateNullable::is_keep")]
520 pub coupon_from: UpdateNullable<DateTime<Utc>>,
521 #[serde(default, skip_serializing_if = "UpdateNullable::is_keep")]
523 pub coupon_to: UpdateNullable<DateTime<Utc>>,
524}
525
526#[cfg(feature = "backend")]
527impl UpdateSubscriptionRecord {
528 #[must_use]
530 pub fn new(stripe_subscription_id: StripeSubscriptionId) -> Self {
531 Self {
532 stripe_subscription_id,
533 subscription_plan_id: Default::default(),
534 status: Default::default(),
535 current_period_end: Default::default(),
536 latest_invoice_id: Default::default(),
537 is_trial: Default::default(),
538 price: Default::default(),
539 coupon_name: Default::default(),
540 coupon_percent: Default::default(),
541 coupon_from: Default::default(),
542 coupon_to: Default::default(),
543 }
544 }
545}
546
547#[cfg(feature = "backend")]
548impl TryFrom<stripe::Subscription> for UpdateSubscriptionRecord {
549 type Error = anyhow::Error;
550
551 fn try_from(value: stripe::Subscription) -> Result<Self, Self::Error> {
552 use chrono::TimeZone;
553
554 let latest_invoice_id = value
555 .latest_invoice
556 .as_ref()
557 .map(|invoice| StripeInvoiceId::from(&invoice.id()))
558 .into();
559
560 let price = AmountInCents::from(
561 value
562 .items
563 .data
564 .get(0)
565 .map(|item| item.clone())
566 .ok_or(anyhow::anyhow!("Missing plan data"))?
567 .plan
568 .ok_or(anyhow::anyhow!("Missing stripe subscription plan"))?
569 .amount
570 .ok_or(anyhow::anyhow!("Missing subscription plan amount"))?,
571 );
572
573 let (coupon_name, coupon_percent, coupon_from, coupon_to) = value.discount.map_or_else(
574 || {
575 Ok((
576 Default::default(),
577 Default::default(),
578 Default::default(),
579 Default::default(),
580 ))
581 },
582 |discount| -> Result<_, Self::Error> {
583 let start_time = Some(
584 Utc.timestamp_opt(discount.start, 0)
585 .latest()
586 .ok_or(anyhow::anyhow!("Invalid timestamp"))?,
587 );
588
589 let end_time = match discount.end {
590 Some(end) => Some(
591 Utc.timestamp_opt(end, 0)
592 .latest()
593 .ok_or(anyhow::anyhow!("Invalid timestamp"))?,
594 ),
595 None => None,
596 };
597
598 Ok((
599 UpdateNullable::from(discount.coupon.name.map(|name| name.to_uppercase())),
600 UpdateNullable::from(
601 discount
602 .coupon
603 .percent_off
604 .map(|percent| Percent::from(percent / 100.0)),
605 ),
606 UpdateNullable::from(start_time),
607 UpdateNullable::from(end_time),
608 ))
609 },
610 )?;
611
612 Ok(Self {
613 stripe_subscription_id: value.id.into(),
614 subscription_plan_id: UpdateNonNullable::Keep,
615 is_trial: UpdateNonNullable::Change(matches!(
616 value.status,
617 stripe::SubscriptionStatus::Trialing
618 )),
619 status: UpdateNonNullable::Change(if value.ended_at.is_some() {
621 SubscriptionStatus::Expired
622 } else if value.canceled_at.is_some() {
623 SubscriptionStatus::Canceled
624 } else if value.pause_collection.is_some() {
625 SubscriptionStatus::Paused
627 } else {
628 SubscriptionStatus::from(value.status)
629 }),
630 current_period_end: UpdateNonNullable::Change(
631 Utc.timestamp_opt(value.current_period_end, 0)
632 .latest()
633 .ok_or(anyhow::anyhow!("Invalid timestamp"))?,
634 ),
635 latest_invoice_id,
636 price: UpdateNonNullable::Change(price),
637 coupon_name,
638 coupon_percent,
639 coupon_from,
640 coupon_to,
641 })
642 }
643}
644
645#[derive(Debug, Serialize, Deserialize, Clone, Copy, Eq, PartialEq, Hash, Ord, PartialOrd)]
647#[cfg_attr(feature = "backend", derive(sqlx::Type), sqlx(transparent))]
648pub struct AccountLimit(i64);
649
650impl From<i64> for AccountLimit {
651 fn from(value: i64) -> Self {
652 Self(value)
653 }
654}
655
656#[derive(Debug, Display, Serialize, Deserialize, Clone, Copy)]
658#[cfg_attr(feature = "backend", derive(sqlx::Type))]
659#[repr(i16)]
660pub enum SubscriptionType {
661 Individual = 0,
663 School = 1,
665}
666
667#[derive(
669 Debug,
670 Default,
671 Display,
672 EnumString,
673 EnumIs,
674 AsRefStr,
675 Serialize,
676 Deserialize,
677 Clone,
678 Copy,
679 PartialEq,
680 Eq,
681)]
682#[cfg_attr(feature = "backend", derive(sqlx::Type))]
683#[repr(i16)]
684pub enum PlanTier {
685 #[default]
687 Free = 0,
688 Basic = 1,
690 Pro = 2,
692}
693
694#[derive(
696 Debug, Serialize, Deserialize, Clone, Copy, Eq, Ord, PartialOrd, PartialEq, Hash, EnumIter,
697)]
698#[serde(rename_all = "kebab-case")]
699#[cfg_attr(feature = "backend", derive(sqlx::Type))]
700#[repr(i16)]
701pub enum PlanType {
702 IndividualBasicMonthly = 0,
704 IndividualBasicAnnually = 1,
706 IndividualProMonthly = 2,
708 IndividualProAnnually = 3,
710 SchoolLevel1Monthly = 4,
712 SchoolLevel2Monthly = 5,
714 SchoolLevel3Monthly = 6,
716 SchoolLevel4Monthly = 7,
718 SchoolUnlimitedMonthly = 8,
720 SchoolLevel1Annually = 9,
722 SchoolLevel2Annually = 10,
724 SchoolLevel3Annually = 11,
726 SchoolLevel4Annually = 12,
728 SchoolUnlimitedAnnually = 13,
730}
731
732impl Display for PlanType {
733 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
734 let s = match self {
735 Self::IndividualBasicMonthly => "Individual Basic Monthly",
736 Self::IndividualBasicAnnually => "Individual Basic Annual",
737 Self::IndividualProMonthly => "Individual Pro Monthly",
738 Self::IndividualProAnnually => "Individual Pro Annual",
739 Self::SchoolLevel1Monthly => formatcp!(
740 "School - Up to {} Monthly",
741 PLAN_SCHOOL_LEVEL_1_TEACHER_COUNT
742 ),
743 Self::SchoolLevel2Monthly => formatcp!(
744 "School - Up to {} Monthly",
745 PLAN_SCHOOL_LEVEL_2_TEACHER_COUNT
746 ),
747 Self::SchoolLevel3Monthly => formatcp!(
748 "School - Up to {} Monthly",
749 PLAN_SCHOOL_LEVEL_3_TEACHER_COUNT
750 ),
751 Self::SchoolLevel4Monthly => formatcp!(
752 "School - Up to {} Monthly",
753 PLAN_SCHOOL_LEVEL_4_TEACHER_COUNT
754 ),
755 Self::SchoolUnlimitedMonthly => {
756 formatcp!("School - {}+ Monthly", PLAN_SCHOOL_LEVEL_4_TEACHER_COUNT)
757 }
758 Self::SchoolLevel1Annually => {
759 formatcp!("School - Up to {}", PLAN_SCHOOL_LEVEL_1_TEACHER_COUNT)
760 }
761 Self::SchoolLevel2Annually => {
762 formatcp!("School - Up to {}", PLAN_SCHOOL_LEVEL_2_TEACHER_COUNT)
763 }
764 Self::SchoolLevel3Annually => {
765 formatcp!("School - Up to {}", PLAN_SCHOOL_LEVEL_3_TEACHER_COUNT)
766 }
767 Self::SchoolLevel4Annually => {
768 formatcp!("School - Up to {}", PLAN_SCHOOL_LEVEL_4_TEACHER_COUNT)
769 }
770 Self::SchoolUnlimitedAnnually => {
771 formatcp!("School - {}+", PLAN_SCHOOL_LEVEL_4_TEACHER_COUNT)
772 }
773 };
774 write!(f, "{}", s)
775 }
776}
777
778impl PlanType {
779 #[must_use]
781 pub const fn as_str(&self) -> &'static str {
782 match self {
783 Self::IndividualBasicMonthly => "individual-basic-monthly",
784 Self::IndividualBasicAnnually => "individual-basic-annually",
785 Self::IndividualProMonthly => "individual-pro-monthly",
786 Self::IndividualProAnnually => "individual-pro-annually",
787 Self::SchoolLevel1Monthly => "school-level-1-monthly",
788 Self::SchoolLevel2Monthly => "school-level-2-monthly",
789 Self::SchoolLevel3Monthly => "school-level-3-monthly",
790 Self::SchoolLevel4Monthly => "school-level-4-monthly",
791 Self::SchoolUnlimitedMonthly => "school-unlimited-monthly",
792 Self::SchoolLevel1Annually => "school-level-1-annually",
793 Self::SchoolLevel2Annually => "school-level-2-annually",
794 Self::SchoolLevel3Annually => "school-level-3-annually",
795 Self::SchoolLevel4Annually => "school-level-4-annually",
796 Self::SchoolUnlimitedAnnually => "school-unlimited-annually",
797 }
798 }
799
800 #[must_use]
802 pub const fn display_name(&self) -> &'static str {
803 match self {
804 Self::IndividualBasicMonthly => "Individual - Basic monthly",
805 Self::IndividualBasicAnnually => "Individual - Basic annual",
806 Self::IndividualProMonthly => "Individual - Pro monthly",
807 Self::IndividualProAnnually => "Individual - Pro annual",
808 Self::SchoolLevel1Monthly => formatcp!(
809 "School - Up to {} teachers - Monthly",
810 PLAN_SCHOOL_LEVEL_1_TEACHER_COUNT
811 ),
812 Self::SchoolLevel2Monthly => formatcp!(
813 "School - Up to {} teachers - Monthly",
814 PLAN_SCHOOL_LEVEL_2_TEACHER_COUNT
815 ),
816 Self::SchoolLevel3Monthly => formatcp!(
817 "School - Up to {} teachers - Monthly",
818 PLAN_SCHOOL_LEVEL_3_TEACHER_COUNT
819 ),
820 Self::SchoolLevel4Monthly => formatcp!(
821 "School - Up to {} teachers - Monthly",
822 PLAN_SCHOOL_LEVEL_4_TEACHER_COUNT
823 ),
824 Self::SchoolUnlimitedMonthly => formatcp!(
825 "School - Up to {} teachers - Monthly",
826 PLAN_SCHOOL_UNLIMITED_TEACHER_COUNT
827 ),
828 Self::SchoolLevel1Annually => formatcp!(
829 "School - Up to {} teachers",
830 PLAN_SCHOOL_LEVEL_1_TEACHER_COUNT
831 ),
832 Self::SchoolLevel2Annually => formatcp!(
833 "School - Up to {} teachers",
834 PLAN_SCHOOL_LEVEL_2_TEACHER_COUNT
835 ),
836 Self::SchoolLevel3Annually => formatcp!(
837 "School - Up to {} teachers",
838 PLAN_SCHOOL_LEVEL_3_TEACHER_COUNT
839 ),
840 Self::SchoolLevel4Annually => formatcp!(
841 "School - Up to {} teachers",
842 PLAN_SCHOOL_LEVEL_4_TEACHER_COUNT
843 ),
844 Self::SchoolUnlimitedAnnually => formatcp!(
845 "School - Up to {} teachers",
846 PLAN_SCHOOL_UNLIMITED_TEACHER_COUNT
847 ),
848 }
849 }
850
851 #[must_use]
853 pub const fn user_display_name(&self) -> &'static str {
854 match self {
855 Self::IndividualBasicMonthly | Self::IndividualBasicAnnually => "Basic",
856 Self::IndividualProMonthly | Self::IndividualProAnnually => "Pro",
857 Self::SchoolLevel1Monthly | Self::SchoolLevel1Annually => {
858 formatcp!("Up to {} teachers", PLAN_SCHOOL_LEVEL_1_TEACHER_COUNT)
859 }
860 Self::SchoolLevel2Monthly | Self::SchoolLevel2Annually => {
861 formatcp!("Up to {} teachers", PLAN_SCHOOL_LEVEL_2_TEACHER_COUNT)
862 }
863 Self::SchoolLevel3Monthly | Self::SchoolLevel3Annually => {
864 formatcp!("Up to {} teachers", PLAN_SCHOOL_LEVEL_3_TEACHER_COUNT)
865 }
866 Self::SchoolLevel4Monthly | Self::SchoolLevel4Annually => {
867 formatcp!("Up to {} teachers", PLAN_SCHOOL_LEVEL_4_TEACHER_COUNT)
868 }
869 Self::SchoolUnlimitedMonthly | Self::SchoolUnlimitedAnnually => {
870 formatcp!("Up to {} teachers", PLAN_SCHOOL_UNLIMITED_TEACHER_COUNT)
871 }
872 }
873 }
874
875 #[must_use]
877 pub const fn subscription_tier(&self) -> SubscriptionTier {
878 match self {
879 Self::IndividualBasicMonthly | Self::IndividualBasicAnnually => SubscriptionTier::Basic,
880 _ => SubscriptionTier::Pro,
881 }
882 }
883
884 #[must_use]
886 pub const fn account_limit(&self) -> Option<AccountLimit> {
887 match self {
888 Self::SchoolLevel1Monthly | Self::SchoolLevel1Annually => {
889 Some(AccountLimit(PLAN_SCHOOL_LEVEL_1_TEACHER_COUNT))
890 }
891 Self::SchoolLevel2Monthly | Self::SchoolLevel2Annually => {
892 Some(AccountLimit(PLAN_SCHOOL_LEVEL_2_TEACHER_COUNT))
893 }
894 Self::SchoolLevel3Monthly | Self::SchoolLevel3Annually => {
895 Some(AccountLimit(PLAN_SCHOOL_LEVEL_3_TEACHER_COUNT))
896 }
897 Self::SchoolLevel4Monthly | Self::SchoolLevel4Annually => {
898 Some(AccountLimit(PLAN_SCHOOL_LEVEL_4_TEACHER_COUNT))
899 }
900 Self::SchoolUnlimitedMonthly | Self::SchoolUnlimitedAnnually => None,
901 Self::IndividualBasicMonthly
902 | Self::IndividualBasicAnnually
903 | Self::IndividualProMonthly
904 | Self::IndividualProAnnually => Some(AccountLimit(1)),
905 }
906 }
907
908 #[must_use]
910 pub const fn subscription_type(&self) -> SubscriptionType {
911 match self {
912 Self::IndividualBasicMonthly
913 | Self::IndividualBasicAnnually
914 | Self::IndividualProMonthly
915 | Self::IndividualProAnnually => SubscriptionType::Individual,
916 _ => SubscriptionType::School,
917 }
918 }
919
920 #[must_use]
922 pub const fn trial_period(&self) -> TrialPeriod {
923 match self.subscription_type() {
924 SubscriptionType::Individual => TrialPeriod(INDIVIDUAL_TRIAL_PERIOD),
925 SubscriptionType::School => TrialPeriod(SCHOOL_TRIAL_PERIOD),
926 }
927 }
928
929 #[must_use]
931 pub const fn billing_interval(&self) -> BillingInterval {
932 match self {
933 Self::IndividualBasicMonthly
934 | Self::IndividualProMonthly
935 | Self::SchoolLevel1Monthly
936 | Self::SchoolLevel2Monthly
937 | Self::SchoolLevel3Monthly
938 | Self::SchoolLevel4Monthly
939 | Self::SchoolUnlimitedMonthly => BillingInterval::Monthly,
940 Self::IndividualBasicAnnually
941 | Self::IndividualProAnnually
942 | Self::SchoolLevel1Annually
943 | Self::SchoolLevel2Annually
944 | Self::SchoolLevel3Annually
945 | Self::SchoolLevel4Annually
946 | Self::SchoolUnlimitedAnnually => BillingInterval::Annually,
947 }
948 }
949
950 #[must_use]
952 pub const fn can_upgrade_from(&self, from_type: &Self) -> bool {
953 match self {
955 Self::IndividualBasicMonthly => false,
956 Self::IndividualBasicAnnually | Self::IndividualProMonthly => {
957 matches!(from_type, Self::IndividualBasicMonthly)
958 }
959 Self::IndividualProAnnually => matches!(
960 from_type,
961 Self::IndividualBasicMonthly
962 | Self::IndividualBasicAnnually
963 | Self::IndividualProMonthly
964 ),
965 Self::SchoolLevel1Monthly => false,
966 Self::SchoolLevel2Monthly => matches!(from_type, Self::SchoolLevel1Monthly),
967 Self::SchoolLevel3Monthly => matches!(
968 from_type,
969 Self::SchoolLevel1Monthly | Self::SchoolLevel2Monthly,
970 ),
971 Self::SchoolLevel4Monthly => matches!(
972 from_type,
973 Self::SchoolLevel1Monthly | Self::SchoolLevel2Monthly | Self::SchoolLevel3Monthly,
974 ),
975 Self::SchoolUnlimitedMonthly => matches!(
976 from_type,
977 Self::SchoolLevel1Monthly
978 | Self::SchoolLevel2Monthly
979 | Self::SchoolLevel3Monthly
980 | Self::SchoolLevel4Monthly,
981 ),
982 Self::SchoolLevel1Annually => matches!(from_type, Self::SchoolLevel1Monthly),
983 Self::SchoolLevel2Annually => matches!(
984 from_type,
985 Self::SchoolLevel1Monthly | Self::SchoolLevel2Monthly | Self::SchoolLevel1Annually
986 ),
987 Self::SchoolLevel3Annually => matches!(
988 from_type,
989 Self::SchoolLevel1Monthly
990 | Self::SchoolLevel2Monthly
991 | Self::SchoolLevel3Monthly
992 | Self::SchoolLevel1Annually
993 | Self::SchoolLevel2Annually
994 ),
995 Self::SchoolLevel4Annually => matches!(
996 from_type,
997 Self::SchoolLevel1Monthly
998 | Self::SchoolLevel2Monthly
999 | Self::SchoolLevel3Monthly
1000 | Self::SchoolLevel4Monthly
1001 | Self::SchoolLevel1Annually
1002 | Self::SchoolLevel2Annually
1003 | Self::SchoolLevel3Annually
1004 ),
1005 Self::SchoolUnlimitedAnnually => matches!(
1006 from_type,
1007 Self::SchoolLevel1Monthly
1008 | Self::SchoolLevel2Monthly
1009 | Self::SchoolLevel3Monthly
1010 | Self::SchoolLevel4Monthly
1011 | Self::SchoolUnlimitedMonthly
1012 | Self::SchoolLevel1Annually
1013 | Self::SchoolLevel2Annually
1014 | Self::SchoolLevel3Annually
1015 | Self::SchoolLevel4Annually
1016 ),
1017 }
1018 }
1019
1020 #[must_use]
1022 pub const fn can_upgrade_from_same_interval(&self, from_type: &Self) -> bool {
1023 match self {
1024 Self::IndividualProAnnually => matches!(from_type, Self::IndividualBasicAnnually,),
1025 Self::SchoolLevel1Monthly => false,
1026 Self::SchoolLevel2Monthly => matches!(from_type, Self::SchoolLevel1Monthly),
1027 Self::SchoolLevel3Monthly => matches!(
1028 from_type,
1029 Self::SchoolLevel1Monthly | Self::SchoolLevel2Monthly,
1030 ),
1031 Self::SchoolLevel4Monthly => matches!(
1032 from_type,
1033 Self::SchoolLevel1Monthly | Self::SchoolLevel2Monthly | Self::SchoolLevel3Monthly,
1034 ),
1035 Self::SchoolUnlimitedMonthly => matches!(
1036 from_type,
1037 Self::SchoolLevel1Monthly
1038 | Self::SchoolLevel2Monthly
1039 | Self::SchoolLevel3Monthly
1040 | Self::SchoolLevel4Monthly,
1041 ),
1042 Self::SchoolLevel1Annually => false,
1043 Self::SchoolLevel2Annually => matches!(from_type, Self::SchoolLevel1Annually),
1044 Self::SchoolLevel3Annually => matches!(
1045 from_type,
1046 Self::SchoolLevel1Annually | Self::SchoolLevel2Annually
1047 ),
1048 Self::SchoolLevel4Annually => matches!(
1049 from_type,
1050 Self::SchoolLevel1Annually
1051 | Self::SchoolLevel2Annually
1052 | Self::SchoolLevel3Annually
1053 ),
1054 Self::SchoolUnlimitedAnnually => matches!(
1055 from_type,
1056 Self::SchoolLevel1Annually
1057 | Self::SchoolLevel2Annually
1058 | Self::SchoolLevel3Annually
1059 | Self::SchoolLevel4Annually
1060 ),
1061 _ => false,
1062 }
1063 }
1064
1065 #[must_use]
1067 pub const fn is_individual_plan(&self) -> bool {
1068 matches!(
1069 self,
1070 Self::IndividualBasicMonthly
1071 | Self::IndividualBasicAnnually
1072 | Self::IndividualProMonthly
1073 | Self::IndividualProAnnually
1074 )
1075 }
1076
1077 #[must_use]
1079 pub const fn is_school_plan(&self) -> bool {
1080 match self {
1081 PlanType::IndividualBasicMonthly
1082 | PlanType::IndividualBasicAnnually
1083 | PlanType::IndividualProMonthly
1084 | PlanType::IndividualProAnnually => false,
1085 PlanType::SchoolLevel1Monthly
1086 | PlanType::SchoolLevel2Monthly
1087 | PlanType::SchoolLevel3Monthly
1088 | PlanType::SchoolLevel4Monthly
1089 | PlanType::SchoolUnlimitedMonthly
1090 | PlanType::SchoolLevel1Annually
1091 | PlanType::SchoolLevel2Annually
1092 | PlanType::SchoolLevel3Annually
1093 | PlanType::SchoolLevel4Annually
1094 | PlanType::SchoolUnlimitedAnnually => true,
1095 }
1096 }
1097
1098 #[must_use]
1100 pub const fn plan_tier(&self) -> PlanTier {
1101 match self {
1102 PlanType::IndividualProMonthly
1103 | PlanType::IndividualProAnnually
1104 | PlanType::SchoolLevel1Monthly
1105 | PlanType::SchoolLevel2Monthly
1106 | PlanType::SchoolLevel3Monthly
1107 | PlanType::SchoolLevel4Monthly
1108 | PlanType::SchoolUnlimitedMonthly
1109 | PlanType::SchoolLevel1Annually
1110 | PlanType::SchoolLevel2Annually
1111 | PlanType::SchoolLevel3Annually
1112 | PlanType::SchoolLevel4Annually
1113 | PlanType::SchoolUnlimitedAnnually => PlanTier::Pro,
1114 PlanType::IndividualBasicMonthly | PlanType::IndividualBasicAnnually => PlanTier::Basic,
1115 }
1116 }
1117
1118 pub const fn plan_price(&self) -> u32 {
1120 match self {
1121 Self::IndividualBasicMonthly => PLAN_PRICE_MONTHLY_BASIC,
1122 Self::IndividualBasicAnnually => PLAN_PRICE_ANNUAL_BASIC,
1123 Self::IndividualProMonthly => PLAN_PRICE_MONTHLY_PRO,
1124 Self::IndividualProAnnually => PLAN_PRICE_ANNUAL_PRO,
1125 Self::SchoolLevel1Monthly => PLAN_PRICE_MONTHLY_SCHOOL_1,
1126 Self::SchoolLevel1Annually => PLAN_PRICE_ANNUAL_SCHOOL_1,
1127 Self::SchoolLevel2Monthly => PLAN_PRICE_MONTHLY_SCHOOL_2,
1128 Self::SchoolLevel2Annually => PLAN_PRICE_ANNUAL_SCHOOL_2,
1129 Self::SchoolLevel3Monthly => PLAN_PRICE_MONTHLY_SCHOOL_3,
1130 Self::SchoolLevel3Annually => PLAN_PRICE_ANNUAL_SCHOOL_3,
1131 Self::SchoolLevel4Monthly => PLAN_PRICE_MONTHLY_SCHOOL_4,
1132 Self::SchoolLevel4Annually => PLAN_PRICE_ANNUAL_SCHOOL_4,
1133 Self::SchoolUnlimitedMonthly => PLAN_PRICE_MONTHLY_SCHOOL_UNLIMITED,
1134 Self::SchoolUnlimitedAnnually => PLAN_PRICE_ANNUAL_SCHOOL_UNLIMITED,
1135 }
1136 }
1137
1138 pub const fn annual_to_monthly(&self) -> PlanType {
1140 match self {
1141 Self::IndividualBasicAnnually => Self::IndividualBasicMonthly,
1142 Self::IndividualProAnnually => Self::IndividualProMonthly,
1143 Self::SchoolLevel1Annually => Self::SchoolLevel1Monthly,
1144 Self::SchoolLevel2Annually => Self::SchoolLevel2Monthly,
1145 Self::SchoolLevel3Annually => Self::SchoolLevel3Monthly,
1146 Self::SchoolLevel4Annually => Self::SchoolLevel4Monthly,
1147 Self::SchoolUnlimitedAnnually => Self::SchoolUnlimitedMonthly,
1148 _ => panic!(),
1149 }
1150 }
1151
1152 pub const fn monthly_to_annual(&self) -> PlanType {
1154 match self {
1155 Self::IndividualBasicMonthly => Self::IndividualBasicAnnually,
1156 Self::IndividualProMonthly => Self::IndividualProAnnually,
1157 Self::SchoolLevel1Monthly => Self::SchoolLevel1Annually,
1158 Self::SchoolLevel2Monthly => Self::SchoolLevel2Annually,
1159 Self::SchoolLevel3Monthly => Self::SchoolLevel3Annually,
1160 Self::SchoolLevel4Monthly => Self::SchoolLevel4Annually,
1161 Self::SchoolUnlimitedMonthly => Self::SchoolUnlimitedAnnually,
1162 _ => panic!(),
1163 }
1164 }
1165
1166 pub const fn basic_to_pro(&self) -> PlanType {
1168 match self {
1169 Self::IndividualBasicMonthly => Self::IndividualProMonthly,
1170 Self::IndividualBasicAnnually => Self::IndividualProAnnually,
1171 _ => panic!(),
1172 }
1173 }
1174}
1175
1176impl TryFrom<&str> for PlanType {
1177 type Error = ();
1178
1179 fn try_from(s: &str) -> Result<Self, Self::Error> {
1180 match s {
1181 "individual-basic-monthly" => Ok(Self::IndividualBasicMonthly),
1182 "individual-basic-annually" => Ok(Self::IndividualBasicAnnually),
1183 "individual-pro-monthly" => Ok(Self::IndividualProMonthly),
1184 "individual-pro-annually" => Ok(Self::IndividualProAnnually),
1185 "school-level-1-monthly" => Ok(Self::SchoolLevel1Monthly),
1186 "school-level-2-monthly" => Ok(Self::SchoolLevel2Monthly),
1187 "school-level-3-monthly" => Ok(Self::SchoolLevel3Monthly),
1188 "school-level-4-monthly" => Ok(Self::SchoolLevel4Monthly),
1189 "school-unlimited-monthly" => Ok(Self::SchoolUnlimitedMonthly),
1190 "school-level-1-annually" => Ok(Self::SchoolLevel1Annually),
1191 "school-level-2-annually" => Ok(Self::SchoolLevel2Annually),
1192 "school-level-3-annually" => Ok(Self::SchoolLevel3Annually),
1193 "school-level-4-annually" => Ok(Self::SchoolLevel4Annually),
1194 "school-unlimited-annually" => Ok(Self::SchoolUnlimitedAnnually),
1195 _ => Err(()),
1196 }
1197 }
1198}
1199
1200#[derive(Debug, Display, Serialize, Deserialize, Clone, Copy)]
1202#[cfg_attr(feature = "backend", derive(sqlx::Type))]
1203#[repr(i16)]
1204pub enum AccountType {
1205 Individual = 0,
1207 School = 1,
1209}
1210
1211impl AccountType {
1212 pub fn has_admin(&self) -> bool {
1214 match self {
1215 Self::School => true,
1216 _ => false,
1217 }
1218 }
1219
1220 pub fn matches_subscription_type(&self, subscription_type: &SubscriptionType) -> bool {
1222 match self {
1223 Self::Individual => matches!(subscription_type, SubscriptionType::Individual),
1224 Self::School => matches!(subscription_type, SubscriptionType::School),
1225 }
1226 }
1227}
1228
1229impl From<SubscriptionType> for AccountType {
1230 fn from(value: SubscriptionType) -> Self {
1231 match value {
1232 SubscriptionType::Individual => Self::Individual,
1233 SubscriptionType::School => Self::School,
1234 }
1235 }
1236}
1237
1238#[derive(Debug, Serialize, Deserialize, Clone)]
1240#[cfg_attr(feature = "backend", derive(sqlx::Type), sqlx(transparent))]
1241pub struct InvoiceNumber(String);
1242
1243#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
1245#[cfg_attr(feature = "backend", derive(sqlx::Type), sqlx(transparent))]
1246pub struct AmountInCents(i64);
1247
1248impl AmountInCents {
1249 pub fn new(amount: i64) -> Self {
1251 Self(amount)
1252 }
1253
1254 pub fn inner(&self) -> i64 {
1256 self.0
1257 }
1258}
1259
1260impl From<i64> for AmountInCents {
1261 fn from(value: i64) -> Self {
1262 Self(value)
1263 }
1264}
1265
1266impl Display for AmountInCents {
1267 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1268 write!(f, "{:.2}", self.0 as f64 / 100.)
1269 }
1270}
1271
1272#[derive(Debug, Serialize, Deserialize, Clone)]
1274#[cfg_attr(feature = "backend", derive(sqlx::Type), sqlx(transparent))]
1275pub struct TrialPeriod(i64);
1276
1277impl TrialPeriod {
1278 pub fn new(length: i64) -> Self {
1280 Self(length)
1281 }
1282
1283 pub fn inner(&self) -> i64 {
1285 self.0
1286 }
1287}
1288
1289wrap_uuid! {
1290 pub struct ChargeId
1292}
1293
1294pub struct Charge {
1297 pub charge_id: ChargeId,
1299 pub charged_at: DateTime<Utc>,
1301 pub subscription_tier: SubscriptionTier,
1303 pub payment_method: PaymentMethod,
1305 pub invoice_number: InvoiceNumber,
1307 pub amount_in_cents: AmountInCents,
1309}
1310
1311wrap_uuid! {
1312 pub struct PlanId
1314}
1315
1316#[derive(Debug, Serialize, Deserialize, Clone)]
1320#[cfg_attr(feature = "backend", derive(sqlx::FromRow))]
1321pub struct SubscriptionPlan {
1322 pub plan_id: PlanId,
1324 pub plan_type: PlanType,
1326 pub price_id: StripePriceId,
1328 pub created_at: DateTime<Utc>,
1330 pub updated_at: Option<DateTime<Utc>>,
1332}
1333
1334make_path_parts!(SubscriptionPlanPath => "/v1/plans");
1335
1336#[derive(Debug, Serialize, Deserialize, Clone)]
1338pub struct UpdateSubscriptionPlansRequest {
1339 #[serde(flatten)]
1341 pub plans: HashMap<PlanType, StripePriceId>,
1342}
1343
1344#[derive(Debug, Serialize, Deserialize, Clone)]
1349pub struct CreateSubscriptionRequest {
1350 pub setup_intent_id: Option<String>,
1353 pub plan_type: PlanType,
1355 pub promotion_code: Option<String>,
1357}
1358
1359make_path_parts!(CreateSubscriptionPath => "/v1/billing/subscribe");
1360
1361#[derive(Debug, Serialize, Deserialize, Clone)]
1363pub struct CreateSubscriptionResponse {
1364 pub subscription_id: StripeSubscriptionId,
1366 pub client_secret: String,
1371}
1372
1373#[derive(Debug, Serialize, Deserialize, Clone)]
1378pub struct CreateSetupIntentRequest {
1379 pub plan_type: PlanType,
1381}
1382
1383make_path_parts!(CreateSetupIntentPath => "/v1/billing/payment-method");
1384
1385wrap_uuid! {
1386 pub struct AccountId
1388}
1389
1390#[derive(Debug, Serialize, Deserialize, Clone)]
1392pub struct Account {
1393 pub account_id: AccountId,
1395 pub account_type: AccountType,
1397 #[serde(default)]
1399 #[serde(skip_serializing_if = "Option::is_none")]
1400 pub stripe_customer_id: Option<CustomerId>,
1401 #[serde(default)]
1403 #[serde(skip_serializing_if = "Option::is_none")]
1404 pub payment_method: Option<PaymentMethod>,
1405 #[serde(default)]
1407 #[serde(skip_serializing_if = "Option::is_none")]
1408 pub subscription: Option<Subscription>,
1409 pub created_at: DateTime<Utc>,
1411 #[serde(default)]
1413 #[serde(skip_serializing_if = "Option::is_none")]
1414 pub updated_at: Option<DateTime<Utc>>,
1415}
1416
1417#[derive(Debug, Serialize, Deserialize, Clone)]
1422pub struct UserAccountSummary {
1423 pub account_id: Option<AccountId>,
1425 pub school_id: Option<SchoolId>,
1427 pub school_name: Option<String>,
1429 pub plan_type: Option<PlanType>,
1431 pub plan_tier: PlanTier,
1433 pub overridden: bool,
1435 pub subscription_status: Option<SubscriptionStatus>,
1437 pub is_admin: bool,
1440 pub overdue: bool,
1442 pub verified: bool,
1444}
1445
1446wrap_uuid! {
1447 pub struct SchoolId
1449}
1450
1451#[derive(Debug, Serialize, Deserialize, Clone)]
1453pub struct School {
1454 pub id: SchoolId,
1456
1457 pub school_name: String,
1459
1460 #[serde(default)]
1462 #[serde(skip_serializing_if = "Option::is_none")]
1463 pub location: Option<Value>,
1464
1465 pub email: String,
1467
1468 #[serde(default)]
1470 #[serde(skip_serializing_if = "Option::is_none")]
1471 pub description: Option<String>,
1472
1473 #[serde(default)]
1475 #[serde(skip_serializing_if = "Option::is_none")]
1476 pub profile_image: Option<ImageId>,
1477
1478 #[serde(default)]
1480 #[serde(skip_serializing_if = "Option::is_none")]
1481 pub website: Option<String>,
1482
1483 #[serde(default)]
1485 #[serde(skip_serializing_if = "Option::is_none")]
1486 pub organization_type: Option<String>,
1487
1488 pub account_id: AccountId,
1490
1491 pub created_at: DateTime<Utc>,
1493
1494 #[serde(default)]
1496 #[serde(skip_serializing_if = "Option::is_none")]
1497 pub updated_at: Option<DateTime<Utc>>,
1498}
1499
1500#[derive(Debug, Serialize, Deserialize, Clone)]
1502pub struct AdminSchool {
1503 pub id: SchoolId,
1505
1506 pub school_name: String,
1508
1509 pub internal_school_name: Option<SchoolName>,
1511
1512 pub verified: bool,
1514
1515 #[serde(default)]
1517 #[serde(skip_serializing_if = "Option::is_none")]
1518 pub location: Option<Value>,
1519
1520 pub email: String,
1522
1523 #[serde(default)]
1525 #[serde(skip_serializing_if = "Option::is_none")]
1526 pub description: Option<String>,
1527
1528 #[serde(default)]
1530 #[serde(skip_serializing_if = "Option::is_none")]
1531 pub profile_image: Option<ImageId>,
1532
1533 #[serde(default)]
1535 #[serde(skip_serializing_if = "Option::is_none")]
1536 pub website: Option<String>,
1537
1538 #[serde(default)]
1540 #[serde(skip_serializing_if = "Option::is_none")]
1541 pub organization_type: Option<String>,
1542
1543 pub account_id: AccountId,
1545
1546 pub created_at: DateTime<Utc>,
1548
1549 #[serde(default)]
1551 #[serde(skip_serializing_if = "Option::is_none")]
1552 pub updated_at: Option<DateTime<Utc>>,
1553}
1554
1555#[derive(Debug, Serialize, Deserialize, Clone)]
1557pub struct AccountUser {
1558 pub user: UserProfile,
1560 pub is_admin: bool,
1563 pub verified: bool,
1565}
1566
1567wrap_uuid! {
1568 pub struct SchoolNameId
1570}
1571
1572#[derive(Debug, Serialize, Deserialize, Clone)]
1574pub struct SchoolName {
1575 pub id: SchoolNameId,
1577 pub name: String,
1579}
1580
1581#[derive(Debug, Serialize, Deserialize, Clone)]
1583#[serde(transparent)]
1584pub struct SchoolNameValue(String);
1585
1586impl fmt::Display for SchoolNameValue {
1587 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1588 write!(f, "{}", self.0)
1589 }
1590}
1591
1592impl From<SchoolNameValue> for String {
1593 fn from(value: SchoolNameValue) -> Self {
1594 value.0
1595 }
1596}
1597
1598impl From<String> for SchoolNameValue {
1599 fn from(value: String) -> Self {
1600 SchoolNameValue(value)
1601 }
1602}
1603
1604impl AsRef<str> for SchoolNameValue {
1605 fn as_ref(&self) -> &str {
1606 &self.0
1607 }
1608}
1609
1610#[derive(Debug, Serialize, Deserialize, Clone)]
1612pub enum SchoolNameRequest {
1613 Value(SchoolNameValue),
1615 Id(SchoolNameId),
1617}
1618
1619make_path_parts!(CreateSchoolAccountPath => "/v1/schools");
1620
1621#[derive(Debug, Serialize, Deserialize, Clone)]
1623pub struct CreateSchoolAccountRequest {
1624 pub name: String,
1626
1627 pub email: String,
1629
1630 #[serde(default)]
1632 #[serde(skip_serializing_if = "Option::is_none")]
1633 pub location: Option<Value>,
1634
1635 #[serde(default)]
1637 #[serde(skip_serializing_if = "Option::is_none")]
1638 pub description: Option<String>,
1639
1640 #[serde(default)]
1642 #[serde(skip_serializing_if = "Option::is_none")]
1643 pub profile_image: Option<ImageId>,
1644
1645 #[serde(default)]
1647 #[serde(skip_serializing_if = "Option::is_none")]
1648 pub website: Option<String>,
1649
1650 #[serde(default)]
1652 #[serde(skip_serializing_if = "Option::is_none")]
1653 pub organization_type: Option<String>,
1654}
1655
1656make_path_parts!(SchoolAccountPath => "/v1/schools/{}" => SchoolId);
1657
1658#[derive(Debug, Serialize, Deserialize, Clone)]
1660pub struct GetSchoolAccountResponse {
1661 pub school: School,
1663 pub account: AccountIfAuthorized,
1665 pub users: Vec<AccountUser>,
1667}
1668
1669#[derive(Debug, Serialize, Deserialize, Clone)]
1671#[allow(clippy::large_enum_variant)]
1672#[serde(untagged)]
1673pub enum AccountIfAuthorized {
1674 Authorized(Account),
1676 Unauthorized,
1678}
1679
1680#[derive(Debug, Default, Serialize, Deserialize, Clone)]
1682pub struct UpdateSchoolAccountRequest {
1683 #[serde(default, skip_serializing_if = "UpdateNonNullable::is_keep")]
1685 pub email: UpdateNonNullable<String>,
1686
1687 #[serde(default, skip_serializing_if = "UpdateNonNullable::is_keep")]
1689 pub school_name: UpdateNonNullable<String>,
1690
1691 #[serde(default, skip_serializing_if = "UpdateNullable::is_keep")]
1693 pub location: UpdateNullable<Value>,
1694
1695 #[serde(default, skip_serializing_if = "UpdateNullable::is_keep")]
1697 pub description: UpdateNullable<String>,
1698
1699 #[serde(default, skip_serializing_if = "UpdateNullable::is_keep")]
1701 pub profile_image: UpdateNullable<ImageId>,
1702
1703 #[serde(default, skip_serializing_if = "UpdateNullable::is_keep")]
1705 pub website: UpdateNullable<String>,
1706
1707 #[serde(default, skip_serializing_if = "UpdateNullable::is_keep")]
1709 pub organization_type: UpdateNullable<String>,
1710}
1711
1712make_path_parts!(IndividualAccountPath => "/v1/user/me/account");
1713
1714#[derive(Debug, Default, Serialize, Deserialize, Clone)]
1716pub struct IndividualAccountResponse {
1717 pub account: Option<Account>,
1719}
1720
1721#[derive(Debug, Serialize, Deserialize, Clone)]
1723#[serde(rename_all = "kebab-case")]
1724pub enum CancellationStatus {
1725 #[serde(rename = "period-end")]
1727 CancelAtPeriodEnd,
1728 #[serde(rename = "remove")]
1730 RemoveCancellation,
1731}
1732
1733#[derive(Debug, Serialize, Deserialize, Clone)]
1735pub struct SubscriptionCancellationStatusRequest {
1736 pub status: CancellationStatus,
1738}
1739
1740make_path_parts!(UpdateSubscriptionCancellationPath => "/v1/billing/subscription/cancel");
1741
1742#[derive(Debug, Serialize, Deserialize, Clone)]
1744pub struct SubscriptionPauseRequest {
1745 pub paused: bool,
1747}
1748
1749make_path_parts!(UpdateSubscriptionPausedPath => "/v1/billing/subscription/pause");
1750
1751#[derive(Debug, Serialize, Deserialize, Clone)]
1753pub struct UpgradeSubscriptionPlanRequest {
1754 pub plan_type: PlanType,
1756 #[serde(default)]
1758 #[serde(skip_serializing_if = "Option::is_none")]
1759 pub promotion_code: Option<String>,
1760}
1761
1762#[derive(Debug, Serialize, Deserialize, Clone)]
1764pub struct AdminUpgradeSubscriptionPlanRequest {
1765 pub plan_type: PlanType,
1767 pub user_id: UserId,
1769}
1770
1771make_path_parts!(UpgradeSubscriptionPlanPath => "/v1/billing/subscription/upgrade");
1772
1773make_path_parts!(AdminUpgradeSubscriptionPlanPath => "/v1/admin/billing/subscription/upgrade");
1774
1775make_path_parts!(CreateCustomerPortalLinkPath => "/v1/billing/customer-portal");