shared/error/
billing.rs

1use crate::domain::billing::{AccountType, PlanType, SubscriptionType};
2use crate::error::service::ServiceError;
3use crate::error::TransientError;
4use serde::{Deserialize, Serialize};
5use thiserror::Error;
6
7#[cfg(feature = "backend")]
8use actix_web::{body::BoxBody, HttpResponse, ResponseError};
9#[cfg(feature = "backend")]
10use stripe::StripeError;
11
12#[allow(missing_docs)]
13#[derive(Debug, Error, Serialize, Deserialize)]
14pub enum BillingError {
15    #[cfg_attr(feature = "backend", error(transparent))]
16    #[cfg_attr(not(feature = "backend"), error("Internal server error"))]
17    InternalServerError(
18        #[serde(skip)]
19        #[from]
20        TransientError<anyhow::Error>,
21    ),
22    #[error(transparent)]
23    Service(ServiceError),
24    #[cfg_attr(feature = "backend", error(transparent))]
25    #[cfg_attr(not(feature = "backend"), error("Stripe error"))]
26    Stripe(
27        #[cfg(feature = "backend")]
28        #[serde(skip)]
29        #[from]
30        TransientError<StripeError>,
31    ),
32    #[error("Not found: {0}")]
33    NotFound(String),
34    #[error("Missing Stripe signature")]
35    MissingStripeSignature,
36    #[error("Invalid Setup Intent ID")]
37    InvalidSetupIntentId,
38    #[error("No active subscription for user")]
39    NoActiveSubscription,
40    #[error("The current subscription is not paused")]
41    SubscriptionNotPaused,
42    #[error("No canceled subscription for user")]
43    NoCanceledSubscription,
44    #[error("Account has an existing subscription")]
45    SubscriptionExists,
46    #[error("School not found")]
47    SchoolNotFound,
48    #[error("Incorrect plan type. Expected {expected}, found {found}.")]
49    IncorrectPlanType {
50        expected: AccountType,
51        found: SubscriptionType,
52    },
53    #[error("Invalid promotion code {0}")]
54    InvalidPromotionCode(String),
55    #[error("Forbidden")]
56    Forbidden,
57    #[error("Cannot upgrade to {upgrade_to} from {upgrade_from}")]
58    InvalidUpgradePlanType {
59        upgrade_to: PlanType,
60        upgrade_from: PlanType,
61    },
62}
63
64#[cfg(feature = "backend")]
65impl From<StripeError> for BillingError {
66    fn from(value: StripeError) -> Self {
67        Self::from(TransientError::from(value))
68    }
69}
70
71impl From<ServiceError> for BillingError {
72    fn from(err: ServiceError) -> Self {
73        Self::Service(err)
74    }
75}
76
77impl From<anyhow::Error> for BillingError {
78    fn from(e: anyhow::Error) -> Self {
79        Self::InternalServerError(e.into())
80    }
81}
82
83#[cfg(feature = "backend")]
84impl ResponseError for BillingError {
85    fn status_code(&self) -> http::StatusCode {
86        match self {
87            Self::InternalServerError { .. } | Self::Stripe { .. } => {
88                http::StatusCode::INTERNAL_SERVER_ERROR
89            }
90            Self::Service(service) => service.status_code(),
91            Self::NotFound(_) | Self::SchoolNotFound => http::StatusCode::NOT_FOUND,
92            Self::Forbidden => http::StatusCode::FORBIDDEN,
93            _ => http::StatusCode::BAD_REQUEST,
94        }
95    }
96
97    fn error_response(&self) -> HttpResponse<BoxBody> {
98        HttpResponse::build(self.status_code()).json(self)
99    }
100}