feat(subscriptions): Add update subscriptions APIs with payments update call (#9778)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Gaurav Rawat
2025-10-14 15:42:48 +05:30
committed by GitHub
parent 859b3b1844
commit 36fbaa0707
21 changed files with 461 additions and 86 deletions

View File

@@ -1,5 +1,5 @@
use common_types::payments::CustomerAcceptance;
use common_utils::{errors::ValidationError, events::ApiEventMetric, types::MinorUnit};
use common_utils::{events::ApiEventMetric, types::MinorUnit};
use masking::Secret;
use utoipa::ToSchema;
@@ -24,6 +24,9 @@ pub struct CreateSubscriptionRequest {
/// Merchant specific Unique identifier.
pub merchant_reference_id: Option<String>,
/// Identifier for the associated item_price_id for the subscription.
pub item_price_id: Option<String>,
/// Identifier for the subscription plan.
pub plan_id: Option<String>,
@@ -60,6 +63,9 @@ pub struct SubscriptionResponse {
/// 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: Option<String>,
/// Associated profile ID.
pub profile_id: common_utils::id_type::ProfileId,
@@ -129,6 +135,7 @@ impl SubscriptionResponse {
merchant_reference_id: Option<String>,
status: SubscriptionStatus,
plan_id: Option<String>,
item_price_id: Option<String>,
profile_id: common_utils::id_type::ProfileId,
merchant_id: common_utils::id_type::MerchantId,
client_secret: Option<Secret<String>>,
@@ -141,6 +148,7 @@ impl SubscriptionResponse {
merchant_reference_id,
status,
plan_id,
item_price_id,
profile_id,
client_secret,
merchant_id,
@@ -327,28 +335,11 @@ pub struct ConfirmSubscriptionRequest {
/// Client secret for SDK based interaction.
pub client_secret: Option<ClientSecret>,
/// 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>,
/// Payment details for the invoice.
pub payment_details: ConfirmSubscriptionPaymentDetails,
}
impl ConfirmSubscriptionRequest {
pub fn get_item_price_id(&self) -> Result<String, error_stack::Report<ValidationError>> {
self.item_price_id.clone().ok_or(error_stack::report!(
ValidationError::MissingRequiredField {
field_name: "item_price_id".to_string()
}
))
}
pub fn get_billing_address(&self) -> Option<Address> {
self.payment_details
.payment_method_data
@@ -421,7 +412,7 @@ pub struct ConfirmSubscriptionResponse {
pub plan_id: Option<String>,
/// Identifier for the associated item_price_id for the subscription.
pub price_id: Option<String>,
pub item_price_id: Option<String>,
/// Optional coupon code applied to this subscription.
pub coupon: Option<String>,
@@ -480,6 +471,20 @@ pub struct Invoice {
impl ApiEventMetric for ConfirmSubscriptionResponse {}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)]
pub struct UpdateSubscriptionRequest {
/// Identifier for the associated plan_id.
pub plan_id: String,
/// Identifier for the associated item_price_id for the subscription.
pub item_price_id: String,
/// Amount to be charged for the invoice.
pub amount: MinorUnit,
/// Currency for the amount.
pub currency: api_enums::Currency,
}
impl ApiEventMetric for UpdateSubscriptionRequest {}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)]
pub struct EstimateSubscriptionQuery {
/// Identifier for the associated subscription plan.

View File

@@ -61,6 +61,8 @@ pub struct InvoiceUpdate {
pub connector_invoice_id: Option<common_utils::id_type::InvoiceId>,
pub modified_at: time::PrimitiveDateTime,
pub payment_intent_id: Option<common_utils::id_type::PaymentId>,
pub amount: Option<MinorUnit>,
pub currency: Option<String>,
}
impl InvoiceNew {
@@ -105,17 +107,21 @@ impl InvoiceNew {
impl InvoiceUpdate {
pub fn new(
amount: Option<MinorUnit>,
currency: Option<String>,
payment_method_id: Option<String>,
status: Option<InvoiceStatus>,
connector_invoice_id: Option<common_utils::id_type::InvoiceId>,
payment_intent_id: Option<common_utils::id_type::PaymentId>,
) -> Self {
Self {
payment_method_id,
status,
payment_method_id,
connector_invoice_id,
payment_intent_id,
modified_at: common_utils::date_time::now(),
payment_intent_id,
amount,
currency,
}
}
}

View File

@@ -1606,6 +1606,10 @@ diesel::table! {
profile_id -> Varchar,
#[max_length = 128]
merchant_reference_id -> Nullable<Varchar>,
#[max_length = 128]
plan_id -> Nullable<Varchar>,
#[max_length = 128]
item_price_id -> Nullable<Varchar>,
}
}

View File

@@ -1543,6 +1543,10 @@ diesel::table! {
profile_id -> Varchar,
#[max_length = 128]
merchant_reference_id -> Nullable<Varchar>,
#[max_length = 128]
plan_id -> Nullable<Varchar>,
#[max_length = 128]
item_price_id -> Nullable<Varchar>,
}
}

View File

@@ -22,6 +22,8 @@ pub struct SubscriptionNew {
modified_at: time::PrimitiveDateTime,
profile_id: common_utils::id_type::ProfileId,
merchant_reference_id: Option<String>,
plan_id: Option<String>,
item_price_id: Option<String>,
}
#[derive(
@@ -43,6 +45,8 @@ pub struct Subscription {
pub modified_at: time::PrimitiveDateTime,
pub profile_id: common_utils::id_type::ProfileId,
pub merchant_reference_id: Option<String>,
pub plan_id: Option<String>,
pub item_price_id: Option<String>,
}
#[derive(Clone, Debug, Eq, PartialEq, AsChangeset, router_derive::DebugAsDisplay, Deserialize)]
@@ -52,6 +56,8 @@ pub struct SubscriptionUpdate {
pub payment_method_id: Option<String>,
pub status: Option<String>,
pub modified_at: time::PrimitiveDateTime,
pub plan_id: Option<String>,
pub item_price_id: Option<String>,
}
impl SubscriptionNew {
@@ -69,6 +75,8 @@ impl SubscriptionNew {
metadata: Option<SecretSerdeValue>,
profile_id: common_utils::id_type::ProfileId,
merchant_reference_id: Option<String>,
plan_id: Option<String>,
item_price_id: Option<String>,
) -> Self {
let now = common_utils::date_time::now();
Self {
@@ -86,6 +94,8 @@ impl SubscriptionNew {
modified_at: now,
profile_id,
merchant_reference_id,
plan_id,
item_price_id,
}
}
@@ -99,15 +109,19 @@ impl SubscriptionNew {
impl SubscriptionUpdate {
pub fn new(
connector_subscription_id: Option<String>,
payment_method_id: Option<Secret<String>>,
status: Option<String>,
connector_subscription_id: Option<String>,
plan_id: Option<String>,
item_price_id: Option<String>,
) -> Self {
Self {
connector_subscription_id,
payment_method_id: payment_method_id.map(|pmid| pmid.peek().clone()),
status,
connector_subscription_id,
modified_at: common_utils::date_time::now(),
plan_id,
item_price_id,
}
}
}

View File

@@ -7,7 +7,7 @@ use common_utils::{
MinorUnit,
},
};
use masking::Secret;
use masking::{PeekInterface, Secret};
use utoipa::ToSchema;
use crate::merchant_key_store::MerchantKeyStore;
@@ -187,7 +187,114 @@ pub struct InvoiceUpdate {
pub connector_invoice_id: Option<common_utils::id_type::InvoiceId>,
pub modified_at: time::PrimitiveDateTime,
pub payment_intent_id: Option<common_utils::id_type::PaymentId>,
pub amount: Option<MinorUnit>,
pub currency: Option<String>,
}
#[derive(Debug, Clone)]
pub struct AmountAndCurrencyUpdate {
pub amount: MinorUnit,
pub currency: String,
}
#[derive(Debug, Clone)]
pub struct ConnectorAndStatusUpdate {
pub connector_invoice_id: common_utils::id_type::InvoiceId,
pub status: common_enums::connector_enums::InvoiceStatus,
}
#[derive(Debug, Clone)]
pub struct PaymentAndStatusUpdate {
pub payment_method_id: Option<Secret<String>>,
pub payment_intent_id: Option<common_utils::id_type::PaymentId>,
pub status: common_enums::connector_enums::InvoiceStatus,
pub connector_invoice_id: Option<common_utils::id_type::InvoiceId>,
}
/// Enum-based invoice update request for different scenarios
#[derive(Debug, Clone)]
pub enum InvoiceUpdateRequest {
/// Update amount and currency
Amount(AmountAndCurrencyUpdate),
/// Update connector invoice ID and status
Connector(ConnectorAndStatusUpdate),
/// Update payment details along with status
PaymentStatus(PaymentAndStatusUpdate),
}
impl InvoiceUpdateRequest {
/// Create an amount and currency update request
pub fn update_amount_and_currency(amount: MinorUnit, currency: String) -> Self {
Self::Amount(AmountAndCurrencyUpdate { amount, currency })
}
/// Create a connector invoice ID and status update request
pub fn update_connector_and_status(
connector_invoice_id: common_utils::id_type::InvoiceId,
status: common_enums::connector_enums::InvoiceStatus,
) -> Self {
Self::Connector(ConnectorAndStatusUpdate {
connector_invoice_id,
status,
})
}
/// Create a combined payment and status update request
pub fn update_payment_and_status(
payment_method_id: Option<Secret<String>>,
payment_intent_id: Option<common_utils::id_type::PaymentId>,
status: common_enums::connector_enums::InvoiceStatus,
connector_invoice_id: Option<common_utils::id_type::InvoiceId>,
) -> Self {
Self::PaymentStatus(PaymentAndStatusUpdate {
payment_method_id,
payment_intent_id,
status,
connector_invoice_id,
})
}
}
impl From<InvoiceUpdateRequest> for InvoiceUpdate {
fn from(request: InvoiceUpdateRequest) -> Self {
let now = common_utils::date_time::now();
match request {
InvoiceUpdateRequest::Amount(update) => Self {
status: None,
payment_method_id: None,
connector_invoice_id: None,
modified_at: now,
payment_intent_id: None,
amount: Some(update.amount),
currency: Some(update.currency),
},
InvoiceUpdateRequest::Connector(update) => Self {
status: Some(update.status),
payment_method_id: None,
connector_invoice_id: Some(update.connector_invoice_id),
modified_at: now,
payment_intent_id: None,
amount: None,
currency: None,
},
InvoiceUpdateRequest::PaymentStatus(update) => Self {
status: Some(update.status),
payment_method_id: update
.payment_method_id
.as_ref()
.map(|id| id.peek())
.cloned(),
connector_invoice_id: update.connector_invoice_id,
modified_at: now,
payment_intent_id: update.payment_intent_id,
amount: None,
currency: None,
},
}
}
}
#[async_trait::async_trait]
impl super::behaviour::Conversion for InvoiceUpdate {
type DstType = diesel_models::invoice::InvoiceUpdate;
@@ -200,6 +307,8 @@ impl super::behaviour::Conversion for InvoiceUpdate {
connector_invoice_id: self.connector_invoice_id,
modified_at: self.modified_at,
payment_intent_id: self.payment_intent_id,
amount: self.amount,
currency: self.currency,
})
}
@@ -218,6 +327,8 @@ impl super::behaviour::Conversion for InvoiceUpdate {
connector_invoice_id: item.connector_invoice_id,
modified_at: item.modified_at,
payment_intent_id: item.payment_intent_id,
amount: item.amount,
currency: item.currency,
})
}
@@ -228,6 +339,8 @@ impl super::behaviour::Conversion for InvoiceUpdate {
connector_invoice_id: self.connector_invoice_id,
modified_at: self.modified_at,
payment_intent_id: self.payment_intent_id,
amount: self.amount,
currency: self.currency,
})
}
}
@@ -238,13 +351,17 @@ impl InvoiceUpdate {
status: Option<common_enums::connector_enums::InvoiceStatus>,
connector_invoice_id: Option<common_utils::id_type::InvoiceId>,
payment_intent_id: Option<common_utils::id_type::PaymentId>,
amount: Option<MinorUnit>,
currency: Option<String>,
) -> Self {
Self {
payment_method_id,
status,
payment_method_id,
connector_invoice_id,
modified_at: common_utils::date_time::now(),
payment_intent_id,
connector_invoice_id,
amount,
currency,
}
}
}

View File

@@ -73,6 +73,8 @@ pub struct Subscription {
pub modified_at: PrimitiveDateTime,
pub profile_id: common_utils::id_type::ProfileId,
pub merchant_reference_id: Option<String>,
pub plan_id: Option<String>,
pub item_price_id: Option<String>,
}
#[derive(Debug, Clone, serde::Serialize)]
@@ -137,6 +139,8 @@ impl super::behaviour::Conversion for Subscription {
modified_at: now,
profile_id: self.profile_id,
merchant_reference_id: self.merchant_reference_id,
plan_id: self.plan_id,
item_price_id: self.item_price_id,
})
}
@@ -164,6 +168,8 @@ impl super::behaviour::Conversion for Subscription {
modified_at: item.modified_at,
profile_id: item.profile_id,
merchant_reference_id: item.merchant_reference_id,
plan_id: item.plan_id,
item_price_id: item.item_price_id,
})
}
@@ -181,6 +187,8 @@ impl super::behaviour::Conversion for Subscription {
self.metadata,
self.profile_id,
self.merchant_reference_id,
self.plan_id,
self.item_price_id,
))
}
}
@@ -218,19 +226,25 @@ pub struct SubscriptionUpdate {
pub payment_method_id: Option<String>,
pub status: Option<String>,
pub modified_at: PrimitiveDateTime,
pub plan_id: Option<String>,
pub item_price_id: Option<String>,
}
impl SubscriptionUpdate {
pub fn new(
connector_subscription_id: Option<String>,
payment_method_id: Option<Secret<String>>,
status: Option<String>,
connector_subscription_id: Option<String>,
plan_id: Option<String>,
item_price_id: Option<String>,
) -> Self {
Self {
connector_subscription_id,
payment_method_id: payment_method_id.map(|pmid| pmid.peek().clone()),
status,
connector_subscription_id,
modified_at: common_utils::date_time::now(),
plan_id,
item_price_id,
}
}
}
@@ -246,6 +260,8 @@ impl super::behaviour::Conversion for SubscriptionUpdate {
payment_method_id: self.payment_method_id,
status: self.status,
modified_at: self.modified_at,
plan_id: self.plan_id,
item_price_id: self.item_price_id,
})
}
@@ -263,6 +279,8 @@ impl super::behaviour::Conversion for SubscriptionUpdate {
payment_method_id: item.payment_method_id,
status: item.status,
modified_at: item.modified_at,
plan_id: item.plan_id,
item_price_id: item.item_price_id,
})
}
@@ -272,6 +290,8 @@ impl super::behaviour::Conversion for SubscriptionUpdate {
payment_method_id: self.payment_method_id,
status: self.status,
modified_at: self.modified_at,
plan_id: self.plan_id,
item_price_id: self.item_price_id,
})
}
}

