mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-27 03:13:56 +08:00
feat(subscription): Add endpoint to get Subscription estimate (#9637)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Co-authored-by: Gaurav Rawat <104276743+GauravRawat369@users.noreply.github.com>
This commit is contained in:
@ -44,7 +44,6 @@ pub mod relay;
|
||||
#[cfg(feature = "v2")]
|
||||
pub mod revenue_recovery_data_backfill;
|
||||
pub mod routing;
|
||||
#[cfg(feature = "v1")]
|
||||
pub mod subscription;
|
||||
pub mod surcharge_decision_configs;
|
||||
pub mod three_ds_decision_rule;
|
||||
|
||||
@ -446,3 +446,52 @@ pub struct Invoice {
|
||||
}
|
||||
|
||||
impl ApiEventMetric for ConfirmSubscriptionResponse {}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)]
|
||||
pub struct EstimateSubscriptionQuery {
|
||||
/// Identifier for the associated subscription plan.
|
||||
pub plan_id: Option<String>,
|
||||
|
||||
/// Identifier for the associated item_price_id for the subscription.
|
||||
pub item_price_id: String,
|
||||
|
||||
/// Idenctifier for the coupon code for the subscription.
|
||||
pub coupon_code: Option<String>,
|
||||
}
|
||||
|
||||
impl ApiEventMetric for EstimateSubscriptionQuery {}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, ToSchema)]
|
||||
pub struct EstimateSubscriptionResponse {
|
||||
/// Estimated amount to be charged for the invoice.
|
||||
pub amount: MinorUnit,
|
||||
/// Currency for the amount.
|
||||
pub currency: api_enums::Currency,
|
||||
/// Identifier for the associated plan_id.
|
||||
pub plan_id: Option<String>,
|
||||
/// Identifier for the associated item_price_id for the subscription.
|
||||
pub item_price_id: Option<String>,
|
||||
/// Idenctifier for the coupon code for the subscription.
|
||||
pub coupon_code: Option<String>,
|
||||
/// Identifier for customer.
|
||||
pub customer_id: Option<common_utils::id_type::CustomerId>,
|
||||
pub line_items: Vec<SubscriptionLineItem>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, ToSchema)]
|
||||
pub struct SubscriptionLineItem {
|
||||
/// Unique identifier for the line item.
|
||||
pub item_id: String,
|
||||
/// Type of the line item.
|
||||
pub item_type: String,
|
||||
/// Description of the line item.
|
||||
pub description: String,
|
||||
/// Amount for the line item.
|
||||
pub amount: MinorUnit,
|
||||
/// Currency for the line item
|
||||
pub currency: common_enums::Currency,
|
||||
/// Quantity of the line item.
|
||||
pub quantity: i64,
|
||||
}
|
||||
|
||||
impl ApiEventMetric for EstimateSubscriptionResponse {}
|
||||
|
||||
@ -1212,11 +1212,26 @@ impl
|
||||
) -> CustomResult<String, errors::ConnectorError> {
|
||||
let metadata: chargebee::ChargebeeMetadata =
|
||||
utils::to_connector_meta_from_secret(req.connector_meta_data.clone())?;
|
||||
let url = self
|
||||
.base_url(connectors)
|
||||
.to_string()
|
||||
.replace("{{merchant_endpoint_prefix}}", metadata.site.peek());
|
||||
Ok(format!("{url}v2/estimates/create_subscription_for_items"))
|
||||
|
||||
let site = metadata.site.peek();
|
||||
|
||||
let mut base = self.base_url(connectors).to_string();
|
||||
|
||||
base = base.replace("{{merchant_endpoint_prefix}}", site);
|
||||
base = base.replace("$", site);
|
||||
|
||||
if base.contains("{{merchant_endpoint_prefix}}") || base.contains('$') {
|
||||
return Err(errors::ConnectorError::InvalidConnectorConfig {
|
||||
config: "Chargebee base_url has an unresolved placeholder (expected `$` or `{{merchant_endpoint_prefix}}`).",
|
||||
}
|
||||
.into());
|
||||
}
|
||||
|
||||
if !base.ends_with('/') {
|
||||
base.push('/');
|
||||
}
|
||||
|
||||
Ok(format!("{base}v2/estimates/create_subscription_for_items"))
|
||||
}
|
||||
fn get_content_type(&self) -> &'static str {
|
||||
self.common_get_content_type()
|
||||
|
||||
@ -1036,6 +1036,7 @@ impl<F, T>
|
||||
currency: estimate.subscription_estimate.currency_code,
|
||||
next_billing_at: estimate.subscription_estimate.next_billing_at,
|
||||
credits_applied: Some(estimate.invoice_estimate.credits_applied),
|
||||
customer_id: Some(estimate.invoice_estimate.customer_id),
|
||||
line_items: estimate
|
||||
.invoice_estimate
|
||||
.line_items
|
||||
@ -1215,6 +1216,7 @@ impl
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ChargebeeSubscriptionEstimateRequest {
|
||||
#[serde(rename = "subscription_items[item_price_id][0]")]
|
||||
pub price_id: String,
|
||||
}
|
||||
|
||||
@ -1351,8 +1353,8 @@ pub struct SubscriptionEstimate {
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct InvoiceEstimate {
|
||||
pub recurring: bool,
|
||||
#[serde(with = "common_utils::custom_serde::iso8601")]
|
||||
pub date: PrimitiveDateTime,
|
||||
#[serde(default, with = "common_utils::custom_serde::timestamp::option")]
|
||||
pub date: Option<PrimitiveDateTime>,
|
||||
pub price_type: String,
|
||||
pub sub_total: MinorUnit,
|
||||
pub total: MinorUnit,
|
||||
@ -1361,7 +1363,7 @@ pub struct InvoiceEstimate {
|
||||
pub amount_due: MinorUnit,
|
||||
/// type of the object will be `invoice_estimate`
|
||||
pub object: String,
|
||||
pub customer_id: String,
|
||||
pub customer_id: CustomerId,
|
||||
pub line_items: Vec<LineItem>,
|
||||
pub currency_code: enums::Currency,
|
||||
pub round_off_amount: MinorUnit,
|
||||
@ -1370,10 +1372,10 @@ pub struct InvoiceEstimate {
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LineItem {
|
||||
pub id: String,
|
||||
#[serde(with = "common_utils::custom_serde::iso8601")]
|
||||
pub date_from: PrimitiveDateTime,
|
||||
#[serde(with = "common_utils::custom_serde::iso8601")]
|
||||
pub date_to: PrimitiveDateTime,
|
||||
#[serde(default, with = "common_utils::custom_serde::timestamp::option")]
|
||||
pub date_from: Option<PrimitiveDateTime>,
|
||||
#[serde(default, with = "common_utils::custom_serde::timestamp::option")]
|
||||
pub date_to: Option<PrimitiveDateTime>,
|
||||
pub unit_amount: MinorUnit,
|
||||
pub quantity: i64,
|
||||
pub amount: MinorUnit,
|
||||
|
||||
@ -174,7 +174,9 @@ pub struct GetSubscriptionPlanPricesData {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GetSubscriptionEstimateData;
|
||||
pub struct GetSubscriptionEstimateData {
|
||||
pub connector_meta_data: Option<pii::SecretSerdeValue>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UasFlowData {
|
||||
|
||||
@ -36,7 +36,6 @@ pub enum SubscriptionStatus {
|
||||
Created,
|
||||
}
|
||||
|
||||
#[cfg(feature = "v1")]
|
||||
impl From<SubscriptionStatus> for api_models::subscription::SubscriptionStatus {
|
||||
fn from(status: SubscriptionStatus) -> Self {
|
||||
match status {
|
||||
@ -82,7 +81,6 @@ 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 {
|
||||
@ -106,7 +104,6 @@ pub enum PeriodUnit {
|
||||
Year,
|
||||
}
|
||||
|
||||
#[cfg(feature = "v1")]
|
||||
impl From<PeriodUnit> for api_models::subscription::PeriodUnit {
|
||||
fn from(unit: PeriodUnit) -> Self {
|
||||
match unit {
|
||||
@ -128,8 +125,28 @@ pub struct GetSubscriptionEstimateResponse {
|
||||
pub currency: Currency,
|
||||
pub next_billing_at: Option<PrimitiveDateTime>,
|
||||
pub line_items: Vec<SubscriptionLineItem>,
|
||||
pub customer_id: Option<id_type::CustomerId>,
|
||||
}
|
||||
|
||||
impl From<GetSubscriptionEstimateResponse>
|
||||
for api_models::subscription::EstimateSubscriptionResponse
|
||||
{
|
||||
fn from(value: GetSubscriptionEstimateResponse) -> Self {
|
||||
Self {
|
||||
amount: value.total,
|
||||
currency: value.currency,
|
||||
plan_id: None,
|
||||
item_price_id: None,
|
||||
coupon_code: None,
|
||||
customer_id: value.customer_id,
|
||||
line_items: value
|
||||
.line_items
|
||||
.into_iter()
|
||||
.map(api_models::subscription::SubscriptionLineItem::from)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SubscriptionLineItem {
|
||||
pub item_id: String,
|
||||
@ -141,3 +158,16 @@ pub struct SubscriptionLineItem {
|
||||
pub quantity: i64,
|
||||
pub pricing_model: Option<String>,
|
||||
}
|
||||
|
||||
impl From<SubscriptionLineItem> for api_models::subscription::SubscriptionLineItem {
|
||||
fn from(value: SubscriptionLineItem) -> Self {
|
||||
Self {
|
||||
item_id: value.item_id,
|
||||
description: value.description,
|
||||
item_type: value.item_type,
|
||||
amount: value.amount,
|
||||
currency: value.currency,
|
||||
quantity: value.quantity,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,10 +11,11 @@ use hyperswitch_domain_models::{
|
||||
flow_common_types::{
|
||||
AccessTokenFlowData, AuthenticationTokenFlowData, BillingConnectorInvoiceSyncFlowData,
|
||||
BillingConnectorPaymentsSyncFlowData, DisputesFlowData, ExternalAuthenticationFlowData,
|
||||
ExternalVaultProxyFlowData, FilesFlowData, GetSubscriptionPlanPricesData,
|
||||
GetSubscriptionPlansData, GiftCardBalanceCheckFlowData, InvoiceRecordBackData,
|
||||
MandateRevokeFlowData, PaymentFlowData, RefundFlowData, SubscriptionCreateData,
|
||||
SubscriptionCustomerData, UasFlowData, VaultConnectorFlowData, WebhookSourceVerifyData,
|
||||
ExternalVaultProxyFlowData, FilesFlowData, GetSubscriptionEstimateData,
|
||||
GetSubscriptionPlanPricesData, GetSubscriptionPlansData, GiftCardBalanceCheckFlowData,
|
||||
InvoiceRecordBackData, MandateRevokeFlowData, PaymentFlowData, RefundFlowData,
|
||||
SubscriptionCreateData, SubscriptionCustomerData, UasFlowData, VaultConnectorFlowData,
|
||||
WebhookSourceVerifyData,
|
||||
},
|
||||
RouterDataV2,
|
||||
},
|
||||
@ -895,6 +896,7 @@ default_router_data_conversion!(GetSubscriptionPlansData);
|
||||
default_router_data_conversion!(GetSubscriptionPlanPricesData);
|
||||
default_router_data_conversion!(SubscriptionCreateData);
|
||||
default_router_data_conversion!(SubscriptionCustomerData);
|
||||
default_router_data_conversion!(GetSubscriptionEstimateData);
|
||||
|
||||
impl<T, Req: Clone, Resp: Clone> RouterDataConversion<T, Req, Resp> for UasFlowData {
|
||||
fn from_old_router_data(
|
||||
|
||||
@ -422,3 +422,27 @@ pub async fn get_subscription(
|
||||
subscription.to_subscription_response(),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn get_estimate(
|
||||
state: SessionState,
|
||||
merchant_context: MerchantContext,
|
||||
profile_id: common_utils::id_type::ProfileId,
|
||||
query: subscription_types::EstimateSubscriptionQuery,
|
||||
) -> RouterResponse<subscription_types::EstimateSubscriptionResponse> {
|
||||
let profile =
|
||||
SubscriptionHandler::find_business_profile(&state, &merchant_context, &profile_id)
|
||||
.await
|
||||
.attach_printable("subscriptions: failed to find business profile in get_estimate")?;
|
||||
let billing_handler = BillingHandler::create(
|
||||
&state,
|
||||
merchant_context.get_merchant_account(),
|
||||
merchant_context.get_merchant_key_store(),
|
||||
None,
|
||||
profile,
|
||||
)
|
||||
.await?;
|
||||
let estimate = billing_handler
|
||||
.get_subscription_estimate(&state, query)
|
||||
.await?;
|
||||
Ok(ApplicationResponse::Json(estimate.into()))
|
||||
}
|
||||
|
||||
@ -5,8 +5,8 @@ use common_utils::{ext_traits::ValueExt, pii};
|
||||
use error_stack::ResultExt;
|
||||
use hyperswitch_domain_models::{
|
||||
router_data_v2::flow_common_types::{
|
||||
GetSubscriptionPlanPricesData, GetSubscriptionPlansData, InvoiceRecordBackData,
|
||||
SubscriptionCreateData, SubscriptionCustomerData,
|
||||
GetSubscriptionEstimateData, GetSubscriptionPlanPricesData, GetSubscriptionPlansData,
|
||||
InvoiceRecordBackData, SubscriptionCreateData, SubscriptionCustomerData,
|
||||
},
|
||||
router_request_types::{
|
||||
revenue_recovery::InvoiceRecordBackRequest, subscriptions as subscription_request_types,
|
||||
@ -20,7 +20,10 @@ use hyperswitch_domain_models::{
|
||||
|
||||
use super::errors;
|
||||
use crate::{
|
||||
core::payments as payments_core, routes::SessionState, services, types::api as api_types,
|
||||
core::{payments as payments_core, subscription::subscription_types},
|
||||
routes::SessionState,
|
||||
services,
|
||||
types::api as api_types,
|
||||
};
|
||||
|
||||
pub struct BillingHandler {
|
||||
@ -283,6 +286,45 @@ impl BillingHandler {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_subscription_estimate(
|
||||
&self,
|
||||
state: &SessionState,
|
||||
estimate_request: subscription_types::EstimateSubscriptionQuery,
|
||||
) -> errors::RouterResult<subscription_response_types::GetSubscriptionEstimateResponse> {
|
||||
let estimate_req = subscription_request_types::GetSubscriptionEstimateRequest {
|
||||
price_id: estimate_request.item_price_id.clone(),
|
||||
};
|
||||
|
||||
let router_data = self.build_router_data(
|
||||
state,
|
||||
estimate_req,
|
||||
GetSubscriptionEstimateData {
|
||||
connector_meta_data: self.connector_metadata.clone(),
|
||||
},
|
||||
)?;
|
||||
let connector_integration = self.connector_data.connector.get_connector_integration();
|
||||
|
||||
let response = Box::pin(self.call_connector(
|
||||
state,
|
||||
router_data,
|
||||
"get subscription estimate from connector",
|
||||
connector_integration,
|
||||
))
|
||||
.await?;
|
||||
|
||||
match response {
|
||||
Ok(response_data) => Ok(response_data),
|
||||
Err(err) => Err(errors::ApiErrorResponse::ExternalConnectorError {
|
||||
code: err.code,
|
||||
message: err.message,
|
||||
connector: self.connector_data.connector_name.to_string(),
|
||||
status_code: err.status_code,
|
||||
reason: err.reason,
|
||||
}
|
||||
.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_subscription_plans(
|
||||
&self,
|
||||
state: &SessionState,
|
||||
|
||||
@ -1187,6 +1187,7 @@ impl Subscription {
|
||||
subscription::create_subscription(state, req, payload)
|
||||
}),
|
||||
))
|
||||
.service(web::resource("/estimate").route(web::get().to(subscription::get_estimate)))
|
||||
.service(
|
||||
web::resource("/plans").route(web::get().to(subscription::get_subscription_plans)),
|
||||
)
|
||||
|
||||
@ -91,6 +91,7 @@ impl From<Flow> for ApiIdentifier {
|
||||
| Flow::ConfirmSubscription
|
||||
| Flow::CreateAndConfirmSubscription
|
||||
| Flow::GetSubscription
|
||||
| Flow::GetSubscriptionEstimate
|
||||
| Flow::GetPlansForSubscription => Self::Subscription,
|
||||
Flow::RetrieveForexFlow => Self::Forex,
|
||||
Flow::AddToBlocklist => Self::Blocklist,
|
||||
|
||||
@ -7,6 +7,7 @@ use std::str::FromStr;
|
||||
|
||||
use actix_web::{web, HttpRequest, HttpResponse, Responder};
|
||||
use api_models::subscription as subscription_types;
|
||||
use error_stack::report;
|
||||
use hyperswitch_domain_models::errors;
|
||||
use router_env::{
|
||||
tracing::{self, instrument},
|
||||
@ -252,3 +253,40 @@ pub async fn create_and_confirm_subscription(
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
/// add support for get subscription estimate
|
||||
#[instrument(skip_all)]
|
||||
pub async fn get_estimate(
|
||||
state: web::Data<AppState>,
|
||||
req: HttpRequest,
|
||||
query: web::Query<subscription_types::EstimateSubscriptionQuery>,
|
||||
) -> impl Responder {
|
||||
let flow = Flow::GetSubscriptionEstimate;
|
||||
let profile_id = match extract_profile_id(&req) {
|
||||
Ok(id) => id,
|
||||
Err(response) => return response,
|
||||
};
|
||||
let api_auth = auth::ApiKeyAuth {
|
||||
is_connected_allowed: false,
|
||||
is_platform_allowed: false,
|
||||
};
|
||||
let (auth_type, _auth_flow) = match auth::get_auth_type_and_flow(req.headers(), api_auth) {
|
||||
Ok(auth) => auth,
|
||||
Err(err) => return oss_api::log_and_return_error_response(report!(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_estimate(state, merchant_context, profile_id.clone(), query)
|
||||
},
|
||||
&*auth_type,
|
||||
api_locking::LockAction::NotApplicable,
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
@ -273,6 +273,8 @@ pub enum Flow {
|
||||
CreateAndConfirmSubscription,
|
||||
/// Get Subscription flow
|
||||
GetSubscription,
|
||||
/// Get Subscription estimate flow
|
||||
GetSubscriptionEstimate,
|
||||
/// Create dynamic routing
|
||||
CreateDynamicRoutingConfig,
|
||||
/// Toggle dynamic routing
|
||||
|
||||
Reference in New Issue
Block a user