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}