use chrono::{DateTime, Utc};
use macros::make_path_parts;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt::{self, Debug, Display, Formatter};
use strum_macros::{AsRefStr, Display, EnumIs, EnumIter, EnumString};
use const_format::formatcp;
use serde_json::Value;
use crate::api::endpoints::PathPart;
use crate::domain::image::ImageId;
use crate::domain::user::{UserId, UserProfile};
use crate::domain::{Percent, UpdateNonNullable, UpdateNullable};
pub const PLAN_SCHOOL_LEVEL_1_TEACHER_COUNT: i64 = 5;
pub const PLAN_SCHOOL_LEVEL_2_TEACHER_COUNT: i64 = 10;
pub const PLAN_SCHOOL_LEVEL_3_TEACHER_COUNT: i64 = 15;
pub const PLAN_SCHOOL_LEVEL_4_TEACHER_COUNT: i64 = 20;
pub const INDIVIDUAL_TRIAL_PERIOD: i64 = 7;
pub const SCHOOL_TRIAL_PERIOD: i64 = 14;
pub const PLAN_PRICE_MONTHLY_BASIC: u32 = 17_99;
pub const PLAN_PRICE_ANNUAL_BASIC: u32 = 180_00;
pub const PLAN_PRICE_MONTHLY_PRO: u32 = 29_99;
pub const PLAN_PRICE_ANNUAL_PRO: u32 = 300_00;
pub const PLAN_PRICE_MONTHLY_SCHOOL_1: u32 = 115_00;
pub const PLAN_PRICE_ANNUAL_SCHOOL_1: u32 = 1_250_00;
pub const PLAN_PRICE_MONTHLY_SCHOOL_2: u32 = 150_00;
pub const PLAN_PRICE_ANNUAL_SCHOOL_2: u32 = 1_500_00;
pub const PLAN_PRICE_MONTHLY_SCHOOL_3: u32 = 200_00;
pub const PLAN_PRICE_ANNUAL_SCHOOL_3: u32 = 2_000_00;
pub const PLAN_PRICE_MONTHLY_SCHOOL_4: u32 = 250_00;
pub const PLAN_PRICE_ANNUAL_SCHOOL_4: u32 = 2_500_00;
pub const PLAN_PRICE_MONTHLY_SCHOOL_UNLIMITED: u32 = 300_00;
pub const PLAN_PRICE_ANNUAL_SCHOOL_UNLIMITED: u32 = 3_000_00;
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "backend", derive(sqlx::Type), sqlx(transparent))]
pub struct CustomerId(String);
#[cfg(feature = "backend")]
impl From<stripe::CustomerId> for CustomerId {
fn from(value: stripe::CustomerId) -> Self {
Self(value.as_str().to_owned())
}
}
#[cfg(feature = "backend")]
impl From<CustomerId> for stripe::CustomerId {
fn from(value: CustomerId) -> Self {
use std::str::FromStr;
Self::from_str(&value.0).unwrap()
}
}
impl CustomerId {
#[cfg(feature = "backend")]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for CustomerId {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct StripePaymentMethodId(String);
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Last4(String);
impl fmt::Display for Last4 {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Serialize, Deserialize, Clone, EnumString)]
#[serde(rename_all = "kebab-case")]
#[strum(serialize_all = "lowercase")]
pub enum PaymentNetwork {
Visa,
Mastercard,
Discover,
JCB,
#[strum(serialize = "amex")]
AmericanExpress,
UnionPay,
#[strum(serialize = "diners")]
DinersClub,
Unknown,
}
impl Default for PaymentNetwork {
fn default() -> Self {
Self::Unknown
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Card {
pub last4: Last4,
pub payment_network: PaymentNetwork,
pub exp_month: u8,
pub exp_year: u16,
}
#[cfg(feature = "backend")]
impl From<stripe::CardDetails> for Card {
fn from(value: stripe::CardDetails) -> Self {
use std::str::FromStr;
Self {
last4: Last4(value.last4),
payment_network: PaymentNetwork::from_str(&value.brand).unwrap_or_default(),
exp_month: value.exp_month as u8,
exp_year: value.exp_year as u16,
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub enum PaymentMethodType {
ApplePay,
GooglePay,
Link,
Card(Card),
Other,
}
wrap_uuid! {
pub struct PaymentMethodId
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PaymentMethod {
pub stripe_payment_method_id: StripePaymentMethodId, pub payment_method_type: PaymentMethodType,
}
#[cfg(feature = "backend")]
impl From<stripe::PaymentMethod> for PaymentMethod {
fn from(value: stripe::PaymentMethod) -> Self {
let payment_method_type = if value.link.is_some() {
PaymentMethodType::Link
} else if let Some(card) = value.card {
if let Some(wallet) = card.wallet {
if wallet.apple_pay.is_some() {
PaymentMethodType::ApplePay
} else if wallet.google_pay.is_some() {
PaymentMethodType::GooglePay
} else {
PaymentMethodType::Other
}
} else {
PaymentMethodType::Card(Card::from(card))
}
} else {
PaymentMethodType::Other
};
Self {
stripe_payment_method_id: StripePaymentMethodId(value.id.as_str().to_string()),
payment_method_type,
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
#[cfg_attr(feature = "backend", derive(sqlx::Type))]
#[repr(i16)]
pub enum SubscriptionTier {
Basic = 0,
Pro = 1,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "backend", derive(sqlx::Type), sqlx(transparent))]
pub struct StripeSubscriptionId(String);
#[cfg(feature = "backend")]
impl From<stripe::SubscriptionId> for StripeSubscriptionId {
fn from(value: stripe::SubscriptionId) -> Self {
Self(value.as_str().to_owned())
}
}
#[cfg(feature = "backend")]
impl TryFrom<StripeSubscriptionId> for stripe::SubscriptionId {
type Error = anyhow::Error;
fn try_from(value: StripeSubscriptionId) -> Result<Self, Self::Error> {
<Self as std::str::FromStr>::from_str(&value.0).map_err(Into::into)
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "backend", derive(sqlx::Type), sqlx(transparent))]
pub struct StripeInvoiceId(String);
impl StripeInvoiceId {
pub fn inner(&self) -> String {
self.0.clone()
}
}
#[cfg(feature = "backend")]
impl From<&stripe::InvoiceId> for StripeInvoiceId {
fn from(value: &stripe::InvoiceId) -> Self {
Self(value.as_str().to_owned())
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "backend", derive(sqlx::Type), sqlx(transparent))]
pub struct StripeProductId(String);
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "backend", derive(sqlx::Type), sqlx(transparent))]
pub struct StripePriceId(String);
impl From<StripePriceId> for String {
fn from(value: StripePriceId) -> Self {
value.0
}
}
#[derive(Debug, Display, Serialize, Deserialize, Clone, Copy)]
#[cfg_attr(feature = "backend", derive(sqlx::Type))]
#[repr(i16)]
pub enum BillingInterval {
Monthly = 0,
Annually = 1,
}
impl BillingInterval {
pub fn as_str(&self) -> &'static str {
self.into()
}
pub fn display_name(&self) -> &'static str {
match self {
BillingInterval::Annually => "Annually",
BillingInterval::Monthly => "Monthly",
}
}
}
impl TryFrom<&str> for BillingInterval {
type Error = ();
fn try_from(value: &str) -> Result<Self, Self::Error> {
match value {
"annually" => Ok(Self::Annually),
"monthly" => Ok(Self::Monthly),
_ => Err(()),
}
}
}
impl From<&BillingInterval> for &str {
fn from(value: &BillingInterval) -> Self {
match value {
BillingInterval::Annually => "annually",
BillingInterval::Monthly => "monthly",
}
}
}
#[derive(Copy, Debug, Display, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "backend", derive(sqlx::Type))]
#[repr(i16)]
pub enum SubscriptionStatus {
Inactive = 0,
Active = 1,
Canceled = 2,
Expired = 3,
Paused = 4,
}
impl SubscriptionStatus {
#[must_use]
pub const fn is_valid(&self) -> bool {
matches!(self, Self::Active | Self::Canceled)
}
#[must_use]
pub const fn is_active(&self) -> bool {
matches!(self, Self::Active)
}
#[must_use]
pub const fn is_canceled(&self) -> bool {
matches!(self, Self::Canceled)
}
#[must_use]
pub const fn is_paused(&self) -> bool {
matches!(self, Self::Paused)
}
}
#[cfg(feature = "backend")]
impl Default for SubscriptionStatus {
fn default() -> Self {
Self::Inactive
}
}
#[cfg(feature = "backend")]
impl From<stripe::SubscriptionStatus> for SubscriptionStatus {
fn from(value: stripe::SubscriptionStatus) -> Self {
match value {
stripe::SubscriptionStatus::Incomplete | stripe::SubscriptionStatus::Paused => {
Self::Inactive
}
stripe::SubscriptionStatus::Active
| stripe::SubscriptionStatus::PastDue
| stripe::SubscriptionStatus::Trialing
| stripe::SubscriptionStatus::Unpaid => Self::Active,
stripe::SubscriptionStatus::Canceled => Self::Canceled,
stripe::SubscriptionStatus::IncompleteExpired => Self::Expired,
}
}
}
wrap_uuid! {
pub struct SubscriptionId
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Subscription {
pub subscription_id: SubscriptionId,
pub stripe_subscription_id: StripeSubscriptionId,
pub subscription_plan_type: PlanType,
pub auto_renew: bool,
pub status: SubscriptionStatus,
pub is_trial: bool,
pub current_period_end: DateTime<Utc>,
pub account_id: AccountId,
pub latest_invoice_id: Option<StripeInvoiceId>,
pub amount_due_in_cents: Option<AmountInCents>,
pub price: AmountInCents,
pub applied_coupon: Option<AppliedCoupon>,
pub created_at: DateTime<Utc>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub updated_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct AppliedCoupon {
pub coupon_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub coupon_percent: Option<Percent>,
pub coupon_from: DateTime<Utc>,
#[serde(skip_serializing_if = "Option::is_none")]
pub coupon_to: Option<DateTime<Utc>>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg(feature = "backend")]
pub struct CreateSubscriptionRecord {
pub stripe_subscription_id: StripeSubscriptionId,
pub subscription_plan_id: PlanId,
pub status: SubscriptionStatus,
pub current_period_end: DateTime<Utc>,
pub account_id: AccountId,
pub latest_invoice_id: Option<StripeInvoiceId>,
pub amount_due_in_cents: Option<AmountInCents>,
pub price: AmountInCents,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg(feature = "backend")]
pub struct UpdateSubscriptionRecord {
pub stripe_subscription_id: StripeSubscriptionId,
pub subscription_plan_id: UpdateNonNullable<PlanId>,
#[serde(default, skip_serializing_if = "UpdateNonNullable::is_keep")]
pub status: UpdateNonNullable<SubscriptionStatus>,
#[serde(default, skip_serializing_if = "UpdateNonNullable::is_keep")]
pub current_period_end: UpdateNonNullable<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "UpdateNonNullable::is_keep")]
pub latest_invoice_id: UpdateNonNullable<StripeInvoiceId>,
#[serde(default, skip_serializing_if = "UpdateNonNullable::is_keep")]
pub is_trial: UpdateNonNullable<bool>,
#[serde(default, skip_serializing_if = "UpdateNonNullable::is_keep")]
pub price: UpdateNonNullable<AmountInCents>,
#[serde(default, skip_serializing_if = "UpdateNullable::is_keep")]
pub coupon_name: UpdateNullable<String>,
#[serde(default, skip_serializing_if = "UpdateNullable::is_keep")]
pub coupon_percent: UpdateNullable<Percent>,
#[serde(default, skip_serializing_if = "UpdateNullable::is_keep")]
pub coupon_from: UpdateNullable<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "UpdateNullable::is_keep")]
pub coupon_to: UpdateNullable<DateTime<Utc>>,
}
#[cfg(feature = "backend")]
impl UpdateSubscriptionRecord {
#[must_use]
pub fn new(stripe_subscription_id: StripeSubscriptionId) -> Self {
Self {
stripe_subscription_id,
subscription_plan_id: Default::default(),
status: Default::default(),
current_period_end: Default::default(),
latest_invoice_id: Default::default(),
is_trial: Default::default(),
price: Default::default(),
coupon_name: Default::default(),
coupon_percent: Default::default(),
coupon_from: Default::default(),
coupon_to: Default::default(),
}
}
}
#[cfg(feature = "backend")]
impl TryFrom<stripe::Subscription> for UpdateSubscriptionRecord {
type Error = anyhow::Error;
fn try_from(value: stripe::Subscription) -> Result<Self, Self::Error> {
use chrono::TimeZone;
let latest_invoice_id = value
.latest_invoice
.as_ref()
.map(|invoice| StripeInvoiceId::from(&invoice.id()))
.into();
let price = AmountInCents::from(
value
.items
.data
.get(0)
.map(|item| item.clone())
.ok_or(anyhow::anyhow!("Missing plan data"))?
.plan
.ok_or(anyhow::anyhow!("Missing stripe subscription plan"))?
.amount
.ok_or(anyhow::anyhow!("Missing subscription plan amount"))?,
);
let (coupon_name, coupon_percent, coupon_from, coupon_to) = value.discount.map_or_else(
|| {
Ok((
Default::default(),
Default::default(),
Default::default(),
Default::default(),
))
},
|discount| -> Result<_, Self::Error> {
let start_time = Some(
Utc.timestamp_opt(discount.start, 0)
.latest()
.ok_or(anyhow::anyhow!("Invalid timestamp"))?,
);
let end_time = match discount.end {
Some(end) => Some(
Utc.timestamp_opt(end, 0)
.latest()
.ok_or(anyhow::anyhow!("Invalid timestamp"))?,
),
None => None,
};
Ok((
UpdateNullable::from(discount.coupon.name.map(|name| name.to_uppercase())),
UpdateNullable::from(
discount
.coupon
.percent_off
.map(|percent| Percent::from(percent / 100.0)),
),
UpdateNullable::from(start_time),
UpdateNullable::from(end_time),
))
},
)?;
Ok(Self {
stripe_subscription_id: value.id.into(),
subscription_plan_id: UpdateNonNullable::Keep,
is_trial: UpdateNonNullable::Change(matches!(
value.status,
stripe::SubscriptionStatus::Trialing
)),
status: UpdateNonNullable::Change(if value.ended_at.is_some() {
SubscriptionStatus::Expired
} else if value.canceled_at.is_some() {
SubscriptionStatus::Canceled
} else if value.pause_collection.is_some() {
SubscriptionStatus::Paused
} else {
SubscriptionStatus::from(value.status)
}),
current_period_end: UpdateNonNullable::Change(
Utc.timestamp_opt(value.current_period_end, 0)
.latest()
.ok_or(anyhow::anyhow!("Invalid timestamp"))?,
),
latest_invoice_id,
price: UpdateNonNullable::Change(price),
coupon_name,
coupon_percent,
coupon_from,
coupon_to,
})
}
}
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Eq, PartialEq, Hash, Ord, PartialOrd)]
#[cfg_attr(feature = "backend", derive(sqlx::Type), sqlx(transparent))]
pub struct AccountLimit(i64);
impl From<i64> for AccountLimit {
fn from(value: i64) -> Self {
Self(value)
}
}
#[derive(Debug, Display, Serialize, Deserialize, Clone, Copy)]
#[cfg_attr(feature = "backend", derive(sqlx::Type))]
#[repr(i16)]
pub enum SubscriptionType {
Individual = 0,
School = 1,
}
#[derive(
Debug,
Default,
Display,
EnumString,
EnumIs,
AsRefStr,
Serialize,
Deserialize,
Clone,
Copy,
PartialEq,
Eq,
)]
#[cfg_attr(feature = "backend", derive(sqlx::Type))]
#[repr(i16)]
pub enum PlanTier {
#[default]
Free = 0,
Basic = 1,
Pro = 2,
}
#[derive(
Debug, Serialize, Deserialize, Clone, Copy, Eq, Ord, PartialOrd, PartialEq, Hash, EnumIter,
)]
#[serde(rename_all = "kebab-case")]
#[cfg_attr(feature = "backend", derive(sqlx::Type))]
#[repr(i16)]
pub enum PlanType {
IndividualBasicMonthly = 0,
IndividualBasicAnnually = 1,
IndividualProMonthly = 2,
IndividualProAnnually = 3,
SchoolLevel1Monthly = 4,
SchoolLevel2Monthly = 5,
SchoolLevel3Monthly = 6,
SchoolLevel4Monthly = 7,
SchoolUnlimitedMonthly = 8,
SchoolLevel1Annually = 9,
SchoolLevel2Annually = 10,
SchoolLevel3Annually = 11,
SchoolLevel4Annually = 12,
SchoolUnlimitedAnnually = 13,
}
impl Display for PlanType {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let s = match self {
Self::IndividualBasicMonthly => "Individual Basic Monthly",
Self::IndividualBasicAnnually => "Individual Basic Annual",
Self::IndividualProMonthly => "Individual Pro Monthly",
Self::IndividualProAnnually => "Individual Pro Annual",
Self::SchoolLevel1Monthly => formatcp!(
"School - Up to {} Monthly",
PLAN_SCHOOL_LEVEL_1_TEACHER_COUNT
),
Self::SchoolLevel2Monthly => formatcp!(
"School - Up to {} Monthly",
PLAN_SCHOOL_LEVEL_2_TEACHER_COUNT
),
Self::SchoolLevel3Monthly => formatcp!(
"School - Up to {} Monthly",
PLAN_SCHOOL_LEVEL_3_TEACHER_COUNT
),
Self::SchoolLevel4Monthly => formatcp!(
"School - Up to {} Monthly",
PLAN_SCHOOL_LEVEL_4_TEACHER_COUNT
),
Self::SchoolUnlimitedMonthly => {
formatcp!("School - {}+ Monthly", PLAN_SCHOOL_LEVEL_4_TEACHER_COUNT)
}
Self::SchoolLevel1Annually => {
formatcp!("School - Up to {}", PLAN_SCHOOL_LEVEL_1_TEACHER_COUNT)
}
Self::SchoolLevel2Annually => {
formatcp!("School - Up to {}", PLAN_SCHOOL_LEVEL_2_TEACHER_COUNT)
}
Self::SchoolLevel3Annually => {
formatcp!("School - Up to {}", PLAN_SCHOOL_LEVEL_3_TEACHER_COUNT)
}
Self::SchoolLevel4Annually => {
formatcp!("School - Up to {}", PLAN_SCHOOL_LEVEL_4_TEACHER_COUNT)
}
Self::SchoolUnlimitedAnnually => {
formatcp!("School - {}+", PLAN_SCHOOL_LEVEL_4_TEACHER_COUNT)
}
};
write!(f, "{}", s)
}
}
impl PlanType {
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::IndividualBasicMonthly => "individual-basic-monthly",
Self::IndividualBasicAnnually => "individual-basic-annually",
Self::IndividualProMonthly => "individual-pro-monthly",
Self::IndividualProAnnually => "individual-pro-annually",
Self::SchoolLevel1Monthly => "school-level-1-monthly",
Self::SchoolLevel2Monthly => "school-level-2-monthly",
Self::SchoolLevel3Monthly => "school-level-3-monthly",
Self::SchoolLevel4Monthly => "school-level-4-monthly",
Self::SchoolUnlimitedMonthly => "school-unlimited-monthly",
Self::SchoolLevel1Annually => "school-level-1-annually",
Self::SchoolLevel2Annually => "school-level-2-annually",
Self::SchoolLevel3Annually => "school-level-3-annually",
Self::SchoolLevel4Annually => "school-level-4-annually",
Self::SchoolUnlimitedAnnually => "school-unlimited-annually",
}
}
#[must_use]
pub const fn display_name(&self) -> &'static str {
match self {
Self::IndividualBasicMonthly => "Individual - Basic monthly",
Self::IndividualBasicAnnually => "Individual - Basic annual",
Self::IndividualProMonthly => "Individual - Pro monthly",
Self::IndividualProAnnually => "Individual - Pro annual",
Self::SchoolLevel1Monthly => formatcp!(
"School - Up to {} teachers - Monthly",
PLAN_SCHOOL_LEVEL_1_TEACHER_COUNT
),
Self::SchoolLevel2Monthly => formatcp!(
"School - Up to {} teachers - Monthly",
PLAN_SCHOOL_LEVEL_2_TEACHER_COUNT
),
Self::SchoolLevel3Monthly => formatcp!(
"School - Up to {} teachers - Monthly",
PLAN_SCHOOL_LEVEL_3_TEACHER_COUNT
),
Self::SchoolLevel4Monthly => formatcp!(
"School - Up to {} teachers - Monthly",
PLAN_SCHOOL_LEVEL_4_TEACHER_COUNT
),
Self::SchoolUnlimitedMonthly => formatcp!(
"School - More than {} teachers - Monthly",
PLAN_SCHOOL_LEVEL_4_TEACHER_COUNT
),
Self::SchoolLevel1Annually => formatcp!(
"School - Up to {} teachers",
PLAN_SCHOOL_LEVEL_1_TEACHER_COUNT
),
Self::SchoolLevel2Annually => formatcp!(
"School - Up to {} teachers",
PLAN_SCHOOL_LEVEL_2_TEACHER_COUNT
),
Self::SchoolLevel3Annually => formatcp!(
"School - Up to {} teachers",
PLAN_SCHOOL_LEVEL_3_TEACHER_COUNT
),
Self::SchoolLevel4Annually => formatcp!(
"School - Up to {} teachers",
PLAN_SCHOOL_LEVEL_4_TEACHER_COUNT
),
Self::SchoolUnlimitedAnnually => formatcp!(
"School - More than {} teachers",
PLAN_SCHOOL_LEVEL_4_TEACHER_COUNT
),
}
}
#[must_use]
pub const fn user_display_name(&self) -> &'static str {
match self {
Self::IndividualBasicMonthly | Self::IndividualBasicAnnually => "Basic",
Self::IndividualProMonthly | Self::IndividualProAnnually => "Pro",
Self::SchoolLevel1Monthly | Self::SchoolLevel1Annually => {
formatcp!("Up to {} teachers", PLAN_SCHOOL_LEVEL_1_TEACHER_COUNT)
}
Self::SchoolLevel2Monthly | Self::SchoolLevel2Annually => {
formatcp!("Up to {} teachers", PLAN_SCHOOL_LEVEL_2_TEACHER_COUNT)
}
Self::SchoolLevel3Monthly | Self::SchoolLevel3Annually => {
formatcp!("Up to {} teachers", PLAN_SCHOOL_LEVEL_3_TEACHER_COUNT)
}
Self::SchoolLevel4Monthly | Self::SchoolLevel4Annually => {
formatcp!("Up to {} teachers", PLAN_SCHOOL_LEVEL_4_TEACHER_COUNT)
}
Self::SchoolUnlimitedMonthly | Self::SchoolUnlimitedAnnually => {
formatcp!("More than {} teachers", PLAN_SCHOOL_LEVEL_4_TEACHER_COUNT)
}
}
}
#[must_use]
pub const fn subscription_tier(&self) -> SubscriptionTier {
match self {
Self::IndividualBasicMonthly | Self::IndividualBasicAnnually => SubscriptionTier::Basic,
_ => SubscriptionTier::Pro,
}
}
#[must_use]
pub const fn account_limit(&self) -> Option<AccountLimit> {
match self {
Self::SchoolLevel1Monthly | Self::SchoolLevel1Annually => {
Some(AccountLimit(PLAN_SCHOOL_LEVEL_1_TEACHER_COUNT))
}
Self::SchoolLevel2Monthly | Self::SchoolLevel2Annually => {
Some(AccountLimit(PLAN_SCHOOL_LEVEL_2_TEACHER_COUNT))
}
Self::SchoolLevel3Monthly | Self::SchoolLevel3Annually => {
Some(AccountLimit(PLAN_SCHOOL_LEVEL_3_TEACHER_COUNT))
}
Self::SchoolLevel4Monthly | Self::SchoolLevel4Annually => {
Some(AccountLimit(PLAN_SCHOOL_LEVEL_4_TEACHER_COUNT))
}
Self::SchoolUnlimitedMonthly | Self::SchoolUnlimitedAnnually => None,
Self::IndividualBasicMonthly
| Self::IndividualBasicAnnually
| Self::IndividualProMonthly
| Self::IndividualProAnnually => Some(AccountLimit(1)),
}
}
#[must_use]
pub const fn subscription_type(&self) -> SubscriptionType {
match self {
Self::IndividualBasicMonthly
| Self::IndividualBasicAnnually
| Self::IndividualProMonthly
| Self::IndividualProAnnually => SubscriptionType::Individual,
_ => SubscriptionType::School,
}
}
#[must_use]
pub const fn trial_period(&self) -> TrialPeriod {
match self.subscription_type() {
SubscriptionType::Individual => TrialPeriod(INDIVIDUAL_TRIAL_PERIOD),
SubscriptionType::School => TrialPeriod(SCHOOL_TRIAL_PERIOD),
}
}
#[must_use]
pub const fn billing_interval(&self) -> BillingInterval {
match self {
Self::IndividualBasicMonthly
| Self::IndividualProMonthly
| Self::SchoolLevel1Monthly
| Self::SchoolLevel2Monthly
| Self::SchoolLevel3Monthly
| Self::SchoolLevel4Monthly
| Self::SchoolUnlimitedMonthly => BillingInterval::Monthly,
Self::IndividualBasicAnnually
| Self::IndividualProAnnually
| Self::SchoolLevel1Annually
| Self::SchoolLevel2Annually
| Self::SchoolLevel3Annually
| Self::SchoolLevel4Annually
| Self::SchoolUnlimitedAnnually => BillingInterval::Annually,
}
}
#[must_use]
pub const fn can_upgrade_from(&self, from_type: &Self) -> bool {
match self {
Self::IndividualBasicMonthly => false,
Self::IndividualBasicAnnually | Self::IndividualProMonthly => {
matches!(from_type, Self::IndividualBasicMonthly)
}
Self::IndividualProAnnually => matches!(
from_type,
Self::IndividualBasicMonthly
| Self::IndividualBasicAnnually
| Self::IndividualProMonthly
),
Self::SchoolLevel1Monthly => false,
Self::SchoolLevel2Monthly => matches!(from_type, Self::SchoolLevel1Monthly),
Self::SchoolLevel3Monthly => matches!(
from_type,
Self::SchoolLevel1Monthly | Self::SchoolLevel2Monthly,
),
Self::SchoolLevel4Monthly => matches!(
from_type,
Self::SchoolLevel1Monthly | Self::SchoolLevel2Monthly | Self::SchoolLevel3Monthly,
),
Self::SchoolUnlimitedMonthly => matches!(
from_type,
Self::SchoolLevel1Monthly
| Self::SchoolLevel2Monthly
| Self::SchoolLevel3Monthly
| Self::SchoolLevel4Monthly,
),
Self::SchoolLevel1Annually => matches!(from_type, Self::SchoolLevel1Monthly),
Self::SchoolLevel2Annually => matches!(
from_type,
Self::SchoolLevel1Monthly | Self::SchoolLevel2Monthly | Self::SchoolLevel1Annually
),
Self::SchoolLevel3Annually => matches!(
from_type,
Self::SchoolLevel1Monthly
| Self::SchoolLevel2Monthly
| Self::SchoolLevel3Monthly
| Self::SchoolLevel1Annually
| Self::SchoolLevel2Annually
),
Self::SchoolLevel4Annually => matches!(
from_type,
Self::SchoolLevel1Monthly
| Self::SchoolLevel2Monthly
| Self::SchoolLevel3Monthly
| Self::SchoolLevel4Monthly
| Self::SchoolLevel1Annually
| Self::SchoolLevel2Annually
| Self::SchoolLevel3Annually
),
Self::SchoolUnlimitedAnnually => matches!(
from_type,
Self::SchoolLevel1Monthly
| Self::SchoolLevel2Monthly
| Self::SchoolLevel3Monthly
| Self::SchoolLevel4Monthly
| Self::SchoolUnlimitedMonthly
| Self::SchoolLevel1Annually
| Self::SchoolLevel2Annually
| Self::SchoolLevel3Annually
| Self::SchoolLevel4Annually
),
}
}
#[must_use]
pub const fn can_upgrade_from_same_interval(&self, from_type: &Self) -> bool {
match self {
Self::IndividualProAnnually => matches!(from_type, Self::IndividualBasicAnnually,),
Self::SchoolLevel1Monthly => false,
Self::SchoolLevel2Monthly => matches!(from_type, Self::SchoolLevel1Monthly),
Self::SchoolLevel3Monthly => matches!(
from_type,
Self::SchoolLevel1Monthly | Self::SchoolLevel2Monthly,
),
Self::SchoolLevel4Monthly => matches!(
from_type,
Self::SchoolLevel1Monthly | Self::SchoolLevel2Monthly | Self::SchoolLevel3Monthly,
),
Self::SchoolUnlimitedMonthly => matches!(
from_type,
Self::SchoolLevel1Monthly
| Self::SchoolLevel2Monthly
| Self::SchoolLevel3Monthly
| Self::SchoolLevel4Monthly,
),
Self::SchoolLevel1Annually => false,
Self::SchoolLevel2Annually => matches!(from_type, Self::SchoolLevel1Annually),
Self::SchoolLevel3Annually => matches!(
from_type,
Self::SchoolLevel1Annually | Self::SchoolLevel2Annually
),
Self::SchoolLevel4Annually => matches!(
from_type,
Self::SchoolLevel1Annually
| Self::SchoolLevel2Annually
| Self::SchoolLevel3Annually
),
Self::SchoolUnlimitedAnnually => matches!(
from_type,
Self::SchoolLevel1Annually
| Self::SchoolLevel2Annually
| Self::SchoolLevel3Annually
| Self::SchoolLevel4Annually
),
_ => false,
}
}
#[must_use]
pub const fn is_individual_plan(&self) -> bool {
matches!(
self,
Self::IndividualBasicMonthly
| Self::IndividualBasicAnnually
| Self::IndividualProMonthly
| Self::IndividualProAnnually
)
}
#[must_use]
pub const fn is_school_plan(&self) -> bool {
match self {
PlanType::IndividualBasicMonthly
| PlanType::IndividualBasicAnnually
| PlanType::IndividualProMonthly
| PlanType::IndividualProAnnually => false,
PlanType::SchoolLevel1Monthly
| PlanType::SchoolLevel2Monthly
| PlanType::SchoolLevel3Monthly
| PlanType::SchoolLevel4Monthly
| PlanType::SchoolUnlimitedMonthly
| PlanType::SchoolLevel1Annually
| PlanType::SchoolLevel2Annually
| PlanType::SchoolLevel3Annually
| PlanType::SchoolLevel4Annually
| PlanType::SchoolUnlimitedAnnually => true,
}
}
#[must_use]
pub const fn plan_tier(&self) -> PlanTier {
match self {
PlanType::IndividualProMonthly
| PlanType::IndividualProAnnually
| PlanType::SchoolLevel1Monthly
| PlanType::SchoolLevel2Monthly
| PlanType::SchoolLevel3Monthly
| PlanType::SchoolLevel4Monthly
| PlanType::SchoolUnlimitedMonthly
| PlanType::SchoolLevel1Annually
| PlanType::SchoolLevel2Annually
| PlanType::SchoolLevel3Annually
| PlanType::SchoolLevel4Annually
| PlanType::SchoolUnlimitedAnnually => PlanTier::Pro,
PlanType::IndividualBasicMonthly | PlanType::IndividualBasicAnnually => PlanTier::Basic,
}
}
pub const fn plan_price(&self) -> u32 {
match self {
Self::IndividualBasicMonthly => PLAN_PRICE_MONTHLY_BASIC,
Self::IndividualBasicAnnually => PLAN_PRICE_ANNUAL_BASIC,
Self::IndividualProMonthly => PLAN_PRICE_MONTHLY_PRO,
Self::IndividualProAnnually => PLAN_PRICE_ANNUAL_PRO,
Self::SchoolLevel1Monthly => PLAN_PRICE_MONTHLY_SCHOOL_1,
Self::SchoolLevel1Annually => PLAN_PRICE_ANNUAL_SCHOOL_1,
Self::SchoolLevel2Monthly => PLAN_PRICE_MONTHLY_SCHOOL_2,
Self::SchoolLevel2Annually => PLAN_PRICE_ANNUAL_SCHOOL_2,
Self::SchoolLevel3Monthly => PLAN_PRICE_MONTHLY_SCHOOL_3,
Self::SchoolLevel3Annually => PLAN_PRICE_ANNUAL_SCHOOL_3,
Self::SchoolLevel4Monthly => PLAN_PRICE_MONTHLY_SCHOOL_4,
Self::SchoolLevel4Annually => PLAN_PRICE_ANNUAL_SCHOOL_4,
Self::SchoolUnlimitedMonthly => PLAN_PRICE_MONTHLY_SCHOOL_UNLIMITED,
Self::SchoolUnlimitedAnnually => PLAN_PRICE_ANNUAL_SCHOOL_UNLIMITED,
}
}
pub const fn annual_to_monthly(&self) -> PlanType {
match self {
Self::IndividualBasicAnnually => Self::IndividualBasicMonthly,
Self::IndividualProAnnually => Self::IndividualProMonthly,
Self::SchoolLevel1Annually => Self::SchoolLevel1Monthly,
Self::SchoolLevel2Annually => Self::SchoolLevel2Monthly,
Self::SchoolLevel3Annually => Self::SchoolLevel3Monthly,
Self::SchoolLevel4Annually => Self::SchoolLevel4Monthly,
Self::SchoolUnlimitedAnnually => Self::SchoolUnlimitedMonthly,
_ => panic!(),
}
}
pub const fn monthly_to_annual(&self) -> PlanType {
match self {
Self::IndividualBasicMonthly => Self::IndividualBasicAnnually,
Self::IndividualProMonthly => Self::IndividualProAnnually,
Self::SchoolLevel1Monthly => Self::SchoolLevel1Annually,
Self::SchoolLevel2Monthly => Self::SchoolLevel2Annually,
Self::SchoolLevel3Monthly => Self::SchoolLevel3Annually,
Self::SchoolLevel4Monthly => Self::SchoolLevel4Annually,
Self::SchoolUnlimitedMonthly => Self::SchoolUnlimitedAnnually,
_ => panic!(),
}
}
pub const fn basic_to_pro(&self) -> PlanType {
match self {
Self::IndividualBasicMonthly => Self::IndividualProMonthly,
Self::IndividualBasicAnnually => Self::IndividualProAnnually,
_ => panic!(),
}
}
}
impl TryFrom<&str> for PlanType {
type Error = ();
fn try_from(s: &str) -> Result<Self, Self::Error> {
match s {
"individual-basic-monthly" => Ok(Self::IndividualBasicMonthly),
"individual-basic-annually" => Ok(Self::IndividualBasicAnnually),
"individual-pro-monthly" => Ok(Self::IndividualProMonthly),
"individual-pro-annually" => Ok(Self::IndividualProAnnually),
"school-level-1-monthly" => Ok(Self::SchoolLevel1Monthly),
"school-level-2-monthly" => Ok(Self::SchoolLevel2Monthly),
"school-level-3-monthly" => Ok(Self::SchoolLevel3Monthly),
"school-level-4-monthly" => Ok(Self::SchoolLevel4Monthly),
"school-unlimited-monthly" => Ok(Self::SchoolUnlimitedMonthly),
"school-level-1-annually" => Ok(Self::SchoolLevel1Annually),
"school-level-2-annually" => Ok(Self::SchoolLevel2Annually),
"school-level-3-annually" => Ok(Self::SchoolLevel3Annually),
"school-level-4-annually" => Ok(Self::SchoolLevel4Annually),
"school-unlimited-annually" => Ok(Self::SchoolUnlimitedAnnually),
_ => Err(()),
}
}
}
#[derive(Debug, Display, Serialize, Deserialize, Clone, Copy)]
#[cfg_attr(feature = "backend", derive(sqlx::Type))]
#[repr(i16)]
pub enum AccountType {
Individual = 0,
School = 1,
}
impl AccountType {
pub fn has_admin(&self) -> bool {
match self {
Self::School => true,
_ => false,
}
}
pub fn matches_subscription_type(&self, subscription_type: &SubscriptionType) -> bool {
match self {
Self::Individual => matches!(subscription_type, SubscriptionType::Individual),
Self::School => matches!(subscription_type, SubscriptionType::School),
}
}
}
impl From<SubscriptionType> for AccountType {
fn from(value: SubscriptionType) -> Self {
match value {
SubscriptionType::Individual => Self::Individual,
SubscriptionType::School => Self::School,
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "backend", derive(sqlx::Type), sqlx(transparent))]
pub struct InvoiceNumber(String);
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
#[cfg_attr(feature = "backend", derive(sqlx::Type), sqlx(transparent))]
pub struct AmountInCents(i64);
impl AmountInCents {
pub fn new(amount: i64) -> Self {
Self(amount)
}
pub fn inner(&self) -> i64 {
self.0
}
}
impl From<i64> for AmountInCents {
fn from(value: i64) -> Self {
Self(value)
}
}
impl Display for AmountInCents {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{:.2}", self.0 as f64 / 100.)
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "backend", derive(sqlx::Type), sqlx(transparent))]
pub struct TrialPeriod(i64);
impl TrialPeriod {
pub fn new(length: i64) -> Self {
Self(length)
}
pub fn inner(&self) -> i64 {
self.0
}
}
wrap_uuid! {
pub struct ChargeId
}
pub struct Charge {
pub charge_id: ChargeId,
pub charged_at: DateTime<Utc>,
pub subscription_tier: SubscriptionTier,
pub payment_method: PaymentMethod,
pub invoice_number: InvoiceNumber,
pub amount_in_cents: AmountInCents,
}
wrap_uuid! {
pub struct PlanId
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "backend", derive(sqlx::FromRow))]
pub struct SubscriptionPlan {
pub plan_id: PlanId,
pub plan_type: PlanType,
pub price_id: StripePriceId,
pub created_at: DateTime<Utc>,
pub updated_at: Option<DateTime<Utc>>,
}
make_path_parts!(SubscriptionPlanPath => "/v1/plans");
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct UpdateSubscriptionPlansRequest {
#[serde(flatten)]
pub plans: HashMap<PlanType, StripePriceId>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct CreateSubscriptionRequest {
pub setup_intent_id: Option<String>,
pub plan_type: PlanType,
pub promotion_code: Option<String>,
}
make_path_parts!(CreateSubscriptionPath => "/v1/billing/subscribe");
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct CreateSubscriptionResponse {
pub subscription_id: StripeSubscriptionId,
pub client_secret: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct CreateSetupIntentRequest {
pub plan_type: PlanType,
}
make_path_parts!(CreateSetupIntentPath => "/v1/billing/payment-method");
wrap_uuid! {
pub struct AccountId
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Account {
pub account_id: AccountId,
pub account_type: AccountType,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub stripe_customer_id: Option<CustomerId>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub payment_method: Option<PaymentMethod>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub subscription: Option<Subscription>,
pub created_at: DateTime<Utc>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub updated_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct UserAccountSummary {
pub account_id: Option<AccountId>,
pub school_id: Option<SchoolId>,
pub school_name: Option<String>,
pub plan_type: Option<PlanType>,
pub plan_tier: PlanTier,
pub overridden: bool,
pub subscription_status: Option<SubscriptionStatus>,
pub is_admin: bool,
pub overdue: bool,
pub verified: bool,
}
wrap_uuid! {
pub struct SchoolId
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct School {
pub id: SchoolId,
pub school_name: String,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub location: Option<Value>,
pub email: String,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub profile_image: Option<ImageId>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub website: Option<String>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub organization_type: Option<String>,
pub account_id: AccountId,
pub created_at: DateTime<Utc>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub updated_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct AdminSchool {
pub id: SchoolId,
pub school_name: String,
pub internal_school_name: Option<SchoolName>,
pub verified: bool,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub location: Option<Value>,
pub email: String,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub profile_image: Option<ImageId>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub website: Option<String>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub organization_type: Option<String>,
pub account_id: AccountId,
pub created_at: DateTime<Utc>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub updated_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct AccountUser {
pub user: UserProfile,
pub is_admin: bool,
pub verified: bool,
}
wrap_uuid! {
pub struct SchoolNameId
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct SchoolName {
pub id: SchoolNameId,
pub name: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(transparent)]
pub struct SchoolNameValue(String);
impl fmt::Display for SchoolNameValue {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<SchoolNameValue> for String {
fn from(value: SchoolNameValue) -> Self {
value.0
}
}
impl From<String> for SchoolNameValue {
fn from(value: String) -> Self {
SchoolNameValue(value)
}
}
impl AsRef<str> for SchoolNameValue {
fn as_ref(&self) -> &str {
&self.0
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub enum SchoolNameRequest {
Value(SchoolNameValue),
Id(SchoolNameId),
}
make_path_parts!(CreateSchoolAccountPath => "/v1/schools");
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct CreateSchoolAccountRequest {
pub name: String,
pub email: String,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub location: Option<Value>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub profile_image: Option<ImageId>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub website: Option<String>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub organization_type: Option<String>,
}
make_path_parts!(SchoolAccountPath => "/v1/schools/{}" => SchoolId);
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct GetSchoolAccountResponse {
pub school: School,
pub account: AccountIfAuthorized,
pub users: Vec<AccountUser>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[allow(clippy::large_enum_variant)]
#[serde(untagged)]
pub enum AccountIfAuthorized {
Authorized(Account),
Unauthorized,
}
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct UpdateSchoolAccountRequest {
#[serde(default, skip_serializing_if = "UpdateNonNullable::is_keep")]
pub email: UpdateNonNullable<String>,
#[serde(default, skip_serializing_if = "UpdateNonNullable::is_keep")]
pub school_name: UpdateNonNullable<String>,
#[serde(default, skip_serializing_if = "UpdateNullable::is_keep")]
pub location: UpdateNullable<Value>,
#[serde(default, skip_serializing_if = "UpdateNullable::is_keep")]
pub description: UpdateNullable<String>,
#[serde(default, skip_serializing_if = "UpdateNullable::is_keep")]
pub profile_image: UpdateNullable<ImageId>,
#[serde(default, skip_serializing_if = "UpdateNullable::is_keep")]
pub website: UpdateNullable<String>,
#[serde(default, skip_serializing_if = "UpdateNullable::is_keep")]
pub organization_type: UpdateNullable<String>,
}
make_path_parts!(IndividualAccountPath => "/v1/user/me/account");
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct IndividualAccountResponse {
pub account: Option<Account>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "kebab-case")]
pub enum CancellationStatus {
#[serde(rename = "period-end")]
CancelAtPeriodEnd,
#[serde(rename = "remove")]
RemoveCancellation,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct SubscriptionCancellationStatusRequest {
pub status: CancellationStatus,
}
make_path_parts!(UpdateSubscriptionCancellationPath => "/v1/billing/subscription/cancel");
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct SubscriptionPauseRequest {
pub paused: bool,
}
make_path_parts!(UpdateSubscriptionPausedPath => "/v1/billing/subscription/pause");
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct UpgradeSubscriptionPlanRequest {
pub plan_type: PlanType,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub promotion_code: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct AdminUpgradeSubscriptionPlanRequest {
pub plan_type: PlanType,
pub user_id: UserId,
}
make_path_parts!(UpgradeSubscriptionPlanPath => "/v1/billing/subscription/upgrade");
make_path_parts!(AdminUpgradeSubscriptionPlanPath => "/v1/admin/billing/subscription/upgrade");
make_path_parts!(CreateCustomerPortalLinkPath => "/v1/billing/customer-portal");