View File

@@ -4,7 +4,9 @@ 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, invoice::InvoiceUpdateRequest, merchant_context::MerchantContext,
};
use super::errors::{self, RouterResponse};
use crate::{
@@ -56,6 +58,8 @@ pub async fn create_subscription(
billing_handler.merchant_connector_id.clone(),
request.merchant_reference_id.clone(),
&profile.clone(),
request.plan_id.clone(),
request.item_price_id.clone(),
)
.await
.attach_printable("subscriptions: failed to create subscription entry")?;
@@ -82,9 +86,11 @@ pub async fn create_subscription(
subscription
.update_subscription(
hyperswitch_domain_models::subscription::SubscriptionUpdate::new(
None,
payment.payment_method_id.clone(),
None,
None,
request.plan_id,
request.item_price_id,
),
)
.await
@@ -179,6 +185,8 @@ pub async fn create_and_confirm_subscription(
billing_handler.merchant_connector_id.clone(),
request.merchant_reference_id.clone(),
&profile.clone(),
request.plan_id.clone(),
request.item_price_id.clone(),
)
.await
.attach_printable("subscriptions: failed to create subscription entry")?;
@@ -255,14 +263,16 @@ pub async fn create_and_confirm_subscription(
subs_handler
.update_subscription(
hyperswitch_domain_models::subscription::SubscriptionUpdate::new(
payment_response.payment_method_id.clone(),
Some(SubscriptionStatus::from(subscription_create_response.status).to_string()),
Some(
subscription_create_response
.subscription_id
.get_string_repr()
.to_string(),
),
payment_response.payment_method_id.clone(),
Some(SubscriptionStatus::from(subscription_create_response.status).to_string()),
request.plan_id,
request.item_price_id,
),
)
.await?;
@@ -271,8 +281,6 @@ pub async fn create_and_confirm_subscription(
&invoice_entry,
&payment_response,
subscription_create_response.status,
request.plan_id.clone(),
request.item_price_id.clone(),
)?;
Ok(ApplicationResponse::Json(response))
@@ -360,25 +368,25 @@ pub async fn confirm_subscription(
let subscription_create_response = billing_handler
.create_subscription_on_connector(
&state,
subscription,
request.item_price_id.clone(),
subscription.clone(),
subscription.item_price_id.clone(),
request.get_billing_address(),
)
.await?;
let invoice_details = subscription_create_response.invoice_details;
let update_request = InvoiceUpdateRequest::update_payment_and_status(
payment_response.payment_method_id.clone(),
Some(payment_response.payment_id.clone()),
invoice_details
.clone()
.and_then(|invoice| invoice.status)
.unwrap_or(connector_enums::InvoiceStatus::InvoiceCreated),
invoice_details.clone().map(|invoice| invoice.id),
);
let invoice_entry = invoice_handler
.update_invoice(
&state,
invoice.id,
payment_response.payment_method_id.clone(),
Some(payment_response.payment_id.clone()),
invoice_details
.clone()
.and_then(|invoice| invoice.status)
.unwrap_or(connector_enums::InvoiceStatus::InvoiceCreated),
invoice_details.clone().map(|invoice| invoice.id),
)
.update_invoice(&state, invoice.id, update_request)
.await?;
invoice_handler
@@ -393,14 +401,16 @@ pub async fn confirm_subscription(
subscription_entry
.update_subscription(
hyperswitch_domain_models::subscription::SubscriptionUpdate::new(
payment_response.payment_method_id.clone(),
Some(SubscriptionStatus::from(subscription_create_response.status).to_string()),
Some(
subscription_create_response
.subscription_id
.get_string_repr()
.to_string(),
),
payment_response.payment_method_id.clone(),
Some(SubscriptionStatus::from(subscription_create_response.status).to_string()),
subscription.plan_id.clone(),
subscription.item_price_id.clone(),
),
)
.await?;
@@ -409,8 +419,6 @@ pub async fn confirm_subscription(
&invoice_entry,
&payment_response,
subscription_create_response.status,
request.plan_id.clone(),
request.item_price_id.clone(),
)?;
Ok(ApplicationResponse::Json(response))
@@ -461,3 +469,65 @@ pub async fn get_estimate(
.await?;
Ok(ApplicationResponse::Json(estimate.into()))
}
pub async fn update_subscription(
state: SessionState,
merchant_context: MerchantContext,
profile_id: common_utils::id_type::ProfileId,
subscription_id: common_utils::id_type::SubscriptionId,
request: subscription_types::UpdateSubscriptionRequest,
) -> RouterResponse<SubscriptionResponse> {
let profile =
SubscriptionHandler::find_business_profile(&state, &merchant_context, &profile_id)
.await
.attach_printable(
"subscriptions: failed to find business profile in get_subscription",
)?;
let handler = SubscriptionHandler::new(&state, &merchant_context);
let mut subscription_entry = handler.find_subscription(subscription_id).await?;
let invoice_handler = subscription_entry.get_invoice_handler(profile.clone());
let invoice = invoice_handler
.get_latest_invoice(&state)
.await
.attach_printable("subscriptions: failed to get latest invoice")?;
let subscription = subscription_entry.subscription.clone();
subscription_entry
.update_subscription(
hyperswitch_domain_models::subscription::SubscriptionUpdate::new(
None,
None,
None,
Some(request.plan_id),
Some(request.item_price_id),
),
)
.await?;
let update_request = InvoiceUpdateRequest::update_amount_and_currency(
request.amount,
request.currency.to_string(),
);
let invoice_entry = invoice_handler
.update_invoice(&state, invoice.id, update_request)
.await?;
let _payment_response = invoice_handler
.update_payment(
&state,
request.amount,
request.currency,
invoice_entry.payment_intent_id.ok_or(
errors::ApiErrorResponse::MissingRequiredField {
field_name: "payment_intent_id",
},
)?,
)
.await?;
get_subscription(state, merchant_context, profile_id, subscription.id).await
}

View File

@@ -6,7 +6,6 @@ use common_enums::connector_enums;
use common_utils::{pii, types::MinorUnit};
use error_stack::ResultExt;
use hyperswitch_domain_models::router_response_types::subscriptions as subscription_response_types;
use masking::{PeekInterface, Secret};
use super::errors;
use crate::{
@@ -82,17 +81,10 @@ impl InvoiceHandler {
&self,
state: &SessionState,
invoice_id: common_utils::id_type::InvoiceId,
payment_method_id: Option<Secret<String>>,
payment_intent_id: Option<common_utils::id_type::PaymentId>,
status: connector_enums::InvoiceStatus,
connector_invoice_id: Option<common_utils::id_type::InvoiceId>,
update_request: hyperswitch_domain_models::invoice::InvoiceUpdateRequest,
) -> errors::RouterResult<hyperswitch_domain_models::invoice::Invoice> {
let update_invoice = hyperswitch_domain_models::invoice::InvoiceUpdate::new(
payment_method_id.as_ref().map(|id| id.peek()).cloned(),
Some(status),
connector_invoice_id,
payment_intent_id,
);
let update_invoice: hyperswitch_domain_models::invoice::InvoiceUpdate =
update_request.into();
let key_manager_state = &(state).into();
state
.store
@@ -336,4 +328,34 @@ impl InvoiceHandler {
)
.await
}
pub async fn update_payment(
&self,
state: &SessionState,
amount: MinorUnit,
currency: common_enums::Currency,
payment_id: common_utils::id_type::PaymentId,
) -> errors::RouterResult<subscription_types::PaymentResponseData> {
let payment_update_request = subscription_types::CreatePaymentsRequestData {
amount,
currency,
customer_id: None,
billing: None,
shipping: None,
profile_id: None,
setup_future_usage: None,
return_url: None,
capture_method: None,
authentication_type: None,
};
payments_api_client::PaymentsApiClient::update_payment(
state,
payment_update_request,
payment_id.get_string_repr().to_string(),
self.merchant_account.get_id().get_string_repr(),
self.profile.get_id().get_string_repr(),
)
.await
}
}

View File

@@ -219,4 +219,28 @@ impl PaymentsApiClient {
)
.await
}
pub async fn update_payment(
state: &SessionState,
request: subscription_types::CreatePaymentsRequestData,
payment_id: String,
merchant_id: &str,
profile_id: &str,
) -> errors::RouterResult<subscription_types::PaymentResponseData> {
let base_url = &state.conf.internal_services.payments_base_url;
let url = format!("{}/payments/{}", base_url, payment_id);
Self::make_payment_api_call(
state,
services::Method::Post,
url,
Some(common_utils::request::RequestContent::Json(Box::new(
request,
))),
"Update Payment",
merchant_id,
profile_id,
)
.await
}
}

View File

@@ -39,6 +39,7 @@ impl<'a> SubscriptionHandler<'a> {
}
/// Helper function to create a subscription entry in the database.
#[allow(clippy::too_many_arguments)]
pub async fn create_subscription_entry(
&self,
subscription_id: common_utils::id_type::SubscriptionId,
@@ -47,6 +48,8 @@ impl<'a> SubscriptionHandler<'a> {
merchant_connector_id: common_utils::id_type::MerchantConnectorAccountId,
merchant_reference_id: Option<String>,
profile: &hyperswitch_domain_models::business_profile::Profile,
plan_id: Option<String>,
item_price_id: Option<String>,
) -> errors::RouterResult<SubscriptionWithHandler<'_>> {
let store = self.state.store.clone();
let db = store.as_ref();
@@ -66,10 +69,12 @@ impl<'a> SubscriptionHandler<'a> {
.clone(),
customer_id: customer_id.clone(),
metadata: None,
profile_id: profile.get_id().clone(),
merchant_reference_id,
created_at: common_utils::date_time::now(),
modified_at: common_utils::date_time::now(),
profile_id: profile.get_id().clone(),
merchant_reference_id,
plan_id,
item_price_id,
};
subscription.generate_and_set_client_secret();
@@ -288,18 +293,16 @@ impl SubscriptionWithHandler<'_> {
invoice: &hyperswitch_domain_models::invoice::Invoice,
payment_response: &subscription_types::PaymentResponseData,
status: subscription_response_types::SubscriptionStatus,
plan_id: Option<String>,
price_id: Option<String>,
) -> errors::RouterResult<subscription_types::ConfirmSubscriptionResponse> {
Ok(subscription_types::ConfirmSubscriptionResponse {
id: self.subscription.id.clone(),
merchant_reference_id: self.subscription.merchant_reference_id.clone(),
status: subscription_types::SubscriptionStatus::from(status),
plan_id,
plan_id: self.subscription.plan_id.clone(),
profile_id: self.subscription.profile_id.to_owned(),
payment: Some(payment_response.clone()),
customer_id: Some(self.subscription.customer_id.clone()),
price_id,
item_price_id: self.subscription.item_price_id.clone(),
coupon: None,
billing_processor_subscription_id: self.subscription.connector_subscription_id.clone(),
invoice: Some(subscription_types::Invoice::foreign_try_from(invoice)?),
@@ -316,7 +319,8 @@ impl SubscriptionWithHandler<'_> {
self.subscription.merchant_reference_id.clone(),
subscription_types::SubscriptionStatus::from_str(&self.subscription.status)
.unwrap_or(subscription_types::SubscriptionStatus::Created),
None,
self.subscription.plan_id.clone(),
self.subscription.item_price_id.clone(),
self.subscription.profile_id.to_owned(),
self.subscription.merchant_id.to_owned(),
self.subscription.client_secret.clone().map(Secret::new),

View File

@@ -17,6 +17,7 @@ use common_utils::{
use diesel_models::{refund as diesel_refund, ConnectorMandateReferenceId};
use error_stack::{report, ResultExt};
use hyperswitch_domain_models::{
invoice::InvoiceUpdateRequest,
mandates::CommonMandateReference,
payments::{payment_attempt::PaymentAttempt, HeaderPayload},
router_request_types::VerifyWebhookSourceRequestData,
@@ -2711,15 +2712,15 @@ async fn subscription_incoming_webhook_flow(
)
.await?;
let update_request = InvoiceUpdateRequest::update_payment_and_status(
payment_response.payment_method_id,
Some(payment_response.payment_id.clone()),
InvoiceStatus::from(payment_response.status),
Some(mit_payment_data.invoice_id.clone()),
);
let _updated_invoice = invoice_handler
.update_invoice(
&state,
invoice_entry.id.clone(),
payment_response.payment_method_id.clone(),
Some(payment_response.payment_id.clone()),
InvoiceStatus::from(payment_response.status),
Some(mit_payment_data.invoice_id.clone()),
)
.update_invoice(&state, invoice_entry.id.clone(), update_request)
.await?;
Ok(WebhookResponseTracker::NoEffect)

View File

@@ -1231,6 +1231,13 @@ impl Subscription {
},
)),
)
.service(
web::resource("/{subscription_id}/update").route(web::put().to(
|state, req, id, payload| {
subscription::update_subscription(state, req, id, payload)
},
)),
)
.service(
web::resource("/{subscription_id}")
.route(web::get().to(subscription::get_subscription)),

View File

@@ -91,6 +91,7 @@ impl From<Flow> for ApiIdentifier {
| Flow::ConfirmSubscription
| Flow::CreateAndConfirmSubscription
| Flow::GetSubscription
| Flow::UpdateSubscription
| Flow::GetSubscriptionEstimate
| Flow::GetPlansForSubscription => Self::Subscription,
Flow::RetrieveForexFlow => Self::Forex,

View File

@@ -727,7 +727,11 @@ pub async fn payments_update(
is_connected_allowed: false,
is_platform_allowed: true,
};
let (auth_type, auth_flow) = match auth::get_auth_type_and_flow(req.headers(), api_auth) {
let (auth_type, auth_flow) = match auth::check_internal_api_key_auth_no_client_secret(
req.headers(),
api_auth,
state.conf.internal_merchant_id_profile_id_auth.clone(),
) {
Ok(auth) => auth,
Err(err) => return api::log_and_return_error_response(report!(err)),
};

View File

@@ -311,3 +311,42 @@ pub async fn get_estimate(
))
.await
}
#[instrument(skip_all)]
pub async fn update_subscription(
state: web::Data<AppState>,
req: HttpRequest,
subscription_id: web::Path<common_utils::id_type::SubscriptionId>,
json_payload: web::Json<subscription_types::UpdateSubscriptionRequest>,
) -> impl Responder {
let flow = Flow::UpdateSubscription;
let subscription_id = subscription_id.into_inner();
let profile_id = match extract_profile_id(&req) {
Ok(id) => id,
Err(response) => return response,
};
Box::pin(oss_api::server_wrap(
flow,
state,
&req,
json_payload.into_inner(),
|state, auth: auth::AuthenticationData, payload, _| {
let merchant_context = domain::MerchantContext::NormalMerchant(Box::new(
domain::Context(auth.merchant_account, auth.key_store),
));
subscription::update_subscription(
state,
merchant_context,
profile_id.clone(),
subscription_id.clone(),
payload.clone(),
)
},
&auth::HeaderAuth(auth::ApiKeyAuth {
is_connected_allowed: false,
is_platform_allowed: false,
}),
api_locking::LockAction::NotApplicable,
))
.await
}

View File

@@ -4511,6 +4511,31 @@ where
}
}
#[cfg(feature = "v1")]
pub fn check_internal_api_key_auth_no_client_secret<T>(
headers: &HeaderMap,
api_auth: ApiKeyAuth,
internal_api_key_auth: settings::InternalMerchantIdProfileIdAuthSettings,
) -> RouterResult<(
Box<dyn AuthenticateAndFetch<AuthenticationData, T>>,
api::AuthFlow,
)>
where
T: SessionStateInfo + Sync + Send,
ApiKeyAuth: AuthenticateAndFetch<AuthenticationData, T>,
{
if is_internal_api_key_merchant_id_profile_id_auth(headers, internal_api_key_auth) {
Ok((
// HeaderAuth(api_auth) will never be called in this case as the internal auth will be checked first
Box::new(InternalMerchantIdProfileIdAuth(HeaderAuth(api_auth))),
api::AuthFlow::Merchant,
))
} else {
let (auth, auth_flow) = get_auth_type_and_flow(headers, api_auth)?;
Ok((auth, auth_flow))
}
}
pub async fn decode_jwt<T>(token: &str, state: &impl SessionStateInfo) -> RouterResult<T>
where
T: serde::de::DeserializeOwned,

View File

@@ -7,6 +7,7 @@ use common_utils::{
};
use diesel_models::process_tracker::business_status;
use error_stack::ResultExt;
use hyperswitch_domain_models::invoice::InvoiceUpdateRequest;
use router_env::logger;
use scheduler::{
consumer::{self, workflows::ProcessTrackerWorkflow},
@@ -221,15 +222,13 @@ impl<'a> InvoiceSyncHandler<'a> {
.await
.attach_printable("Failed to record back to billing processor")?;
let update_request = InvoiceUpdateRequest::update_connector_and_status(
connector_invoice_id,
common_enums::connector_enums::InvoiceStatus::from(invoice_sync_status),
);
invoice_handler
.update_invoice(
self.state,
self.invoice.id.to_owned(),
None,
None,
common_enums::connector_enums::InvoiceStatus::from(invoice_sync_status),
Some(connector_invoice_id),
)
.update_invoice(self.state, self.invoice.id.to_owned(), update_request)
.await
.attach_printable("Failed to update invoice in DB")?;

View File

@@ -275,6 +275,8 @@ pub enum Flow {
CreateAndConfirmSubscription,
/// Get Subscription flow
GetSubscription,
/// Update Subscription flow
UpdateSubscription,
/// Get Subscription estimate flow
GetSubscriptionEstimate,
/// Create dynamic routing

View File

@@ -0,0 +1,3 @@
-- This file should undo anything in `up.sql`
ALTER TABLE subscription DROP COLUMN IF EXISTS plan_id;
ALTER TABLE subscription DROP COLUMN IF EXISTS item_price_id;

View File

@@ -0,0 +1,4 @@
-- Your SQL goes here
ALTER TABLE subscription
ADD COLUMN IF NOT EXISTS plan_id VARCHAR(128),
ADD COLUMN IF NOT EXISTS item_price_id VARCHAR(128);