diff --git a/crates/api_models/src/subscription.rs b/crates/api_models/src/subscription.rs index c9cc3acd2c..9378d7796f 100644 --- a/crates/api_models/src/subscription.rs +++ b/crates/api_models/src/subscription.rs @@ -141,8 +141,58 @@ impl SubscriptionResponse { } } +#[derive(Debug, Clone, serde::Serialize)] +pub struct GetPlansResponse { + pub plan_id: String, + pub name: String, + pub description: Option, + pub price_id: Vec, +} + +#[derive(Debug, Clone, serde::Serialize)] +pub struct SubscriptionPlanPrices { + pub price_id: String, + pub plan_id: Option, + pub amount: MinorUnit, + pub currency: api_enums::Currency, + pub interval: PeriodUnit, + pub interval_count: i64, + pub trial_period: Option, + pub trial_period_unit: Option, +} + +#[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, + pub limit: Option, + pub offset: Option, +} + 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, pub error_message: Option, pub payment_method_type: Option, + pub client_secret: Option, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] pub struct ConfirmSubscriptionRequest { diff --git a/crates/hyperswitch_connectors/src/connectors/chargebee.rs b/crates/hyperswitch_connectors/src/connectors/chargebee.rs index b2d66db1f4..f3e935c6dd 100644 --- a/crates/hyperswitch_connectors/src/connectors/chargebee.rs +++ b/crates/hyperswitch_connectors/src/connectors/chargebee.rs @@ -843,7 +843,13 @@ fn get_chargebee_plans_query_params( ) -> CustomResult { // 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) } diff --git a/crates/hyperswitch_connectors/src/connectors/chargebee/transformers.rs b/crates/hyperswitch_connectors/src/connectors/chargebee/transformers.rs index 4c7b135f1a..324418e22b 100644 --- a/crates/hyperswitch_connectors/src/connectors/chargebee/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/chargebee/transformers.rs @@ -1248,12 +1248,13 @@ pub struct ChargebeePlanPriceItem { pub period: i64, pub period_unit: ChargebeePeriodUnit, pub trial_period: Option, - pub trial_period_unit: ChargebeeTrialPeriodUnit, + pub trial_period_unit: Option, 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 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(); diff --git a/crates/hyperswitch_domain_models/src/lib.rs b/crates/hyperswitch_domain_models/src/lib.rs index 70f7c4830c..3ed5130e07 100644 --- a/crates/hyperswitch_domain_models/src/lib.rs +++ b/crates/hyperswitch_domain_models/src/lib.rs @@ -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; diff --git a/crates/hyperswitch_domain_models/src/router_request_types/subscriptions.rs b/crates/hyperswitch_domain_models/src/router_request_types/subscriptions.rs index 8599116e8f..f93185b7d3 100644 --- a/crates/hyperswitch_domain_models/src/router_request_types/subscriptions.rs +++ b/crates/hyperswitch_domain_models/src/router_request_types/subscriptions.rs @@ -29,6 +29,21 @@ pub struct GetSubscriptionPlansRequest { pub offset: Option, } +impl GetSubscriptionPlansRequest { + pub fn new(limit: Option, offset: Option) -> 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, diff --git a/crates/hyperswitch_domain_models/src/router_response_types/subscriptions.rs b/crates/hyperswitch_domain_models/src/router_response_types/subscriptions.rs index 3da78e63be..183b2f7ed3 100644 --- a/crates/hyperswitch_domain_models/src/router_response_types/subscriptions.rs +++ b/crates/hyperswitch_domain_models/src/router_response_types/subscriptions.rs @@ -33,6 +33,7 @@ pub enum SubscriptionStatus { Onetime, Cancelled, Failed, + Created, } #[cfg(feature = "v1")] @@ -47,6 +48,7 @@ impl From 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, } +#[cfg(feature = "v1")] +impl From 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 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, diff --git a/crates/hyperswitch_domain_models/src/subscription.rs b/crates/hyperswitch_domain_models/src/subscription.rs new file mode 100644 index 0000000000..af2390de3f --- /dev/null +++ b/crates/hyperswitch_domain_models/src/subscription.rs @@ -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 { + 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 for ClientSecret { + fn from(api_secret: api_models::subscription::ClientSecret) -> Self { + Self::new(api_secret.as_str().to_string()) + } +} + +#[cfg(feature = "v1")] +impl From for api_models::subscription::ClientSecret { + fn from(domain_secret: ClientSecret) -> Self { + Self::new(domain_secret.to_string()) + } +} diff --git a/crates/router/src/core/subscription.rs b/crates/router/src/core/subscription.rs index 7ef35b8c0d..1ec8514fc6 100644 --- a/crates/router/src/core/subscription.rs +++ b/crates/router/src/core/subscription.rs @@ -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 { +) -> RouterResponse { 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> { + 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::>(), + }) + } + + 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?; diff --git a/crates/router/src/core/subscription/billing_processor_handler.rs b/crates/router/src/core/subscription/billing_processor_handler.rs index 4cc8ad3107..2e1d3b7ff7 100644 --- a/crates/router/src/core/subscription/billing_processor_handler.rs +++ b/crates/router/src/core/subscription/billing_processor_handler.rs @@ -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, - pub customer: hyperswitch_domain_models::customer::Customer, + pub customer: Option, 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, profile: hyperswitch_domain_models::business_profile::Profile, ) -> errors::RouterResult { let merchant_connector_id = profile.get_billing_processor_id()?; @@ -109,8 +110,15 @@ impl BillingHandler { billing_address: Option, payment_method_data: Option, ) -> errors::RouterResult { + 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, + offset: Option, + ) -> errors::RouterResult { + 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 { + 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( &self, state: &SessionState, diff --git a/crates/router/src/core/subscription/subscription_handler.rs b/crates/router/src/core/subscription/subscription_handler.rs index 24c08313b8..b11cf517b0 100644 --- a/crates/router/src/core/subscription/subscription_handler.rs +++ b/crates/router/src/core/subscription/subscription_handler.rs @@ -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, diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 60724830e0..57a9aeb1f7 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -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| { diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index f6ba2af979..713b51a9d7 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -87,12 +87,11 @@ impl From 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, diff --git a/crates/router/src/routes/subscription.rs b/crates/router/src/routes/subscription.rs index a762da7197..4643281045 100644 --- a/crates/router/src/routes/subscription.rs +++ b/crates/router/src/routes/subscription.rs @@ -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, + req: HttpRequest, + query: web::Query, +) -> 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( diff --git a/crates/router/src/workflows/invoice_sync.rs b/crates/router/src/workflows/invoice_sync.rs index 83aa45dc84..ec2d2d7a91 100644 --- a/crates/router/src/workflows/invoice_sync.rs +++ b/crates/router/src/workflows/invoice_sync.rs @@ -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 diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 46eef6fab6..95582124cc 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -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,