mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-29 00:49:42 +08:00
feat(subscription): get plans for subscription (#9251)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Co-authored-by: Sarthak Soni <sarthakasoni@gmail.com> Co-authored-by: Jagan <jaganelavarasan@gmail.com>
This commit is contained in:
@ -141,8 +141,58 @@ impl SubscriptionResponse {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct GetPlansResponse {
|
||||
pub plan_id: String,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub price_id: Vec<SubscriptionPlanPrices>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct SubscriptionPlanPrices {
|
||||
pub price_id: String,
|
||||
pub plan_id: Option<String>,
|
||||
pub amount: MinorUnit,
|
||||
pub currency: api_enums::Currency,
|
||||
pub interval: PeriodUnit,
|
||||
pub interval_count: i64,
|
||||
pub trial_period: Option<i64>,
|
||||
pub trial_period_unit: Option<PeriodUnit>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub enum PeriodUnit {
|
||||
Day,
|
||||
Week,
|
||||
Month,
|
||||
Year,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ClientSecret(String);
|
||||
|
||||
impl ClientSecret {
|
||||
pub fn new(secret: String) -> Self {
|
||||
Self(secret)
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize, Debug)]
|
||||
pub struct GetPlansQuery {
|
||||
pub client_secret: Option<ClientSecret>,
|
||||
pub limit: Option<u32>,
|
||||
pub offset: Option<u32>,
|
||||
}
|
||||
|
||||
impl ApiEventMetric for SubscriptionResponse {}
|
||||
impl ApiEventMetric for CreateSubscriptionRequest {}
|
||||
impl ApiEventMetric for GetPlansQuery {}
|
||||
impl ApiEventMetric for GetPlansResponse {}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)]
|
||||
pub struct ConfirmSubscriptionPaymentDetails {
|
||||
@ -227,6 +277,7 @@ pub struct PaymentResponseData {
|
||||
pub error_code: Option<String>,
|
||||
pub error_message: Option<String>,
|
||||
pub payment_method_type: Option<api_enums::PaymentMethodType>,
|
||||
pub client_secret: Option<String>,
|
||||
}
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)]
|
||||
pub struct ConfirmSubscriptionRequest {
|
||||
|
||||
@ -843,7 +843,13 @@ fn get_chargebee_plans_query_params(
|
||||
) -> CustomResult<String, errors::ConnectorError> {
|
||||
// Try to get limit from request, else default to 10
|
||||
let limit = _req.request.limit.unwrap_or(10);
|
||||
let param = format!("?limit={}&type[is]={}", limit, constants::PLAN_ITEM_TYPE);
|
||||
let offset = _req.request.offset.unwrap_or(0);
|
||||
let param = format!(
|
||||
"?limit={}&offset={}&type[is]={}",
|
||||
limit,
|
||||
offset,
|
||||
constants::PLAN_ITEM_TYPE
|
||||
);
|
||||
Ok(param)
|
||||
}
|
||||
|
||||
|
||||
@ -1248,12 +1248,13 @@ pub struct ChargebeePlanPriceItem {
|
||||
pub period: i64,
|
||||
pub period_unit: ChargebeePeriodUnit,
|
||||
pub trial_period: Option<i64>,
|
||||
pub trial_period_unit: ChargebeeTrialPeriodUnit,
|
||||
pub trial_period_unit: Option<ChargebeeTrialPeriodUnit>,
|
||||
pub price: MinorUnit,
|
||||
pub pricing_model: ChargebeePricingModel,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ChargebeePricingModel {
|
||||
FlatFee,
|
||||
PerUnit,
|
||||
@ -1263,6 +1264,7 @@ pub enum ChargebeePricingModel {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ChargebeePeriodUnit {
|
||||
Day,
|
||||
Week,
|
||||
@ -1271,6 +1273,7 @@ pub enum ChargebeePeriodUnit {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ChargebeeTrialPeriodUnit {
|
||||
Day,
|
||||
Month,
|
||||
@ -1308,8 +1311,9 @@ impl<F, T>
|
||||
interval_count: prices.item_price.period,
|
||||
trial_period: prices.item_price.trial_period,
|
||||
trial_period_unit: match prices.item_price.trial_period_unit {
|
||||
ChargebeeTrialPeriodUnit::Day => Some(subscriptions::PeriodUnit::Day),
|
||||
ChargebeeTrialPeriodUnit::Month => Some(subscriptions::PeriodUnit::Month),
|
||||
Some(ChargebeeTrialPeriodUnit::Day) => Some(subscriptions::PeriodUnit::Day),
|
||||
Some(ChargebeeTrialPeriodUnit::Month) => Some(subscriptions::PeriodUnit::Month),
|
||||
None => None,
|
||||
},
|
||||
})
|
||||
.collect();
|
||||
|
||||
@ -37,6 +37,7 @@ pub mod router_flow_types;
|
||||
pub mod router_request_types;
|
||||
pub mod router_response_types;
|
||||
pub mod routing;
|
||||
pub mod subscription;
|
||||
#[cfg(feature = "tokenization_v2")]
|
||||
pub mod tokenization;
|
||||
pub mod transformers;
|
||||
|
||||
@ -29,6 +29,21 @@ pub struct GetSubscriptionPlansRequest {
|
||||
pub offset: Option<u32>,
|
||||
}
|
||||
|
||||
impl GetSubscriptionPlansRequest {
|
||||
pub fn new(limit: Option<u32>, offset: Option<u32>) -> Self {
|
||||
Self { limit, offset }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for GetSubscriptionPlansRequest {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
limit: Some(10),
|
||||
offset: Some(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GetSubscriptionPlanPricesRequest {
|
||||
pub plan_price_id: String,
|
||||
|
||||
@ -33,6 +33,7 @@ pub enum SubscriptionStatus {
|
||||
Onetime,
|
||||
Cancelled,
|
||||
Failed,
|
||||
Created,
|
||||
}
|
||||
|
||||
#[cfg(feature = "v1")]
|
||||
@ -47,6 +48,7 @@ impl From<SubscriptionStatus> for api_models::subscription::SubscriptionStatus {
|
||||
SubscriptionStatus::Onetime => Self::Onetime,
|
||||
SubscriptionStatus::Cancelled => Self::Cancelled,
|
||||
SubscriptionStatus::Failed => Self::Failed,
|
||||
SubscriptionStatus::Created => Self::Created,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -80,6 +82,22 @@ pub struct SubscriptionPlanPrices {
|
||||
pub trial_period_unit: Option<PeriodUnit>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "v1")]
|
||||
impl From<SubscriptionPlanPrices> for api_models::subscription::SubscriptionPlanPrices {
|
||||
fn from(item: SubscriptionPlanPrices) -> Self {
|
||||
Self {
|
||||
price_id: item.price_id,
|
||||
plan_id: item.plan_id,
|
||||
amount: item.amount,
|
||||
currency: item.currency,
|
||||
interval: item.interval.into(),
|
||||
interval_count: item.interval_count,
|
||||
trial_period: item.trial_period,
|
||||
trial_period_unit: item.trial_period_unit.map(Into::into),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum PeriodUnit {
|
||||
Day,
|
||||
@ -88,6 +106,18 @@ pub enum PeriodUnit {
|
||||
Year,
|
||||
}
|
||||
|
||||
#[cfg(feature = "v1")]
|
||||
impl From<PeriodUnit> for api_models::subscription::PeriodUnit {
|
||||
fn from(unit: PeriodUnit) -> Self {
|
||||
match unit {
|
||||
PeriodUnit::Day => Self::Day,
|
||||
PeriodUnit::Week => Self::Week,
|
||||
PeriodUnit::Month => Self::Month,
|
||||
PeriodUnit::Year => Self::Year,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GetSubscriptionEstimateResponse {
|
||||
pub sub_total: MinorUnit,
|
||||
|
||||
50
crates/hyperswitch_domain_models/src/subscription.rs
Normal file
50
crates/hyperswitch_domain_models/src/subscription.rs
Normal file
@ -0,0 +1,50 @@
|
||||
use common_utils::events::ApiEventMetric;
|
||||
use error_stack::ResultExt;
|
||||
|
||||
use crate::errors::api_error_response::ApiErrorResponse;
|
||||
|
||||
const SECRET_SPLIT: &str = "_secret";
|
||||
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ClientSecret(String);
|
||||
|
||||
impl ClientSecret {
|
||||
pub fn new(secret: String) -> Self {
|
||||
Self(secret)
|
||||
}
|
||||
|
||||
pub fn get_subscription_id(&self) -> error_stack::Result<String, ApiErrorResponse> {
|
||||
let sub_id = self
|
||||
.0
|
||||
.split(SECRET_SPLIT)
|
||||
.next()
|
||||
.ok_or(ApiErrorResponse::MissingRequiredField {
|
||||
field_name: "client_secret",
|
||||
})
|
||||
.attach_printable("Failed to extract subscription_id from client_secret")?;
|
||||
|
||||
Ok(sub_id.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ClientSecret {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ApiEventMetric for ClientSecret {}
|
||||
|
||||
#[cfg(feature = "v1")]
|
||||
impl From<api_models::subscription::ClientSecret> for ClientSecret {
|
||||
fn from(api_secret: api_models::subscription::ClientSecret) -> Self {
|
||||
Self::new(api_secret.as_str().to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "v1")]
|
||||
impl From<ClientSecret> for api_models::subscription::ClientSecret {
|
||||
fn from(domain_secret: ClientSecret) -> Self {
|
||||
Self::new(domain_secret.to_string())
|
||||
}
|
||||
}
|
||||
@ -4,7 +4,10 @@ use api_models::subscription::{
|
||||
use common_enums::connector_enums;
|
||||
use common_utils::id_type::GenerateId;
|
||||
use error_stack::ResultExt;
|
||||
use hyperswitch_domain_models::{api::ApplicationResponse, merchant_context::MerchantContext};
|
||||
use hyperswitch_domain_models::{
|
||||
api::ApplicationResponse, merchant_context::MerchantContext,
|
||||
router_response_types::subscriptions as subscription_response_types,
|
||||
};
|
||||
|
||||
use super::errors::{self, RouterResponse};
|
||||
use crate::{
|
||||
@ -28,7 +31,7 @@ pub async fn create_subscription(
|
||||
merchant_context: MerchantContext,
|
||||
profile_id: common_utils::id_type::ProfileId,
|
||||
request: subscription_types::CreateSubscriptionRequest,
|
||||
) -> RouterResponse<SubscriptionResponse> {
|
||||
) -> RouterResponse<subscription_types::ConfirmSubscriptionResponse> {
|
||||
let subscription_id = common_utils::id_type::SubscriptionId::generate();
|
||||
|
||||
let profile =
|
||||
@ -43,7 +46,7 @@ pub async fn create_subscription(
|
||||
&state,
|
||||
merchant_context.get_merchant_account(),
|
||||
merchant_context.get_merchant_key_store(),
|
||||
customer,
|
||||
Some(customer),
|
||||
profile.clone(),
|
||||
)
|
||||
.await?;
|
||||
@ -65,7 +68,7 @@ pub async fn create_subscription(
|
||||
.create_payment_with_confirm_false(subscription.handler.state, &request)
|
||||
.await
|
||||
.attach_printable("subscriptions: failed to create payment")?;
|
||||
invoice_handler
|
||||
let invoice_entry = invoice_handler
|
||||
.create_invoice_entry(
|
||||
&state,
|
||||
billing_handler.merchant_connector_id,
|
||||
@ -88,9 +91,67 @@ pub async fn create_subscription(
|
||||
.await
|
||||
.attach_printable("subscriptions: failed to update subscription")?;
|
||||
|
||||
Ok(ApplicationResponse::Json(
|
||||
subscription.to_subscription_response(),
|
||||
))
|
||||
let response = subscription.generate_response(
|
||||
&invoice_entry,
|
||||
&payment,
|
||||
subscription_response_types::SubscriptionStatus::Created,
|
||||
)?;
|
||||
|
||||
Ok(ApplicationResponse::Json(response))
|
||||
}
|
||||
|
||||
pub async fn get_subscription_plans(
|
||||
state: SessionState,
|
||||
merchant_context: MerchantContext,
|
||||
profile_id: common_utils::id_type::ProfileId,
|
||||
query: subscription_types::GetPlansQuery,
|
||||
) -> RouterResponse<Vec<subscription_types::GetPlansResponse>> {
|
||||
let profile =
|
||||
SubscriptionHandler::find_business_profile(&state, &merchant_context, &profile_id)
|
||||
.await
|
||||
.attach_printable("subscriptions: failed to find business profile")?;
|
||||
|
||||
let subscription_handler = SubscriptionHandler::new(&state, &merchant_context);
|
||||
|
||||
if let Some(client_secret) = query.client_secret {
|
||||
subscription_handler
|
||||
.find_and_validate_subscription(&client_secret.into())
|
||||
.await?
|
||||
};
|
||||
|
||||
let billing_handler = BillingHandler::create(
|
||||
&state,
|
||||
merchant_context.get_merchant_account(),
|
||||
merchant_context.get_merchant_key_store(),
|
||||
None,
|
||||
profile.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let get_plans_response = billing_handler
|
||||
.get_subscription_plans(&state, query.limit, query.offset)
|
||||
.await?;
|
||||
|
||||
let mut response = Vec::new();
|
||||
|
||||
for plan in &get_plans_response.list {
|
||||
let plan_price_response = billing_handler
|
||||
.get_subscription_plan_prices(&state, plan.subscription_provider_plan_id.clone())
|
||||
.await?;
|
||||
|
||||
response.push(subscription_types::GetPlansResponse {
|
||||
plan_id: plan.subscription_provider_plan_id.clone(),
|
||||
name: plan.name.clone(),
|
||||
description: plan.description.clone(),
|
||||
price_id: plan_price_response
|
||||
.list
|
||||
.into_iter()
|
||||
.map(subscription_types::SubscriptionPlanPrices::from)
|
||||
.collect::<Vec<_>>(),
|
||||
})
|
||||
}
|
||||
|
||||
Ok(ApplicationResponse::Json(response))
|
||||
}
|
||||
|
||||
/// Creates and confirms a subscription in one operation.
|
||||
@ -116,7 +177,7 @@ pub async fn create_and_confirm_subscription(
|
||||
&state,
|
||||
merchant_context.get_merchant_account(),
|
||||
merchant_context.get_merchant_key_store(),
|
||||
customer,
|
||||
Some(customer),
|
||||
profile.clone(),
|
||||
)
|
||||
.await?;
|
||||
@ -259,7 +320,7 @@ pub async fn confirm_subscription(
|
||||
&state,
|
||||
merchant_context.get_merchant_account(),
|
||||
merchant_context.get_merchant_key_store(),
|
||||
customer,
|
||||
Some(customer),
|
||||
profile.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@ -5,7 +5,8 @@ use common_utils::{ext_traits::ValueExt, pii};
|
||||
use error_stack::ResultExt;
|
||||
use hyperswitch_domain_models::{
|
||||
router_data_v2::flow_common_types::{
|
||||
InvoiceRecordBackData, SubscriptionCreateData, SubscriptionCustomerData,
|
||||
GetSubscriptionPlanPricesData, GetSubscriptionPlansData, InvoiceRecordBackData,
|
||||
SubscriptionCreateData, SubscriptionCustomerData,
|
||||
},
|
||||
router_request_types::{
|
||||
revenue_recovery::InvoiceRecordBackRequest, subscriptions as subscription_request_types,
|
||||
@ -27,7 +28,7 @@ pub struct BillingHandler {
|
||||
pub connector_data: api_types::ConnectorData,
|
||||
pub connector_params: hyperswitch_domain_models::connector_endpoints::ConnectorParams,
|
||||
pub connector_metadata: Option<pii::SecretSerdeValue>,
|
||||
pub customer: hyperswitch_domain_models::customer::Customer,
|
||||
pub customer: Option<hyperswitch_domain_models::customer::Customer>,
|
||||
pub merchant_connector_id: common_utils::id_type::MerchantConnectorAccountId,
|
||||
}
|
||||
|
||||
@ -37,7 +38,7 @@ impl BillingHandler {
|
||||
state: &SessionState,
|
||||
merchant_account: &hyperswitch_domain_models::merchant_account::MerchantAccount,
|
||||
key_store: &hyperswitch_domain_models::merchant_key_store::MerchantKeyStore,
|
||||
customer: hyperswitch_domain_models::customer::Customer,
|
||||
customer: Option<hyperswitch_domain_models::customer::Customer>,
|
||||
profile: hyperswitch_domain_models::business_profile::Profile,
|
||||
) -> errors::RouterResult<Self> {
|
||||
let merchant_connector_id = profile.get_billing_processor_id()?;
|
||||
@ -109,8 +110,15 @@ impl BillingHandler {
|
||||
billing_address: Option<api_models::payments::Address>,
|
||||
payment_method_data: Option<api_models::payments::PaymentMethodData>,
|
||||
) -> errors::RouterResult<ConnectorCustomerResponseData> {
|
||||
let customer =
|
||||
self.customer
|
||||
.as_ref()
|
||||
.ok_or(errors::ApiErrorResponse::MissingRequiredField {
|
||||
field_name: "customer",
|
||||
})?;
|
||||
|
||||
let customer_req = ConnectorCustomerData {
|
||||
email: self.customer.email.clone().map(pii::Email::from),
|
||||
email: customer.email.clone().map(pii::Email::from),
|
||||
payment_method_data: payment_method_data.clone().map(|pmd| pmd.into()),
|
||||
description: None,
|
||||
phone: None,
|
||||
@ -275,6 +283,87 @@ impl BillingHandler {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_subscription_plans(
|
||||
&self,
|
||||
state: &SessionState,
|
||||
limit: Option<u32>,
|
||||
offset: Option<u32>,
|
||||
) -> errors::RouterResult<subscription_response_types::GetSubscriptionPlansResponse> {
|
||||
let get_plans_request =
|
||||
subscription_request_types::GetSubscriptionPlansRequest::new(limit, offset);
|
||||
|
||||
let router_data = self.build_router_data(
|
||||
state,
|
||||
get_plans_request,
|
||||
GetSubscriptionPlansData {
|
||||
connector_meta_data: self.connector_metadata.clone(),
|
||||
},
|
||||
)?;
|
||||
|
||||
let connector_integration = self.connector_data.connector.get_connector_integration();
|
||||
|
||||
let response = self
|
||||
.call_connector(
|
||||
state,
|
||||
router_data,
|
||||
"get subscription plans",
|
||||
connector_integration,
|
||||
)
|
||||
.await?;
|
||||
|
||||
match response {
|
||||
Ok(resp) => Ok(resp),
|
||||
Err(err) => Err(errors::ApiErrorResponse::ExternalConnectorError {
|
||||
code: err.code,
|
||||
message: err.message,
|
||||
connector: self.connector_data.connector_name.to_string().clone(),
|
||||
status_code: err.status_code,
|
||||
reason: err.reason,
|
||||
}
|
||||
.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_subscription_plan_prices(
|
||||
&self,
|
||||
state: &SessionState,
|
||||
plan_price_id: String,
|
||||
) -> errors::RouterResult<subscription_response_types::GetSubscriptionPlanPricesResponse> {
|
||||
let get_plan_prices_request =
|
||||
subscription_request_types::GetSubscriptionPlanPricesRequest { plan_price_id };
|
||||
|
||||
let router_data = self.build_router_data(
|
||||
state,
|
||||
get_plan_prices_request,
|
||||
GetSubscriptionPlanPricesData {
|
||||
connector_meta_data: self.connector_metadata.clone(),
|
||||
},
|
||||
)?;
|
||||
|
||||
let connector_integration = self.connector_data.connector.get_connector_integration();
|
||||
|
||||
let response = self
|
||||
.call_connector(
|
||||
state,
|
||||
router_data,
|
||||
"get subscription plan prices",
|
||||
connector_integration,
|
||||
)
|
||||
.await?;
|
||||
|
||||
match response {
|
||||
Ok(resp) => Ok(resp),
|
||||
Err(err) => Err(errors::ApiErrorResponse::ExternalConnectorError {
|
||||
code: err.code,
|
||||
message: err.message,
|
||||
connector: self.connector_data.connector_name.to_string().clone(),
|
||||
status_code: err.status_code,
|
||||
reason: err.reason,
|
||||
}
|
||||
.into()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn call_connector<F, ResourceCommonData, Req, Resp>(
|
||||
&self,
|
||||
state: &SessionState,
|
||||
|
||||
@ -5,6 +5,7 @@ use api_models::{
|
||||
subscription::{self as subscription_types, SubscriptionResponse, SubscriptionStatus},
|
||||
};
|
||||
use common_enums::connector_enums;
|
||||
use common_utils::{consts, ext_traits::OptionExt};
|
||||
use diesel_models::subscription::SubscriptionNew;
|
||||
use error_stack::ResultExt;
|
||||
use hyperswitch_domain_models::{
|
||||
@ -122,6 +123,60 @@ impl<'a> SubscriptionHandler<'a> {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn find_and_validate_subscription(
|
||||
&self,
|
||||
client_secret: &hyperswitch_domain_models::subscription::ClientSecret,
|
||||
) -> errors::RouterResult<()> {
|
||||
let subscription_id = client_secret.get_subscription_id()?;
|
||||
|
||||
let subscription = self
|
||||
.state
|
||||
.store
|
||||
.find_by_merchant_id_subscription_id(
|
||||
self.merchant_context.get_merchant_account().get_id(),
|
||||
subscription_id.to_string(),
|
||||
)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::GenericNotFoundError {
|
||||
message: format!("Subscription not found for id: {subscription_id}"),
|
||||
})
|
||||
.attach_printable("Unable to find subscription")?;
|
||||
|
||||
self.validate_client_secret(client_secret, &subscription)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_client_secret(
|
||||
&self,
|
||||
client_secret: &hyperswitch_domain_models::subscription::ClientSecret,
|
||||
subscription: &diesel_models::subscription::Subscription,
|
||||
) -> errors::RouterResult<()> {
|
||||
let stored_client_secret = subscription
|
||||
.client_secret
|
||||
.clone()
|
||||
.get_required_value("client_secret")
|
||||
.change_context(errors::ApiErrorResponse::MissingRequiredField {
|
||||
field_name: "client_secret",
|
||||
})
|
||||
.attach_printable("client secret not found in db")?;
|
||||
|
||||
if client_secret.to_string() != stored_client_secret {
|
||||
Err(errors::ApiErrorResponse::ClientSecretInvalid.into())
|
||||
} else {
|
||||
let current_timestamp = common_utils::date_time::now();
|
||||
let session_expiry = subscription
|
||||
.created_at
|
||||
.saturating_add(time::Duration::seconds(consts::DEFAULT_SESSION_EXPIRY));
|
||||
|
||||
if current_timestamp > session_expiry {
|
||||
Err(errors::ApiErrorResponse::ClientSecretExpired.into())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn find_subscription(
|
||||
&self,
|
||||
subscription_id: common_utils::id_type::SubscriptionId,
|
||||
|
||||
@ -1187,6 +1187,9 @@ impl Subscription {
|
||||
subscription::create_subscription(state, req, payload)
|
||||
}),
|
||||
))
|
||||
.service(
|
||||
web::resource("/plans").route(web::get().to(subscription::get_subscription_plans)),
|
||||
)
|
||||
.service(
|
||||
web::resource("/{subscription_id}/confirm").route(web::post().to(
|
||||
|state, req, id, payload| {
|
||||
|
||||
@ -87,12 +87,11 @@ impl From<Flow> for ApiIdentifier {
|
||||
| Flow::VolumeSplitOnRoutingType
|
||||
| Flow::DecisionEngineDecideGatewayCall
|
||||
| Flow::DecisionEngineGatewayFeedbackCall => Self::Routing,
|
||||
|
||||
Flow::CreateSubscription
|
||||
| Flow::ConfirmSubscription
|
||||
| Flow::CreateAndConfirmSubscription
|
||||
| Flow::GetSubscription => Self::Subscription,
|
||||
|
||||
| Flow::GetSubscription
|
||||
| Flow::GetPlansForSubscription => Self::Subscription,
|
||||
Flow::RetrieveForexFlow => Self::Forex,
|
||||
Flow::AddToBlocklist => Self::Blocklist,
|
||||
Flow::DeleteFromBlocklist => Self::Blocklist,
|
||||
|
||||
@ -60,6 +60,7 @@ pub async fn create_subscription(
|
||||
Ok(id) => id,
|
||||
Err(response) => return response,
|
||||
};
|
||||
|
||||
Box::pin(oss_api::server_wrap(
|
||||
flow,
|
||||
state,
|
||||
@ -104,6 +105,7 @@ pub async fn confirm_subscription(
|
||||
Ok(id) => id,
|
||||
Err(response) => return response,
|
||||
};
|
||||
|
||||
Box::pin(oss_api::server_wrap(
|
||||
flow,
|
||||
state,
|
||||
@ -136,6 +138,41 @@ pub async fn confirm_subscription(
|
||||
.await
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn get_subscription_plans(
|
||||
state: web::Data<AppState>,
|
||||
req: HttpRequest,
|
||||
query: web::Query<subscription_types::GetPlansQuery>,
|
||||
) -> impl Responder {
|
||||
let flow = Flow::GetPlansForSubscription;
|
||||
let api_auth = auth::ApiKeyAuth::default();
|
||||
|
||||
let profile_id = match extract_profile_id(&req) {
|
||||
Ok(profile_id) => profile_id,
|
||||
Err(response) => return response,
|
||||
};
|
||||
|
||||
let auth_data = match auth::is_ephemeral_auth(req.headers(), api_auth) {
|
||||
Ok(auth) => auth,
|
||||
Err(err) => return crate::services::api::log_and_return_error_response(err),
|
||||
};
|
||||
Box::pin(oss_api::server_wrap(
|
||||
flow,
|
||||
state,
|
||||
&req,
|
||||
query.into_inner(),
|
||||
|state, auth: auth::AuthenticationData, query, _| {
|
||||
let merchant_context = domain::MerchantContext::NormalMerchant(Box::new(
|
||||
domain::Context(auth.merchant_account, auth.key_store),
|
||||
));
|
||||
subscription::get_subscription_plans(state, merchant_context, profile_id.clone(), query)
|
||||
},
|
||||
&*auth_data,
|
||||
api_locking::LockAction::NotApplicable,
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
/// Add support for get subscription by id
|
||||
#[instrument(skip_all)]
|
||||
pub async fn get_subscription(
|
||||
|
||||
@ -171,7 +171,7 @@ impl<'a> InvoiceSyncHandler<'a> {
|
||||
self.state,
|
||||
&self.merchant_account,
|
||||
&self.key_store,
|
||||
self.customer.clone(),
|
||||
Some(self.customer.clone()),
|
||||
self.profile.clone(),
|
||||
)
|
||||
.await
|
||||
|
||||
@ -265,6 +265,8 @@ pub enum Flow {
|
||||
RoutingDeleteConfig,
|
||||
/// Subscription create flow,
|
||||
CreateSubscription,
|
||||
/// Subscription get plans flow,
|
||||
GetPlansForSubscription,
|
||||
/// Subscription confirm flow,
|
||||
ConfirmSubscription,
|
||||
/// Subscription create and confirm flow,
|
||||
|
||||
Reference in New Issue
Block a user