mirror of
https://github.com/juspay/hyperswitch.git
synced 2026-03-13 09:02:06 +08:00
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:
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)),
|
||||
};
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")?;
|
||||
|
||||
|
||||
@@ -275,6 +275,8 @@ pub enum Flow {
|
||||
CreateAndConfirmSubscription,
|
||||
/// Get Subscription flow
|
||||
GetSubscription,
|
||||
/// Update Subscription flow
|
||||
UpdateSubscription,
|
||||
/// Get Subscription estimate flow
|
||||
GetSubscriptionEstimate,
|
||||
/// Create dynamic routing
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user